xo 1.0.0 → 1.1.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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NTMyNzA0ODhkYmI4OGE5ZjM3ZDc3YjZkNDA2NGQ0ZWZjMWI3ODYwYQ==
4
+ ZmU4ZmMyZTlhMjE5NWI1OWI1YmZmNGNjYjYwZTQwZjEzZGE3YzQxZQ==
5
5
  data.tar.gz: !binary |-
6
- OWNlNjMxM2JmMDEzMTQ0YWJjNjdjMDUzYmQ0ZDQ3YmY3OWUzMDk4OA==
6
+ MzBlZDEyZDZlZjQyOTRjYzE3Mjc4NzA2NzBmNWU5Y2RlODU5OGQzZA==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- OGY3M2IyNWE0Y2YzYzZkMzlhMmEzNzIyODg3YTFiNzg1MzFiMjZkZDBiMjZl
10
- OWMzYzllZjVhYTVjOWM1NTYxY2MyYWQ3ODdlZDhmNzUxMjNlNTA0N2VhYzQ2
11
- MjE4OWJhNGE0NTk3YTFiYTU3MWViMjAxODcxZGQ0OGFiYjAwMGU=
9
+ MzNlNWQ2Zjg0ZjdjNjIzZGY1MWM3NDE5ZmM0ODNkMDU1ZDdlMWYxM2E3NWU4
10
+ MmQ3Zjk2ZGU3M2Q1YmFiMzZjZDgwMDcwY2RiY2VjMzA5ZjU5ZTNiM2U3MDM4
11
+ MWQyMzEzOGVmM2E1OTcwYWM5MWVmOWQ3YjE0MThiYWIyNWUzZGI=
12
12
  data.tar.gz: !binary |-
