xo 0.0.1 → 1.0.0

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