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 ADDED
@@ -0,0 +1,4 @@
1
+ liars_dice
2
+ ==========
3
+
4
+ Liar's Dice Game
@@ -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
@@ -0,0 +1,8 @@
1
+ require 'liars_dice/watcher'
2
+ require 'liars_dice/bid'
3
+ require 'liars_dice/engine'
4
+ require 'liars_dice/event'
5
+ require 'liars_dice/seat'
6
+ require 'liars_dice/command_line_watcher'
7
+ require 'liars_dice/random_bot'
8
+ require 'liars_dice/human_bot'
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
@@ -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
+
@@ -0,0 +1,11 @@
1
+ gem 'rspec'
2
+ require 'ostruct'
3
+
4
+ require 'liars_dice'
5
+
6
+ include LiarsDice
7
+
8
+ RSpec.configure do |c|
9
+ c.filter_run :focus => true
10
+ c.run_all_when_everything_filtered = true
11
+ end
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