Showing posts with label ruby. Show all posts
Showing posts with label ruby. Show all posts

testing a random die roll

What tests would I write? Maybe I'd start by testing it only rolls 1 to 6
def test_doesnt_roll_less_than_1_or_greater_than_6
 1200.times do
 die = roll_die
 assert 1 <= die && die <= 6 end end 
That's a good start. But it's not nearly enough. One way to think about testing is to imagine someone is deliberately writing the code so that it passes the tests but is nevertheless completely incorrect. Take, for example, sorting an array of N integers. What tests would you write? Now suppose I tell you the incorrect implementation simply returns an array of N 42's. Are you testing the output array is a permutation of the input array?

Back to rolling the die... an implementation of roll_die() which always returned 3 would pass my first test. I need to test all of 1-6 get returned.
def test_rolls_1_to_6_at_least_once_each_in_600_rolls
 rolls = [-1,0,0,0,0,0,0]
 600.times do
 die = roll_die
 rolls[die] += 1
 end
 assert rolls[1]>= 1
 assert rolls[2]>= 1
 assert rolls[3]>= 1
 assert rolls[4]>= 1
 assert rolls[5]>= 1
 assert rolls[6]>= 1
end
Why 600 rolls? You might be thinking it's too large. That a decent roll_die() should return at least one each of 1-6 after a lot fewer than 600 rolls. Or, to put it another way, if I have to wait till the 600th roll to get my first 5 then roll_die() is looking a bit suspect. Fair enough.

The tests form a specification. If I want to specify a "tighter" implementation of roll_die() I can simply change 600 to, 200 say. The 200 is then part of the specification.

Once again, it's easy to imagine an incorrect implementation that passes all the tests. How about one that simply cycles repeatedly through 1-6...
$n = 1
def roll_die
 $n += 1
 $n %= 6
 $n + 1
end
I've tested the "die" part of "random die". I've got to the "random" part of "random die". My incorrect implementation is not very random. It's very regular. It's very ordered. Suppose I call roll_die() 1200 times, save the 1200 values in a file, and then compress the file. I should get a lot of compression. Let's try it…
dice = ""
1200.times { dice += roll_die.to_s }
File.open('dice.txt', 'w') { |f| f.write(dice) }
unzipped_size = File.size('dice.txt')
assert_equal 1200, unzipped_size
`zip dice.txt.zip dice.txt`
zipped_size = File.size('dice.txt.zip')
p zipped_size
On my macbook I get a value of 184. As expected, a lot of compression. Let's compare the compression to an implementation that isn't deliberately incorrect.
def roll_die
 [1,2,3,4,5,6].shuffle[0]
end
This gives a zipped file size of around 680. A lot less compression. I can use that as part of the specification.
def test_roll_is_random_entropically
 dice = ""
 1200.times { dice += roll_die().to_s }
 File.open('dice.txt', 'w') { |f| f.write(dice) }
 unzipped_size = File.size('dice.txt')
 assert_equal 1200, unzipped_size
 `zip dice.txt.zip dice.txt`
 zipped_size = File.size('dice.txt.zip')
 assert zipped_size> 600
end
Imagine someone is deliberately writing the code so that it passes the tests but is nevertheless completely incorrect…


Poker hands in Ruby

John Cleary (@TheRealBifter) is doing a nice project - The 12 TDD's of Xmas. Day 11 was Poker Hands. I did it in Ruby. Here's the code from traffic-light 116.