13
- ZjllZDA0YzY0MDYzMTVlY2EzMzlkZmE4NjU2ZjEzNDYwOTJiM2EwNDYyNmI0
14
- YTRlZWRlOWM0N2MyNjFkM2FhYmU0MTJlNzA5NDVhMjFmZjk0OTQ0NTMzYzYx
15
- Yzk2ZjFmNzAyY2JlNzY0OWM3OTEyMTNkYjkyOTBiMTcwNGJjZDQ=
13
+ NjNhMzIyZTNjODgxNjE2Yzg0ZTk3ZDg1MjEzMjFhZTI4ZTdkYjExMWNjNDk3
14
+ ZjI0ZTQ5YmU5MDdhZjY3ZDRjMjI1Yjc0OGIwMWUzMDdhMGFhMGE1M2U0ODE0
15
+ ODcyZTllODk0NjQzY2RlYzFiMmQzODcyNjlmMzA1MTk4MDA0MmY=
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- xo (0.0.1)
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 [Tic-tac-toe](http://en.wikipedia.org/wiki/Tic-tac-toe).
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 [read of the documentation](http://rubydoc.info/github/dwayne/xo) and have a [look at the tests](https://github.com/dwayne/xo/tree/master/spec/xo).
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) might be a little different than what you've seen before. It uses symmetry to significantly reduce the search space and in so doing we get good [performance out of the algorithm](#Performance_of_the_Minimax_Algorithm). However, I still want to get it under 1 second. I'd love to get your thoughts on how I can make it happen. Have a [look](https://github.com/dwayne/xo/blob/master/lib/xo/ai/minimax.rb#L23).
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.analyze(g, Grid::X) # => { status: :ok }
45
+ evaluator = Evaluator.instance
46
+
47
+ evaluator.analyze(g, Grid::X) # => { status: :ok }
38
48
 
39
49
  g[1, 3] = Grid::X
40
- Evaluator.analyze(g, Grid::X) # => { status: :game_over,
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
- Evaluator.analyze(g, Grid::O) # => { status: :game_over,
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 you from making bad moves. For example, playing twice.
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
- Evaluator.analyze(g, Grid::O) # => { status: :invalid_grid,
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 plays first, then it ensures that the game play follows the rules of [Tic-tac-toe](http://en.wikipedia.org/wiki/Tic-tac-toe).
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
- e.last_event # => { name: :game_over,
74
- # type: :winner,
75
- # last_move: { turn: :o, r: 2, c: 3 },
76
- # details: [{
77
- # where: :row,
78
- # index: 2,
79
- # positions: [[2, 1], [2, 2], [2, 3]]
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) command-line client that uses the library. See how everything comes together by viewing its implementation right [here](https://github.com/dwayne/xo/blob/master/bin/xo).
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 fix/feature has a high chance of being included, please read the following guidelines:
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. It's a rare time when explicit tests aren't needed. If you have questions about writing tests for xo, please open a [GitHub issue](https://github.com/dwayne/xo/issues/new).
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 redistributed under the terms specified in the MIT-LICENSE file.
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
- if turn
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 while @engine.state != :game_over
181
+ run_one_turn until engine.current_state == GameOver
191
182
  end
192
183
 
193
- private
184
+ def update(event)
185
+ player = players[engine.turn]
194
186
 
195
- def run_one_turn
196
- turn = @engine.turn
197
- player = @players[turn]
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
- r, c = player.get_move(@engine.grid, turn)
197
+ private
200
198
 
201
- event = @engine.play(r, c).last_event
199
+ attr_reader :players, :engine
202
200
 
203
- case event[:name]
204
- when :next_turn
205
- player.handle_next_turn(@engine.grid)
206
- when :game_over
207
- player.handle_game_over(@engine.grid, event)
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
@@ -1,4 +1,4 @@
1
1
  require 'xo/grid'
2
2
  require 'xo/evaluator'
3
3
  require 'xo/engine'
4
- require 'xo/ai'
4
+ require 'xo/ai/minimax'
@@ -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
@@ -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
- # Given any node in the tree and an indication of whose turn it is to play next, all the node's children can be determined by
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 instantaneous.
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
- raise ArgumentError, "illegal token #{turn}" unless GeometricGrid.is_token?(turn)
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 :the_grid, :scores
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
- @the_grid = GeometricGrid.new
86
- @scores = {}
58
+ @master_grid = GeometricGrid.new
59
+ @scores = {}
87
60
  end
88
61
 
89
- def build_search_tree(player = MaxPlayer)
62
+ def build_search_tree(player_a = MAX_PLAYER, player_b = MIN_PLAYER)
90
63
  return if has_score?
91
64
 
92
- analyze_grid(player)
65
+ analyze_grid(player_a)
93
66
 
94
67
  if terminal?
95
- set_score(player)
68
+ set_terminal_score(player_a)
96
69
  else
97
70
  next_grids = []
98
71
 
99
- the_grid.each_open do |r, c|
100
- the_grid[r, c] = player.token
101
- next_grids << the_grid.dup
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(player.other)
76
+ build_search_tree(player_b, player_a)
104
77
 
105
- the_grid[r, c] = :e
78
+ master_grid[r, c] = EMPTY
106
79
  end
107
80
 
108
- set_final_score(player, next_grids)
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?(the_grid)
86
+ scores.key?(master_grid)
114
87
  end
115
88
 
116
89
  def analyze_grid(player)
117
- @results = Evaluator.instance.analyze(the_grid, player.token)
90
+ @result = Evaluator.instance.analyze(master_grid, player.token)
118
91
  end
119
92
 
120
93
  def terminal?
121
- @results[:status] == :game_over
94
+ @result[:status] == :game_over
122
95
  end
123
96
 
124
- def set_score(player)
125
- scores[the_grid.dup] = player.score(@results[:type])
97
+ def set_terminal_score(player)
98
+ scores[master_grid.dup] = player.terminal_score(@result[:type])
126
99
  end
127
100
 
128
- def set_final_score(player, next_grids)
129
- scores[the_grid.dup] = player.final_score(next_grids, 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 {XO::Grid::X} is assumed to
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 {XO::Grid::O} played first then we need to
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 lift(grid, turn)
138
- xs, os = Evaluator.instance.xos(grid)
110
+ def normalize(grid, turn)
111
+ xs, os = Evaluator.xos(grid)
139
112
 
140
- if turn == GeometricGrid::X
113
+ if turn == X
141
114
  if xs == os
142
- [GeometricGrid.new(grid.inspect), GeometricGrid::X]
115
+ [GeometricGrid.new(grid.inspect), X]
143
116
  elsif xs < os
144
- [invert(grid), GeometricGrid::O]
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), GeometricGrid::X]
123
+ [invert(grid), X]
151
124
  elsif xs > os
152
- [GeometricGrid.new(grid.inspect), GeometricGrid::O]
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] = :e
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