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/grid.rb
CHANGED
@@ -1,30 +1,114 @@
|
|
1
1
|
module XO
|
2
2
|
|
3
|
+
# A data structure for storing {X}'s and {O}'s in a 3x3 grid.
|
4
|
+
#
|
5
|
+
# The grid is structured as follows:
|
6
|
+
#
|
7
|
+
# column
|
8
|
+
# 1 2 3
|
9
|
+
# row
|
10
|
+
# 1 | |
|
11
|
+
# ---+---+---
|
12
|
+
# 2 | |
|
13
|
+
# ---+---+---
|
14
|
+
# 3 | |
|
15
|
+
#
|
16
|
+
# It is important to note that if a position stores anything other than
|
17
|
+
# {X} or {O} then that position is considered to be open.
|
3
18
|
class Grid
|
4
19
|
|
20
|
+
X = :x
|
21
|
+
O = :o
|
22
|
+
|
5
23
|
ROWS = 3
|
6
24
|
COLS = 3
|
7
25
|
|
26
|
+
N = ROWS * COLS
|
27
|
+
|
28
|
+
# Determines whether or not position (r, c) is such that 1 <= r <= 3 and 1 <= c <= 3.
|
29
|
+
#
|
30
|
+
# @param r [Integer] the row
|
31
|
+
# @param c [Integer] the column
|
32
|
+
# @return [Boolean] true iff the position is contained within a 3x3 grid
|
8
33
|
def self.contains?(r, c)
|
9
34
|
r.between?(1, ROWS) && c.between?(1, COLS)
|
10
35
|
end
|
11
36
|
|
12
|
-
|
13
|
-
|
37
|
+
# Classifies what is and isn't considered to be a token.
|
38
|
+
#
|
39
|
+
# @param val [Object]
|
40
|
+
# @return [Boolean] true iff val is {X} or {O}
|
41
|
+
def self.is_token?(val)
|
42
|
+
val == X || val == O
|
43
|
+
end
|
44
|
+
|
45
|
+
# Determines the other token.
|
46
|
+
#
|
47
|
+
# @param val [Object]
|
48
|
+
# @return [Object] {X} given {O}, {O} given {X} or the original value
|
49
|
+
def self.other_token(val)
|
50
|
+
val == X ? O : (val == O ? X : val)
|
14
51
|
end
|
15
52
|
|
53
|
+
attr_reader :grid
|
54
|
+
private :grid
|
55
|
+
|
56
|
+
# Creates a new empty grid by default. You can also create a
|
57
|
+
# prepopulated grid by passing in a string representation.
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# g = Grid.new('xo ox o')
|
61
|
+
def initialize(g = '')
|
62
|
+
@grid = from_string(g)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Creates a copy of the given grid. Use #dup to get your copy.
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# g = Grid.new
|
69
|
+
# g_copy = g.dup
|
70
|
+
#
|
71
|
+
# @param orig [Grid] the original grid
|
72
|
+
# @return [Grid] a copy
|
16
73
|
def initialize_copy(orig)
|
17
74
|
@grid = orig.instance_variable_get(:@grid).dup
|
18
75
|
end
|
19
76
|
|
77
|
+
# Determines whether or not there are any tokens on the grid.
|
78
|
+
#
|
79
|
+
# @return [Boolean] true iff there are no tokens on the grid
|
80
|
+
def empty?
|
81
|
+
grid.all? { |val| !self.class.is_token?(val) }
|
82
|
+
end
|
83
|
+
|
84
|
+
# Determines whether or not every position on the grid has a token?
|
85
|
+
#
|
86
|
+
# @return [Boolean] true iff every position on the grid has a token
|
87
|
+
def full?
|
88
|
+
grid.all? { |val| self.class.is_token?(val) }
|
89
|
+
end
|
90
|
+
|
91
|
+
# Sets position (r, c) to the given value.
|
92
|
+
#
|
93
|
+
# @param r [Integer] the row
|
94
|
+
# @param c [Integer] the column
|
95
|
+
# @param val [Object]
|
96
|
+
# @raise [IndexError] if the position is off the grid
|
97
|
+
# @return [Object] the value it was given
|
20
98
|
def []=(r, c, val)
|
21
99
|
if self.class.contains?(r, c)
|
22
|
-
grid[idx(r, c)] = val
|
100
|
+
grid[idx(r, c)] = self.class.is_token?(val) ? val : :e
|
23
101
|
else
|
24
102
|
raise IndexError, "position (#{r}, #{c}) is off the grid"
|
25
103
|
end
|
26
104
|
end
|
27
105
|
|
106
|
+
# Retrieves the value at the given position (r, c).
|
107
|
+
#
|
108
|
+
# @param r [Integer] the row
|
109
|
+
# @param c [Integer] the column
|
110
|
+
# @raise [IndexError] if the position is off the grid
|
111
|
+
# @return [Object]
|
28
112
|
def [](r, c)
|
29
113
|
if self.class.contains?(r, c)
|
30
114
|
grid[idx(r, c)]
|
@@ -33,49 +117,90 @@ module XO
|
|
33
117
|
end
|
34
118
|
end
|
35
119
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
def free?(r, c)
|
45
|
-
!XO.is_token?(self[r, c])
|
120
|
+
# Determines whether or not position (r, c) contains a token.
|
121
|
+
#
|
122
|
+
# @param r [Integer] the row
|
123
|
+
# @param c [Integer] the column
|
124
|
+
# @raise [IndexError] if the position is off the grid
|
125
|
+
# @return true iff the position does not contain a token
|
126
|
+
def open?(r, c)
|
127
|
+
!self.class.is_token?(self[r, c])
|
46
128
|
end
|
47
129
|
|
130
|
+
# Removes all tokens from the grid.
|
48
131
|
def clear
|
49
132
|
grid.fill(:e)
|
133
|
+
|
134
|
+
self
|
50
135
|
end
|
51
136
|
|
137
|
+
# Used for iterating over all the positions of the grid from left to right and top to bottom.
|
138
|
+
#
|
139
|
+
# @example
|
140
|
+
# g = Grid.new
|
141
|
+
# g.each do |r, c, val|
|
142
|
+
# puts "(#{r}, #{c}) -> #{val}"
|
143
|
+
# end
|
52
144
|
def each
|
53
145
|
(1..ROWS).each do |r|
|
54
146
|
(1..COLS).each do |c|
|
55
147
|
yield(r, c, self[r, c])
|
56
148
|
end
|
57
149
|
end
|
58
|
-
|
59
|
-
self
|
60
150
|
end
|
61
151
|
|
62
|
-
|
63
|
-
|
152
|
+
# Used for iterating over all the open positions of the grid from left to right and top to bottom.
|
153
|
+
#
|
154
|
+
# @example
|
155
|
+
# g = Grid.new
|
156
|
+
#
|
157
|
+
# g[1, 1] = g[2, 1] = Grid::X
|
158
|
+
# g[2, 2] = g[3, 1] = Grid::O
|
159
|
+
#
|
160
|
+
# g.each_open do |r, c|
|
161
|
+
# puts "(#{r}, #{c}) is open"
|
162
|
+
# end
|
163
|
+
def each_open
|
164
|
+
self.each { |r, c, _| yield(r, c) if open?(r, c) }
|
64
165
|
end
|
65
166
|
|
66
|
-
|
67
|
-
|
68
|
-
|
167
|
+
# Returns a string representation of the grid which may be useful
|
168
|
+
# for debugging.
|
169
|
+
def inspect
|
170
|
+
grid.map { |val| t(val) }.join
|
69
171
|
end
|
70
|
-
alias_method :eql?, :==
|
71
172
|
|
72
|
-
|
73
|
-
|
173
|
+
# Returns a string representation of the grid which may be useful
|
174
|
+
# for display.
|
175
|
+
def to_s
|
176
|
+
g = grid.map { |val| t(val) }
|
177
|
+
|
178
|
+
[" #{g[0]} | #{g[1]} | #{g[2]} ",
|
179
|
+
"---+---+---",
|
180
|
+
" #{g[3]} | #{g[4]} | #{g[5]} ",
|
181
|
+
"---+---+---",
|
182
|
+
" #{g[6]} | #{g[7]} | #{g[8]} "].join("\n")
|
74
183
|
end
|
75
184
|
|
76
185
|
private
|
77
186
|
|
78
|
-
|
187
|
+
def from_string(g)
|
188
|
+
g = g.to_s
|
189
|
+
l = g.length
|
190
|
+
|
191
|
+
g = if l < N
|
192
|
+
g + ' ' * (N - l)
|
193
|
+
elsif l > N
|
194
|
+
g[0..N-1]
|
195
|
+
else
|
196
|
+
g
|
197
|
+
end
|
198
|
+
|
199
|
+
g.split('').map do |ch|
|
200
|
+
sym = ch.to_sym
|
201
|
+
sym == X || sym == O ? sym : :e
|
202
|
+
end
|
203
|
+
end
|
79
204
|
|
80
205
|
# Computes the 0-based index of position (r, c) on a 3x3 grid.
|
81
206
|
#
|
@@ -91,5 +216,9 @@ module XO
|
|
91
216
|
def idx(r, c)
|
92
217
|
COLS * (r - 1) + (c - 1)
|
93
218
|
end
|
219
|
+
|
220
|
+
def t(val)
|
221
|
+
self.class.is_token?(val) ? val : ' '
|
222
|
+
end
|
94
223
|
end
|
95
224
|
end
|
data/lib/xo/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module XO::AI
|
4
|
+
|
5
|
+
describe GeometricGrid do
|
6
|
+
|
7
|
+
let(:grid) { GeometricGrid.new }
|
8
|
+
|
9
|
+
describe "#rotate" do
|
10
|
+
|
11
|
+
let (:grid) { GeometricGrid.new("xoooxxoxo") }
|
12
|
+
|
13
|
+
it "correctly performs a single rotation" do
|
14
|
+
rotated_grid = grid.rotate
|
15
|
+
|
16
|
+
rotated_grid.inspect.must_equal "ooxxxooxo"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "returns the original on the 4th rotation" do
|
20
|
+
original_grid = grid.rotate.rotate.rotate.rotate
|
21
|
+
|
22
|
+
original_grid.same?(grid).must_equal true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#reflect" do
|
27
|
+
|
28
|
+
let (:grid) { GeometricGrid.new("oxxxoooxo") }
|
29
|
+
|
30
|
+
it "correctly performs a single reflection" do
|
31
|
+
reflected_grid = grid.reflect
|
32
|
+
|
33
|
+
reflected_grid.inspect.must_equal "xxoooxoxo"
|
34
|
+
end
|
35
|
+
|
36
|
+
it "returns the original on the 2nd reflection" do
|
37
|
+
original_grid = grid.reflect.reflect
|
38
|
+
|
39
|
+
original_grid.same?(grid).must_equal true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#same?" do
|
44
|
+
|
45
|
+
it "returns true iff two grids have the same occupied positions" do
|
46
|
+
a = GeometricGrid.new('x')
|
47
|
+
b = GeometricGrid.new(' x')
|
48
|
+
|
49
|
+
a.same?(a).must_equal true
|
50
|
+
a.same?(b).must_equal false # but they are equivalent
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#equivalent?" do
|
55
|
+
|
56
|
+
it "correctly determines equivalent grids" do
|
57
|
+
a = GeometricGrid.new('x')
|
58
|
+
|
59
|
+
[' x', ' x', ' x'].each do |g|
|
60
|
+
a.equivalent?(GeometricGrid.new(g)).must_equal true
|
61
|
+
end
|
62
|
+
|
63
|
+
b = GeometricGrid.new(' x')
|
64
|
+
|
65
|
+
[' x', ' x', ' x'].each do |g|
|
66
|
+
b.equivalent?(GeometricGrid.new(g)).must_equal true
|
67
|
+
end
|
68
|
+
|
69
|
+
c = GeometricGrid.new('xo')
|
70
|
+
|
71
|
+
[' ox', ' o x', ' xo', 'x o'].each do |g|
|
72
|
+
c.equivalent?(GeometricGrid.new(g)).must_equal true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
it "can only determine the equivalence of two geometric grids" do
|
77
|
+
a = GeometricGrid.new('x')
|
78
|
+
b = XO::Grid.new(' x')
|
79
|
+
|
80
|
+
a.equivalent?(b).must_equal false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "equality" do
|
85
|
+
|
86
|
+
it "is reflexive, symmetric and transitive" do
|
87
|
+
a = GeometricGrid.new('x')
|
88
|
+
b = GeometricGrid.new(' x')
|
89
|
+
c = GeometricGrid.new(' x')
|
90
|
+
|
91
|
+
# reflexive, not quite since we didn't test for all a
|
92
|
+
a.must_equal a
|
93
|
+
|
94
|
+
# symmetric
|
95
|
+
a.must_equal b
|
96
|
+
b.must_equal a
|
97
|
+
|
98
|
+
# transitive
|
99
|
+
b.must_equal c
|
100
|
+
a.must_equal c
|
101
|
+
end
|
102
|
+
|
103
|
+
describe "#== and #eql?" do
|
104
|
+
|
105
|
+
it "must be the case that if two geometric grid are #== then they are also #eql?" do
|
106
|
+
a = GeometricGrid.new('x')
|
107
|
+
b = GeometricGrid.new(' x')
|
108
|
+
|
109
|
+
a.eql?(b).must_equal true
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "#hash" do
|
115
|
+
|
116
|
+
it "must return the same hash for equal geometric grids" do
|
117
|
+
a = GeometricGrid.new(' x')
|
118
|
+
b = GeometricGrid.new(' x')
|
119
|
+
|
120
|
+
a.hash.must_equal b.hash
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "when used within a hash" do
|
125
|
+
|
126
|
+
it "must be the case that equivalent grids map to the same key" do
|
127
|
+
a = GeometricGrid.new('x')
|
128
|
+
b = GeometricGrid.new(' x')
|
129
|
+
|
130
|
+
hash = {}
|
131
|
+
hash[a] = :any_value
|
132
|
+
|
133
|
+
hash[b].must_equal :any_value
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
data/spec/xo/ai/minimax_spec.rb
CHANGED
@@ -1,63 +1,83 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
module XO
|
3
|
+
module XO::AI
|
4
4
|
|
5
|
-
describe
|
5
|
+
describe Minimax do
|
6
6
|
|
7
|
-
|
7
|
+
let(:minimax) { Minimax.instance }
|
8
8
|
|
9
|
-
|
9
|
+
describe "immediate wins" do
|
10
10
|
|
11
|
-
|
11
|
+
it "should return (1, 3)" do
|
12
|
+
grid = XO::Grid.new('xx oo')
|
12
13
|
|
13
|
-
|
14
|
-
grid[1, 1] = grid[1, 2] = :x
|
15
|
-
grid[2, 1] = grid[2, 2] = :o
|
14
|
+
moves = minimax.moves(grid, XO::Grid::X)
|
16
15
|
|
17
|
-
|
16
|
+
moves.must_equal [[1, 3]]
|
17
|
+
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
it "should return (1, 3), (3, 2) and (3, 3)" do
|
20
|
+
grid = XO::Grid.new('oo xoxx')
|
21
|
+
|
22
|
+
moves = minimax.moves(grid, XO::Grid::O)
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
moves.must_equal [[1, 3], [3, 2], [3, 3]]
|
25
|
+
end
|
26
|
+
end
|
26
27
|
|
27
|
-
|
28
|
+
describe "blocking moves" do
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
it "should return (2, 1)" do
|
31
|
+
grid = XO::Grid.new('x o x')
|
32
|
+
|
33
|
+
moves = minimax.moves(grid, XO::Grid::O)
|
34
|
+
|
35
|
+
moves.must_equal [[2, 1]]
|
34
36
|
end
|
37
|
+
end
|
35
38
|
|
36
|
-
|
39
|
+
describe "smart moves" do
|
37
40
|
|
38
|
-
|
39
|
-
|
40
|
-
grid[2, 2] = :o
|
41
|
+
it "should return (1, 3)" do
|
42
|
+
grid = XO::Grid.new('x o x o')
|
41
43
|
|
42
|
-
|
44
|
+
moves = minimax.moves(grid, XO::Grid::X)
|
43
45
|
|
44
|
-
|
45
|
-
[moves[0].row, moves[0].column].must_equal [2, 1]
|
46
|
-
end
|
46
|
+
moves.must_equal [[1, 3]]
|
47
47
|
end
|
48
48
|
|
49
|
-
|
49
|
+
it "should return (1, 2), (2, 1), (2, 3), (3, 2)" do
|
50
|
+
grid = XO::Grid.new(' o x o')
|
50
51
|
|
51
|
-
|
52
|
-
grid[1, 1] = grid[3, 1] = :x
|
53
|
-
grid[2, 1] = grid[3, 3] = :o
|
52
|
+
moves = minimax.moves(grid, XO::Grid::X)
|
54
53
|
|
55
|
-
|
54
|
+
moves.must_equal [[1, 2], [2, 1], [2, 3], [3, 2]]
|
55
|
+
end
|
56
56
|
|
57
|
-
|
58
|
-
|
57
|
+
it "should return (1, 3), (3, 1)" do
|
58
|
+
grid = XO::Grid.new('x x o')
|
59
|
+
|
60
|
+
moves = minimax.moves(grid, XO::Grid::O)
|
61
|
+
|
62
|
+
moves.must_equal [[1, 3], [3, 1]]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "moves" do
|
67
|
+
|
68
|
+
it "raises ArgumentError if turn is not a token" do
|
69
|
+
proc { minimax.moves(XO::Grid.new, :not_a_token) }.must_raise ArgumentError
|
70
|
+
end
|
71
|
+
|
72
|
+
it "raises ArgumentError for an invalid grid and/or turn combination" do
|
73
|
+
[['x', XO::Grid::X], ['xoo', XO::Grid::O], ['oo', XO::Grid::O], ['xx', XO::Grid::X]].each do |input|
|
74
|
+
proc { minimax.moves(XO::Grid.new(input[0]), input[1]) }.must_raise ArgumentError
|
59
75
|
end
|
60
76
|
end
|
77
|
+
|
78
|
+
it "returns [] for a terminal grid" do
|
79
|
+
minimax.moves(XO::Grid.new('xx ooo'), XO::Grid::X).must_equal []
|
80
|
+
end
|
61
81
|
end
|
62
82
|
end
|
63
83
|
end
|