Tests first:
require './card'
require './hand'
require 'test/unit'
class TestUntitled < Test::Unit::TestCase def test_start hand = Hand.new("2H 4S 4C 2D 4H") assert_equal Card.new('2',:hearts), hand[0] assert_equal Card.new('4',:spades), hand[1] assert_equal Card.new('4',:clubs), hand[2] assert_equal Card.new('2',:diamonds), hand[3] assert_equal Card.new('4',:hearts), hand[4] end def test_card_has_pips_and_suit_set_on_creation card = Card.new('2',:hearts) assert_equal '2', card.pips assert_equal :hearts, card.suit card = Card.new('T',:hearts) assert_equal 'T', card.pips assert_equal :hearts, card.suit card = Card.new('J',:hearts) assert_equal 'J', card.pips assert_equal :hearts, card.suit card = Card.new('Q',:hearts) assert_equal 'Q', card.pips assert_equal :hearts, card.suit card = Card.new('K',:hearts) assert_equal 'K', card.pips assert_equal :hearts, card.suit card = Card.new('A',:hearts) assert_equal 'A', card.pips assert_equal :hearts, card.suit end def test_hand_ranked_three_of_a_kind assert_equal :three_of_a_kind, Hand.new("2H 4S 4C AD 4H").rank end def test_hand_ranked_one_pair assert_equal :one_pair, Hand.new("2H 4S 5C JD 4H").rank end def test_hand_ranked_two_pairs assert_equal :two_pairs, Hand.new("2H 4S 5C 2D 4H").rank end def test_hand_ranked_flush assert_equal :flush, Hand.new("2H 4H 6H 8H TH").rank end def test_hand_ranked_straight assert_equal :straight, Hand.new("2H 3C 4H 5H 6H").rank end def test_hand_ranked_full_house assert_equal :full_house, Hand.new("2H 4S 4C 2D 4H").rank end def test_hand_ranked_four_of_a_kind assert_equal :four_of_a_kind, Hand.new("2H 4S 4C 4D 4H").rank end def test_hand_ranked_straight_flush assert_equal :straight_flush, Hand.new("2H 4H 3H 5H 6H").rank end def test_hand_ranked_high_card assert_equal :high_card, Hand.new("2C 3H 4S 8C AH").rank end def test_full_house_beats_flush black = Hand.new("2H 4S 4C 2D 4H") white = Hand.new("2S 8S AS QS 3S") assert_equal 1, black <=> white
 end
 def test_higher_card_wins_if_equal_rank
 black = Hand.new("2H 3D 5S 9C KD")
 assert_equal :high_card, black.rank
 white = Hand.new("2C 3H 4S 8C AH")
 assert_equal :high_card, white.rank
 assert_equal -1, black <=> white
 end
 def test_equal_hands
 black = Hand.new("2H 3D 5S 9C KD")
 assert_equal :high_card, black.rank
 white = Hand.new("2D 3H 5C 9S KH")
 assert_equal :high_card, white.rank
 assert_equal 0, black <=> white
 end
end
Code second:
class Hand
 def initialize(cards)
 @cards = 
 cards.gsub(/\s+/, "")
 .scan(/.{2}/)
 .map{|ch| Card.new(ch[0],suit(ch[1]))}
 end
 def [](n)
 return @cards[n]
 end
 def rank
 return :straight_flush if straight? && flush?
 return :flush if flush?
 return :straight if straight?
 pip_tallies = pip_counts.sort.reverse
 return {
 [4,1] => :four_of_a_kind,
 [3,2] => :full_house,
 [3,1] => :three_of_a_kind,
 [2,2] => :two_pairs,
 [2,1] => :one_pair,
 [1,1] => :high_card
 }[pip_tallies[0..1]]
 end
 def <=>(other)
 keys <=> other.keys
 end
 def keys
 [ranking,pip_counts]
 end
private
 def ranking
 ranks.index(rank)
 end
 def ranks
 [
 :high_card,
 :pair,
 :two_pairs,
 :three_of_a_kind,
 :straight,
 :flush, 
 :full_house,
 :four_of_a_kind,
 :straight_flush
 ]
 end
 def pip_counts
 "23456789TJQKA"
 .chars
 .collect {|pips| pip_count(pips)}
 end
 def pip_count(pips)
 @cards.count{|card| card.pips == pips}
 end
 def pip_flags
 pip_counts.map{|n| n> 0 ? 'T' : 'F'}.join
 end
 def straight?
 pip_flags.include? 'TTTTT'
 end
 def flush?
 suit_counts.any?{|n| n == 5}
 end
 def suit_counts
 suits.collect{|suit| suit_count(suit)}
 end
 def suits 
 [:clubs,:diamonds,:hearts,:spades]
 end
 def suit_count(suit)
 @cards.count{|card| card.suit == suit}
 end
 def suit(ch)
 return suits["CDHS".index(ch)]
 end
