xo 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Coverage Status](https://coveralls.io/repos/dwayne/xo/badge.png)](https://coveralls.io/r/dwayne/xo)
|
6
6
|
[![Code Climate](https://codeclimate.com/github/dwayne/xo.png)](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
|