liars_dice 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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