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.
@@ -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