ttt 1.0.0

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.
@@ -0,0 +1,10 @@
1
+ require 'bundler/bouncer'
2
+ require 'ttt'
3
+
4
+ def enumerator
5
+ if RUBY_VERSION == '1.8.7'
6
+ Enumerable::Enumerator
7
+ else
8
+ Enumerator
9
+ end
10
+ end
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+ require 'ttt/computer_player'
3
+
4
+ module TTT
5
+ describe ComputerPlayer do
6
+
7
+ def self.move_for(configuration, player, possible_boards, description)
8
+ it "takes moves correctly when #{description}" do
9
+ game = Game.new configuration
10
+ computer = ComputerPlayer.new game
11
+ computer.take_turn
12
+ possible_boards.should include game.board
13
+ end
14
+ end
15
+
16
+ def self.moves_for(scenarios)
17
+ scenarios.each { |scenario| move_for *scenario }
18
+ end
19
+
20
+
21
+ context 'always takes a win when it is available' do
22
+ moves_for [
23
+ ['110200200', 1, ['111200200'] , 'can win across top' ],
24
+ ['220000110', 1, ['220000111'] , 'can win across bottom and opponent can win across top' ],
25
+ ['201201000', 1, ['201201001'] , 'can win vertically on RHS opponent can win too' ],
26
+ ['120120000', 1, ['120120100'] , 'can win vertically on RHS opponent can win too' ],
27
+ ['102210000', 1, ['102210001'] , 'can win diagonally' ],
28
+ ['120112020', 1, ['120112120', '120112021'] , 'can win in two positions' ],
29
+ ['120021001', 2, ['120021021'] , '2nd player and 1st can also win' ],
30
+ ]
31
+ end
32
+
33
+ context "Computer blocks opponent's win" do
34
+ moves_for [
35
+ ['120100000', 2, ['120100200'] , 'blocks lhs' ],
36
+ ['122110000', 2, ['122110200', '122110002'] , "blocks either of opponent's possible wins" ],
37
+ ['211200000', 1, ['211200100'] , 'blocks when first player' ],
38
+ ]
39
+ end
40
+
41
+ context 'Finds best moves for likely game states' do
42
+ moves_for [
43
+ ['000000000', 1, ['100000000', '001000000', '000000100', '000000001'] , 'makes best 1st move' ],
44
+ ['120000000', 1, ['120000100', '120010000', '120100000'] , 'makes move that will guarantee win in future' ],
45
+ ['100000002', 1, ['101000002', '100000102'] , 'makes move that will guarantee win in future' ],
46
+ ['100000020', 1, ['100000120', '101000020', '100010020'] , 'makes move that will guarantee win in future' ],
47
+ ['102000000', 1, ['102100000', '102000100', '102000001'] , 'makes move that will guarantee win in future' ],
48
+ ['102100200', 1, ['102110200'] , 'makes move that will guarantee win next turn' ],
49
+ ['100020000', 1, ['110020000', '100120000'] , 'makes move with highest probability of win in future' ],
50
+ ['100000000', 2, ['100020000'] , 'makes move with lowest probability of losing in future' ],
51
+ ]
52
+ end
53
+
54
+ describe '#best_move' do
55
+ it 'powers the #take_turn method by finding the best move' do
56
+ game = Game.new '000000000'
57
+ computer = ComputerPlayer.new game
58
+ [1, 3, 7, 9].should include computer.best_move
59
+ end
60
+ end
61
+
62
+ describe '#moves_by_rating' do
63
+ let(:game) { Game.new '121122000' }
64
+ let(:computer) { ComputerPlayer.new game }
65
+ subject { computer.moves_by_rating }
66
+ context 'when invoked without a block' do
67
+ it { should be_an_instance_of enumerator }
68
+ end
69
+ context 'when invoked with a block' do
70
+ it 'yields available moves and ratings' do
71
+ seen = []
72
+ computer.moves_by_rating do |move, rating|
73
+ seen << [move, rating]
74
+ end
75
+ # if we move to position 7, we win, so it should be first
76
+ # if we move to position 8, we probably tie (but maybe win if opponent messes up)
77
+ # if we move to position 9, we lose, so it should be last
78
+ # on 9, even though we could still tie, if opponent messes up, we don't consider that since it could cause a loss
79
+ seen.size.should be 3
80
+ seen.map(&:first).should == [7, 8, 9]
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,294 @@
1
+ require 'spec_helper'
2
+
3
+ module TTT
4
+ describe Game do
5
+ it 'has no moves when created without an argument' do
6
+ Game.new.board.should == '0'*9
7
+ end
8
+
9
+ context 'when it is created with an unfinished board' do
10
+ context 'with an equal number of 1s and 2s' do
11
+ let(:board) { '120000000' }
12
+ subject { Game.new board }
13
+ its(:board) { should == board }
14
+ its(:turn) { should == 1 }
15
+ it 'marks the board with the current players number' do
16
+ subject.mark(4)
17
+ subject.board.should == '120100000'
18
+ end
19
+ end
20
+ context 'with more 1s than 2s' do
21
+ let(:board) { '120100000' }
22
+ subject { Game.new board }
23
+ its(:board) { should == board }
24
+ its(:turn) { should == 2 }
25
+ it 'marks the board with the current players number' do
26
+ subject.mark(7)
27
+ subject.board.should == '120100200'
28
+ end
29
+ end
30
+ end
31
+
32
+ describe 'board(:ttt)' do
33
+ it 'renders the board in tic-tac-toe format' do
34
+ @game = Game.new 'abcdefghi'
35
+ @game.board(:ttt).should == " a | b | c \n----|---|----\n d | e | f \n----|---|----\n g | h | i "
36
+ end
37
+ it 'renders empty spaces with spaces' do
38
+ @game = Game.new '120000000'
39
+ @game.board(:ttt).should == " 1 | 2 | \n----|---|----\n | | \n----|---|----\n | | "
40
+ end
41
+ end
42
+
43
+ context 'when player1 can win' do
44
+ let(:game) { Game.new '120120000' }
45
+ it { should_not be_over }
46
+ context 'and player1 wins' do
47
+ before { game.mark 7 }
48
+ subject { game }
49
+ its(:board) { should == '120120100' }
50
+ it { should be_over }
51
+ it { should_not be_a_tie }
52
+ its(:turn) { should be nil }
53
+ specify('player1 should be the winner') { subject.status(1).should be :wins }
54
+ specify('player2 should be the loser') { subject.status(2).should be :loses }
55
+ end
56
+ end
57
+
58
+ context 'when player2 can win' do
59
+ let(:game) { Game.new '121120000' }
60
+ it { should_not be_over }
61
+ context 'and player2 wins' do
62
+ before { game.mark 8 }
63
+ subject { game }
64
+ it { should be_over }
65
+ it { should_not be_a_tie }
66
+ its(:board) { should == '121120020' }
67
+ its(:turn) { should be nil }
68
+ specify('player1 should be the loser') { subject.status(1).should be :loses }
69
+ specify('player2 should be the winner') { subject.status(2).should be :wins }
70
+ end
71
+ end
72
+
73
+ context 'when the game can end in a tie' do
74
+ let(:game) { Game.new '121221012' }
75
+ it { should_not be_over }
76
+ context 'and the game ends in a tie' do
77
+ before { game.mark 7 }
78
+ subject { game }
79
+ it { should be_over }
80
+ it { should be_a_tie }
81
+ its(:board) { should == '121221112' }
82
+ its(:turn) { should be nil }
83
+ specify('player1 should tie') { subject.status(1).should be :ties }
84
+ specify('player2 should tie') { subject.status(2).should be :ties }
85
+ end
86
+ end
87
+
88
+ describe 'winning states' do
89
+ [ ['111000000', 1],
90
+ ['000111000', 1],
91
+ ['000000111', 1],
92
+ ['222000000', 2],
93
+ ['000222000', 2],
94
+ ['000000222', 2],
95
+ ['100100100', 1],
96
+ ['010010010', 1],
97
+ ['001001001', 1],
98
+ ['200200200', 2],
99
+ ['020020020', 2],
100
+ ['002002002', 2],
101
+ ['100010001', 1],
102
+ ['001010100', 1],
103
+ ['200020002', 2],
104
+ ['002020200', 2], ].each do |configuration, winner|
105
+ context configuration do
106
+ subject { Game.new configuration }
107
+ it { should be_over }
108
+ it { should_not be_a_tie }
109
+ its(:winner) { should be winner }
110
+ specify { subject.status(winner).should be :wins }
111
+ end
112
+ end
113
+ end
114
+
115
+ describe 'non winning states' do
116
+ [ '000000000',
117
+ '100000000',
118
+ '100020000',
119
+ '101020000',
120
+ '121020000',
121
+ '121020010',
122
+ '121220010',
123
+ '121221010',
124
+ '121221012', ].each do |configuration|
125
+ context configuration do
126
+ subject { Game.new configuration }
127
+ it { should_not be_over }
128
+ it { should_not be_a_tie }
129
+ its(:winner) { should be nil }
130
+ end
131
+ end
132
+ context "121221112, a tied state" do
133
+ subject { Game.new '121221112' }
134
+ it { should be_over }
135
+ it { should be_a_tie }
136
+ its(:winner) { should be nil }
137
+ specify("status(1) should be :ties") { subject.status(1).should be :ties }
138
+ specify("status(2) should be :ties") { subject.status(2).should be :ties }
139
+ end
140
+ end
141
+
142
+ describe '#available_moves' do
143
+ { '000000000' => [1,2,3,4,5,6,7,8,9],
144
+ '100000000' => [ 2,3,4,5,6,7,8,9],
145
+ '100020000' => [ 2,3,4, 6,7,8,9],
146
+ '101020000' => [ 2, 4, 6,7,8,9],
147
+ '121020000' => [ 4, 6,7,8,9],
148
+ '121020010' => [ 4, 6,7, 9],
149
+ '121220010' => [ 6,7, 9],
150
+ '121221010' => [ 7, 9],
151
+ '121221210' => [ 9],
152
+ '121221211' => [ ],
153
+ }.each do |board, available_moves|
154
+ specify "#{board} should be able to move to #{available_moves.inspect}" do
155
+ Game.new(board).available_moves.should == available_moves
156
+ end
157
+ end
158
+ specify "It doesn't show available moves when a game is over" do
159
+ Game.new('111000000').available_moves.should be_empty
160
+ end
161
+ end
162
+
163
+ describe '#winning_positions' do
164
+ [ ['111220000', [1, 2, 3]],
165
+ ['220111000', [4, 5, 6]],
166
+ ['220000111', [7, 8, 9]],
167
+ ['120120100', [1, 4, 7]],
168
+ ['210210010', [2, 5, 8]],
169
+ ['201201001', [3, 6, 9]],
170
+ ['120210001', [1, 5, 9]],
171
+ ['021210100', [3, 5, 7]],
172
+ ['021210100', [3, 5, 7]],
173
+ ['211210200', [1, 4, 7]],
174
+ ].each do |board, positions|
175
+ it "Knows which positions won" do
176
+ Game.new(board).winning_positions.should == positions
177
+ end
178
+ end
179
+ end
180
+
181
+ describe '#pristine_mark' do
182
+ let(:game) { Game.new '000000000' }
183
+ subject { game.pristine_mark 1 }
184
+ it { should be_an_instance_of Game }
185
+ its(:board) { should == '100000000' }
186
+ it 'leaves the original in its current condition' do
187
+ subject
188
+ game.board.should == '000000000'
189
+ end
190
+ end
191
+
192
+ describe '.congruent?' do
193
+ [ %w[100000000 001000000 000000100 000000001],
194
+ %w[120000000 100200000],
195
+ %w[100020000 001020000 000020100 000020001],
196
+ %w[120000001 100200001],
197
+ %w[100020100 000020101 001020001 101020000],
198
+ ].each do |boards|
199
+ specify "knows #{boards.inspect} are all congruent" do
200
+ boards.each do |board1|
201
+ boards.each { |board2| TTT::Game.should be_congruent(board1, board2) }
202
+ end
203
+ end
204
+ end
205
+
206
+ [ %w[100000000 010000000 000010000 120000000],
207
+ %w[120000000 102000000 100020000 100000002],
208
+ %w[110020000 101020000 100020001],
209
+ ].each do |boards|
210
+ specify "knows #{boards.inspect} are not congruent" do
211
+ boards.each do |board1|
212
+ boards.each do |board2|
213
+ next if board1 == board2
214
+ TTT::Game.should_not be_congruent(board1, board2)
215
+ end
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ describe '.reflect_board' do
222
+ specify 'abcdefghi should reflect to ghidefabc' do
223
+ Game.reflect_board('abcdefghi').should == 'ghidefabc'
224
+ end
225
+ specify 'it should not mutate the original board' do
226
+ board = 'abcdefghi'
227
+ Game.reflect_board board
228
+ board.should == 'abcdefghi'
229
+ end
230
+ end
231
+
232
+ describe '.rotate_board' do
233
+ specify 'abcdefghi should rotate to gdahebifc' do
234
+ Game.rotate_board('abcdefghi').should == 'gdahebifc'
235
+ end
236
+ specify 'it should not mutate the original board' do
237
+ board = 'abcdefghi'
238
+ Game.rotate_board board
239
+ board.should == 'abcdefghi'
240
+ end
241
+ end
242
+
243
+ describe '.each_rotation' do
244
+ let(:rotations) { %w[abcdefghi gdahebifc ihgfedcba cfibehadg] }
245
+ subject { Game.each_rotation rotations.first }
246
+ context 'when not passed a block' do
247
+ it { should be_an_instance_of enumerator }
248
+ its(:to_a) { should == rotations }
249
+ end
250
+ context 'when passed a block' do
251
+ it 'yields each rotation' do
252
+ boards = []
253
+ Game.each_rotation rotations.first do |rotation|
254
+ boards << rotation
255
+ end
256
+ boards.should == rotations
257
+ end
258
+ end
259
+ it "isn't impacted by mutations" do
260
+ Game.each_rotation(rotations.first).with_index do |rotation, index|
261
+ rotation.should == rotations[index]
262
+ rotation[2..5] = 'xxxx'
263
+ rotations.first.should == 'abcdefghi'
264
+ end
265
+ end
266
+ end
267
+
268
+ describe '.each_congruent' do
269
+ let(:congruents) { %w[abcdefghi gdahebifc ihgfedcba cfibehadg ghidefabc adgbehcfi cbafedihg ifchebgda ] }
270
+ context 'when not passed a block' do
271
+ subject { Game.each_congruent congruents.first }
272
+ it { should be_an_instance_of enumerator }
273
+ its(:to_a) { should == congruents }
274
+ end
275
+ context 'when passed a block' do
276
+ it 'should yield each congruent board' do
277
+ boards = []
278
+ Game.each_congruent congruents.first do |congruent|
279
+ boards << congruent
280
+ end
281
+ boards.should == congruents
282
+ end
283
+ end
284
+ it "isn't impacted by mutations" do
285
+ Game.each_congruent(congruents.first).with_index do |congruent, index|
286
+ congruent.should == congruents[index]
287
+ congruent[2..5] = 'xxxx'
288
+ congruents.first.should == "abcdefghi"
289
+ end
290
+ end
291
+ end
292
+
293
+ end
294
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+ require 'ttt/ratings'
3
+
4
+ module TTT
5
+
6
+ rate = lambda do |board, player|
7
+ Rating.new(board).rating_for(player)
8
+ end
9
+
10
+ describe Rating do
11
+ it 'rates as 1 if a board is a guaranteed win' do
12
+ rate['110220000', 1].should == 1
13
+ end
14
+
15
+ it 'rates as -1 if a board can lead to a loss' do
16
+ rate['110220100', 1].should == -1
17
+ end
18
+
19
+ it 'rates as 0 if a board leads to a guaranteed tie' do
20
+ rate['112221100', 1].should == 0
21
+ end
22
+
23
+ it 'will rate a board for both player1 and player2' do
24
+ rate['110220000', 1].should == 1
25
+ rate['110220000', 2].should == -1
26
+ end
27
+
28
+ it 'rates player1 and player2 the same if board is guaranteed to tie' do
29
+ rate['112221100', 1].should == rate['112221100', 2]
30
+ end
31
+
32
+ context "when it doesn't have a guaranteed win" do
33
+ specify "ratings have relative merit such that a move with more opportunities to win will be rated higher" do
34
+ better, worse = '121001200', '121100200'
35
+ rate[better, 1].should be > rate[worse, 1]
36
+ rate[better, 1].should_not == 1
37
+ rate[worse, 1].should_not == 1
38
+ end
39
+ end
40
+ end
41
+
42
+ describe RATINGS do
43
+ it "holds the calculated values of Rating so we don't need to calculate in real time" do
44
+ %w[121020010 121020210 121000200 121100200 121100220].each do |board|
45
+ RATINGS[board].should == { 1 => rate[board, 1], 2 => rate[board, 2] }
46
+ end
47
+ end
48
+
49
+ it 'knows ratings for congruent boards' do
50
+ topleft = RATINGS['100000000']
51
+ topright = RATINGS['001000000']
52
+ botleft = RATINGS['000000100']
53
+ botright = RATINGS['000000001']
54
+ topleft.should == topright
55
+ topleft.should == botleft
56
+ topleft.should == botright
57
+ end
58
+
59
+ describe "if for some reason it doesn't know the rating" do
60
+ let(:board) { '121001200' }
61
+ let(:cached_value) { RATINGS[board] }
62
+ let(:empty_ratings) { r = RATINGS.dup; r.clear; r }
63
+ it "will calculate the boards" do
64
+ empty_ratings[board].should == cached_value
65
+ end
66
+ it 'will add the board to the RATINGS cache' do
67
+ empty_ratings.size.should be 0
68
+ empty_ratings[board]
69
+ empty_ratings.size.should be 1
70
+ empty_ratings[board]
71
+ empty_ratings.size.should be 1
72
+ end
73
+ end
74
+ end
75
+ end