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/lib/xo/engine.rb
CHANGED
@@ -1,137 +1,256 @@
|
|
1
|
-
require 'observer'
|
2
|
-
require 'ostruct'
|
3
|
-
|
4
1
|
require 'xo/grid'
|
5
2
|
require 'xo/evaluator'
|
6
3
|
|
7
4
|
module XO
|
8
5
|
|
6
|
+
# A state machine that encapsulates the game logic for Tic-tac-toe. The operation
|
7
|
+
# of the engine is completely determined by the properties:
|
8
|
+
#
|
9
|
+
# - {#state},
|
10
|
+
# - {#turn},
|
11
|
+
# - {#grid}, and
|
12
|
+
# - {#last_event}.
|
13
|
+
#
|
14
|
+
# The engine can be in one of the 3 following states (represented by a symbol):
|
15
|
+
#
|
16
|
+
# - :init
|
17
|
+
# - :playing
|
18
|
+
# - :game_over
|
19
|
+
#
|
20
|
+
# The engine begins in the :init state. And, the following methods are used to advance
|
21
|
+
# a single game of Tic-tac-toe (and transition the engine between its states) by obeying
|
22
|
+
# the standard rules of Tic-tac-toe:
|
23
|
+
#
|
24
|
+
# - {#start}: [:init]
|
25
|
+
# - {#stop}: [:playing, :game_over]
|
26
|
+
# - {#play}: [:playing]
|
27
|
+
# - {#continue_playing}: [:game_over]
|
28
|
+
#
|
29
|
+
# The array of symbols after each method lists the states in which the method is allowed
|
30
|
+
# to be called.
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# e = Engine.new
|
34
|
+
# e.start(Grid::X).play(1, 1).play(2, 1).play(1, 2).play(2, 2).play(1, 3)
|
35
|
+
#
|
36
|
+
# event = e.last_event
|
37
|
+
# puts event[:name] # => :game_over
|
38
|
+
# puts event[:type] # => :winner
|
39
|
+
# puts event[:last_move][:turn] # => Grid::X
|
40
|
+
#
|
41
|
+
# e.continue_playing(Grid::O).play(2, 2).play(1, 1).play(3, 3).play(1, 3).play(1, 2).play(3, 2).play(2, 1).play(2, 3).play(3, 1)
|
42
|
+
#
|
43
|
+
# event = e.last_event
|
44
|
+
# puts event[:name] # => :game_over
|
45
|
+
# puts event[:type] # => :squashed
|
46
|
+
# puts event[:last_move][:turn] # => Grid::O
|
9
47
|
class Engine
|
10
|
-
include Observable
|
11
48
|
|
12
|
-
|
49
|
+
# @return [:init, :playing, :game_over]
|
50
|
+
attr_reader :state
|
51
|
+
|
52
|
+
# @return [Grid::X, Grid::O, :nobody]
|
53
|
+
attr_reader :turn
|
54
|
+
|
55
|
+
# @return [Hash]
|
56
|
+
attr_reader :last_event
|
13
57
|
|
58
|
+
# Creates a new {Engine} with its state set to :init, turn set to :nobody, an empty grid and
|
59
|
+
# last_event set to { name: :new }.
|
14
60
|
def initialize
|
15
61
|
@grid = Grid.new
|
16
|
-
|
17
|
-
|
62
|
+
|
63
|
+
reset
|
64
|
+
|
65
|
+
set_event(:new)
|
18
66
|
end
|
19
67
|
|
68
|
+
# Get the grid that's managed by the engine.
|
69
|
+
#
|
70
|
+
# @return [Grid] a copy of the grid that the engine uses
|
20
71
|
def grid
|
21
72
|
@grid.dup
|
22
73
|
end
|
23
74
|
|
75
|
+
# If the current turn is either {Grid::X}, {Grid::O} or :nobody then
|
76
|
+
# it returns {Grid::O}, {Grid::X}, :nobody respectively.
|
77
|
+
#
|
78
|
+
# @return [Grid::X, Grid::O, :nobody]
|
24
79
|
def next_turn
|
25
|
-
|
80
|
+
Grid.other_token(turn)
|
26
81
|
end
|
27
82
|
|
28
|
-
|
29
|
-
|
83
|
+
# Transitions the engine from the :init state into the :playing state.
|
84
|
+
#
|
85
|
+
# Sets the last event to be:
|
86
|
+
#
|
87
|
+
# { name: :game_started }
|
88
|
+
#
|
89
|
+
# @param turn [Grid::X, Grid::O] the token to have first play
|
90
|
+
# @raise [ArgumentError] unless turn is either {Grid::X} or {Grid::O}
|
91
|
+
# @raise [IllegalStateError] unless it's called in the :init state
|
92
|
+
# @return [self]
|
93
|
+
def start(turn)
|
94
|
+
check_turn(turn)
|
30
95
|
|
31
96
|
case state
|
32
|
-
when :
|
33
|
-
handle_start(
|
97
|
+
when :init
|
98
|
+
handle_start(turn)
|
34
99
|
else
|
35
|
-
raise
|
100
|
+
raise IllegalStateError, "must be in the :init state but state = :#{state}"
|
36
101
|
end
|
37
|
-
|
38
|
-
self
|
39
102
|
end
|
40
103
|
|
104
|
+
# Transitions the engine from the :playing or :game_over state into the :game_over state.
|
105
|
+
#
|
106
|
+
# Sets the last event to be:
|
107
|
+
#
|
108
|
+
# { name: :game_over }
|
109
|
+
#
|
110
|
+
# @raise [IllegalStateError] unless it's called in the :playing or :game_over state
|
111
|
+
# @return [self]
|
41
112
|
def stop
|
42
113
|
case state
|
43
114
|
when :playing, :game_over
|
44
115
|
handle_stop
|
45
116
|
else
|
46
|
-
raise
|
117
|
+
raise IllegalStateError, "must be in the :playing or :game_over state but state = :#{state}"
|
47
118
|
end
|
48
|
-
|
49
|
-
self
|
50
119
|
end
|
51
120
|
|
121
|
+
# Makes a move at the given position (r, c) which may transition the engine into the :game_over state
|
122
|
+
# or leave it in the :playing state.
|
123
|
+
#
|
124
|
+
# Sets the last event as follows:
|
125
|
+
#
|
126
|
+
# - If the position is out of bounds, then
|
127
|
+
#
|
128
|
+
# { name: :invalid_move, type: :out_of_bounds }
|
129
|
+
#
|
130
|
+
# - If the position is occupied, then
|
131
|
+
#
|
132
|
+
# { name: :invalid_move, type: :occupied }
|
133
|
+
#
|
134
|
+
# - If the move was allowed and didn't result in ending the game, then
|
135
|
+
#
|
136
|
+
# { name: :next_turn, last_move: { turn: :a_token, r: :a_row, c: :a_column } }
|
137
|
+
#
|
138
|
+
# - If the move was allowed and resulted in a win, then
|
139
|
+
#
|
140
|
+
# { name: :game_over, type: :winner, last_move: { turn: :a_token, r: :a_row, c: :a_column }, details: :the_details }
|
141
|
+
#
|
142
|
+
# - If the move was allowed and resulted in a squashed game, then
|
143
|
+
#
|
144
|
+
# { name: :game_over, type: :squashed, last_move: { turn: :a_token, r: :a_row, c: :a_column } }
|
145
|
+
#
|
146
|
+
# Legend:
|
147
|
+
#
|
148
|
+
# - :a_token is one of {Grid::X} or {Grid::O}
|
149
|
+
# - :a_row is one of 1, 2 or 3
|
150
|
+
# - :a_column is one of 1, 2 or 3
|
151
|
+
# - :the_details is taken verbatim from the :details key of the returned hash of {Evaluator.analyze}
|
152
|
+
#
|
153
|
+
# @param r [Integer] the row
|
154
|
+
# @param c [Integer] the column
|
155
|
+
# @raise [IllegalStateError] unless it's called in the :playing state
|
156
|
+
# @return [self]
|
52
157
|
def play(r, c)
|
53
158
|
case state
|
54
159
|
when :playing
|
55
|
-
handle_play(r
|
160
|
+
handle_play(r, c)
|
56
161
|
else
|
57
|
-
raise
|
162
|
+
raise IllegalStateError, "must be in the :playing state but state = :#{state}"
|
58
163
|
end
|
59
|
-
|
60
|
-
self
|
61
164
|
end
|
62
165
|
|
63
|
-
|
64
|
-
|
166
|
+
# Similar to start but should only be used to play another round when a game has ended. It transitions
|
167
|
+
# the engine from the :game_over state into the :playing state.
|
168
|
+
#
|
169
|
+
# Sets the last event to be:
|
170
|
+
#
|
171
|
+
# { name: :game_started, type: :continue_playing }
|
172
|
+
#
|
173
|
+
# @param turn [Grid::X, Grid::O] the token to have first play
|
174
|
+
# @raise [ArgumentError] unless turn is either {Grid::X} or {Grid::O}
|
175
|
+
# @raise [IllegalStateError] unless it's called in the :game_over state
|
176
|
+
# @return [self]
|
177
|
+
def continue_playing(turn)
|
178
|
+
check_turn(turn)
|
65
179
|
|
66
180
|
case state
|
67
181
|
when :game_over
|
68
|
-
handle_continue_playing(
|
182
|
+
handle_continue_playing(turn)
|
69
183
|
else
|
70
|
-
raise
|
184
|
+
raise IllegalStateError, "must be in the :game_over state but state = :#{state}"
|
71
185
|
end
|
72
|
-
|
73
|
-
self
|
74
186
|
end
|
75
187
|
|
76
|
-
|
188
|
+
# The exception raised by {#start}, {#stop}, {#play} and {#continue_playing} whenever
|
189
|
+
# these methods are called and the engine is in the wrong state.
|
190
|
+
class IllegalStateError < StandardError; end
|
77
191
|
|
78
|
-
|
192
|
+
private
|
79
193
|
|
80
|
-
def handle_start(
|
81
|
-
|
82
|
-
|
194
|
+
def handle_start(turn)
|
195
|
+
@state = :playing
|
196
|
+
@turn = turn
|
83
197
|
@grid.clear
|
84
198
|
|
85
|
-
|
199
|
+
set_event(:game_started)
|
86
200
|
end
|
87
201
|
|
88
202
|
def handle_stop
|
89
|
-
|
203
|
+
reset
|
90
204
|
|
91
|
-
|
205
|
+
set_event(:game_stopped)
|
92
206
|
end
|
93
207
|
|
94
208
|
def handle_play(r, c)
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
end
|
116
|
-
else
|
117
|
-
send_event(:invalid_move, type: :occupied)
|
209
|
+
return set_event(:invalid_move, type: :out_of_bounds) unless Grid.contains?(r, c)
|
210
|
+
return set_event(:invalid_move, type: :occupied) unless @grid.open?(r, c)
|
211
|
+
|
212
|
+
@grid[r, c] = turn
|
213
|
+
last_move = { turn: turn, r: r, c: c }
|
214
|
+
|
215
|
+
result = Evaluator.instance.analyze(@grid, turn)
|
216
|
+
|
217
|
+
case result[:status]
|
218
|
+
when :ok
|
219
|
+
@turn = next_turn
|
220
|
+
set_event(:next_turn, last_move: last_move)
|
221
|
+
when :game_over
|
222
|
+
@state = :game_over
|
223
|
+
|
224
|
+
case result[:type]
|
225
|
+
when :winner
|
226
|
+
set_event(:game_over, type: :winner, last_move: last_move, details: result[:details])
|
227
|
+
when :squashed
|
228
|
+
set_event(:game_over, type: :squashed, last_move: last_move)
|
118
229
|
end
|
119
|
-
else
|
120
|
-
send_event(:invalid_move, type: :out_of_bounds)
|
121
230
|
end
|
122
231
|
end
|
123
232
|
|
124
|
-
def handle_continue_playing(
|
125
|
-
|
126
|
-
|
233
|
+
def handle_continue_playing(turn)
|
234
|
+
@turn = turn
|
235
|
+
@state = :playing
|
127
236
|
@grid.clear
|
128
237
|
|
129
|
-
|
238
|
+
set_event(:game_started, type: :continue_playing)
|
239
|
+
end
|
240
|
+
|
241
|
+
def check_turn(turn)
|
242
|
+
raise ArgumentError, "illegal token #{turn}" unless Grid.is_token?(turn)
|
243
|
+
end
|
244
|
+
|
245
|
+
def reset
|
246
|
+
@state = :init
|
247
|
+
@turn = :nobody
|
248
|
+
@grid.clear
|
130
249
|
end
|
131
250
|
|
132
|
-
def
|
133
|
-
|
134
|
-
|
251
|
+
def set_event(name, message = {})
|
252
|
+
@last_event = { name: name }.merge(message)
|
253
|
+
self
|
135
254
|
end
|
136
255
|
end
|
137
256
|
end
|
data/lib/xo/evaluator.rb
CHANGED
@@ -1,31 +1,125 @@
|
|
1
|
-
|
1
|
+
require 'singleton'
|
2
2
|
|
3
|
-
|
3
|
+
require 'xo/grid'
|
4
4
|
|
5
|
-
|
6
|
-
@grid = grid
|
7
|
-
@player = player
|
5
|
+
module XO
|
8
6
|
|
7
|
+
# This class defines an {Evaluator#analyze} method than can be used to look at a grid and
|
8
|
+
# answer the following questions:
|
9
|
+
#
|
10
|
+
# 1. Is it a valid grid? A grid is considered valid if it possible for
|
11
|
+
# two players, taking turns, to reach the given grid configuration.
|
12
|
+
# 2. Is there a winner/loser or is the grid squashed?
|
13
|
+
# 3. Who is the winner/loser?
|
14
|
+
# 4. Which positions make up the winning row, column and/or diagonal?
|
15
|
+
#
|
16
|
+
# The Evaluator class is a Singleton class and can be used as follows:
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# Evaluator.instance.analyze(Grid.new('xo'), Grid::X)
|
20
|
+
class Evaluator
|
21
|
+
include Singleton
|
22
|
+
|
23
|
+
# Analyze a given grid assuming that the given token is the one that was last placed on it.
|
24
|
+
#
|
25
|
+
# It can return a hash in following formats:
|
26
|
+
#
|
27
|
+
# - If everything is fine, then
|
28
|
+
#
|
29
|
+
# { status: :ok }
|
30
|
+
#
|
31
|
+
# - If the game is over and the given token is in a winning position, then
|
32
|
+
#
|
33
|
+
# { status: :game_over, type: :winner, details: [{ where: :a_where, index: :an_index, positions: :the_positions }] }
|
34
|
+
#
|
35
|
+
# - If the game is over and the other token is in a winning position, then
|
36
|
+
#
|
37
|
+
# { status: :game_over, type: :loser, details: [{ where: :a_where, index: :an_index, positions: :the_positions }] }
|
38
|
+
#
|
39
|
+
# - If the game is over due to a squashed grid, then
|
40
|
+
#
|
41
|
+
# { status: :game_over, type: :squashed }
|
42
|
+
#
|
43
|
+
# - If there is too much of one token, then
|
44
|
+
#
|
45
|
+
# { status: :invalid_grid, type: :too_many_moves_ahead }
|
46
|
+
#
|
47
|
+
# - If both tokens are arranged in winning positions, then
|
48
|
+
#
|
49
|
+
# { status: :invalid_grid, type: :two_winners }
|
50
|
+
#
|
51
|
+
# Legend:
|
52
|
+
#
|
53
|
+
# - :a_where is one of :row, :column, :diagonal
|
54
|
+
# - :an_index is one of 1, 2, 3 if :a_where is :row or :column and one of 1, 2 if :a_where is :diagonal
|
55
|
+
# - :the_positions is a 3 element array having the row, column values of the winning position
|
56
|
+
#
|
57
|
+
# Notice that the :details key is an array since it is possible to win a game in two different ways. For
|
58
|
+
# example:
|
59
|
+
#
|
60
|
+
# x | o | x
|
61
|
+
# ---+---+---
|
62
|
+
# o | x | o
|
63
|
+
# ---+---+---
|
64
|
+
# x | o | x
|
65
|
+
#
|
66
|
+
# # Position (2, 2) would have to be the last position played for this to happen.
|
67
|
+
#
|
68
|
+
# @param grid [Grid] the grid to be analyzed
|
69
|
+
# @param token [Grid::X, Grid::O] the token that was last placed on the grid
|
70
|
+
# @raise [ArgumentError] unless token is either {Grid::X} or {Grid::O}
|
71
|
+
# @return [Hash]
|
72
|
+
def analyze(grid, token)
|
73
|
+
check_token(token)
|
74
|
+
init_analyzer(grid, token)
|
9
75
|
perform_analysis
|
10
76
|
end
|
11
77
|
|
78
|
+
# Returns the number of {Grid::X}'s and {Grid::O}'s in the given grid.
|
79
|
+
#
|
80
|
+
# @example
|
81
|
+
# g = Grid.new('xoxxo')
|
82
|
+
# xs, os = Evaluator.instance.xos(g)
|
83
|
+
# puts xs # => 3
|
84
|
+
# puts os # => 2
|
85
|
+
#
|
86
|
+
# @return [Array(Integer, Integer)]
|
87
|
+
def xos(grid)
|
88
|
+
xs = os = 0
|
89
|
+
|
90
|
+
grid.each do |_, _, val|
|
91
|
+
xs += 1 if val == Grid::X
|
92
|
+
os += 1 if val == Grid::O
|
93
|
+
end
|
94
|
+
|
95
|
+
[xs, os]
|
96
|
+
end
|
97
|
+
|
12
98
|
private
|
13
99
|
|
14
|
-
|
15
|
-
|
100
|
+
attr_reader :grid, :token, :winners
|
101
|
+
|
102
|
+
def check_token(token)
|
103
|
+
raise ArgumentError, "illegal token #{token}" unless Grid.is_token?(token)
|
16
104
|
end
|
17
105
|
|
18
|
-
def
|
19
|
-
|
106
|
+
def init_analyzer(grid, token)
|
107
|
+
@grid = grid
|
108
|
+
@token = token
|
109
|
+
@winners = {}
|
110
|
+
end
|
111
|
+
|
112
|
+
def perform_analysis
|
113
|
+
return { status: :invalid_grid, type: :too_many_moves_ahead } if two_or_more_moves_ahead?
|
20
114
|
|
21
115
|
find_winners
|
22
116
|
|
23
117
|
if two_winners?
|
24
|
-
{ status: :
|
25
|
-
elsif winners[
|
26
|
-
{ status: :game_over, type: :winner, details: winners[
|
27
|
-
elsif winners[
|
28
|
-
{ status: :game_over, type: :loser, details: winners[
|
118
|
+
{ status: :invalid_grid, type: :two_winners }
|
119
|
+
elsif winners[token]
|
120
|
+
{ status: :game_over, type: :winner, details: winners[token] }
|
121
|
+
elsif winners[other_token]
|
122
|
+
{ status: :game_over, type: :loser, details: winners[other_token] }
|
29
123
|
else
|
30
124
|
if grid.full?
|
31
125
|
{ status: :game_over, type: :squashed }
|
@@ -35,74 +129,55 @@ module XO
|
|
35
129
|
end
|
36
130
|
end
|
37
131
|
|
38
|
-
def
|
132
|
+
def two_or_more_moves_ahead?
|
39
133
|
moves_ahead >= 2
|
40
134
|
end
|
41
135
|
|
42
|
-
def
|
43
|
-
xs
|
44
|
-
|
45
|
-
grid.each do |_, _, val|
|
46
|
-
xs += 1 if val == XO::X
|
47
|
-
os += 1 if val == XO::O
|
48
|
-
end
|
136
|
+
def moves_ahead
|
137
|
+
xs, os = xos(grid)
|
49
138
|
|
50
139
|
(xs - os).abs
|
51
140
|
end
|
52
141
|
|
53
|
-
def
|
54
|
-
|
142
|
+
def find_winners
|
143
|
+
winning_positions.each do |w|
|
144
|
+
a = grid[*w[:positions][0]]
|
145
|
+
b = grid[*w[:positions][1]]
|
146
|
+
c = grid[*w[:positions][2]]
|
55
147
|
|
56
|
-
|
57
|
-
if XO.is_token?(grid[1, 1]) && grid[1, 1] == grid[1, 2] && grid[1, 2] == grid[1, 3]
|
58
|
-
add_winner(grid[1, 1], { where: :row, index: 1, positions: [[1, 1], [1, 2], [1, 3]] })
|
59
|
-
end
|
60
|
-
|
61
|
-
if XO.is_token?(grid[2, 1]) && grid[2, 1] == grid[2, 2] && grid[2, 2] == grid[2, 3]
|
62
|
-
add_winner(grid[2, 1], { where: :row, index: 2, positions: [[2, 1], [2, 2], [2, 3]] })
|
63
|
-
end
|
64
|
-
|
65
|
-
if XO.is_token?(grid[3, 1]) && grid[3, 1] == grid[3, 2] && grid[3, 2] == grid[3, 3]
|
66
|
-
add_winner(grid[3, 1], { where: :row, index: 3, positions: [[3, 1], [3, 2], [3, 3]] })
|
67
|
-
end
|
68
|
-
|
69
|
-
# check columns
|
70
|
-
if XO.is_token?(grid[1, 1]) && grid[1, 1] == grid[2, 1] && grid[2, 1] == grid[3, 1]
|
71
|
-
add_winner(grid[1, 1], { where: :column, index: 1, positions: [[1, 1], [2, 1], [3, 1]] })
|
72
|
-
end
|
73
|
-
|
74
|
-
if XO.is_token?(grid[1, 2]) && grid[1, 2] == grid[2, 2] && grid[2, 2] == grid[3, 2]
|
75
|
-
add_winner(grid[1, 2], { where: :column, index: 2, positions: [[1, 2], [2, 2], [3, 2]] })
|
148
|
+
add_winner(a, w) if Grid.is_token?(a) && a == b && b == c
|
76
149
|
end
|
150
|
+
end
|
77
151
|
|
78
|
-
|
79
|
-
|
80
|
-
|
152
|
+
def winning_positions
|
153
|
+
@winning_positions ||= [
|
154
|
+
{ where: :row, index: 1, positions: [[1, 1], [1, 2], [1, 3]] },
|
155
|
+
{ where: :row, index: 2, positions: [[2, 1], [2, 2], [2, 3]] },
|
156
|
+
{ where: :row, index: 3, positions: [[3, 1], [3, 2], [3, 3]] },
|
81
157
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
end
|
158
|
+
{ where: :column, index: 1, positions: [[1, 1], [2, 1], [3, 1]] },
|
159
|
+
{ where: :column, index: 2, positions: [[1, 2], [2, 2], [3, 2]] },
|
160
|
+
{ where: :column, index: 3, positions: [[1, 3], [2, 3], [3, 3]] },
|
86
161
|
|
87
|
-
|
88
|
-
|
89
|
-
|
162
|
+
{ where: :diagonal, index: 1, positions: [[1, 1], [2, 2], [3, 3]] },
|
163
|
+
{ where: :diagonal, index: 2, positions: [[1, 3], [2, 2], [3, 1]] }
|
164
|
+
]
|
90
165
|
end
|
91
166
|
|
92
|
-
def
|
93
|
-
if winners.key?(
|
94
|
-
winners[
|
167
|
+
def add_winner(token, details)
|
168
|
+
if winners.key?(token)
|
169
|
+
winners[token] << details
|
95
170
|
else
|
96
|
-
winners[
|
171
|
+
winners[token] = [details]
|
97
172
|
end
|
98
173
|
end
|
99
174
|
|
100
|
-
def
|
101
|
-
winners[
|
175
|
+
def two_winners?
|
176
|
+
winners[Grid::X] && winners[Grid::O]
|
102
177
|
end
|
103
178
|
|
104
|
-
def
|
105
|
-
|
179
|
+
def other_token
|
180
|
+
Grid.other_token(token)
|
106
181
|
end
|
107
182
|
end
|
108
183
|
end
|