end
class Card
 def initialize(pips,suit)
 @pips,@suit = pips,suit
 end
 def ==(other)
 pips == other.pips && suit == other.suit
 end
 def pips
 @pips
 end
 def suit
 @suit
 end
end
You can replay my entire progression (warts and all) on Cyber-Dojo (naturally).

Phone Numbers in Ruby

John Cleary (@TheRealBifter) is doing a nice project - The 12 TDD's of Xmas. Day 10 was the Phone number prefix problem. I did it in Ruby. Here's the code from traffic-light 14.

Tests first (very minimal - I'm a bit pressed for time):
require './consistent'
require 'test/unit'
class TestUntitled < Test::Unit::TestCase def test_consistent_phone_list list = { 'Bob' => '91125426',
 'Alice' => '97625992',
 }
 assert consistent(list)
 end
 def test_inconsistent_phone_list
 list = { 
 'Bob' => '91125426',
 'Alice' => '97625992',
 'Emergency' => '911'
 }
 assert !consistent(list)
 end
end
Code second:
def consistent(list)
 list.values.sort.each_cons(2).none? { |pair| prefix(*pair) }
end
def prefix(lhs,rhs)
 rhs.start_with? lhs
end
The each_cons from the previous problem proved very handy here. As did none? I really feel I'm starting to get the hang of ruby. You can replay my entire progression (warts and all) on Cyber-Dojo (naturally).

Monty Hall in Ruby

John Cleary (@TheRealBifter) is doing a nice project - The 12 TDD's of Xmas. Day 4 was the Monty Hall problem. I did it in Ruby. Here's the code from traffic-light 111.

Tests first:
require './monty_hall'
require 'test/unit'
class TestMontyHall < Test::Unit::TestCase def test_either_goat_door_is_opened_when_you_choose_the_car_door check_either_goat([:car, :goat, :goat], { :chosen_door => 0, 
 :goat_doors => [1, 2] 
 })
 check_either_goat([:goat, :car, :goat],
 { :chosen_door => 1, 
 :goat_doors => [0, 2]
 })
 check_either_goat([:goat, :goat, :car],
 { :chosen_door => 2, 
 :goat_doors => [0, 1]
 })
 end
 def test_other_goat_door_is_opened_when_you_choose_a_goat_door
 prizes = [:car, :goat, :goat]
 check_other_goat(prizes, 
 { :chosen_door => 1, 
 :opened_door => 2, 
 :offered_door => 0
 })
 check_other_goat(prizes, 
 { :chosen_door => 2, 
 :opened_door => 1, 
 :offered_door => 0 
 })
 prizes = [:goat, :car, :goat]
 check_other_goat(prizes, 
 { :chosen_door => 0, 
 :opened_door => 2, 
 :offered_door => 1
 })
 check_other_goat(prizes, 
 { :chosen_door => 2, 
 :opened_door => 0, 
 :offered_door => 1
 })
 prizes = [:goat, :goat, :car]
 check_other_goat(prizes, 
 { :chosen_door => 0, 
 :opened_door => 1, 
 :offered_door => 2
 })
 check_other_goat(prizes, 
 { :chosen_door => 1, 
 :opened_door => 0, 
 :offered_door => 2
 })
 end
 def test_strategy_of_sticking_with_chosen_door
 wins = Array.new(big) { MontyHall.new() }
 .count { |game| game.chosen_door == game.car_door }
 puts "Win car(sticking with chosen door):#{wins}/#{big}"
 end
 def test_strategy_of_switching_to_offered_door
 wins = Array.new(big) { MontyHall.new() }
 .count { |game| game.offered_door == game.car_door }
 puts "Win car(switching to offered door):#{wins}/#{big}"
 end
 #- - - - - - - - - - - - - - - - - - - - - - - -
 def check_either_goat(prizes, expected)
 chosen_door = expected[:chosen_door]
 goat_doors = expected[:goat_doors]
 check_params(prizes, chosen_door, goat_doors[0], goat_doors[1])
 
 goat_counts = [0,0]
 100.times do |n|
 game = MontyHall.new(prizes,chosen_door)
 opened_door = game.opened_door
 offered_door = game.offered_door
 assert_equal chosen_door, game.chosen_door
 assert_equal goat_doors.sort, [opened_door,offered_door].sort
 assert_equal doors, [chosen_door,opened_door,offered_door].sort
 assert_equal :car , prizes[chosen_door]
 assert_equal :goat, prizes[opened_door]
 assert_equal :goat, prizes[offered_door] 
 [0,1].each do |n|
 goat_counts[n] += (offered_door == goat_doors[n] ? 1 : 0)
 end
 end
 [0,1].each { |n| assert goat_counts[n]> 25 }
 end
 def check_other_goat(prizes, expected)
 chosen_door = expected[:chosen_door]
 opened_door = expected[:opened_door]
 offered_door = expected[:offered_door]
 check_params(prizes, chosen_door, opened_door, offered_door)
 game = MontyHall.new(prizes, chosen_door)
 assert_equal chosen_door, game.chosen_door
 assert_equal opened_door, game.opened_door
 assert_equal offered_door, game.offered_door
 assert_equal :goat, prizes[ chosen_door]
 assert_equal :goat, prizes[ opened_door]
 assert_equal :car , prizes[offered_door]
 end
 def check_params(prizes, door1, door2, door3)
 assert_equal 3, prizes.length
 prizes.each { |prize| assert [:goat,:car].include? prize }
 assert_equal doors, [door1,door2,door3].sort
 end
 def doors
 [0,1,2]
 end
 def big
 1000
 end
end
Code second:
class MontyHall
 def initialize(prizes = [:goat,:goat,:car].shuffle, 
 chosen_door = doors.shuffle[0])
 @prizes = prizes
 @chosen_door = chosen_door
 @car_door = prizes.find_index { |prize| prize == :car }
 if prizes[chosen_door] == :car
 @opened_door = goat_doors.shuffle[0] 
 end
 if prizes[chosen_door] == :goat
 @opened_door = (goat_doors - [chosen_door])[0] 
 end
 @offered_door = (doors - [chosen_door, opened_door])[0]
 end
 def chosen_door
 @chosen_door
 end
 def car_door
 @car_door
 end
 def opened_door
 @opened_door
 end
 def offered_door
 @offered_door
 end
private 
 def doors
 [0,1,2]
 end
 def goat_doors
 doors.select { |door| @prizes[door] == :goat }
 end
end
You can replay my entire progression (warts and all) on Cyber-Dojo (naturally).

Bowling Game in Ruby

John Cleary (@TheRealBifter) is doing a nice project - The 12 TDD's of Xmas. Day 9 was the Bowling Game problem. I did it in Ruby. Here's the code from traffic-light 100.

Tests first:
require './score'
require 'test/unit'
class TestScore < Test::Unit::TestCase def test_score_uninteresting_game # 7 7 7 7 7 = 35 # 6 6 6 6 6 = 30 balls = "51|52|51|52|51|52|51|52|51|52" assert_equal 65, score(balls) end def test_score_all_frames_5_spare # 15 15 15 15 15 = 75 # 15 15 15 15 15 = 75 balls = "5/|5/|5/|5/|5/|5/|5/|5/|5/|5/|5" assert_equal 150, score(balls) end def test_perfect_score # 30 30 30 30 30 = 150 # 30 30 30 30 30 = 150 balls = "X|X|X|X|X|X|X|X|X|X|XX" assert_equal 300, score(balls) end def test_10_strikes_then_33 # 30 30 30 30 16 = 136 # 30 30 30 30 23 = 143 balls = "X|X|X|X|X|X|X|X|X|X|33" assert_equal 279, score(balls) end def test_game_with_spare_in_middle_of_strikes # 20 30 30 30 30 = 140 # 25 20 30 30 30 = 135 balls = "X|X|5/|X|X|X|X|X|X|X|XX" assert_equal 275, score(balls) end def test_game_with_strike_in_middle_of_spares # 15 20 15 13 20 = 83 # 13 11 20 14 12 = 70 balls = "2/|3/|5/|1/|X|3/|5/|4/|3/|2/|X" assert_equal 153, score(balls) end def test_game_with_zero_balls # 8 7 7 7 7 = 36 # 6 5 6 6 6 = 29 balls = "51|62|50|52|51|52|51|52|51|52" assert_equal 65, score(balls) end def test_game_with_dash_as_zero_balls # 8 7 7 7 8 = 37 # 6 5 6 6 6 = 29 balls = "51|62|5-|52|51|52|51|52|51|62" assert_equal 66, score(balls) end end 
Code second:
def score(balls)
 frames = (balls).split("|")
 while frames.length != 12
 frames << "0" end frames.each_cons(3).collect{ |frame| frame_score(frame) }.inject(:+) end def frame_score(frames) if strike? frames[0] 10 + strike_bonus(frames[1..2]) elsif spare? frames[0] 10 + ball_score(frames[1][0]) else frames[0].chars.collect{ |ball| ball_score(ball) }.inject(:+) end end def strike_bonus(frames) if frames[0] == "XX" 20 elsif strike? frames[0] 10 + ball_score(frames[1][0]) elsif spare? frames[0] 10 else ball_score(frames[0][0]) + ball_score(frames[0][1]) end end def ball_score(ball) if strike? ball 10 else ball.to_i end end def strike?(frame) frame == "X" end def spare?(frame) frame[-1] == "/" end 
It took me a while to realize that each_slice should have been each_cons. You can replay my entire progression (warts and all) on Cyber-Dojo (naturally).

assert_not_diff

The output of Ruby's Test::Unit::TestCase's
assert_equal(expected, actual) when expected and actual are not the same is often not as useful as it could be. For example:
expected = [1,99,2,3,{:a=>34,:b=>43}]
actual = [1,2,3,4,{:a=>324,:c=>555,:b=>43},5]
assert_equal expected, actual
produces:
<[1, 99, 2, 3, {:a=>34, :b=>43}]> expected but was
<[1, 2, 3, 4, {:a=>324, :c=>555, :b=>43}, 5]>.
For large objects which differ only slightly the output that tells you where they differ quickly gets lost in the mass of output telling you where they don't.

Perhaps what's happened here is a sort of Primitive Obsession.
It's as though assert_equal assumes it will only ever be called with primitives.

So I put together a little bit of code to help out. The simplest solution I can think of uses json and diff, so I thought I'd call it assert_not_diff
require 'json'
require 'Tempfile'
def assert_not_diff(lhs,rhs)
 if lhs != rhs
 puts `diff -y #{file_for lhs} #{file_for rhs}`
 end
end
def file_for(obj)
 exp = Tempfile.new("bk", "/tmp").open
 exp.write(JSON.pretty_generate(obj))
 exp.close
 exp.path
end
expected = [1,99,2,3,{:a=>34,:b=>43}]
actual = [1,2,3,4,{:a=>324,:c=>555,:b=>43},5]
assert_not_diff expected, actual
This produces:
[ [
 1, 1,
 99, < 2, 2, 3, 3,> 4,
 { {
 "a": 34, | "a": 324,
> "c": 555,
 "b": 43 "b": 43
 } | },
> 5
] ]
Which I think is a lot more useful.
Hope this proves useful to someone!

Bare bones ruby unit testing

This morning I spent a happy hour exploring a little of ruby's Test::Unit::TestCase. I started with this:
require 'test/unit'
class MyTest < Test::Unit::TestCase def test_one_plus_one_equals_two assert_equal 2, 1+1.1 end end 
I wanted to see how little I needed to write my own, super-minimal implementation of Test::Unit::TestCase...
require 'test/unit'
class MyTest < MyTestCase def test_one_plus_one_equals_two assert_equal 2, 1+1.1 end end 
After 204 traffic lights in cyber-dojo I ended up with this...
require 'assertion_failed_error'
class MyTestCase
 def self.test_names
 public_instance_methods.select{|name| name =~ /^test_/}
 end
 
 def assert_equal( expected, actual )
 message = 
 "#{expected.inspect} expected but was\n" +
 "#{actual.inspect}\n"
 assert_block(message) { expected == actual }
 end
 def assert_block( message )
 if (! yield)
 raise AssertionFailedError.new(message.to_s)
 end
 end
end
at_exit do
 ::ObjectSpace.each_object(Class) do |klass|
 if (klass < MyTestCase) klass.test_names.each do |method_name| begin klass.new.send method_name rescue AssertionFailedError => error
 print "#{klass.name}:#{method_name}:\n" +
 "#{error.message}"
 end
 end
 end
 end
end
class AssertionFailedError < RuntimeError; end 
which allowed me write...
require 'my_test_case'
class MyTest < MyTestCase def test_one_plus_one_equals_two assert_equal 2, 1+1.1 end end 
and finally, I added this...
class MyTestCase
 ...
 def self.test( name, &block )
 define_method("test_#{name}".to_sym, &block)
 end
 ...
end
which allowed me to rewrite the test as...
require 'my_test_case'
class MyTest < MyTestCase test "1+1 == 2" do assert_equal 2, 1+1.1 end end 
Fun :-)

