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
Back to rolling the die... an implementation of
The tests form a specification. If I want to specify a "tighter" implementation of
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...
def test_doesnt_roll_less_than_1_or_greater_than_6 1200.times do die = roll_die assert 1 <= die && die <= 6 end endThat'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 endWhy 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 endI'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] endThis 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:
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 endYou 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):
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:
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:
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 endCode 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:
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
Hope this proves useful to someone!
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 endI 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 endAfter 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; endwhich allowed me write...
require 'my_test_case' class MyTest < MyTestCase def test_one_plus_one_equals_two assert_equal 2, 1+1.1 end endand 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 endFun :-)
Micro refactoring - from here to there
One of my Mastery book snippets from a few days ago reads:
I was doing the roman numerals kata in ruby and something related to small incremental steps occurred to me. Here's three tests:
and here's the first code snippet:
I can refactor these three if statements into a single while statement, in very small steps, as follows:
and then:
and then:
Ok. Now here's the original again:
And this time I refactor in a different direction, towards an array lookup:
And then:
And then:
And then:
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:
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
The old farmer looks at them and says
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
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)