LearnAI ToolsCareerPractice BuildsPlayDevelopmentContact
RubyBeginner~1 hour

Number Guessing Game

Build an interactive terminal game with input validation, hot/cold hints, limited tries and a best-score tracker.

Control FlowMethodsRangesUser Input

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.

What You'll Build
  • A read_guess method that keeps asking until the player types a valid whole number in range.
  • A hint_for method that uses case/when with ranges to describe how close a guess is.
  • A play_round method that tracks attempts and returns the score (or nil on 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, while and loop — 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 = 1
MAX = 100
MAX_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
end
end

Step 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}."
nil
end

Step 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 = nil
puts "=== 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
# guess.rb — run with: ruby guess.rb
MIN = 1
MAX = 100
MAX_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
end
end
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}."
nil
end
best = nil
puts "=== 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

ruby guess.rb

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.write and load it with File.read on startup.
  • Turn the game around: you pick the number and the computer guesses it with binary search.
  • Wrap the game in a GuessingGame class (lesson 9) with @secret and @attempts instance 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.