Micro refactoring - from here to there

One of my Mastery book snippets from a few days ago reads:

Masters ... are zealots of practice, connoisseurs of the small, incremental step.

I was doing the roman numerals kata in ruby and something related to small incremental steps occurred to me. Here's three tests:

def test_to_roman
 assert_equal "I", to_roman(1)
 assert_equal "II", to_roman(2)
 assert_equal "III", to_roman(3)
end

and here's the first code snippet:

def to_roman(n) 
 roman = ''
 if n == 3
 roman = 'III' 
 end
 if n == 2
 roman = 'II'
 end
 if n == 1
 roman = 'I'
 end
 roman
end

I can refactor these three if statements into a single while statement, in very small steps, as follows:

def to_roman(n) 
 roman = ''
 if n>= 3
 roman += 'I'
 n -= 1
 end
 if n>= 2
 roman += 'I'
 n -= 1
 end
 if n>= 1
 roman += 'I'
 n -= 1 
 end
 roman
end

and then:

def to_roman(n) 
 roman = ''
 if n>= 1
 roman += 'I'
 n -= 1
 end
 if n>= 1
 roman += 'I'
 n -= 1
 end
 if n>= 1
 roman += 'I'
 n -= 1 
 end
 roman
end

and then:

def to_roman(n) 
 roman = ''
 while n>= 1
 roman += 'I'
 n -= 1
 end
 roman
