```When I was young, I was in a programming club that often had us build
working programs from simple games.  Blackjack, Yahtzee, etc.  There
was usually a twist though, like a time limit or having to create the
game on your TI calculator.  In that club, I built Yahtzee in just
under one hour.

That code (in Pascal) was ugly and more than a little hostile to the
user.  As a penance, I tried to make this one a little more
professional looking, but that took me more than an hour...

James Edward Gray II

#!/usr/local/bin/ruby -w

#  yahtzee.rb
#
#  Created by James Edward Gray II on 2005-02-15.

# Namespace for all things Yahtzee.
module Yahtzee
# An object for managing the rolls of a Yahtzee game.
class Roll
# Create an instance of Roll.  Methods can then be used the examine
the
# results of the roll and reroll dice.
def initialize(  )
@dice = Array.new(5) { rand(6) + 1 }
end

# Examine the individual dice of a Roll.
def []( index )
@dice[index]
end

# Count occurrences of a set of pips.
def count( *pips )
@dice.inject(0) do |total, die|
if pips.include?(die) then total + 1 else total end
end
end

# Add all occurrences of a set of pips, or all the dice.
def sum( *pips )
if pips.size == 0
@dice.inject(0) { |total, die| total + die }
else
@dice.inject(0) do |total, die|
if pips.include?(die) then total + die else total end
end
end
end

#
# Examines Roll for a pattern of dice, returning true if found.
# Patterns can be of the form:
#
#     roll.matches?(1, 2, 3, 4)
#
# Which validates a sequence, regardless of the actual pips on the
dice.
#
# You can also use the form:
#
#     roll.matches?(*%w{x x x y y})
#
# To validate repititions.
#
# The two forms can be mixed in any combination and when they are,
both
# must match completely.
#
def matches?( *pattern )
digits, letters = pattern.partition { |e| e.is_a?(Integer) }
matches_digits?(digits) and matches_letters?(letters)
end

# Reroll selected _dice_.
def reroll( *dice )
if dice.size == 0
@dice = Array.new(5) { rand(6) + 1 }
else
indices = [ ]
pool    = @dice.dup
dice.each do |d|
indices << i
pool[i] = -1
end

indices.each { |i| @dice[i] = rand(6) + 1 }
end
end

# To make printing out rolls easier.
def to_s(  )
"#{@dice[0..-2].join(',')} and #{@dice[-1]}"
end

private

# Verifies matching of sequence patterns.
def matches_digits?( digits )
return true if digits.size < 2

digits.sort!
test = @dice.uniq.sort
loop do
(0..(@dice.length - digits.length)).each do |index|
return true if test[index, digits.length] == digits
end

digits.collect! { |d| d + 1 }
break if digits.last > 6
end

false
end

# Verifies matching of repitition patterns.
def matches_letters?( letters )
return true if letters.size < 2

counts = Hash.new(0)
letters.each { |l| counts[l] += 1 }
counts = counts.values.sort.reverse

pips = @dice.uniq
counts.each do |c|
return false unless match = pips.find { |p| count(p) >= c }
pips.delete(match)
end

true
end
end

# A basic score tracking object.
class Scorecard
# Create an instance of Scorecard.  Add categories and totals, track
# score and display results as needed.
def initialize(  )
@categories = [ ]
end

# Add one or more categories to this Scorecard.  Order is maintained.
categories.each do |cat|
@categories << [cat, 0]
end
end

#
# Add a total, with a block to calculate it from passed a
# categories Hash.
#
@categories << [name, calculator]
end

#
# The primary score action method.  Adds _count_ points to the
category
# at _index_.
#
def count( index, count )
@categories.assoc(category(index))[1] += count
end

# Lookup the score of a given category.
def []( name )
@categories.assoc(name)[1]
end

# Lookup a category name, by _index.
def category( index )
id = 0
@categories.each_with_index do |(name, count_or_calc), i|
next unless count_or_calc.is_a?(Numeric)
id += 1
return @categories[i][0] if id == index
end

raise ArgumentError, "Invalid category."
end

# Support for easy printing.
def to_s(  )
id = 0
@categories.inject("") do |disp, (name, count_or_calc)|
if count_or_calc.is_a?(Numeric)
id += 1
disp + "%3d: %-20s %4d\n" % [id, name, count_or_calc]
else
disp + "     %-20s %4d\n" %
[name, count_or_calc.call(to_hash)]
end
end
end

# Convert category listing to a Hash.
def to_hash(  )
@categories.inject(Hash.new) do |hash, (name, count_or_calc)|
hash[name] = count_or_calc if count_or_calc.is_a?(Numeric)
hash
end
end
end
end

# Console game interface.
if __FILE__ == \$0
# Assemble Scorecard.
score = Yahtzee::Scorecard.new()
UPPER = %w{Ones Twos Threes Fours Fives Sixes}
UPPER_TOTAL = lambda do |cats|
cats.inject(0) do |total, (cat, count)|
if UPPER.include?(cat) then total + count else total end
end
end
upper = UPPER_TOTAL.call(cats)
if upper >= 63 then 35 else 0 end
end
upper = UPPER_TOTAL.call(cats)
if upper >= 63 then upper + 35 else upper end
end
LOWER = [ "Three of a Kind", "Four of a Kind", "Full House",
"Small Straight", "Large Straight", "Yahtzee", "Chance" ]
bonus_yahtzees = 0
LOWER_TOTAL = lambda do |cats|
cats.inject(bonus_yahtzees) do |total, (cat, count)|
if LOWER.include?(cat) then total + count else total end
end
end
upper = UPPER_TOTAL.call(cats)
if upper >= 63
upper + 35 + LOWER_TOTAL.call(cats)
else
upper + LOWER_TOTAL.call(cats)
end
end

# Game.
puts "\nWelcome to Yahtzee!"
scratches = (1..13).to_a
13.times do
# Rolling...
roll = Yahtzee::Roll.new
rolls = 2
while rolls > 0
puts "\nYou rolled #{roll}."
print "Action:  (c)heck score, (s)core, (q)uit or #s to reroll?  "
choice = STDIN.gets.chomp
case choice
when /^c/i
puts "\nScore:\n#{score}"
when /^s/i
break
when /^q/i
exit
else
begin
pips = choice.gsub(/\s+/, "").split(//).map do |n|
Integer(n)
end
roll.reroll(*pips)
rolls -= 1
rescue
puts "Error:  That not a valid reroll."
end
end
end

# Scoring...
loop do
if roll.matches?(*%w{x x x x x}) and score["Yahtzee"] == 50
bonus_yahtzees += 100

if scratches.include?(roll[0])
score.count(roll[0], roll.sum(roll[0]))
scratches.delete(choice)
puts "Bonus Yahtzee scored in #{score.category(roll[0])}."
break
end

puts "Bonus Yahtzee!  100 points added.  " +
"Score in lower section as a wildcard."
bonus_yahtzee = true
else
bonus_yahtzee = false
end

print "\nScore:\n#{score}\n" +
"Where would you like to count your #{roll} " +
"(# of category)?  "
begin
choice = Integer(STDIN.gets.chomp)
case choice
when 1..6
score.count(choice, roll.sum(choice))
when 7
if roll.matches?(*%w{x x x}) or bonus_yahtzee
score.count(choice, roll.sum())
end
when 8
if roll.matches?(*%w{x x x x}) or bonus_yahtzee
score.count(choice, roll.sum())
end
when 9
if roll.matches?(*%w{x x x y y}) or bonus_yahtzee
score.count(choice, 25)
end
when 10
if roll.matches?(1, 2, 3, 4) or bonus_yahtzee
score.count(choice, 30)
end
when 11
if roll.matches?(1, 2, 3, 4, 5) or bonus_yahtzee
score.count(choice, 40)
end
when 12
if roll.matches?(*%w{x x x x x})
score.count(choice, 50)
end
when 13
score.count(choice, roll.sum)
end
scratches.delete(choice)
break
rescue
puts "Error:  Invalid category choice."
end
end
end

print "\nFinal Score:\n#{score}\nThanks for playing.\n\n"
end

__END__

```