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.
- data/Gemfile +6 -0
- data/Gemfile.lock +51 -0
- data/MIT-License.md +8 -0
- data/Rakefile +324 -0
- data/Readme.md +35 -0
- data/bin/ttt +6 -0
- data/features/binary.feature +39 -0
- data/features/computer_player.feature +62 -0
- data/features/create_game.feature +63 -0
- data/features/finish_game.feature +53 -0
- data/features/finished_states.feature +56 -0
- data/features/mark_board.feature +24 -0
- data/features/step_definitions/binary_steps.rb +42 -0
- data/features/step_definitions/ttt_steps.rb +82 -0
- data/features/support/env.rb +8 -0
- data/features/view_board_as_developer.feature +13 -0
- data/features/view_board_as_tester.feature +9 -0
- data/lib/ttt.rb +1 -0
- data/lib/ttt/binary.rb +96 -0
- data/lib/ttt/computer_player.rb +74 -0
- data/lib/ttt/game.rb +129 -0
- data/lib/ttt/interface.rb +22 -0
- data/lib/ttt/interface/cli.rb +95 -0
- data/lib/ttt/interface/cli/players.rb +56 -0
- data/lib/ttt/interface/cli/views.rb +124 -0
- data/lib/ttt/interface/limelight.rb +18 -0
- data/lib/ttt/interface/limelight/players/restart_as_first.rb +5 -0
- data/lib/ttt/interface/limelight/players/restart_as_second.rb +6 -0
- data/lib/ttt/interface/limelight/players/square.rb +105 -0
- data/lib/ttt/interface/limelight/props.rb +10 -0
- data/lib/ttt/interface/limelight/styles.rb +101 -0
- data/lib/ttt/ratings.rb +849 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/ttt/computer_player_spec.rb +86 -0
- data/spec/ttt/game_spec.rb +294 -0
- data/spec/ttt/rating_spec.rb +75 -0
- metadata +139 -0
data/spec/spec_helper.rb
ADDED
@@ -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
|