end

Ok. Now here's the original again:

def to_roman(n) 
 roman = ''
 if n == 3
 roman = 'III'
 end
 if n == 2
 roman = 'II'
 end
 if n == 1
 roman = 'I'
 end
 roman
end

And this time I refactor in a different direction, towards an array lookup:

def to_roman(n) 
 units = [ '', 'I', 'II', 'III' ]
 roman = ''
 if n == 3
 roman = units[3]
 end
 if n == 2
 roman = units[2]
 end
 if n == 1
 roman = units[1]
 end
 roman
end

And then:

def to_roman(n) 
 units = [ '', 'I', 'II', 'III' ]
 roman = ''
 if n == 3
 roman = units[n]
 end
 if n == 2
 roman = units[n]
 end
 if n == 1
 roman = units[n]
 end
 roman
end

And then:

def to_roman(n) 
 units = [ '', 'I', 'II', 'III' ]
 roman = ''
 if n == 3
 roman = units[n]
 end
 if n == 2
 roman = units[n]
 end
 if n == 1
 roman = units[n]
 end
 roman
 units[n]
end

And then:

def to_roman(n) 
 units = [ '', 'I', 'II', 'III' ]
 units[n]
end

In this case I can refactor from a sequence of if statements in two directions equally easy - towards a while or towards an array lookup. Ok. Now consider the situation if I was starting from a switch instead of a sequence of ifs:

