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
         |