xo 1.0.0 → 1.1.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 +8 -8
- data/Gemfile.lock +3 -1
- data/README.md +59 -23
- data/bin/xo +22 -27
- data/lib/xo.rb +1 -1
- data/lib/xo/ai/max_player.rb +22 -0
- data/lib/xo/ai/min_player.rb +22 -0
- data/lib/xo/ai/minimax.rb +47 -108
- data/lib/xo/ai/player.rb +44 -0
- data/lib/xo/engine.rb +32 -234
- data/lib/xo/engine/game_context.rb +28 -0
- data/lib/xo/engine/game_over.rb +33 -0
- data/lib/xo/engine/game_state.rb +21 -0
- data/lib/xo/engine/init.rb +24 -0
- data/lib/xo/engine/playing.rb +88 -0
- data/lib/xo/evaluator.rb +50 -66
- data/lib/xo/grid.rb +41 -93
- data/lib/xo/version.rb +1 -1
- data/spec/engine_spec.rb +323 -0
- data/spec/{xo/evaluator_spec.rb → evaluator_spec.rb} +25 -10
- data/spec/{xo/ai/geometric_grid_spec.rb → geometric_grid_spec.rb} +1 -3
- data/spec/{xo/grid_spec.rb → grid_spec.rb} +13 -16
- data/spec/{xo/ai/minimax_spec.rb → minimax_spec.rb} +0 -0
- data/xo.gemspec +3 -1
- metadata +36 -14
- data/lib/xo/ai.rb +0 -2
- data/spec/xo/engine_spec.rb +0 -331
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
ZmU4ZmMyZTlhMjE5NWI1OWI1YmZmNGNjYjYwZTQwZjEzZGE3YzQxZQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MzBlZDEyZDZlZjQyOTRjYzE3Mjc4NzA2NzBmNWU5Y2RlODU5OGQzZA==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
MzNlNWQ2Zjg0ZjdjNjIzZGY1MWM3NDE5ZmM0ODNkMDU1ZDdlMWYxM2E3NWU4
|
10
|
+
MmQ3Zjk2ZGU3M2Q1YmFiMzZjZDgwMDcwY2RiY2VjMzA5ZjU5ZTNiM2U3MDM4
|
11
|
+
MWQyMzEzOGVmM2E1OTcwYWM5MWVmOWQ3YjE0MThiYWIyNWUzZGI=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
NjNhMzIyZTNjODgxNjE2Yzg0ZTk3ZDg1MjEzMjFhZTI4ZTdkYjExMWNjNDk3
|
14
|
+
ZjI0ZTQ5YmU5MDdhZjY3ZDRjMjI1Yjc0OGIwMWUzMDdhMGFhMGE1M2U0ODE0
|
15
|
+
ODcyZTllODk0NjQzY2RlYzFiMmQzODcyNjlmMzA1MTk4MDA0MmY=
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
xo (0.0
|
4
|
+
xo (1.0.0)
|
5
|
+
state_design_pattern (~> 0.0.2)
|
5
6
|
|
6
7
|
GEM
|
7
8
|
remote: https://rubygems.org/
|
@@ -25,6 +26,7 @@ GEM
|
|
25
26
|
multi_json
|
26
27
|
simplecov-html (~> 0.8.0)
|
27
28
|
simplecov-html (0.8.0)
|
29
|
+
state_design_pattern (0.0.2)
|
28
30
|
term-ansicolor (1.3.0)
|
29
31
|
tins (~> 1.0)
|
30
32
|
thor (0.19.1)
|
data/README.md
CHANGED
@@ -5,11 +5,20 @@
|
|
5
5
|
[](https://coveralls.io/r/dwayne/xo)
|
6
6
|
[](https://codeclimate.com/github/dwayne/xo)
|
7
7
|
|
8
|
-
A [Ruby](http://www.ruby-lang.org/en/) library for
|
8
|
+
A [Ruby](http://www.ruby-lang.org/en/) library for
|
9
|
+
[Tic-tac-toe](http://en.wikipedia.org/wiki/Tic-tac-toe).
|
9
10
|
|
10
|
-
The code is well documented and fully tested, so please have a
|
11
|
+
The code is well documented and fully tested, so please have a
|
12
|
+
[read of the documentation](http://rubydoc.info/github/dwayne/xo) and have a
|
13
|
+
[look at the tests](https://github.com/dwayne/xo/tree/master/spec/xo).
|
11
14
|
|
12
|
-
My implementation of the [Minimax algorithm](http://en.wikipedia.org/wiki/Minimax#Minimax_algorithm_with_alternate_moves)
|
15
|
+
My implementation of the [Minimax algorithm](http://en.wikipedia.org/wiki/Minimax#Minimax_algorithm_with_alternate_moves)
|
16
|
+
might be a little different than what you've seen before. It uses symmetry to
|
17
|
+
significantly reduce the search space and in so doing we get good
|
18
|
+
[performance out of the algorithm](#performance-of-the-minimax-algorithm).
|
19
|
+
However, I still want to get it under 1 second. I'd love to hear your thoughts
|
20
|
+
on how I can make it happen. Have a
|
21
|
+
[look](https://github.com/dwayne/xo/blob/master/lib/xo/ai/minimax.rb#L23).
|
13
22
|
|
14
23
|
## Installation
|
15
24
|
|
@@ -23,7 +32,6 @@ Managing the grid yourself.
|
|
23
32
|
|
24
33
|
```ruby
|
25
34
|
require 'xo'
|
26
|
-
|
27
35
|
include XO
|
28
36
|
|
29
37
|
g = Grid.new('xx oo')
|
@@ -34,10 +42,12 @@ puts g # => x | x |
|
|
34
42
|
# ---+---+---
|
35
43
|
# | |
|
36
44
|
|
37
|
-
Evaluator.
|
45
|
+
evaluator = Evaluator.instance
|
46
|
+
|
47
|
+
evaluator.analyze(g, Grid::X) # => { status: :ok }
|
38
48
|
|
39
49
|
g[1, 3] = Grid::X
|
40
|
-
|
50
|
+
evaluator.analyze(g, Grid::X) # => { status: :game_over,
|
41
51
|
# type: :winner,
|
42
52
|
# details: [{
|
43
53
|
# where: :row,
|
@@ -46,7 +56,7 @@ Evaluator.analyze(g, Grid::X) # => { status: :game_over,
|
|
46
56
|
# }]
|
47
57
|
# }
|
48
58
|
|
49
|
-
|
59
|
+
evaluator.analyze(g, Grid::O) # => { status: :game_over,
|
50
60
|
# type: :loser,
|
51
61
|
# details: [{
|
52
62
|
# where: :row,
|
@@ -56,32 +66,52 @@ Evaluator.analyze(g, Grid::O) # => { status: :game_over,
|
|
56
66
|
# }
|
57
67
|
```
|
58
68
|
|
59
|
-
The problem with managing the grid yourself is that there is nothing stopping
|
69
|
+
The problem with managing the grid yourself is that there is nothing stopping
|
70
|
+
you from making bad moves. For example, playing twice.
|
60
71
|
|
61
72
|
```ruby
|
62
73
|
g = Grid.new('xx')
|
63
|
-
|
74
|
+
evaluator.analyze(g, Grid::O) # => { status: :invalid_grid,
|
64
75
|
# type: :too_many_moves_ahead
|
65
76
|
# }
|
66
77
|
```
|
67
78
|
|
68
|
-
To avoid such situations, let the engine handle game play. Once you tell it who
|
79
|
+
To avoid such situations, let the engine handle game play. Once you tell it who
|
80
|
+
plays first, then it ensures that the game play follows the rules of
|
81
|
+
[Tic-tac-toe](http://en.wikipedia.org/wiki/Tic-tac-toe).
|
69
82
|
|
70
83
|
```ruby
|
71
84
|
e = Engine.new
|
85
|
+
|
86
|
+
class EngineObserver
|
87
|
+
|
88
|
+
attr_reader :last_event
|
89
|
+
|
90
|
+
def update(event)
|
91
|
+
@last_event = event
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
observer = EngineObserver.new
|
96
|
+
e.add_observer(observer)
|
97
|
+
|
72
98
|
e.start(Grid::O).play(2, 1).play(1, 1).play(2, 2).play(1, 2).play(2, 3)
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
99
|
+
observer.last_event # => { name: :game_over,
|
100
|
+
# source: #<XO::Engine...>
|
101
|
+
# type: :winner,
|
102
|
+
# last_move: { turn: :o, r: 2, c: 3 },
|
103
|
+
# details: [{
|
104
|
+
# where: :row,
|
105
|
+
# index: 2,
|
106
|
+
# positions: [[2, 1], [2, 2], [2, 3]]
|
107
|
+
# }]
|
108
|
+
# }
|
82
109
|
```
|
83
110
|
|
84
|
-
I also built a [Tic-tac-toe](http://en.wikipedia.org/wiki/Tic-tac-toe)
|
111
|
+
I also built a [Tic-tac-toe](http://en.wikipedia.org/wiki/Tic-tac-toe)
|
112
|
+
command-line client that uses the library. See how everything comes together by
|
113
|
+
viewing its implementation right
|
114
|
+
[here](https://github.com/dwayne/xo/blob/master/bin/xo).
|
85
115
|
|
86
116
|
To use the [client](https://github.com/dwayne/xo/blob/master/bin/xo) just type,
|
87
117
|
|
@@ -110,11 +140,17 @@ You can run:
|
|
110
140
|
|
111
141
|
## Contributing
|
112
142
|
|
113
|
-
If you'd like to contribute a feature or bugfix: Thanks! To make sure your
|
143
|
+
If you'd like to contribute a feature or bugfix: Thanks! To make sure your
|
144
|
+
fix/feature has a high chance of being included, please read the following
|
145
|
+
guidelines:
|
114
146
|
|
115
147
|
1. Post a [pull request](https://github.com/dwayne/xo/compare/).
|
116
|
-
2. Make sure there are tests! I will not accept any patch that is not tested.
|
148
|
+
2. Make sure there are tests! I will not accept any patch that is not tested.
|
149
|
+
It's a rare time when explicit tests aren't needed. If you have questions
|
150
|
+
about writing tests for xo, please open a
|
151
|
+
[GitHub issue](https://github.com/dwayne/xo/issues/new).
|
117
152
|
|
118
153
|
## License
|
119
154
|
|
120
|
-
xo is Copyright © 2014 Dwayne R. Crooks. It is free software, and may be
|
155
|
+
xo is Copyright © 2014 Dwayne R. Crooks. It is free software, and may be
|
156
|
+
redistributed under the terms specified in the MIT-LICENSE file.
|
data/bin/xo
CHANGED
@@ -168,46 +168,41 @@ class Game
|
|
168
168
|
|
169
169
|
def initialize(players)
|
170
170
|
@players = players
|
171
|
+
|
171
172
|
@engine = Engine.new
|
173
|
+
@engine.add_observer(self)
|
172
174
|
end
|
173
175
|
|
174
176
|
def start(turn = false)
|
175
|
-
|
176
|
-
@engine.start(turn)
|
177
|
-
else
|
178
|
-
event = @engine.last_event
|
179
|
-
|
180
|
-
case event[:type]
|
181
|
-
when :winner
|
182
|
-
@engine.continue_playing(@engine.turn)
|
183
|
-
when :squashed
|
184
|
-
@engine.continue_playing(@engine.next_turn)
|
185
|
-
end
|
186
|
-
end
|
177
|
+
turn ? engine.start(turn) : engine.continue_playing
|
187
178
|
end
|
188
179
|
|
189
180
|
def run
|
190
|
-
run_one_turn
|
181
|
+
run_one_turn until engine.current_state == GameOver
|
191
182
|
end
|
192
183
|
|
193
|
-
|
184
|
+
def update(event)
|
185
|
+
player = players[engine.turn]
|
194
186
|
|
195
|
-
|
196
|
-
|
197
|
-
player
|
187
|
+
case event[:name]
|
188
|
+
when :next_turn
|
189
|
+
player.handle_next_turn(engine.grid)
|
190
|
+
when :game_over
|
191
|
+
player.handle_game_over(engine.grid, event)
|
192
|
+
when :invalid_move
|
193
|
+
player.handle_invalid_move(event)
|
194
|
+
end
|
195
|
+
end
|
198
196
|
|
199
|
-
|
197
|
+
private
|
200
198
|
|
201
|
-
|
199
|
+
attr_reader :players, :engine
|
202
200
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
when :invalid_move
|
209
|
-
player.handle_invalid_move(event)
|
210
|
-
end
|
201
|
+
def run_one_turn
|
202
|
+
turn = engine.turn
|
203
|
+
r, c = players[turn].get_move(engine.grid, turn)
|
204
|
+
|
205
|
+
engine.play(r, c)
|
211
206
|
end
|
212
207
|
end
|
213
208
|
|
data/lib/xo.rb
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'xo/ai/player'
|
2
|
+
|
3
|
+
module XO
|
4
|
+
|
5
|
+
module AI
|
6
|
+
|
7
|
+
class MaxPlayer < Player
|
8
|
+
|
9
|
+
def best_score(next_grids_scores)
|
10
|
+
next_grids_scores.max
|
11
|
+
end
|
12
|
+
|
13
|
+
def winner_value
|
14
|
+
1
|
15
|
+
end
|
16
|
+
|
17
|
+
def loser_value
|
18
|
+
-1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'xo/ai/player'
|
2
|
+
|
3
|
+
module XO
|
4
|
+
|
5
|
+
module AI
|
6
|
+
|
7
|
+
class MinPlayer < Player
|
8
|
+
|
9
|
+
def best_score(next_grids_scores)
|
10
|
+
next_grids_scores.min
|
11
|
+
end
|
12
|
+
|
13
|
+
def winner_value
|
14
|
+
-1
|
15
|
+
end
|
16
|
+
|
17
|
+
def loser_value
|
18
|
+
1
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/xo/ai/minimax.rb
CHANGED
@@ -2,6 +2,8 @@ require 'singleton'
|
|
2
2
|
|
3
3
|
require 'xo/evaluator'
|
4
4
|
require 'xo/ai/geometric_grid'
|
5
|
+
require 'xo/ai/min_player'
|
6
|
+
require 'xo/ai/max_player'
|
5
7
|
|
6
8
|
module XO
|
7
9
|
|
@@ -14,49 +16,13 @@ module XO
|
|
14
16
|
# The search space forms a tree where the root is the empty grid and every other node is a possible grid configuration that
|
15
17
|
# can be reached by playing through a game of Tic-tac-toe.
|
16
18
|
#
|
17
|
-
#
|
18
|
-
# making one move in each of its open positions. For example, given the node
|
19
|
-
#
|
20
|
-
# x | o | x
|
21
|
-
# ---+---+---
|
22
|
-
# | x |
|
23
|
-
# ---+---+---
|
24
|
-
# o | | o
|
25
|
-
#
|
26
|
-
# and knowing that it's {XO::Grid::O}'s (the min player) turn to play. Then, its children will be the 3 nodes
|
27
|
-
#
|
28
|
-
# A B C
|
29
|
-
#
|
30
|
-
# x | o | x x | o | x x | o | x
|
31
|
-
# ---+---+--- ---+---+--- ---+---+---
|
32
|
-
# o | x | | x | o | x |
|
33
|
-
# ---+---+--- ---+---+--- ---+---+---
|
34
|
-
# o | | o o | | o o | o | o
|
35
|
-
#
|
36
|
-
# since there are 3 open positions in which {XO::Grid::O} can make a move.
|
37
|
-
#
|
38
|
-
# Within the implementation, A and B will be considered intermediate nodes and so the search algorithm will have to continue until
|
39
|
-
# it can make a conclusive determination. That occurs when it reaches a terminal node, like C. In that case, the algorithm assigns
|
40
|
-
# a value to the terminal node from the perspective of the player that has to play next. So in C's case,
|
41
|
-
# {XO::Grid::X} (the max player) has to play next. But {XO::Grid::X} can't play because {XO::Grid::O} won. So {XO::Grid::X} would
|
42
|
-
# value C with a low value, -1 in this case.
|
43
|
-
#
|
44
|
-
# Each intermediate node can now get a value in the following way. Consider node A. It's {XO::Grid::X}'s turn to play and
|
45
|
-
# {XO::Grid::X} is the max player. The max player seeks to maximize their value over all the values of its children (conversely,
|
46
|
-
# the min player seeks to minimize their value over all its children). It has 2 children and they will eventually be determined
|
47
|
-
# to have the values 0 and -1. Since 0 is greater than -1, A will get the value of 0. What this means essentially is that the max
|
48
|
-
# player will play to favor a squashed game rather than a losing game in this particular instance.
|
49
|
-
#
|
50
|
-
# It is interesting to note that B is simply a reflection of A and so will end up having the same value. The algorithm below is
|
51
|
-
# smart enough to recognize that and so it will not have to perform a similar calculation in B's case.
|
52
|
-
#
|
53
|
-
# The Minimax class is a Singleton class. You use it as follows:
|
19
|
+
# {Minimax} is a Singleton which can be used as follows:
|
54
20
|
#
|
55
21
|
# @example
|
56
22
|
# Minimax.instance.moves(XO::Grid.new('xox x o o'), XO::Grid::O) # => [[3, 2]]
|
57
23
|
#
|
58
24
|
# The first time the instance of Minimax is created, it runs the minimax algorithm to compute the value of all the nodes in the
|
59
|
-
# search space. This of course takes a bit of time (~ 4 seconds), but subsequent calls are
|
25
|
+
# search space. This of course takes a bit of time (~ 4 seconds), but subsequent calls are super fast.
|
60
26
|
class Minimax
|
61
27
|
include Singleton
|
62
28
|
|
@@ -67,14 +33,21 @@ module XO
|
|
67
33
|
# @raise [ArgumentError] if turn is not a token or the combination of the values of grid and turn doesn't make sense
|
68
34
|
# @return [Array<Array(Integer, Integer)>]
|
69
35
|
def moves(grid, turn)
|
70
|
-
|
71
|
-
|
72
|
-
best_moves(*lift(grid, turn))
|
36
|
+
check_turn(turn)
|
37
|
+
best_moves(*normalize(grid, turn))
|
73
38
|
end
|
74
39
|
|
75
40
|
private
|
76
41
|
|
77
|
-
attr_reader :
|
42
|
+
attr_reader :master_grid, :scores
|
43
|
+
|
44
|
+
X = GeometricGrid::X
|
45
|
+
O = GeometricGrid::O
|
46
|
+
|
47
|
+
EMPTY = GeometricGrid::EMPTY
|
48
|
+
|
49
|
+
MAX_PLAYER = MaxPlayer.new(X)
|
50
|
+
MIN_PLAYER = MinPlayer.new(O)
|
78
51
|
|
79
52
|
def initialize
|
80
53
|
init_search
|
@@ -82,74 +55,74 @@ module XO
|
|
82
55
|
end
|
83
56
|
|
84
57
|
def init_search
|
85
|
-
@
|
86
|
-
@scores
|
58
|
+
@master_grid = GeometricGrid.new
|
59
|
+
@scores = {}
|
87
60
|
end
|
88
61
|
|
89
|
-
def build_search_tree(
|
62
|
+
def build_search_tree(player_a = MAX_PLAYER, player_b = MIN_PLAYER)
|
90
63
|
return if has_score?
|
91
64
|
|
92
|
-
analyze_grid(
|
65
|
+
analyze_grid(player_a)
|
93
66
|
|
94
67
|
if terminal?
|
95
|
-
|
68
|
+
set_terminal_score(player_a)
|
96
69
|
else
|
97
70
|
next_grids = []
|
98
71
|
|
99
|
-
|
100
|
-
|
101
|
-
next_grids <<
|
72
|
+
master_grid.each_open do |r, c|
|
73
|
+
master_grid[r, c] = player_a.token
|
74
|
+
next_grids << master_grid.dup
|
102
75
|
|
103
|
-
build_search_tree(
|
76
|
+
build_search_tree(player_b, player_a)
|
104
77
|
|
105
|
-
|
78
|
+
master_grid[r, c] = EMPTY
|
106
79
|
end
|
107
80
|
|
108
|
-
|
81
|
+
set_non_terminal_score(player_a, next_grids)
|
109
82
|
end
|
110
83
|
end
|
111
84
|
|
112
85
|
def has_score?
|
113
|
-
scores.key?(
|
86
|
+
scores.key?(master_grid)
|
114
87
|
end
|
115
88
|
|
116
89
|
def analyze_grid(player)
|
117
|
-
@
|
90
|
+
@result = Evaluator.instance.analyze(master_grid, player.token)
|
118
91
|
end
|
119
92
|
|
120
93
|
def terminal?
|
121
|
-
@
|
94
|
+
@result[:status] == :game_over
|
122
95
|
end
|
123
96
|
|
124
|
-
def
|
125
|
-
scores[
|
97
|
+
def set_terminal_score(player)
|
98
|
+
scores[master_grid.dup] = player.terminal_score(@result[:type])
|
126
99
|
end
|
127
100
|
|
128
|
-
def
|
129
|
-
scores[
|
101
|
+
def set_non_terminal_score(player, next_grids)
|
102
|
+
scores[master_grid.dup] = player.non_terminal_score(next_grids, scores)
|
130
103
|
end
|
131
104
|
|
132
|
-
# The search tree that gets built is for the situation when
|
105
|
+
# The search tree that gets built is for the situation when X is assumed to
|
133
106
|
# have played first. However, if we are given a grid to evaluate such that
|
134
|
-
# it can only be reached by assuming that
|
107
|
+
# it can only be reached by assuming that O played first then we need to
|
135
108
|
# patch things up so that we can find a representative in our search space
|
136
109
|
# for the given configuration.
|
137
|
-
def
|
138
|
-
xs, os = Evaluator.
|
110
|
+
def normalize(grid, turn)
|
111
|
+
xs, os = Evaluator.xos(grid)
|
139
112
|
|
140
|
-
if turn ==
|
113
|
+
if turn == X
|
141
114
|
if xs == os
|
142
|
-
[GeometricGrid.new(grid.inspect),
|
115
|
+
[GeometricGrid.new(grid.inspect), X]
|
143
116
|
elsif xs < os
|
144
|
-
[invert(grid),
|
117
|
+
[invert(grid), O]
|
145
118
|
else
|
146
119
|
raise ArgumentError, "#{grid} and #{turn} is not a valid combination, too many X's"
|
147
120
|
end
|
148
121
|
else
|
149
122
|
if xs == os
|
150
|
-
[invert(grid),
|
123
|
+
[invert(grid), X]
|
151
124
|
elsif xs > os
|
152
|
-
[GeometricGrid.new(grid.inspect),
|
125
|
+
[GeometricGrid.new(grid.inspect), O]
|
153
126
|
else
|
154
127
|
raise ArgumentError, "#{grid} and #{turn} is not a valid combination, too many O's"
|
155
128
|
end
|
@@ -166,6 +139,10 @@ module XO
|
|
166
139
|
inverted_grid
|
167
140
|
end
|
168
141
|
|
142
|
+
def check_turn(turn)
|
143
|
+
raise ArgumentError, "illegal token #{turn}" unless GeometricGrid.is_token?(turn)
|
144
|
+
end
|
145
|
+
|
169
146
|
def best_moves(grid, turn)
|
170
147
|
final_score = @scores[grid]
|
171
148
|
moves = []
|
@@ -175,49 +152,11 @@ module XO
|
|
175
152
|
|
176
153
|
moves << [r, c] if @scores[grid] == final_score
|
177
154
|
|
178
|
-
grid[r, c] =
|
155
|
+
grid[r, c] = EMPTY
|
179
156
|
end
|
180
157
|
|
181
158
|
moves
|
182
159
|
end
|
183
160
|
end
|
184
|
-
|
185
|
-
module MaxPlayer
|
186
|
-
|
187
|
-
def self.token
|
188
|
-
GeometricGrid::X
|
189
|
-
end
|
190
|
-
|
191
|
-
def self.other
|
192
|
-
MinPlayer
|
193
|
-
end
|
194
|
-
|
195
|
-
def self.score(type)
|
196
|
-
{ winner: 1, loser: -1, squashed: 0 }[type]
|
197
|
-
end
|
198
|
-
|
199
|
-
def self.final_score(next_grids, scores)
|
200
|
-
next_grids.map { |grid| scores[grid] }.max
|
201
|
-
end
|
202
|
-
end
|
203
|
-
|
204
|
-
module MinPlayer
|
205
|
-
|
206
|
-
def self.token
|
207
|
-
GeometricGrid::O
|
208
|
-
end
|
209
|
-
|
210
|
-
def self.other
|
211
|
-
MaxPlayer
|
212
|
-
end
|
213
|
-
|
214
|
-
def self.score(type)
|
215
|
-
{ winner: -1, loser: 1, squashed: 0 }[type]
|
216
|
-
end
|
217
|
-
|
218
|
-
def self.final_score(next_grids, scores)
|
219
|
-
next_grids.map { |grid| scores[grid] }.min
|
220
|
-
end
|
221
|
-
end
|
222
161
|
end
|
223
162
|
end
|