xo 0.0.1 → 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.
- checksums.yaml +13 -5
- data/.gitignore +4 -0
- data/.travis.yml +3 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +43 -0
- data/README.md +92 -27
- data/Rakefile +2 -0
- data/bin/xo +314 -0
- data/lib/xo.rb +0 -21
- data/lib/xo/ai.rb +1 -3
- data/lib/xo/ai/geometric_grid.rb +113 -0
- data/lib/xo/ai/minimax.rb +187 -89
- data/lib/xo/engine.rb +187 -68
- data/lib/xo/evaluator.rb +137 -62
- data/lib/xo/grid.rb +153 -24
- data/lib/xo/version.rb +1 -1
- data/spec/spec_helper.rb +3 -0
- data/spec/xo/ai/geometric_grid_spec.rb +137 -0
- data/spec/xo/ai/minimax_spec.rb +56 -36
- data/spec/xo/engine_spec.rb +296 -20
- data/spec/xo/evaluator_spec.rb +210 -39
- data/spec/xo/grid_spec.rb +198 -55
- data/xo.gemspec +9 -2
- metadata +63 -27
- data/lib/xo/ai/advanced_beginner.rb +0 -17
- data/lib/xo/ai/expert.rb +0 -64
- data/lib/xo/ai/novice.rb +0 -11
data/spec/xo/engine_spec.rb
CHANGED
@@ -6,49 +6,325 @@ module XO
|
|
6
6
|
|
7
7
|
let (:engine) { Engine.new }
|
8
8
|
|
9
|
-
describe
|
9
|
+
describe "its initial state" do
|
10
10
|
|
11
|
-
it
|
12
|
-
engine.
|
11
|
+
it "is in the :init state" do
|
12
|
+
engine.state.must_equal :init
|
13
13
|
end
|
14
14
|
|
15
15
|
it "is nobody's turn" do
|
16
16
|
engine.turn.must_equal :nobody
|
17
17
|
end
|
18
18
|
|
19
|
-
it
|
20
|
-
engine.
|
19
|
+
it "has an empty grid" do
|
20
|
+
engine.grid.empty?.must_equal true
|
21
|
+
end
|
22
|
+
|
23
|
+
it "has last event set to :new" do
|
24
|
+
event = engine.last_event
|
25
|
+
|
26
|
+
event[:name].must_equal :new
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#next_turn" do
|
30
|
+
|
31
|
+
it "returns :nobody" do
|
32
|
+
engine.next_turn.must_equal :nobody
|
33
|
+
end
|
21
34
|
end
|
22
35
|
end
|
23
36
|
|
24
|
-
describe
|
37
|
+
describe "#grid" do
|
25
38
|
|
26
|
-
it
|
39
|
+
it "returns a copy of the underlying grid" do
|
27
40
|
grid = engine.grid
|
28
|
-
grid[1, 1] = X
|
29
41
|
|
30
|
-
|
31
|
-
|
32
|
-
engine.
|
42
|
+
grid[1, 1] = Grid::X
|
43
|
+
|
44
|
+
engine.grid.open?(1, 1).must_equal true
|
33
45
|
end
|
34
46
|
end
|
35
47
|
|
36
|
-
describe
|
48
|
+
describe "how the state machine works" do
|
49
|
+
|
50
|
+
describe "state :init" do
|
51
|
+
|
52
|
+
it "is in state :init" do
|
53
|
+
engine.state.must_equal :init
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#start" do
|
57
|
+
|
58
|
+
before { engine.start(Grid::X) }
|
37
59
|
|
38
|
-
|
39
|
-
|
60
|
+
it "changes state to :playing" do
|
61
|
+
engine.state.must_equal :playing
|
62
|
+
end
|
63
|
+
|
64
|
+
it "sets turn to the value passed in" do
|
65
|
+
engine.turn.must_equal Grid::X
|
66
|
+
end
|
67
|
+
|
68
|
+
it "starts with an empty grid" do
|
69
|
+
engine.grid.empty?.must_equal true
|
70
|
+
end
|
71
|
+
|
72
|
+
it "sets last event" do
|
73
|
+
event = engine.last_event
|
40
74
|
|
41
|
-
|
42
|
-
if e[:event] == :game_over && e[:type] == :winner
|
43
|
-
@winner = e[:who]
|
75
|
+
event[:name].must_equal :game_started
|
44
76
|
end
|
77
|
+
|
78
|
+
describe "#next_turn" do
|
79
|
+
|
80
|
+
it "returns O" do
|
81
|
+
engine.next_turn.must_equal Grid::O
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
it "only allows #start to be called" do
|
87
|
+
proc { engine.stop }.must_raise Engine::IllegalStateError
|
88
|
+
proc { engine.play(1, 1) }.must_raise Engine::IllegalStateError
|
89
|
+
proc { engine.continue_playing(Grid::X) }.must_raise Engine::IllegalStateError
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "state :playing" do
|
94
|
+
|
95
|
+
before do
|
96
|
+
engine.start(Grid::X)
|
45
97
|
end
|
46
98
|
|
47
|
-
|
99
|
+
it "is in state :playing" do
|
100
|
+
engine.state.must_equal :playing
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "#play" do
|
104
|
+
|
105
|
+
describe "valid moves" do
|
106
|
+
|
107
|
+
describe "when given (1, 1)" do
|
108
|
+
|
109
|
+
before { engine.play(1, 1) }
|
110
|
+
|
111
|
+
it "remains in state :playing" do
|
112
|
+
engine.state.must_equal :playing
|
113
|
+
end
|
114
|
+
|
115
|
+
it "sets turn to O" do
|
116
|
+
engine.turn.must_equal Grid::O
|
117
|
+
end
|
118
|
+
|
119
|
+
it "updates the grid at that position" do
|
120
|
+
engine.grid[1, 1].must_equal Grid::X
|
121
|
+
end
|
122
|
+
|
123
|
+
it "sets last event" do
|
124
|
+
event = engine.last_event
|
125
|
+
|
126
|
+
event[:name].must_equal :next_turn
|
127
|
+
event[:last_move][:turn].must_equal Grid::X
|
128
|
+
event[:last_move][:r].must_equal 1
|
129
|
+
event[:last_move][:c].must_equal 1
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe "when the next move results in the game being over" do
|
134
|
+
|
135
|
+
describe "winning" do
|
136
|
+
|
137
|
+
before do
|
138
|
+
engine.play(1, 1).play(2, 1).play(1, 2).play(2, 2).play(1, 3)
|
139
|
+
end
|
140
|
+
|
141
|
+
it "changes state to :game_over" do
|
142
|
+
engine.state.must_equal :game_over
|
143
|
+
end
|
144
|
+
|
145
|
+
it "sets turn to the winner" do
|
146
|
+
engine.turn.must_equal Grid::X
|
147
|
+
end
|
148
|
+
|
149
|
+
it "leaves the grid unchanged" do
|
150
|
+
engine.grid.inspect.must_equal 'xxxoo '
|
151
|
+
end
|
152
|
+
|
153
|
+
it "sets last event" do
|
154
|
+
event = engine.last_event
|
155
|
+
|
156
|
+
event[:name].must_equal :game_over
|
157
|
+
event[:type].must_equal :winner
|
158
|
+
event[:last_move][:turn].must_equal Grid::X
|
159
|
+
event[:last_move][:r].must_equal 1
|
160
|
+
event[:last_move][:c].must_equal 3
|
161
|
+
event[:details].must_equal [
|
162
|
+
{ where: :row, index: 1, positions: [[1, 1], [1, 2], [1, 3]] }
|
163
|
+
]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe "squashed" do
|
168
|
+
|
169
|
+
before do
|
170
|
+
engine.play(1, 1).play(1, 2).play(1, 3).play(2, 2).play(3, 2).play(2, 1).play(2, 3).play(3, 3).play(3, 1)
|
171
|
+
end
|
172
|
+
|
173
|
+
it "changes state to :game_over" do
|
174
|
+
engine.state.must_equal :game_over
|
175
|
+
end
|
176
|
+
|
177
|
+
it "leaves turn set to the last one played" do
|
178
|
+
engine.turn.must_equal Grid::X
|
179
|
+
end
|
180
|
+
|
181
|
+
it "leaves the grid unchanged" do
|
182
|
+
engine.grid.inspect.must_equal 'xoxooxxxo'
|
183
|
+
end
|
184
|
+
|
185
|
+
it "sets last event" do
|
186
|
+
event = engine.last_event
|
187
|
+
|
188
|
+
event[:name].must_equal :game_over
|
189
|
+
event[:type].must_equal :squashed
|
190
|
+
event[:last_move][:turn].must_equal Grid::X
|
191
|
+
event[:last_move][:r].must_equal 3
|
192
|
+
event[:last_move][:c].must_equal 1
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
describe "invalid moves" do
|
199
|
+
|
200
|
+
describe "when given (0, 0)" do
|
201
|
+
|
202
|
+
it "sets last event to out of bounds" do
|
203
|
+
event = engine.play(0, 0).last_event
|
204
|
+
|
205
|
+
event[:name].must_equal :invalid_move
|
206
|
+
event[:type].must_equal :out_of_bounds
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
describe "when given (1, 1) and it already has a token there" do
|
211
|
+
|
212
|
+
it "sets last event to occupied" do
|
213
|
+
event = engine.play(1, 1).play(1, 1).last_event
|
214
|
+
|
215
|
+
event[:name].must_equal :invalid_move
|
216
|
+
event[:type].must_equal :occupied
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
describe "#stop" do
|
223
|
+
|
224
|
+
before { engine.stop }
|
225
|
+
|
226
|
+
it "changes state to :init" do
|
227
|
+
engine.state.must_equal :init
|
228
|
+
end
|
229
|
+
|
230
|
+
it "sets turn to :nobody" do
|
231
|
+
engine.turn.must_equal :nobody
|
232
|
+
end
|
233
|
+
|
234
|
+
it "clears the grid" do
|
235
|
+
engine.grid.empty?.must_equal true
|
236
|
+
end
|
237
|
+
|
238
|
+
it "sets last event" do
|
239
|
+
event = engine.last_event
|
240
|
+
|
241
|
+
event[:name].must_equal :game_stopped
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
it "only allows #play and #stop to be called" do
|
246
|
+
proc { engine.start(Grid::O) }.must_raise Engine::IllegalStateError
|
247
|
+
proc { engine.continue_playing(Grid::O) }.must_raise Engine::IllegalStateError
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
describe "state :game_over" do
|
252
|
+
|
253
|
+
before do
|
254
|
+
engine
|
255
|
+
.start(Grid::O)
|
256
|
+
.play(1, 1).play(2, 1).play(1, 2).play(2, 2).play(1, 3)
|
257
|
+
end
|
258
|
+
|
259
|
+
it "is in state :game_over" do
|
260
|
+
engine.state.must_equal :game_over
|
261
|
+
end
|
262
|
+
|
263
|
+
describe "#continue_playing" do
|
264
|
+
|
265
|
+
before { engine.continue_playing(Grid::O) }
|
266
|
+
|
267
|
+
it "changes state to :playing" do
|
268
|
+
engine.state.must_equal :playing
|
269
|
+
end
|
270
|
+
|
271
|
+
it "sets turn to O" do
|
272
|
+
engine.turn.must_equal Grid::O
|
273
|
+
end
|
274
|
+
|
275
|
+
it "clears the grid" do
|
276
|
+
engine.grid.empty?.must_equal true
|
277
|
+
end
|
278
|
+
|
279
|
+
it "sets last event" do
|
280
|
+
event = engine.last_event
|
281
|
+
|
282
|
+
event[:name].must_equal :game_started
|
283
|
+
event[:type].must_equal :continue_playing
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
describe "#stop" do
|
288
|
+
|
289
|
+
before { engine.stop }
|
290
|
+
|
291
|
+
it "changes state to :init" do
|
292
|
+
engine.state.must_equal :init
|
293
|
+
end
|
294
|
+
|
295
|
+
it "sets turn to :nobody" do
|
296
|
+
engine.turn.must_equal :nobody
|
297
|
+
end
|
298
|
+
|
299
|
+
it "clears the grid" do
|
300
|
+
engine.grid.empty?.must_equal true
|
301
|
+
end
|
302
|
+
|
303
|
+
it "sets last event" do
|
304
|
+
event = engine.last_event
|
305
|
+
|
306
|
+
event[:name].must_equal :game_stopped
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
it "only allows #continue_playing and #stop to be called" do
|
311
|
+
proc { engine.start(Grid::X) }.must_raise Engine::IllegalStateError
|
312
|
+
proc { engine.play(1, 1) }.must_raise Engine::IllegalStateError
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
describe "#start with invalid input" do
|
318
|
+
|
319
|
+
it "raises an ArgumentError" do
|
320
|
+
proc { engine.start(:invalid_input) }.must_raise ArgumentError
|
321
|
+
end
|
322
|
+
end
|
48
323
|
|
49
|
-
|
324
|
+
describe "#continue_playing with invalid input" do
|
50
325
|
|
51
|
-
|
326
|
+
it "raises an ArgumentError" do
|
327
|
+
proc { engine.continue_playing(:invalid_input) }.must_raise ArgumentError
|
52
328
|
end
|
53
329
|
end
|
54
330
|
end
|
data/spec/xo/evaluator_spec.rb
CHANGED
@@ -4,40 +4,33 @@ module XO
|
|
4
4
|
|
5
5
|
describe Evaluator do
|
6
6
|
|
7
|
-
describe
|
7
|
+
describe ".analyze" do
|
8
8
|
|
9
|
+
let (:evaluator) { Evaluator.instance }
|
9
10
|
let (:grid) { Grid.new }
|
10
11
|
|
11
|
-
describe
|
12
|
+
describe "standard play" do
|
12
13
|
|
13
|
-
it
|
14
|
-
|
15
|
-
grid[2, 1] = O
|
16
|
-
|
17
|
-
result = { status: :error, type: :too_many_moves_ahead }
|
18
|
-
|
19
|
-
Evaluator.analyze(grid, X).must_equal result
|
20
|
-
Evaluator.analyze(grid, O).must_equal result
|
21
|
-
end
|
14
|
+
it "returns ok" do
|
15
|
+
result = { status: :ok }
|
22
16
|
|
23
|
-
|
24
|
-
grid
|
25
|
-
grid[2, 1] = grid[2, 2] = grid[2, 3] = O
|
17
|
+
evaluator.analyze(grid, Grid::X).must_equal result
|
18
|
+
evaluator.analyze(grid, Grid::O).must_equal result
|
26
19
|
|
27
|
-
|
20
|
+
grid[1, 1] = Grid::X
|
28
21
|
|
29
|
-
|
30
|
-
|
22
|
+
evaluator.analyze(grid, Grid::X).must_equal result
|
23
|
+
evaluator.analyze(grid, Grid::O).must_equal result
|
31
24
|
end
|
32
25
|
end
|
33
26
|
|
34
|
-
describe
|
27
|
+
describe "game over" do
|
35
28
|
|
36
|
-
describe
|
29
|
+
describe "winners and losers" do
|
37
30
|
|
38
|
-
it
|
39
|
-
grid[1, 1] = grid[1, 2] = grid[1, 3] = X
|
40
|
-
grid[2, 1] = grid[2, 2] = O
|
31
|
+
it "returns a row 1 winner/loser" do
|
32
|
+
grid[1, 1] = grid[1, 2] = grid[1, 3] = Grid::X
|
33
|
+
grid[2, 1] = grid[2, 2] = Grid::O
|
41
34
|
|
42
35
|
result = {
|
43
36
|
status: :game_over,
|
@@ -49,40 +42,218 @@ module XO
|
|
49
42
|
}]
|
50
43
|
}
|
51
44
|
|
52
|
-
|
45
|
+
evaluator.analyze(grid, Grid::X).must_equal result
|
46
|
+
|
47
|
+
result[:type] = :loser
|
48
|
+
evaluator.analyze(grid, Grid::O).must_equal result
|
49
|
+
end
|
50
|
+
|
51
|
+
it "returns a row 2 winner/loser" do
|
52
|
+
grid[2, 1] = grid[2, 2] = grid[2, 3] = Grid::X
|
53
|
+
grid[1, 1] = grid[1, 2] = Grid::O
|
54
|
+
|
55
|
+
result = {
|
56
|
+
status: :game_over,
|
57
|
+
type: :winner,
|
58
|
+
details: [{
|
59
|
+
where: :row,
|
60
|
+
index: 2,
|
61
|
+
positions: [[2, 1], [2, 2], [2, 3]]
|
62
|
+
}]
|
63
|
+
}
|
64
|
+
|
65
|
+
evaluator.analyze(grid, Grid::X).must_equal result
|
66
|
+
|
67
|
+
result[:type] = :loser
|
68
|
+
evaluator.analyze(grid, Grid::O).must_equal result
|
69
|
+
end
|
70
|
+
|
71
|
+
it "returns a row 3 winner/loser" do
|
72
|
+
grid[3, 1] = grid[3, 2] = grid[3, 3] = Grid::X
|
73
|
+
grid[1, 1] = grid[1, 2] = Grid::O
|
74
|
+
|
75
|
+
result = {
|
76
|
+
status: :game_over,
|
77
|
+
type: :winner,
|
78
|
+
details: [{
|
79
|
+
where: :row,
|
80
|
+
index: 3,
|
81
|
+
positions: [[3, 1], [3, 2], [3, 3]]
|
82
|
+
}]
|
83
|
+
}
|
84
|
+
|
85
|
+
evaluator.analyze(grid, Grid::X).must_equal result
|
86
|
+
|
87
|
+
result[:type] = :loser
|
88
|
+
evaluator.analyze(grid, Grid::O).must_equal result
|
89
|
+
end
|
90
|
+
|
91
|
+
it "returns a column 1 winner/loser" do
|
92
|
+
grid[1, 1] = grid[2, 1] = grid[3, 1] = Grid::X
|
93
|
+
grid[1, 2] = grid[2, 2] = Grid::O
|
94
|
+
|
95
|
+
result = {
|
96
|
+
status: :game_over,
|
97
|
+
type: :winner,
|
98
|
+
details: [{
|
99
|
+
where: :column,
|
100
|
+
index: 1,
|
101
|
+
positions: [[1, 1], [2, 1], [3, 1]]
|
102
|
+
}]
|
103
|
+
}
|
104
|
+
|
105
|
+
evaluator.analyze(grid, Grid::X).must_equal result
|
106
|
+
|
107
|
+
result[:type] = :loser
|
108
|
+
evaluator.analyze(grid, Grid::O).must_equal result
|
109
|
+
end
|
110
|
+
|
111
|
+
it "returns a column 2 winner/loser" do
|
112
|
+
grid[1, 2] = grid[2, 2] = grid[3, 2] = Grid::X
|
113
|
+
grid[1, 1] = grid[2, 1] = Grid::O
|
114
|
+
|
115
|
+
result = {
|
116
|
+
status: :game_over,
|
117
|
+
type: :winner,
|
118
|
+
details: [{
|
119
|
+
where: :column,
|
120
|
+
index: 2,
|
121
|
+
positions: [[1, 2], [2, 2], [3, 2]]
|
122
|
+
}]
|
123
|
+
}
|
124
|
+
|
125
|
+
evaluator.analyze(grid, Grid::X).must_equal result
|
126
|
+
|
127
|
+
result[:type] = :loser
|
128
|
+
evaluator.analyze(grid, Grid::O).must_equal result
|
129
|
+
end
|
130
|
+
|
131
|
+
it "returns a column 3 winner/loser" do
|
132
|
+
grid[1, 3] = grid[2, 3] = grid[3, 3] = Grid::X
|
133
|
+
grid[1, 1] = grid[2, 1] = Grid::O
|
134
|
+
|
135
|
+
result = {
|
136
|
+
status: :game_over,
|
137
|
+
type: :winner,
|
138
|
+
details: [{
|
139
|
+
where: :column,
|
140
|
+
index: 3,
|
141
|
+
positions: [[1, 3], [2, 3], [3, 3]]
|
142
|
+
}]
|
143
|
+
}
|
144
|
+
|
145
|
+
evaluator.analyze(grid, Grid::X).must_equal result
|
146
|
+
|
147
|
+
result[:type] = :loser
|
148
|
+
evaluator.analyze(grid, Grid::O).must_equal result
|
149
|
+
end
|
150
|
+
|
151
|
+
it "returns a diagonal 1 winner/loser" do
|
152
|
+
grid[1, 1] = grid[2, 2] = grid[3, 3] = Grid::X
|
153
|
+
grid[1, 2] = grid[2, 1] = Grid::O
|
154
|
+
|
155
|
+
result = {
|
156
|
+
status: :game_over,
|
157
|
+
type: :winner,
|
158
|
+
details: [{
|
159
|
+
where: :diagonal,
|
160
|
+
index: 1,
|
161
|
+
positions: [[1, 1], [2, 2], [3, 3]]
|
162
|
+
}]
|
163
|
+
}
|
164
|
+
|
165
|
+
evaluator.analyze(grid, Grid::X).must_equal result
|
166
|
+
|
167
|
+
result[:type] = :loser
|
168
|
+
evaluator.analyze(grid, Grid::O).must_equal result
|
169
|
+
end
|
170
|
+
|
171
|
+
it "returns a diagonal 2 winner/loser" do
|
172
|
+
grid[1, 3] = grid[2, 2] = grid[3, 1] = Grid::X
|
173
|
+
grid[1, 2] = grid[2, 3] = Grid::O
|
174
|
+
|
175
|
+
result = {
|
176
|
+
status: :game_over,
|
177
|
+
type: :winner,
|
178
|
+
details: [{
|
179
|
+
where: :diagonal,
|
180
|
+
index: 2,
|
181
|
+
positions: [[1, 3], [2, 2], [3, 1]]
|
182
|
+
}]
|
183
|
+
}
|
184
|
+
|
185
|
+
evaluator.analyze(grid, Grid::X).must_equal result
|
53
186
|
|
54
187
|
result[:type] = :loser
|
55
|
-
|
188
|
+
evaluator.analyze(grid, Grid::O).must_equal result
|
56
189
|
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe "a highly unlikely but definitely possible two-way winner/loser" do
|
193
|
+
# I mean you have to be real messed up to lose a game in this manner. :P
|
194
|
+
|
195
|
+
# The X win
|
196
|
+
it "returns a diagonal 1 and 2 winner/loser" do
|
197
|
+
grid[1, 1] = grid[1, 3] = grid[2, 2] = grid[3, 1] = grid[3, 3] = Grid::X
|
198
|
+
grid[1, 2] = grid[2, 1] = grid[2, 3] = grid[3, 2] = Grid::O
|
57
199
|
|
58
|
-
|
200
|
+
result = {
|
201
|
+
status: :game_over,
|
202
|
+
type: :winner,
|
203
|
+
details: [{
|
204
|
+
where: :diagonal,
|
205
|
+
index: 1,
|
206
|
+
positions: [[1, 1], [2, 2], [3, 3]]
|
207
|
+
}, {
|
208
|
+
where: :diagonal,
|
209
|
+
index: 2,
|
210
|
+
positions: [[1, 3], [2, 2], [3, 1]]
|
211
|
+
}]
|
212
|
+
}
|
213
|
+
end
|
59
214
|
end
|
60
215
|
|
61
|
-
describe
|
216
|
+
describe "a squashed grid" do
|
62
217
|
|
63
|
-
it
|
64
|
-
grid[1, 1] = grid[1, 2] = grid[2, 3] = grid[3, 1] = grid[3, 3] = X
|
65
|
-
grid[1, 3] = grid[2, 1] = grid[2, 2] = grid[3, 2] = O
|
218
|
+
it "returns squashed" do
|
219
|
+
grid[1, 1] = grid[1, 2] = grid[2, 3] = grid[3, 1] = grid[3, 3] = Grid::X
|
220
|
+
grid[1, 3] = grid[2, 1] = grid[2, 2] = grid[3, 2] = Grid::O
|
66
221
|
|
67
222
|
result = { status: :game_over, type: :squashed }
|
68
223
|
|
69
|
-
|
70
|
-
|
224
|
+
evaluator.analyze(grid, Grid::X).must_equal result
|
225
|
+
evaluator.analyze(grid, Grid::O).must_equal result
|
71
226
|
end
|
72
227
|
end
|
73
228
|
end
|
74
229
|
|
75
|
-
describe
|
230
|
+
describe "invalid token input" do
|
76
231
|
|
77
|
-
it
|
78
|
-
|
232
|
+
it "raises ArgumentError" do
|
233
|
+
proc { evaluator.analyze(Grid.new, :not_a_token) }.must_raise ArgumentError
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
describe "invalid grid input" do
|
238
|
+
|
239
|
+
it "returns too many moves ahead" do
|
240
|
+
grid[1, 1] = grid[1, 2] = grid[1, 3] = Grid::X
|
241
|
+
grid[2, 1] = Grid::O
|
242
|
+
|
243
|
+
result = { status: :invalid_grid, type: :too_many_moves_ahead }
|
244
|
+
|
245
|
+
evaluator.analyze(grid, Grid::X).must_equal result
|
246
|
+
evaluator.analyze(grid, Grid::O).must_equal result
|
247
|
+
end
|
248
|
+
|
249
|
+
it "returns two winners" do
|
250
|
+
grid[1, 1] = grid[1, 2] = grid[1, 3] = Grid::X
|
251
|
+
grid[2, 1] = grid[2, 2] = grid[2, 3] = Grid::O
|
79
252
|
|
80
|
-
|
81
|
-
Evaluator.analyze(grid, O).must_equal result
|
253
|
+
result = { status: :invalid_grid, type: :two_winners }
|
82
254
|
|
83
|
-
grid
|
84
|
-
|
85
|
-
Evaluator.analyze(grid, O).must_equal result
|
255
|
+
evaluator.analyze(grid, Grid::X).must_equal result
|
256
|
+
evaluator.analyze(grid, Grid::O).must_equal result
|
86
257
|
end
|
87
258
|
end
|
88
259
|
end
|