Overview
A number guessing game is the perfect first Ruby program because it is small enough to finish in one sitting, yet it exercises nearly every fundamental: reading user input with gets, converting it safely, branching with if and case, looping, and splitting work into small methods. It is also genuinely fun to play, which makes testing it painless.
The computer picks a secret number between 1 and 100 and you get seven tries to find it. After each wrong guess the game tells you whether to go higher or lower, and how close you are — "Very hot!", "Warm." or "Cold." — and it remembers your best score across rounds until you quit.
- A
read_guessmethod that keeps asking until the player types a valid whole number in range. - A
hint_formethod that usescase/whenwith ranges to describe how close a guess is. - A
play_roundmethod that tracks attempts and returns the score (ornilon a loss). - A main loop that offers "Play again?" and keeps a best score for the session.
- Graceful handling of end-of-input (Ctrl+D / Ctrl+Z) so the program never crashes.
Prerequisites
- Ruby 3.x installed and the ability to run
ruby file.rb— lesson 3. - Variables, strings and string interpolation — lesson 4.
- Control flow:
if,case/when,whileandloop— lesson 6. - Defining methods and returning values — lesson 7.
Project Structure
Everything lives in a single file, guess.rb. Three constants describe the game's rules, three small methods each do one job (read a guess, describe a guess, play a round), and a short loop at the bottom of the file ties them together. Keeping input handling out of the game logic means you could later swap the terminal for a web form without touching hint_for at all.
Step 1: Constants & Reading a Safe Guess
Integer(input, 10, exception: false) converts a string to an integer and returns nil instead of raising an error when the text is not a number. The explicit base 10 matters: without it, Ruby treats a leading zero as an octal number, so "08" would be rejected. (MIN..MAX).cover?(guess) checks the range in one readable call.
MIN = 1MAX = 100MAX_ATTEMPTS = 7
def read_guess loop do print "Your guess (#{MIN}-#{MAX}): " line = gets return nil if line.nil? # end of input (Ctrl+D / Ctrl+Z)
input = line.chomp guess = Integer(input, 10, exception: false) if guess.nil? puts " '#{input}' is not a whole number. Try again." elsif !(MIN..MAX).cover?(guess) puts " Please stay between #{MIN} and #{MAX}." else return guess end endendStep 2: Hot/Cold Hints with case/when
case compares its value against each when using the === operator, and for a range === means "is inside this range". That makes when 1..3 read exactly like the rule it implements. The method returns a string instead of printing it, which keeps it easy to test on its own.
def hint_for(guess, secret) difference = (guess - secret).abs direction = guess < secret ? "higher" : "lower" closeness = case difference when 1..3 then "Very hot!" when 4..10 then "Warm." else "Cold." end "#{closeness} Go #{direction}."end
puts hint_for(40, 42) # => Very hot! Go higher.puts hint_for(90, 42) # => Cold. Go lower.Step 3: Play One Round
rand(MIN..MAX) picks a random integer including both ends. The method returns three different kinds of value — the number of attempts on a win, nil on a loss and the symbol :quit if input ended — and the caller decides what each one means. Returning a symbol for a special case like this is a common, readable Ruby idiom.
def play_round secret = rand(MIN..MAX) attempts = 0
while attempts < MAX_ATTEMPTS guess = read_guess return :quit if guess.nil?
attempts += 1 if guess == secret puts "Correct! You found #{secret} in #{attempts} #{attempts == 1 ? 'try' : 'tries'}." return attempts end
remaining = MAX_ATTEMPTS - attempts puts " #{hint_for(guess, secret)} (#{remaining} #{remaining == 1 ? 'try' : 'tries'} left)" end
puts "Out of tries! The number was #{secret}." nilendStep 4: The Game Loop & Best Score
best starts as nil and is only replaced when a new score is better. gets&.chomp&.downcase uses the safe navigation operator &., so if the input ends, the whole expression becomes nil instead of crashing with "undefined method for nil".
best = nilputs "=== NUMBER GUESSING GAME ==="puts "I'm thinking of a number between #{MIN} and #{MAX}. You have #{MAX_ATTEMPTS} tries."
loop do result = play_round break if result == :quit
if result best = result if best.nil? || result < best puts "Best score this session: #{best} #{best == 1 ? 'try' : 'tries'}." end
print "Play again? (y/n): " break unless gets&.chomp&.downcase == "y"end
puts "Thanks for playing!"Complete Code
# guess.rb — run with: ruby guess.rbMIN = 1MAX = 100MAX_ATTEMPTS = 7
def read_guess loop do print "Your guess (#{MIN}-#{MAX}): " line = gets return nil if line.nil? # end of input (Ctrl+D / Ctrl+Z)
input = line.chomp guess = Integer(input, 10, exception: false) if guess.nil? puts " '#{input}' is not a whole number. Try again." elsif !(MIN..MAX).cover?(guess) puts " Please stay between #{MIN} and #{MAX}." else return guess end endend
def hint_for(guess, secret) difference = (guess - secret).abs direction = guess < secret ? "higher" : "lower" closeness = case difference when 1..3 then "Very hot!" when 4..10 then "Warm." else "Cold." end "#{closeness} Go #{direction}."end
def play_round secret = rand(MIN..MAX) attempts = 0
while attempts < MAX_ATTEMPTS guess = read_guess return :quit if guess.nil?
attempts += 1 if guess == secret puts "Correct! You found #{secret} in #{attempts} #{attempts == 1 ? 'try' : 'tries'}." return attempts end
remaining = MAX_ATTEMPTS - attempts puts " #{hint_for(guess, secret)} (#{remaining} #{remaining == 1 ? 'try' : 'tries'} left)" end
puts "Out of tries! The number was #{secret}." nilend
best = nilputs "=== NUMBER GUESSING GAME ==="puts "I'm thinking of a number between #{MIN} and #{MAX}. You have #{MAX_ATTEMPTS} tries."
loop do result = play_round break if result == :quit
if result best = result if best.nil? || result < best puts "Best score this session: #{best} #{best == 1 ? 'try' : 'tries'}." end
print "Play again? (y/n): " break unless gets&.chomp&.downcase == "y"end
puts "Thanks for playing!"Sample Run
Click Run to see what this code prints.
Extend This Project
- Add difficulty levels (easy 1–50 with 10 tries, hard 1–1000 with 10 tries) chosen at the start of each round.
- Remember guesses in an array and warn the player when they repeat a number they already tried.
- Save the all-time best score to a file with
File.writeand load it withFile.readon startup. - Turn the game around: you pick the number and the computer guesses it with binary search.
- Wrap the game in a
GuessingGameclass (lesson 9) with@secretand@attemptsinstance variables.
Summary
You built a complete interactive game from small, single-purpose methods: safe input parsing with Integer(..., exception: false), range-based case/when hints, a round that returns meaningful values, and a session loop with a best score. These same patterns — validate input, keep logic in pure methods, loop until the user is done — appear in every command-line tool you will write in Ruby.