xo 1.0.0 → 1.1.0

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