xo 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,49 +6,325 @@ module XO
6
6
 
7
7
  let (:engine) { Engine.new }
8
8
 
9
- describe 'initial state' do
9
+ describe "its initial state" do
10
10
 
11
- it 'has an empty grid' do
12
- engine.grid.empty?.must_equal true
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 'is in the idle state' do
20
- engine.state.must_equal :idle
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 '#grid' do
37
+ describe "#grid" do
25
38
 
26
- it 'returns a copy' do
39
+ it "returns a copy of the underlying grid" do
27
40
  grid = engine.grid
28
- grid[1, 1] = X
29
41
 
30
- # FIXME: How else can I test this requirement? I don't like that the test
31
- # depends on knowing the name of the internal private instance variable.
32
- engine.instance_variable_get(:@grid).empty?.must_equal true
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 'a single round of play' do
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
- it 'works as follows' do
39
- observer = Object.new
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
- def observer.handle_event(e)
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
- engine.add_observer(observer, :handle_event)
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
- engine.start(X).play(1, 1).play(2, 1).play(1, 2).play(2, 2).play(1, 3)
324
+ describe "#continue_playing with invalid input" do
50
325
 
51
- observer.instance_variable_get(:@winner).must_equal X
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
@@ -4,40 +4,33 @@ module XO
4
4
 
5
5
  describe Evaluator do
6
6
 
7
- describe 'analyze' do
7
+ describe ".analyze" do
8
8
 
9
+ let (:evaluator) { Evaluator.instance }
9
10
  let (:grid) { Grid.new }
10
11
 
11
- describe 'error statuses' do
12
+ describe "standard play" do
12
13
 
13
- it 'returns too many moves ahead' do
14
- grid[1, 1] = grid[1, 2] = grid[1, 3] = X
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
- it 'returns two winners' do
24
- grid[1, 1] = grid[1, 2] = grid[1, 3] = X
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
- result = { status: :error, type: :two_winners }
20
+ grid[1, 1] = Grid::X
28
21
 
29
- Evaluator.analyze(grid, X).must_equal result
30
- Evaluator.analyze(grid, O).must_equal result
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 'game over statuses' do
27
+ describe "game over" do
35
28
 
36
- describe 'wins and losses' do
29
+ describe "winners and losers" do
37
30
 
38
- it 'returns a win/loss in the first row' do
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
- Evaluator.analyze(grid, X).must_equal result
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
- Evaluator.analyze(grid, O).must_equal result
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
- # TODO: Test the winners/losers in the other rows, the columns and the diagonals.
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 'squashed' do
216
+ describe "a squashed grid" do
62
217
 
63
- it 'returns squashed' do
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
- Evaluator.analyze(grid, X).must_equal result
70
- Evaluator.analyze(grid, O).must_equal result
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 'ok status' do
230
+ describe "invalid token input" do
76
231
 
77
- it 'returns ok' do
78
- result = { status: :ok }
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
- Evaluator.analyze(grid, X).must_equal result
81
- Evaluator.analyze(grid, O).must_equal result
253
+ result = { status: :invalid_grid, type: :two_winners }
82
254
 
83
- grid[1, 1] = X
84
- Evaluator.analyze(grid, X).must_equal result
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