xo 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|