def to_roman(n) 
 case n
 when 3 then 'III'
 when 2 then 'II'
 when 1 then 'I'
 end
end

From this I can easily refactor towards an array lookup but refactoring towards a while is perhaps not quite so straightforward.

I'm suggesting that a construct is useful not just in it's own right but also in relation to all the other constructs it might get refactored to or from. How easily can I move from one construct to another? Do some constructs live only on one-way roads? Do some lead you down more dead-ends than others?

I'm reminded of the family on holiday in the West Country who got thoroughly lost. Spotting a farmer leaning on a gate they stop the car, wind down the window and ask

Excuse me. Can you tell me how to get to Nempnett Thrubwell?

The old farmer looks at them and says

Well.... you don't want to start from here.


Yahtzee in ruby in cyber-dojo

I've been doing the Yahtzee kata in cyber-dojo to improve my Ruby. During the 247 increments I learned about...
  • any?
  • send
  • [lo...hi]
  • reduce
  • :+
  • module names start with a capital
It's also on pastie if you like a black background. You can use Cyber-Dojo to step through the diffs of every single increment!
module Yahtzee
 def self.score dice, category, n=nil
 return -1 if invalid(dice, category)
 if category == :value
 value(dice, n)
 else
 send(category, dice)
 end
 end
 def self.chance dice
 dice.reduce(:+)
 end
 def self.yahtzee dice
 dice.uniq.length == 1 ? 50 : 0
 end
 def self.value dice, n
 dice.count{|die| die == n } * n
 end
 def self.one_pair dice
 die(tally(dice).find{|n| pair?n }) * 2
 end
 def self.two_pair dice
 score_tally dice, [2,2]
 end
 def self.three_of_a_kind dice
 score_tally dice, [3]
 end
 def self.four_of_a_kind dice
 score_tally dice, [4]
 end
 def self.full_house dice
 score_tally dice, [3,2]
 end
 
 def self.small_straight dice
 dice.sort == [1,2,3,4,5] ? 15 : 0
 end
 def self.large_straight dice
 dice.sort == [2,3,4,5,6] ? 20 : 0
 end
 #- - - - - - - - - - - - - - - - - - - - - - - 
 def self.categories
 [:value, :chance, :yahtzee, :one_pair, :two_pair,
 :three_of_a_kind, :four_of_a_kind, :full_house,
 :small_straight, :large_straight
 ]
 end
 def self.invalid dice, category
 dice == nil \
 or dice.length != 5 \
 or dice.any?{|die| die < 1 or die> 6 } \
 or !categories.include? category
 end
 def self.score_tally dice, pattern
 starts?(pattern, tallies(dice)) \
 ? sum(dice, pattern.length) \
 : 0
 end
 def self.starts? short, long
 long[0...short.length] == short
 end
 def self.tallies dice
 tally(dice).map{|n| count(n) }
 end
 def self.rolls dice
 tally(dice).map{|n| die(n) }
 end
 def self.tally dice
 dice.uniq.map {|die| [dice.count(die),die] }.sort.reverse 
 end
 def self.sum dice, n
 sum2(rolls(dice)[0...n], dice)
 end
 def self.sum2 scorers, dice
 dice.select {|die| scorers.include?die }.reduce(:+)
 end
 def self.pair? n
 count(n) == 2
 end
 def self.count n
 n[0]
 end
 def self.die n
 n ? n[1] : 0
 end
end
I challenged myself to write the code in a functional style with no local variables - it's a bit 'dense' in places as a result.

cyber-dojo seems to work as planned!

I'm hoping to run at least one cyber-dojo as a birds-of-a-feather session at the upcoming accu conference. cyber-dojo allows you to perform code-katas using a web browser front end. To make sure it all works as planned I connected a client and a server together via a switch+router. It came together fairly painlessly! cyber-dojo is written in ruby and rails served by passenger and apache2 all running in an Ubuntu VirtualBox inside the MacBook on the right. The client on the left is Firefox on an MSI netbook also running Ubuntu.

Subscribe to: Comments (Atom)

AltStyle によって変換されたページ (->オリジナル) /