liars_dice 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +4 -0
- data/lib/liars_dice/bid.rb +42 -0
- data/lib/liars_dice/command_line_watcher.rb +13 -0
- data/lib/liars_dice/engine.rb +194 -0
- data/lib/liars_dice/event.rb +57 -0
- data/lib/liars_dice/human_bot.rb +45 -0
- data/lib/liars_dice/random_bot.rb +42 -0
- data/lib/liars_dice/seat.rb +25 -0
- data/lib/liars_dice/watcher.rb +62 -0
- data/lib/liars_dice.rb +8 -0
- data/spec/bid_spec.rb +28 -0
- data/spec/engine_spec.rb +577 -0
- data/spec/seat_spec.rb +48 -0
- data/spec/spec_helper.rb +11 -0
- metadata +65 -0
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module LiarsDice
|
2
|
+
class Bid
|
3
|
+
attr_accessor :total, :face_value
|
4
|
+
|
5
|
+
def initialize(total, face_value)
|
6
|
+
self.total = total
|
7
|
+
self.face_value = face_value
|
8
|
+
end
|
9
|
+
|
10
|
+
def bs_called?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
"#{total} #{face_value}#{"s" if total > 1}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def valid?
|
19
|
+
if face_value < 1 || face_value > 6
|
20
|
+
# Can't bid a face_value that doesn't exist
|
21
|
+
return false
|
22
|
+
elsif total < 1
|
23
|
+
# Have to bid a positive total
|
24
|
+
return false
|
25
|
+
end
|
26
|
+
true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class BS < Bid
|
31
|
+
def initialize
|
32
|
+
end
|
33
|
+
|
34
|
+
def bs_called?
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
"BS"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module LiarsDice
|
2
|
+
class CommandLineWatcher
|
3
|
+
include Watcher
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
append_after_dice_rolled lambda{|dice| puts "Dice Rolled: #{dice.inspect.to_s}" }
|
7
|
+
append_after_bid lambda{|seat, bid| puts "Seat #{seat} bids #{bid}" }
|
8
|
+
append_after_bs lambda{|seat| puts "Seat #{seat} calls BS" }
|
9
|
+
append_after_game lambda{|winner| puts "Game over. Seat #{winner} wins." }
|
10
|
+
append_after_round lambda{|loser| puts "Seat #{loser} loses a die" }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
module LiarsDice
|
2
|
+
class Engine
|
3
|
+
include Watcher
|
4
|
+
attr_accessor :seats, :starting_seat, :bids, :watcher
|
5
|
+
attr_reader :loser, :seat_index
|
6
|
+
|
7
|
+
def initialize(player_classes, dice_per_player, watcher=nil)
|
8
|
+
self.seats = []
|
9
|
+
player_classes.shuffle.each_with_index do |klass, i|
|
10
|
+
player = klass.new(i, player_classes.count, dice_per_player)
|
11
|
+
self.seats << Seat.new(i, player, dice_per_player)
|
12
|
+
end
|
13
|
+
self.seat_index = 0
|
14
|
+
self.watcher = watcher || self
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
until winner?
|
19
|
+
roll_dice
|
20
|
+
run_round
|
21
|
+
end
|
22
|
+
notify_winner
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_bid(seat)
|
26
|
+
bid = seat.player.bid
|
27
|
+
unless valid_bid?(bid)
|
28
|
+
raise StandardError.new("Invalid Bid")
|
29
|
+
end
|
30
|
+
bid
|
31
|
+
end
|
32
|
+
|
33
|
+
def next_seat
|
34
|
+
# If no seats are alive, we'd loop forever
|
35
|
+
return nil if alive_seats.empty?
|
36
|
+
|
37
|
+
seat = seats[seat_index]
|
38
|
+
self.seat_index += 1
|
39
|
+
|
40
|
+
# If the seat at seat_index is alive, return it
|
41
|
+
# Otherwise, we've already updated seat_index (and wrapped it, if necessary)
|
42
|
+
# so just call next_seat again
|
43
|
+
seat.alive? ? seat : next_seat
|
44
|
+
end
|
45
|
+
|
46
|
+
def total_dice_with_face_value(face_value)
|
47
|
+
seats.map{|seat| seat.dice.count(face_value) }.reduce(0, :+)
|
48
|
+
end
|
49
|
+
|
50
|
+
def bid_is_correct?(bid, use_wilds)
|
51
|
+
total = total_dice_with_face_value(bid.face_value)
|
52
|
+
total += total_dice_with_face_value(1) if use_wilds
|
53
|
+
total >= bid.total
|
54
|
+
end
|
55
|
+
|
56
|
+
def run_round
|
57
|
+
self.bids = []
|
58
|
+
|
59
|
+
previous_seat = nil
|
60
|
+
aces_wild = true
|
61
|
+
while true
|
62
|
+
seat = next_seat
|
63
|
+
bid = get_bid(seat)
|
64
|
+
aces_wild = false if bid.face_value == 1
|
65
|
+
|
66
|
+
if bid.bs_called?
|
67
|
+
notify_bs(seat)
|
68
|
+
self.loser = bid_is_correct?(previous_bid, aces_wild) ? seat : previous_seat
|
69
|
+
notify_loser(loser)
|
70
|
+
loser.lose_die
|
71
|
+
break
|
72
|
+
end
|
73
|
+
|
74
|
+
self.bids << bid
|
75
|
+
notify_bid(seat, bid)
|
76
|
+
previous_seat = seat
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def roll_dice
|
81
|
+
die = (1..6).to_a
|
82
|
+
alive_seats.each do |seat|
|
83
|
+
dice = []
|
84
|
+
seat.dice_left.times do
|
85
|
+
dice << die.sample
|
86
|
+
end
|
87
|
+
|
88
|
+
seat.dice = dice
|
89
|
+
end
|
90
|
+
notify_roll
|
91
|
+
end
|
92
|
+
|
93
|
+
# ===========================================
|
94
|
+
# ==== Notification Events ====
|
95
|
+
# ===========================================
|
96
|
+
def notify_bid(seat, bid)
|
97
|
+
event = BidMadeEvent.new(seat.number, bid)
|
98
|
+
notify_players(event)
|
99
|
+
notify_watcher(event)
|
100
|
+
end
|
101
|
+
|
102
|
+
def notify_bs(seat)
|
103
|
+
event = BSCalledEvent.new(seat.number, previous_bid)
|
104
|
+
notify_players(event)
|
105
|
+
notify_watcher(event)
|
106
|
+
end
|
107
|
+
|
108
|
+
def notify_loser(seat)
|
109
|
+
dice = seats.map(&:dice)
|
110
|
+
event = LoserEvent.new(seat.number, dice)
|
111
|
+
notify_players(event)
|
112
|
+
notify_watcher(event)
|
113
|
+
end
|
114
|
+
|
115
|
+
def notify_winner
|
116
|
+
event = WinnerEvent.new(winner.number)
|
117
|
+
notify_players(event)
|
118
|
+
notify_watcher(event)
|
119
|
+
end
|
120
|
+
|
121
|
+
def notify_roll
|
122
|
+
dice = seats.map(&:dice)
|
123
|
+
event = DiceRolledEvent.new(dice)
|
124
|
+
notify_watcher(event)
|
125
|
+
end
|
126
|
+
|
127
|
+
def notify_players(event)
|
128
|
+
seats.each{|s| s.player.handle_event(event) }
|
129
|
+
end
|
130
|
+
|
131
|
+
def notify_watcher(event)
|
132
|
+
watcher.handle_event(event)
|
133
|
+
end
|
134
|
+
|
135
|
+
# ===========================================
|
136
|
+
# ==== Validation Methods ====
|
137
|
+
# ===========================================
|
138
|
+
def valid_bs?(bid)
|
139
|
+
# Cannot bid BS if there isn't a previous bid
|
140
|
+
!!previous_bid
|
141
|
+
end
|
142
|
+
|
143
|
+
def valid_bid?(bid)
|
144
|
+
return false unless bid
|
145
|
+
|
146
|
+
if bid.bs_called?
|
147
|
+
return valid_bs?(bid)
|
148
|
+
end
|
149
|
+
|
150
|
+
if bid.face_value < 1 || bid.face_value > 6
|
151
|
+
# Can't bid a face_value that doesn't exist
|
152
|
+
return false
|
153
|
+
elsif bid.total < 1
|
154
|
+
# Have to bid a positive total
|
155
|
+
return false
|
156
|
+
elsif previous_bid && bid.total < previous_bid.total
|
157
|
+
# The total must be monotonically increasing
|
158
|
+
return false
|
159
|
+
elsif previous_bid && bid.total == previous_bid.total && bid.face_value <= previous_bid.face_value
|
160
|
+
# If the total does not increase, the face_value must
|
161
|
+
return false
|
162
|
+
end
|
163
|
+
|
164
|
+
true
|
165
|
+
end
|
166
|
+
|
167
|
+
def previous_bid
|
168
|
+
bids[-1]
|
169
|
+
end
|
170
|
+
|
171
|
+
def alive_seats
|
172
|
+
seats.select(&:alive?)
|
173
|
+
end
|
174
|
+
|
175
|
+
def winner?
|
176
|
+
alive_seats.count == 1
|
177
|
+
end
|
178
|
+
|
179
|
+
def winner
|
180
|
+
return nil unless winner?
|
181
|
+
alive_seats.first
|
182
|
+
end
|
183
|
+
|
184
|
+
def loser=(seat)
|
185
|
+
@loser = seat
|
186
|
+
self.seat_index = seat.number + 1
|
187
|
+
end
|
188
|
+
|
189
|
+
def seat_index=(index)
|
190
|
+
@seat_index = index
|
191
|
+
@seat_index = 0 if @seat_index == seats.count
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module LiarsDice
|
2
|
+
class Event
|
3
|
+
attr_accessor :message
|
4
|
+
|
5
|
+
def initialize(message)
|
6
|
+
self.message = message
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class BidMadeEvent < Event
|
11
|
+
attr_accessor :seat_number, :bid
|
12
|
+
|
13
|
+
def initialize(seat_number, bid)
|
14
|
+
self.seat_number = seat_number
|
15
|
+
self.bid = bid
|
16
|
+
super("Seat #{seat_number} bid #{bid.to_s}")
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class BSCalledEvent < Event
|
21
|
+
attr_accessor :seat_number, :previous_bid
|
22
|
+
|
23
|
+
def initialize(seat_number, previous_bid)
|
24
|
+
self.seat_number = seat_number
|
25
|
+
self.previous_bid = previous_bid
|
26
|
+
super("Seat #{seat_number} called BS")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class DiceRolledEvent < Event
|
31
|
+
attr_accessor :dice
|
32
|
+
|
33
|
+
def initialize(dice)
|
34
|
+
self.dice = dice
|
35
|
+
super("Dice rolled")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class LoserEvent < Event
|
40
|
+
attr_accessor :seat_number, :dice
|
41
|
+
|
42
|
+
def initialize(seat_number, dice)
|
43
|
+
self.seat_number = seat_number
|
44
|
+
self.dice = dice
|
45
|
+
super("Seat #{seat_number} lost a die")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class WinnerEvent < Event
|
50
|
+
attr_accessor :seat_number
|
51
|
+
|
52
|
+
def initialize(seat_number)
|
53
|
+
self.seat_number = seat_number
|
54
|
+
super("Game is over. Seat #{seat_number} is the winner.")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module LiarsDice
|
2
|
+
class HumanBot
|
3
|
+
attr_accessor :prev_bid, :dice
|
4
|
+
|
5
|
+
def initialize(seat_number, number_of_players, number_of_dice)
|
6
|
+
puts "You're playing as HumanBot in seat #{seat_number}"
|
7
|
+
puts "When asked for a bid, either enter <TOTAL> <FACE_VALUE> or BS"
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
def handle_event(event)
|
12
|
+
if event.is_a? LiarsDice::BidMadeEvent
|
13
|
+
self.prev_bid = event.bid
|
14
|
+
elsif event.is_a? LiarsDice::LoserEvent
|
15
|
+
self.prev_bid = nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def dice=(dice)
|
20
|
+
@dice = dice
|
21
|
+
puts "Your dice are #{dice.inspect.to_s}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def bid
|
25
|
+
if prev_bid
|
26
|
+
puts "Previous bid was #{prev_bid}"
|
27
|
+
else
|
28
|
+
puts "You're bidding first"
|
29
|
+
end
|
30
|
+
|
31
|
+
print "What do you bid? "
|
32
|
+
bid_string = gets.chomp
|
33
|
+
if bid_string.downcase == "bs"
|
34
|
+
return BS.new
|
35
|
+
end
|
36
|
+
parts = bid_string.split(" ").map(&:to_i)
|
37
|
+
if parts.length == 2
|
38
|
+
ret = Bid.new(*parts)
|
39
|
+
return ret if ret.valid?
|
40
|
+
end
|
41
|
+
print "Invalid bid"
|
42
|
+
return bid
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module LiarsDice
|
2
|
+
class RandomBot
|
3
|
+
attr_accessor :prev_bid
|
4
|
+
|
5
|
+
def initialize(seat_number, number_of_players, number_of_dice); end
|
6
|
+
|
7
|
+
|
8
|
+
def handle_event(event)
|
9
|
+
if event.is_a? LiarsDice::BidMadeEvent
|
10
|
+
self.prev_bid = event.bid
|
11
|
+
elsif event.is_a? LiarsDice::LoserEvent
|
12
|
+
self.prev_bid = nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def dice=(dice); end
|
17
|
+
|
18
|
+
def bid
|
19
|
+
if prev_bid
|
20
|
+
choice = (0...10).to_a.sample
|
21
|
+
|
22
|
+
if choice == 0
|
23
|
+
# Call BS 10% of the time
|
24
|
+
LiarsDice::BS.new
|
25
|
+
elsif choice <= 6
|
26
|
+
# Up the total 50% of the time
|
27
|
+
LiarsDice::Bid.new(prev_bid.total + 1, prev_bid.face_value)
|
28
|
+
else
|
29
|
+
# Up the number 40% of the time
|
30
|
+
r = LiarsDice::Bid.new(prev_bid.total, prev_bid.face_value + 1)
|
31
|
+
if r.face_value > 6
|
32
|
+
r.total += 1
|
33
|
+
r.face_value = 2
|
34
|
+
end
|
35
|
+
r
|
36
|
+
end
|
37
|
+
else
|
38
|
+
LiarsDice::Bid.new(1, 2)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module LiarsDice
|
2
|
+
class Seat
|
3
|
+
attr_accessor :number, :player
|
4
|
+
attr_reader :dice_left, :dice
|
5
|
+
|
6
|
+
def initialize(number, player, starting_dice)
|
7
|
+
self.number = number
|
8
|
+
@dice_left = starting_dice
|
9
|
+
self.player = player
|
10
|
+
end
|
11
|
+
|
12
|
+
def alive?
|
13
|
+
dice_left > 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def dice=(value)
|
17
|
+
@dice = value
|
18
|
+
player.dice = value
|
19
|
+
end
|
20
|
+
|
21
|
+
def lose_die
|
22
|
+
@dice_left -= 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module LiarsDice
|
2
|
+
module Watcher
|
3
|
+
attr_reader :after_roll, :after_bid, :after_round, :after_bs, :after_game, :after_dice_rolled
|
4
|
+
|
5
|
+
def append_after_bid(callback)
|
6
|
+
append_callback(:after_bid, callback)
|
7
|
+
end
|
8
|
+
|
9
|
+
def append_after_round(callback)
|
10
|
+
append_callback(:after_round, callback)
|
11
|
+
end
|
12
|
+
|
13
|
+
def append_after_bs(callback)
|
14
|
+
append_callback(:after_bs, callback)
|
15
|
+
end
|
16
|
+
|
17
|
+
def append_after_game(callback)
|
18
|
+
append_callback(:after_game, callback)
|
19
|
+
end
|
20
|
+
|
21
|
+
def append_after_dice_rolled(callback)
|
22
|
+
append_callback(:after_dice_rolled, callback)
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle_event(event)
|
26
|
+
if event.is_a? BidMadeEvent
|
27
|
+
fire(:after_bid, event.seat_number, event.bid)
|
28
|
+
elsif event.is_a? BSCalledEvent
|
29
|
+
fire(:after_bs, event.seat_number)
|
30
|
+
elsif event.is_a? LoserEvent
|
31
|
+
fire(:after_round, event.seat_number)
|
32
|
+
elsif event.is_a? WinnerEvent
|
33
|
+
fire(:after_game, event.seat_number)
|
34
|
+
elsif event.is_a? DiceRolledEvent
|
35
|
+
fire(:after_dice_rolled, event.dice)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def allowed_callbacks
|
41
|
+
[:after_bid, :after_bs, :after_dice_rolled, :after_game, :after_round]
|
42
|
+
end
|
43
|
+
|
44
|
+
def append_callback(callback_name, callback)
|
45
|
+
raise ArgumentError.new("Callback does not respond to call") unless callback.respond_to? :call
|
46
|
+
raise ArgumentError.new("Unsupported callback #{callback_name}") unless allowed_callbacks.include? callback_name
|
47
|
+
watcher_callbacks[callback_name] << callback
|
48
|
+
end
|
49
|
+
|
50
|
+
def fire(callback_name, *args)
|
51
|
+
raise ArgumentError.new("Unsupported callback #{callback_name}") unless allowed_callbacks.include? callback_name
|
52
|
+
watcher_callbacks[callback_name].each do |callback|
|
53
|
+
callback.call(*args)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def watcher_callbacks
|
58
|
+
# Create a hash where any missing key returns an empty array
|
59
|
+
@watcher_callbacks ||= Hash.new {|hash, key| hash[key] = [] }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/liars_dice.rb
ADDED
data/spec/bid_spec.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Bid" do
|
4
|
+
describe "#valid?" do
|
5
|
+
it "returns false if total is negative" do
|
6
|
+
bid = Bid.new(-1, 5)
|
7
|
+
bid.should_not be_valid
|
8
|
+
end
|
9
|
+
|
10
|
+
it "returns false if total is negative" do
|
11
|
+
bid = Bid.new(0, 5)
|
12
|
+
bid.should_not be_valid
|
13
|
+
end
|
14
|
+
|
15
|
+
it "returns false for invalid die face_values" do
|
16
|
+
bid = Bid.new(5, 0)
|
17
|
+
bid.should_not be_valid
|
18
|
+
|
19
|
+
bid = Bid.new(5, 10)
|
20
|
+
bid.should_not be_valid
|
21
|
+
end
|
22
|
+
|
23
|
+
it "returns true for valid bids" do
|
24
|
+
bid = Bid.new(5, 3)
|
25
|
+
bid.should be_valid
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/spec/engine_spec.rb
ADDED
@@ -0,0 +1,577 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Engine" do
|
4
|
+
let(:engine) { Engine.new([], 5) }
|
5
|
+
let(:seat) { Seat.new(0, nil, 5) }
|
6
|
+
let(:bid) { Bid.new(1, 1) }
|
7
|
+
|
8
|
+
describe "run_round" do
|
9
|
+
let(:bid_one) { Bid.new(1, 1) }
|
10
|
+
let(:bid_two) { Bid.new(1, 2) }
|
11
|
+
let(:bs) { BS.new }
|
12
|
+
|
13
|
+
before do
|
14
|
+
engine.stub(:next_seat).and_return(seat)
|
15
|
+
engine.stub(:get_bid).and_return(bid_one, bid_two, bs)
|
16
|
+
engine.stub(:bid_is_correct).and_return(true)
|
17
|
+
engine.stub(:notify_loser)
|
18
|
+
seat.stub(:lose_die)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "properly sets #previous_bid" do
|
22
|
+
engine.run_round
|
23
|
+
engine.previous_bid.should == bid_two
|
24
|
+
end
|
25
|
+
|
26
|
+
context "tracking whether aces are wild" do
|
27
|
+
it "doesn't count aces wild after aces are bet" do
|
28
|
+
engine.should_receive(:bid_is_correct?).with(bid_two, false).and_return(true)
|
29
|
+
engine.run_round
|
30
|
+
end
|
31
|
+
|
32
|
+
it "counts aces wild if aces aren't bet" do
|
33
|
+
engine.stub(:get_bid).and_return(bid_two, bs)
|
34
|
+
engine.should_receive(:bid_is_correct?).with(bid_two, true).and_return(true)
|
35
|
+
engine.run_round
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "bid notification" do
|
40
|
+
it "notifies of each bid" do
|
41
|
+
engine.should_receive(:notify_bid).twice
|
42
|
+
engine.run_round
|
43
|
+
end
|
44
|
+
|
45
|
+
it "notifies of the seat and the bid" do
|
46
|
+
engine.stub(:get_bid).and_return(bid_two, bs)
|
47
|
+
engine.should_receive(:notify_bid).with(seat, bid_two)
|
48
|
+
engine.run_round
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it "notifies of a bs" do
|
53
|
+
engine.should_receive(:notify_bs).with(seat)
|
54
|
+
engine.run_round
|
55
|
+
end
|
56
|
+
|
57
|
+
it "notifies of a loser" do
|
58
|
+
engine.should_receive(:notify_loser).with(seat)
|
59
|
+
engine.run_round
|
60
|
+
end
|
61
|
+
|
62
|
+
context "picking a loser" do
|
63
|
+
let (:seat1) { Seat.new(1, nil, 5) }
|
64
|
+
let (:seat2) { Seat.new(2, nil, 5) }
|
65
|
+
|
66
|
+
before do
|
67
|
+
engine.stub(:next_seat).and_return(seat1, seat2)
|
68
|
+
engine.stub(:get_bid).and_return(bid_two, bs)
|
69
|
+
engine.stub_chain(:loser, :lose_die)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "picks the bidder if the bid was incorrect" do
|
73
|
+
engine.stub(:bid_is_correct?).and_return(false)
|
74
|
+
engine.should_receive(:loser=).with(seat1)
|
75
|
+
engine.run_round
|
76
|
+
end
|
77
|
+
|
78
|
+
it "picks the caller if the bid was correct" do
|
79
|
+
engine.stub(:bid_is_correct?).and_return(true)
|
80
|
+
engine.should_receive(:loser=).with(seat2)
|
81
|
+
engine.run_round
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it "removes a dice from the loser" do
|
86
|
+
seat1 = Seat.new(1, nil, 5)
|
87
|
+
seat2 = Seat.new(2, nil, 5)
|
88
|
+
|
89
|
+
engine.stub(:next_seat).and_return(seat1, seat2)
|
90
|
+
engine.stub(:get_bid).and_return(bid_two, bs)
|
91
|
+
engine.stub(:bid_is_correct?).and_return(false)
|
92
|
+
seat1.should_receive(:lose_die)
|
93
|
+
engine.run_round
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "run" do
|
98
|
+
before do
|
99
|
+
engine.stub(:roll_dice)
|
100
|
+
engine.stub(:run_round)
|
101
|
+
engine.stub(:notify_winner)
|
102
|
+
engine.stub(:winner?).and_return(false, true)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "goes until there's a winner" do
|
106
|
+
engine.should_receive(:winner?).exactly(4).times.and_return(false, false, false, true)
|
107
|
+
engine.run
|
108
|
+
end
|
109
|
+
|
110
|
+
it "rolls the dice" do
|
111
|
+
engine.should_receive(:roll_dice)
|
112
|
+
engine.run
|
113
|
+
end
|
114
|
+
|
115
|
+
it "runs a round" do
|
116
|
+
engine.should_receive(:run_round)
|
117
|
+
engine.run
|
118
|
+
end
|
119
|
+
|
120
|
+
it "notifies of a winner" do
|
121
|
+
engine.should_receive(:notify_winner)
|
122
|
+
engine.run
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
it "correctly advances seats after a round" do
|
127
|
+
bs = BS.new
|
128
|
+
seat0 = Seat.new(0, nil, 5)
|
129
|
+
seat1 = Seat.new(1, nil, 5)
|
130
|
+
seat2 = Seat.new(2, nil, 5)
|
131
|
+
seat3 = Seat.new(3, nil, 5)
|
132
|
+
seat4 = Seat.new(4, nil, 5)
|
133
|
+
seats = [seat0, seat1, seat2, seat3, seat4]
|
134
|
+
|
135
|
+
engine.stub(:winner?).and_return(false, false, false, true)
|
136
|
+
engine.stub(:roll_dice)
|
137
|
+
engine.stub(:notify_bid)
|
138
|
+
engine.stub(:notify_bs)
|
139
|
+
engine.stub(:notify_loser)
|
140
|
+
engine.stub(:notify_winner)
|
141
|
+
engine.stub(:bid_is_correct?).and_return(true, false, true)
|
142
|
+
|
143
|
+
engine.seat_index = 0
|
144
|
+
engine.stub(:seats).and_return(seats)
|
145
|
+
seats.each{|s| s.stub(:lose_die) }
|
146
|
+
|
147
|
+
# We're set up to run 3 rounds:
|
148
|
+
# R1: Seat0 bids, Seat1 calls, Seat1 loses
|
149
|
+
# R2: Seat2 bids, Seat3 calls, Seat2 loses
|
150
|
+
# R3: Seat3 bids, Seat4 calls, Seat4 loses
|
151
|
+
engine.should_receive(:get_bid).with(seat0).ordered.and_return(bid)
|
152
|
+
engine.should_receive(:get_bid).with(seat1).ordered.and_return(bs)
|
153
|
+
engine.should_receive(:get_bid).with(seat2).ordered.and_return(bid)
|
154
|
+
engine.should_receive(:get_bid).with(seat3).ordered.and_return(bs)
|
155
|
+
engine.should_receive(:get_bid).with(seat3).ordered.and_return(bid)
|
156
|
+
engine.should_receive(:get_bid).with(seat4).ordered.and_return(bs)
|
157
|
+
|
158
|
+
engine.run
|
159
|
+
end
|
160
|
+
|
161
|
+
describe "#get_bid" do
|
162
|
+
before do
|
163
|
+
seat.stub_chain(:player, :bid).and_return("bid")
|
164
|
+
end
|
165
|
+
|
166
|
+
it "gets a bid from the seat's user" do
|
167
|
+
engine.stub(:valid_bid?).and_return(true)
|
168
|
+
engine.get_bid(seat).should == "bid"
|
169
|
+
end
|
170
|
+
|
171
|
+
it "raises an error if given an invalid bid" do
|
172
|
+
engine.stub(:valid_bid?).and_return(false)
|
173
|
+
expect { engine.get_bid(seat) }.to raise_error
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "winner?" do
|
178
|
+
let(:seat0) { OpenStruct.new(:alive? => true) }
|
179
|
+
let(:seat1) { OpenStruct.new(:alive? => true) }
|
180
|
+
let(:seat2) { OpenStruct.new(:alive? => false) }
|
181
|
+
let(:seat3) { OpenStruct.new(:alive? => false) }
|
182
|
+
|
183
|
+
it "returns true if there's one alive seat left" do
|
184
|
+
engine.stub(:seats).and_return([seat0, seat2, seat3])
|
185
|
+
engine.winner?.should be_true
|
186
|
+
end
|
187
|
+
|
188
|
+
it "returns false if there's more than one alive seat left" do
|
189
|
+
engine.stub(:seats).and_return([seat0, seat1, seat2, seat3])
|
190
|
+
engine.winner?.should be_false
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
describe "winner" do
|
195
|
+
let(:seat0) { OpenStruct.new(:alive? => true) }
|
196
|
+
let(:seat1) { OpenStruct.new(:alive? => true) }
|
197
|
+
let(:seat2) { OpenStruct.new(:alive? => false) }
|
198
|
+
let(:seat3) { OpenStruct.new(:alive? => false) }
|
199
|
+
|
200
|
+
it "returns nil if there isn't a winner" do
|
201
|
+
engine.stub(:seats).and_return([seat0, seat1, seat2, seat3])
|
202
|
+
engine.winner.should == nil
|
203
|
+
end
|
204
|
+
|
205
|
+
it "returns the sole alive seat" do
|
206
|
+
engine.stub(:seats).and_return([seat0, seat2, seat3])
|
207
|
+
engine.winner.should == seat0
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
describe "#total_dice_with_face_value" do
|
212
|
+
let(:seat0) { OpenStruct.new(dice: [2, 2, 3]) }
|
213
|
+
let(:seat1) { OpenStruct.new(dice: [4, 2, 3]) }
|
214
|
+
let(:seat2) { OpenStruct.new(dice: [2, 4, 5]) }
|
215
|
+
let(:seat3) { OpenStruct.new(dice: []) }
|
216
|
+
|
217
|
+
before do
|
218
|
+
engine.stub(:seats).and_return([seat0, seat1, seat2, seat3])
|
219
|
+
end
|
220
|
+
|
221
|
+
it "returns the correct counts" do
|
222
|
+
engine.total_dice_with_face_value(1).should == 0
|
223
|
+
engine.total_dice_with_face_value(2).should == 4
|
224
|
+
engine.total_dice_with_face_value(3).should == 2
|
225
|
+
engine.total_dice_with_face_value(4).should == 2
|
226
|
+
engine.total_dice_with_face_value(5).should == 1
|
227
|
+
engine.total_dice_with_face_value(6).should == 0
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
describe "#bid_is_correct?" do
|
232
|
+
let(:bid) { Bid.new(3, 3) }
|
233
|
+
|
234
|
+
before do
|
235
|
+
engine.stub(:total_dice_with_face_value).with(1).and_return(1)
|
236
|
+
end
|
237
|
+
|
238
|
+
context "with wilds" do
|
239
|
+
it "returns false when none of the face_values were rolled" do
|
240
|
+
engine.stub(:total_dice_with_face_value).with(3).and_return(0)
|
241
|
+
engine.bid_is_correct?(bid, true).should == false
|
242
|
+
end
|
243
|
+
|
244
|
+
it "returns false when less than total of the face_values were rolled" do
|
245
|
+
engine.stub(:total_dice_with_face_value).with(3).and_return(1)
|
246
|
+
engine.bid_is_correct?(bid, true).should == false
|
247
|
+
end
|
248
|
+
|
249
|
+
it "returns true when exactly total of the face_values were rolled" do
|
250
|
+
engine.stub(:total_dice_with_face_value).with(3).and_return(2)
|
251
|
+
engine.bid_is_correct?(bid, true).should == true
|
252
|
+
end
|
253
|
+
|
254
|
+
it "returns true when more than total of the face_values were rolled" do
|
255
|
+
engine.stub(:total_dice_with_face_value).with(3).and_return(6)
|
256
|
+
engine.bid_is_correct?(bid, true).should == true
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
context "without wilds" do
|
261
|
+
it "returns false when none of the face_values were rolled" do
|
262
|
+
engine.stub(:total_dice_with_face_value).with(3).and_return(0)
|
263
|
+
engine.bid_is_correct?(bid, false).should == false
|
264
|
+
end
|
265
|
+
|
266
|
+
it "returns false when less than total of the face_values were rolled" do
|
267
|
+
engine.stub(:total_dice_with_face_value).with(3).and_return(1)
|
268
|
+
engine.bid_is_correct?(bid, false).should == false
|
269
|
+
end
|
270
|
+
|
271
|
+
it "returns true when exactly total of the face_values were rolled" do
|
272
|
+
engine.stub(:total_dice_with_face_value).with(3).and_return(3)
|
273
|
+
engine.bid_is_correct?(bid, false).should == true
|
274
|
+
end
|
275
|
+
|
276
|
+
it "returns true when more than total of the face_values were rolled" do
|
277
|
+
engine.stub(:total_dice_with_face_value).with(3).and_return(6)
|
278
|
+
engine.bid_is_correct?(bid, false).should == true
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
describe "#next_seat" do
|
284
|
+
let(:seat1) { Seat.new(0, nil, 1) }
|
285
|
+
let(:seat2) { Seat.new(0, nil, 1) }
|
286
|
+
let(:seat3) { Seat.new(0, nil, 1) }
|
287
|
+
|
288
|
+
it "only returns seats that are alive" do
|
289
|
+
seat1.stub(:alive?).and_return(true)
|
290
|
+
seat2.stub(:alive?).and_return(false)
|
291
|
+
seat3.stub(:alive?).and_return(true)
|
292
|
+
|
293
|
+
engine.stub(:seats).and_return([seat1, seat2, seat3])
|
294
|
+
returned_seats = []
|
295
|
+
3.times { returned_seats << engine.next_seat }
|
296
|
+
returned_seats.should include(seat1)
|
297
|
+
returned_seats.should_not include(seat2)
|
298
|
+
returned_seats.should include(seat3)
|
299
|
+
end
|
300
|
+
|
301
|
+
it "wraps around if necessary" do
|
302
|
+
seat1.stub(:alive?).and_return(true)
|
303
|
+
seat2.stub(:alive?).and_return(false)
|
304
|
+
seat3.stub(:alive?).and_return(true)
|
305
|
+
|
306
|
+
engine.stub(:seats).and_return([seat1, seat2, seat3])
|
307
|
+
returned_seats = []
|
308
|
+
3.times { returned_seats << engine.next_seat }
|
309
|
+
returned_seats.should == [seat1, seat3, seat1]
|
310
|
+
end
|
311
|
+
|
312
|
+
it "returns seats in order" do
|
313
|
+
seat1.stub(:alive?).and_return(true)
|
314
|
+
seat2.stub(:alive?).and_return(true)
|
315
|
+
seat3.stub(:alive?).and_return(true)
|
316
|
+
|
317
|
+
engine.stub(:seats).and_return([seat1, seat2, seat3])
|
318
|
+
returned_seats = []
|
319
|
+
3.times { returned_seats << engine.next_seat }
|
320
|
+
returned_seats.should == [seat1, seat2, seat3]
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
describe "#roll_dice" do
|
325
|
+
context "randomness" do
|
326
|
+
before do
|
327
|
+
# Roll the dice a bunch of times and capture the result in a hash
|
328
|
+
# keyed by die face_value
|
329
|
+
engine.stub(:alive_seats).and_return([seat])
|
330
|
+
seat.stub(:dice_left).and_return(6000)
|
331
|
+
rolled_dice = []
|
332
|
+
seat.stub(:dice=) { |val| rolled_dice = val }
|
333
|
+
engine.roll_dice
|
334
|
+
@histogram = Hash.new(0)
|
335
|
+
rolled_dice.each{|die| @histogram[die] += 1 }
|
336
|
+
end
|
337
|
+
|
338
|
+
it "chooses valid face_values" do
|
339
|
+
@histogram.keys.sort.should == [1,2,3,4,5,6]
|
340
|
+
end
|
341
|
+
|
342
|
+
it "randomly distributes face_values" do
|
343
|
+
6.times do |i|
|
344
|
+
@histogram[i+1].should > 800
|
345
|
+
@histogram[i+1].should < 1200
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
it "only rolls dice for seats that are alive" do
|
351
|
+
s1 = Seat.new(0, {}, 5)
|
352
|
+
s1.stub(:alive?).and_return(false)
|
353
|
+
s2 = Seat.new(1, {}, 5)
|
354
|
+
s2.stub(:alive?).and_return(true)
|
355
|
+
engine.stub(:seats).and_return([s1, s2])
|
356
|
+
s1.should_not_receive(:dice=)
|
357
|
+
s2.should_receive(:dice=)
|
358
|
+
engine.roll_dice
|
359
|
+
end
|
360
|
+
|
361
|
+
it "rolls the correct number of dice for each seat" do
|
362
|
+
s1 = Seat.new(0, {}, 5)
|
363
|
+
s1.stub(:dice_left).and_return(3)
|
364
|
+
s1.stub(:dice=) { |val| val.count.should == 3 }
|
365
|
+
s2 = Seat.new(1, {}, 5)
|
366
|
+
s2.stub(:dice_left).and_return(5)
|
367
|
+
s2.stub(:dice=) { |val| val.count.should == 5 }
|
368
|
+
|
369
|
+
engine.stub(:alive_seats).and_return([s1, s2])
|
370
|
+
engine.roll_dice
|
371
|
+
end
|
372
|
+
|
373
|
+
it "notifies after rolling" do
|
374
|
+
engine.should_receive(:notify_roll)
|
375
|
+
engine.stub(:alive_seats).and_return([])
|
376
|
+
engine.roll_dice
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
describe "#valid_bid?" do
|
381
|
+
before do
|
382
|
+
engine.stub(:previous_bid).and_return(nil)
|
383
|
+
end
|
384
|
+
|
385
|
+
context "when given a BS object" do
|
386
|
+
it "calls valid_bs?" do
|
387
|
+
engine.should_receive(:valid_bs?)
|
388
|
+
engine.valid_bid?(BS.new)
|
389
|
+
end
|
390
|
+
|
391
|
+
it "returns the result of valid_bs?" do
|
392
|
+
engine.stub(:valid_bs?).and_return("foobar")
|
393
|
+
engine.valid_bid?(BS.new).should == "foobar"
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
it "returns false if total is negative" do
|
398
|
+
bid = Bid.new(-1, 5)
|
399
|
+
engine.valid_bid?(bid).should == false
|
400
|
+
end
|
401
|
+
|
402
|
+
it "returns false for invalid die face_values" do
|
403
|
+
bid = Bid.new(5, 0)
|
404
|
+
engine.valid_bid?(bid).should == false
|
405
|
+
bid = Bid.new(5, 10)
|
406
|
+
engine.valid_bid?(bid).should == false
|
407
|
+
end
|
408
|
+
|
409
|
+
context "with a previous bid" do
|
410
|
+
before do
|
411
|
+
@prev = Bid.new(3, 3)
|
412
|
+
engine.stub(:previous_bid).and_return(@prev)
|
413
|
+
end
|
414
|
+
|
415
|
+
it "validates all the possible bids correctly" do
|
416
|
+
[[-1, -1, false],
|
417
|
+
[-1, 0, false],
|
418
|
+
[-1, 1, false],
|
419
|
+
[-1, -1, false],
|
420
|
+
[-1, 0, false],
|
421
|
+
[-1, 1, false],
|
422
|
+
[-1, -1, false],
|
423
|
+
[-1, 0, false],
|
424
|
+
[-1, 1, false]].each do |total_delta, face_value_delta, result|
|
425
|
+
bid = Bid.new(@prev.total + total_delta, @prev.face_value + face_value_delta)
|
426
|
+
engine.valid_bid?(bid).should == result
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
describe "#valid_bs?" do
|
433
|
+
it "returns true if there's a previous bid" do
|
434
|
+
prev = Bid.new(3, 3)
|
435
|
+
engine.stub(:previous_bid).and_return(prev)
|
436
|
+
engine.valid_bs?(BS.new).should == true
|
437
|
+
end
|
438
|
+
|
439
|
+
it "returns false if there's not a previous bid" do
|
440
|
+
engine.stub(:previous_bid).and_return(nil)
|
441
|
+
engine.valid_bs?(BS.new).should == false
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
describe "#alive_seats" do
|
446
|
+
it "doesn't return any seats that aren't alive" do
|
447
|
+
s1 = Seat.new(0, nil, 6)
|
448
|
+
s2 = Seat.new(0, nil, 6)
|
449
|
+
s1.stub(:alive?).and_return(true)
|
450
|
+
s2.stub(:alive?).and_return(false)
|
451
|
+
engine.stub(:seats).and_return([s1, s2])
|
452
|
+
engine.alive_seats.should == [s1]
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
describe "notify_bid" do
|
457
|
+
it "passes a BidMadeEvent to notify_players" do
|
458
|
+
engine.should_receive(:notify_players).with(an_instance_of(BidMadeEvent))
|
459
|
+
engine.notify_bid(seat, nil)
|
460
|
+
end
|
461
|
+
|
462
|
+
it "passes a BidMadeEvent to notify_watcher" do
|
463
|
+
engine.should_receive(:notify_watcher).with(an_instance_of(BidMadeEvent))
|
464
|
+
engine.notify_bid(seat, nil)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
describe "notify_bs" do
|
469
|
+
it "passes a BSCalledEvent to notify_players" do
|
470
|
+
engine.stub(:previous_bid).and_return(nil)
|
471
|
+
engine.should_receive(:notify_players).with(an_instance_of(BSCalledEvent))
|
472
|
+
engine.notify_bs(seat)
|
473
|
+
end
|
474
|
+
|
475
|
+
it "passes a BSCalledEvent to notify_watcher" do
|
476
|
+
engine.stub(:previous_bid).and_return(nil)
|
477
|
+
engine.should_receive(:notify_watcher).with(an_instance_of(BSCalledEvent))
|
478
|
+
engine.notify_bs(seat)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
describe "notify_loser" do
|
483
|
+
it "passes a LoserEvent to notify_players" do
|
484
|
+
engine.should_receive(:notify_players).with(an_instance_of(LoserEvent))
|
485
|
+
engine.stub(:seats).and_return([])
|
486
|
+
engine.notify_loser(seat)
|
487
|
+
end
|
488
|
+
|
489
|
+
it "passes a LoserEvent to notify_watcher" do
|
490
|
+
engine.should_receive(:notify_watcher).with(an_instance_of(LoserEvent))
|
491
|
+
engine.stub(:seats).and_return([])
|
492
|
+
engine.notify_loser(seat)
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
describe "notify_winner" do
|
497
|
+
it "passes a WinnerEvent to notify_players" do
|
498
|
+
engine.should_receive(:notify_players).with(an_instance_of(WinnerEvent))
|
499
|
+
engine.stub(:winner).and_return(seat)
|
500
|
+
engine.notify_winner
|
501
|
+
end
|
502
|
+
|
503
|
+
it "passes a WinnerEvent to notify_watcher" do
|
504
|
+
engine.should_receive(:notify_watcher).with(an_instance_of(WinnerEvent))
|
505
|
+
engine.stub(:winner).and_return(seat)
|
506
|
+
engine.notify_winner
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
describe "notify_players" do
|
511
|
+
let (:player1) { {} }
|
512
|
+
let (:player2) { {} }
|
513
|
+
let (:player3) { {} }
|
514
|
+
let (:watcher) { {} }
|
515
|
+
|
516
|
+
before do
|
517
|
+
player1.stub(:handle_event)
|
518
|
+
player2.stub(:handle_event)
|
519
|
+
player3.stub(:handle_event)
|
520
|
+
|
521
|
+
s1 = Seat.new(0, player1, 5)
|
522
|
+
s2 = Seat.new(0, player2, 5)
|
523
|
+
s3 = Seat.new(0, player3, 5)
|
524
|
+
engine.stub(:seats).and_return([s1, s2, s3])
|
525
|
+
engine.stub(:watcher).and_return(watcher)
|
526
|
+
end
|
527
|
+
|
528
|
+
it "passes the event to all players" do
|
529
|
+
event = WinnerEvent.new(seat)
|
530
|
+
player1.should_receive(:handle_event).with(event)
|
531
|
+
player2.should_receive(:handle_event).with(event)
|
532
|
+
player3.should_receive(:handle_event).with(event)
|
533
|
+
|
534
|
+
engine.notify_players(event)
|
535
|
+
end
|
536
|
+
|
537
|
+
it "does not pass the event to the watcher" do
|
538
|
+
event = WinnerEvent.new(seat)
|
539
|
+
watcher.should_not_receive(:handle_event).with(event)
|
540
|
+
|
541
|
+
engine.notify_players(event)
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
describe "notify_watcher" do
|
546
|
+
let (:player1) { {} }
|
547
|
+
let (:player2) { {} }
|
548
|
+
let (:player3) { {} }
|
549
|
+
let (:watcher) { {} }
|
550
|
+
|
551
|
+
before do
|
552
|
+
watcher.stub(:handle_event)
|
553
|
+
|
554
|
+
s1 = Seat.new(0, player1, 5)
|
555
|
+
s2 = Seat.new(0, player2, 5)
|
556
|
+
s3 = Seat.new(0, player3, 5)
|
557
|
+
engine.stub(:seats).and_return([s1, s2, s3])
|
558
|
+
engine.stub(:watcher).and_return(watcher)
|
559
|
+
end
|
560
|
+
|
561
|
+
it "does not pass the event to any player" do
|
562
|
+
event = WinnerEvent.new(seat)
|
563
|
+
player1.should_not_receive(:handle_event).with(event)
|
564
|
+
player2.should_not_receive(:handle_event).with(event)
|
565
|
+
player3.should_not_receive(:handle_event).with(event)
|
566
|
+
|
567
|
+
engine.notify_watcher(event)
|
568
|
+
end
|
569
|
+
|
570
|
+
it "passes the event to the watcher" do
|
571
|
+
event = WinnerEvent.new(seat)
|
572
|
+
watcher.should_receive(:handle_event).with(event)
|
573
|
+
|
574
|
+
engine.notify_watcher(event)
|
575
|
+
end
|
576
|
+
end
|
577
|
+
end
|
data/spec/seat_spec.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Seat" do
|
4
|
+
let (:player) { {} }
|
5
|
+
let (:seat) { Seat.new(0, player, 5) }
|
6
|
+
|
7
|
+
before do
|
8
|
+
player.stub(:dice=)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#alive?" do
|
12
|
+
it "returns true when there are dice left" do
|
13
|
+
seat.stub(:dice_left).and_return(4)
|
14
|
+
seat.should be_alive
|
15
|
+
end
|
16
|
+
|
17
|
+
it "returns false when there are no dice left" do
|
18
|
+
seat.stub(:dice_left).and_return(0)
|
19
|
+
seat.should_not be_alive
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#dice=" do
|
24
|
+
it "passes the dice to the player" do
|
25
|
+
player.should_receive(:dice=).with([1, 2, 3])
|
26
|
+
seat.dice = [1,2,3]
|
27
|
+
end
|
28
|
+
|
29
|
+
it "remembers the dice" do
|
30
|
+
seat.dice = [4, 5, 6]
|
31
|
+
seat.dice.should == [4, 5, 6]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#lose_die" do
|
36
|
+
it "adjusts the dice_left count" do
|
37
|
+
count = seat.dice_left
|
38
|
+
seat.lose_die
|
39
|
+
seat.dice_left.should == (count - 1)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: liars_dice
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ben Schmeckpeper
|
9
|
+
- Chris Doyle
|
10
|
+
- Max Page
|
11
|
+
- Molly Struve
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
date: 2013-06-28 00:00:00.000000000Z
|
16
|
+
dependencies: []
|
17
|
+
description: A liar's dice botting environment, developed by Aisle50
|
18
|
+
email: dev@aisle50.com
|
19
|
+
executables: []
|
20
|
+
extensions: []
|
21
|
+
extra_rdoc_files: []
|
22
|
+
files:
|
23
|
+
- README.md
|
24
|
+
- lib/liars_dice.rb
|
25
|
+
- lib/liars_dice/bid.rb
|
26
|
+
- lib/liars_dice/command_line_watcher.rb
|
27
|
+
- lib/liars_dice/engine.rb
|
28
|
+
- lib/liars_dice/event.rb
|
29
|
+
- lib/liars_dice/human_bot.rb
|
30
|
+
- lib/liars_dice/random_bot.rb
|
31
|
+
- lib/liars_dice/seat.rb
|
32
|
+
- lib/liars_dice/watcher.rb
|
33
|
+
- spec/bid_spec.rb
|
34
|
+
- spec/engine_spec.rb
|
35
|
+
- spec/seat_spec.rb
|
36
|
+
- spec/spec_helper.rb
|
37
|
+
homepage:
|
38
|
+
licenses: []
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ! '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.8.15
|
58
|
+
signing_key:
|
59
|
+
specification_version: 3
|
60
|
+
summary: Liar's Dice game
|
61
|
+
test_files:
|
62
|
+
- spec/bid_spec.rb
|
63
|
+
- spec/engine_spec.rb
|
64
|
+
- spec/seat_spec.rb
|
65
|
+
- spec/spec_helper.rb
|