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.
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
- def initialize
13
- @grid = Array.new(ROWS * COLS, :e)
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
- def empty?
37
- grid.all? { |val| !XO.is_token?(val) }
38
- end
39
-
40
- def full?
41
- grid.all? { |val| XO.is_token?(val) }
42
- end
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
- def each_free
63
- self.each { |r, c, _| yield(r, c) if free?(r, c) }
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
- def ==(other)
67
- return false unless other.instance_of?(self.class)
68
- grid == other.instance_variable_get(:@grid)
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
- def hash
73
- grid.hash
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
- attr_reader :grid
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
@@ -1,3 +1,3 @@
1
1
  module XO
2
- VERSION = '0.0.1'
2
+ VERSION = '1.0.0'
3
3
  end
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,6 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
1
4
  require 'minitest/autorun'
2
5
  require 'minitest/spec'
3
6
 
@@ -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
@@ -1,63 +1,83 @@
1
1
  require 'spec_helper'
2
2
 
3
- module XO
3
+ module XO::AI
4
4
 
5
- describe AI do
5
+ describe Minimax do
6
6
 
7
- describe 'minimax' do
7
+ let(:minimax) { Minimax.instance }
8
8
 
9
- let(:grid) { Grid.new }
9
+ describe "immediate wins" do
10
10
 
11
- describe 'immediate wins' do
11
+ it "should return (1, 3)" do
12
+ grid = XO::Grid.new('xx oo')
12
13
 
13
- it 'should return (1, 3)' do
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
- moves = AI.minimax(grid, :x).moves
16
+ moves.must_equal [[1, 3]]
17
+ end
18
18
 
19
- moves.size.must_equal 1
20
- [moves[0].row, moves[0].column].must_equal [1, 3]
21
- end
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
- it 'should return (1, 3), (3, 2) and (3, 3)' do
24
- grid[2, 1] = grid[2, 3] = grid[3, 1] = :x
25
- grid[1, 1] = grid[1, 2] = grid[2, 2] = :o
24
+ moves.must_equal [[1, 3], [3, 2], [3, 3]]
25
+ end
26
+ end
26
27
 
27
- moves = AI.minimax(grid, :o).moves
28
+ describe "blocking moves" do
28
29
 
29
- moves.size.must_equal 3
30
- [moves[0].row, moves[0].column].must_equal [1, 3]
31
- [moves[1].row, moves[1].column].must_equal [3, 2]
32
- [moves[2].row, moves[2].column].must_equal [3, 3]
33
- end
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
- describe 'blocking moves' do
39
+ describe "smart moves" do
37
40
 
38
- it 'should return (2, 1)' do
39
- grid[1, 1] = grid[3, 1] = :x
40
- grid[2, 2] = :o
41
+ it "should return (1, 3)" do
42
+ grid = XO::Grid.new('x o x o')
41
43
 
42
- moves = AI.minimax(grid, :o).moves
44
+ moves = minimax.moves(grid, XO::Grid::X)
43
45
 
44
- moves.size.must_equal 1
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
- describe 'smart moves' do
49
+ it "should return (1, 2), (2, 1), (2, 3), (3, 2)" do
50
+ grid = XO::Grid.new(' o x o')
50
51
 
51
- it 'should return (1, 3)' do
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
- moves = AI.minimax(grid, :x).moves
54
+ moves.must_equal [[1, 2], [2, 1], [2, 3], [3, 2]]
55
+ end
56
56
 
57
- moves.size.must_equal 1
58
- [moves[0].row, moves[0].column].must_equal [1, 3]
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