erics_tic_tac_toe 0.1.0 → 0.5.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.
@@ -0,0 +1,28 @@
1
+ module TicTacToe
2
+
3
+ class HumanPlayer
4
+ attr_reader :letter
5
+ attr_writer :move
6
+
7
+ def initialize(params)
8
+ @letter, @move = (params['letter'] || params[:letter]),
9
+ (params['move'] || params[:move])
10
+ end
11
+
12
+ def get_move(_board)
13
+ move = @move
14
+ @move = nil
15
+ move
16
+ end
17
+
18
+ def has_next_move?
19
+ !!@move
20
+ end
21
+
22
+ def type
23
+ Player::HUMAN
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,16 @@
1
+ module TicTacToe
2
+ class GamePresentor
3
+
4
+ def initialize(game)
5
+ @game = game
6
+ end
7
+
8
+ def grid
9
+ @game.grid.each_with_index.map do |row, i|
10
+ row.each_with_index.map do |cell, j|
11
+ cell || ((i*3)+(j+1)).to_s
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,13 @@
1
+ require 'json'
2
+
3
+ module TicTacToe
4
+ class PlayerPresenter
5
+ def initialize(player)
6
+ @player = player
7
+ end
8
+
9
+ def move_json(move=nil)
10
+ {letter: @player.letter, type: @player.type, move: move}.to_json
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,91 @@
1
+ module TicTacToe
2
+ # This implements the Minimax algorithum with AlphaBeta Prunning
3
+ # http://en.wikipedia.org/wiki/Minimax
4
+ # http://en.wikipedia.org/wiki/Alpha-beta_pruning
5
+ class MinimaxStrategy
6
+
7
+ attr_reader :board # Avoid getters, setters and properties
8
+
9
+ def initialize(board, letter)
10
+ @board, @letter = board, letter
11
+ end
12
+
13
+ def solve
14
+ raise "Can not solve a full board" if @board.full?
15
+ Minimax.new(@board, @letter).best_move
16
+ end
17
+ end
18
+
19
+ # The game tree needs a evaluator for generating rankings,
20
+ # an initial game state,
21
+ # and a player
22
+ #
23
+ # This class uses 5 instance variables: @evaluator, @state,
24
+ # @depth, @alpha, @beta. Should be < 3
25
+ #
26
+ # This class has 63 lines. Should be <= 50
27
+ class Minimax
28
+
29
+ MAXDEPTH = 6
30
+ PositiveInfinity = +1.0/0.0
31
+ NegativeInfinity = -1.0/0.0
32
+
33
+ def initialize(board, player)
34
+ @start_board = board
35
+ @player = player
36
+ end
37
+
38
+ def best_move
39
+ @start_board.empty_positions.max_by do |column, row|
40
+ score(@start_board.clone.play_at(column, row, @player), next_turn(@player))
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def score(board, whos_turn, depth=1,
47
+ alpha=NegativeInfinity, beta=PositiveInfinity)
48
+
49
+ if board.full? || board.solved?
50
+ return 1.0 / depth if board.winner == @player
51
+ return -1.0 if board.solved?
52
+ return 0
53
+ end
54
+
55
+ if whos_turn == @player
56
+ board.empty_positions.each do |column, row|
57
+ alpha = [
58
+ alpha,
59
+ next_score(board, column, row, whos_turn, depth, alpha, beta)
60
+ ].max
61
+ break if beta <= alpha || depth >= MAXDEPTH
62
+ end
63
+ return alpha
64
+ end
65
+
66
+ board.empty_positions.each do |column, row|
67
+ beta = [
68
+ beta,
69
+ next_score(board, column, row, whos_turn, depth, alpha, beta)
70
+ ].min
71
+ break if alpha >= beta || depth >= MAXDEPTH
72
+ end
73
+ beta
74
+ end
75
+
76
+ def next_score(board, column, row, whos_turn, depth, alpha, beta)
77
+ score( board.clone.play_at(column, row, whos_turn),
78
+ next_turn(whos_turn),
79
+ depth+1, alpha, beta
80
+ )
81
+ end
82
+
83
+ def next_turn(player)
84
+ case player
85
+ when X then O
86
+ when O then X
87
+ end
88
+ end
89
+ end
90
+ end
91
+
@@ -0,0 +1,260 @@
1
+ module TicTacToe
2
+ # Strategy used when playing on a 3x3 board
3
+ class ThreeByThreeStrategy
4
+ def initialize(board, letter)
5
+ @heuristic = ThreeByThree::Heuristic.new(board, letter)
6
+ end
7
+
8
+ # The strategy is from the Wikipedia article on Tic-Tac-Toe
9
+ # 1) Try to win
10
+ # 2) Try to block if they're about to win
11
+ # 3) Try to fork so you'll win next turn
12
+ # 4) Try to block their fork so they will not win next turn
13
+ # 5) Take the center if its not already taken
14
+ # 6) Play the opposite corner of your opponent
15
+ # 7) Play in an empty corner
16
+ # 8) Play in an empty side
17
+ def solve
18
+ [:win, :block, :fork, :block_fork,
19
+ :center, :opposite_corner, :empty_corner, :empty_side].each do |step|
20
+ move = @heuristic.send(step)
21
+ return move if move
22
+ end
23
+
24
+ raise "No possible moves to play!"
25
+ end
26
+ end
27
+
28
+ module ThreeByThree
29
+ # Brute Force Implementation for the Three by Three Strategy
30
+ # This implementation uses loops to try a change all of the board values and
31
+ # checking the result
32
+ #
33
+ # For example, win! is implemented by trying every cell, and checking if
34
+ # it was a winning solution
35
+ class Heuristic
36
+
37
+ attr_reader :board
38
+
39
+ def initialize(board, letter)
40
+ @board, @letter, @state = board, letter, PotentialState.new(board, letter)
41
+ @other_player = other_player
42
+ end
43
+
44
+ # Try placing letter at every available position
45
+ # If the board is solved, do that
46
+ def win
47
+ each_position do |row, column|
48
+ if @state.at(row, column).solved?
49
+ return [row, column]
50
+ end
51
+ end
52
+ false
53
+ end
54
+
55
+ # Try placing the opponent's letter at every available position
56
+ # If the board is solved, block them at that position
57
+ def block(board = @board, letter = @letter)
58
+ state = PotentialState.new(board, other_player(letter))
59
+ each_position do |row, column|
60
+ if state.at(row, column).solved?
61
+ return [row, column]
62
+ end
63
+ end
64
+ false
65
+ end
66
+
67
+ # Try placing the letter at every position.
68
+ # If there are now two winning solutions for next turn, go there
69
+ def fork
70
+ @state.each_forking_position do |row, column|
71
+ return [row, column]
72
+ end
73
+ false
74
+ end
75
+
76
+ # Try placing the opponent's letter at every position.
77
+ # If there are now two winning solutions for next turn, block them there
78
+ def block_fork
79
+ PotentialState.new(@board, @other_player).each_forking_position do |row, column|
80
+
81
+ # Simulate blocking the fork
82
+ temp_board = @board.clone
83
+ temp_board.play_at(row, column, @letter)
84
+
85
+ # Search for the elusive double fork
86
+ if PotentialState.new(temp_board, @other_player).forking_positions.any?
87
+ return force_a_block
88
+ end
89
+
90
+ return [row, column]
91
+ end
92
+ false
93
+ end
94
+
95
+ def center
96
+ return false if @board.get_cell(@board.size/2, @board.size/2)
97
+ [1,1]
98
+ end
99
+
100
+ # Cycle through all of the corners looking for the opponent's letter
101
+ # If one is found, place letter at the opposite corner
102
+ def opposite_corner
103
+ first = 0
104
+ last = @board.size - 1
105
+ corners.each_with_index do |corner, index|
106
+ if corner == @other_player
107
+ next if @board.get_cell(*opposite_corner_from_index(index).compact)
108
+ return opposite_corner_from_index(index)
109
+ end
110
+ end
111
+ false
112
+ end
113
+
114
+ # Cycle though all of the corners, until one is found that is empty
115
+ def empty_corner
116
+ corners.each_with_index do |corner, index|
117
+ next if corner
118
+ return corner_from_index(index)
119
+ end
120
+ false
121
+ end
122
+
123
+ # Place letter at a random empty cell, at this point it should only be sides left
124
+ def empty_side
125
+ @board.empty_positions do |row, column|
126
+ return [row, column]
127
+ end
128
+ false
129
+ end
130
+
131
+ private
132
+
133
+ def corners
134
+ [@board.get_cell(0, 0), # Top Left
135
+ @board.get_cell(@board.size-1, 0), # Top Right
136
+ @board.get_cell(@board.size-1, @board.size-1), # Bottom Right
137
+ @board.get_cell(0, @board.size-1)] # Bottom Left
138
+ end
139
+
140
+ def corner_from_index(index)
141
+ first = 0
142
+ last = @board.size - 1
143
+ case index
144
+ when 0 # Top Left
145
+ [first, first]
146
+ when 1 # Top Right
147
+ [last, first]
148
+ when 2 # Bottom Right
149
+ [last, last]
150
+ when 3 # Bottom Left
151
+ [first, last]
152
+ end
153
+ end
154
+
155
+ def opposite_corner_from_index(index)
156
+ first = 0
157
+ last = @board.size - 1
158
+ case index
159
+ when 0 # Top Left
160
+ [last, last]
161
+ when 1 # Top Right
162
+ [first, last]
163
+ when 2 # Bottom Right
164
+ [first, first]
165
+ when 3 # Bottom Left
166
+ [last, first]
167
+ end
168
+ end
169
+
170
+ def other_player(letter = @letter)
171
+ letter == X ? O : X
172
+ end
173
+
174
+ def each_position(&block)
175
+ @board.size.times do |column|
176
+ @board.size.times do |row|
177
+ yield(row, column)
178
+ end
179
+ end
180
+ end
181
+
182
+ def force_a_block
183
+ # Force them to block without creating another fork
184
+ each_position do |row, column|
185
+ if @state.at(row, column).can_win_next_turn?
186
+
187
+ # Simulate forcing them to block
188
+ temp_board = @board.clone
189
+ temp_board.play_at(row, column, @letter)
190
+ temp_board.play_at(*block(temp_board, @other_player), @other_player)
191
+
192
+ # Did I just create another fork with that block?
193
+ next if PotentialState.new(temp_board, @other_player).fork_exists?
194
+
195
+ return [row, column]
196
+
197
+ end
198
+ end
199
+ end
200
+ end
201
+ # Represents a state of players move
202
+ # This is comprised of a board and letter (player)
203
+ class PotentialState
204
+ def initialize(board, letter)
205
+ @board, @letter = board, letter
206
+ end
207
+
208
+ def at(row, column)
209
+ new_board = @board.clone
210
+ new_board.play_at(row, column, @letter)
211
+ PotentialState.new(new_board, @letter)
212
+ end
213
+
214
+ def solved?
215
+ @board.solved?
216
+ end
217
+
218
+ def each_forking_position(&block)
219
+ forking_positions.each { |position| yield(*position) }
220
+ end
221
+
222
+ def fork_exists?
223
+ winning_positions_count >= 2
224
+ end
225
+
226
+ def can_win_next_turn?
227
+ each_position do |row, column|
228
+ return true if at(row, column).solved?
229
+ end
230
+ false
231
+ end
232
+
233
+ def forking_positions
234
+ positions = []
235
+ each_position do |row, column|
236
+ positions << [row, column] if at(row, column).fork_exists?
237
+ end
238
+ positions
239
+ end
240
+
241
+ private
242
+
243
+ def winning_positions_count
244
+ count = 0
245
+ each_position do |row, column|
246
+ count += 1 if at(row, column).solved?
247
+ end
248
+ count
249
+ end
250
+
251
+ def each_position(&block)
252
+ @board.size.times do |column|
253
+ @board.size.times do |row|
254
+ yield(row, column)
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
@@ -1,3 +1,3 @@
1
1
  module TicTacToe
2
- VERSION = '0.1.0'
2
+ VERSION = '0.5.0'
3
3
  end
data/test/board_test.rb CHANGED
@@ -2,45 +2,64 @@ require 'test_helper'
2
2
 
3
3
  class BoardTest < MiniTest::Unit::TestCase
4
4
  def setup
5
- TicTacToe::Board.instance_eval { attr_accessor :grid } # For testing purposes
6
5
  @board = TicTacToe::Board.new
7
6
  end
8
7
 
9
- def test_empty
8
+ def test_grid_can_be_set
9
+ @board.grid = 'Foo'
10
+ assert_equal 'Foo', @board.grid
11
+ end
12
+
13
+ def test_a_new_board_is_empty
10
14
  # Board empty when created
11
15
  assert @board.empty?
12
-
16
+ end
17
+
18
+ def test_a_board_after_played_is_no_longer_empty
13
19
  # Board not longer empty after letter placed
14
20
  @board.play_at(0,0,'o')
15
21
  refute @board.empty?
16
22
  end
17
23
 
18
- def test_full
19
- # Board not full when empty
24
+ def test_a_new_board_is_not_empty
20
25
  refute @board.full?
26
+ end
21
27
 
28
+ def test_full
22
29
  # Board full when no cells are nil
23
30
  @board.grid = [ %w(x o x), %w(o x o), %w(x o x)]
24
31
  assert @board.full?
25
32
  end
26
33
 
27
- def test_only_one
34
+ def test_a_empty_board_is_not_only_one
28
35
  # False when empty
29
36
  refute @board.only_one?
30
-
37
+ end
38
+
39
+ def test_a_board_knows_when_only_one_peice
31
40
  # True when one
32
41
  @board.grid = [ ['x',nil,nil], [nil,nil,nil], [nil,nil,nil] ]
33
42
  assert @board.only_one?
43
+ end
34
44
 
45
+ def test_a_board_with_more_than_one_place_is_not_only_one
35
46
  # False when more than one
36
47
  @board.grid = [ ['x','x',nil], [nil,nil,nil], [nil,nil,nil] ]
37
48
  refute @board.only_one?
38
49
  end
39
50
 
40
- def test_get_cell
51
+ def test_get_cell_returns_nil_when_nothing_there
41
52
  # Returns nil when nothing in cell
42
53
  assert_nil @board.get_cell(0, 0)
54
+ end
43
55
 
56
+ def test_play_at
57
+ # Cells are proper set and override nil values
58
+ @board.play_at(1, 1, 'x')
59
+ assert_equal 'x', @board.get_cell(1,1)
60
+ end
61
+
62
+ def test_get_cell
44
63
  # Returns the right letter after they've been played
45
64
  @board.play_at(0,0,"o")
46
65
  assert_equal "o", @board.get_cell(0, 0)
@@ -49,49 +68,7 @@ class BoardTest < MiniTest::Unit::TestCase
49
68
  assert_equal "o", @board.get_cell(1, 2)
50
69
  end
51
70
 
52
- def test_center_cell
53
- # Center cell is nil in empty grid
54
- assert_nil @board.center_cell
55
-
56
- # Returns the correct letter after its been placed
57
- @board.play_at(1,1,'o')
58
- assert_equal "o", @board.center_cell
59
-
60
- # Does not override values after they've been placed
61
- @board.center_cell = 'x'
62
- assert_equal "o", @board.center_cell
63
-
64
- # Can use helper when the center cell is nil
65
- @board = TicTacToe::Board.new
66
- @board.center_cell = 'x'
67
- assert_equal "x", @board.center_cell
68
- end
69
-
70
- def test_corners
71
- # All of the corners are nil in an empty grid
72
- @board.corners.each { |corner| assert_nil corner }
73
-
74
- # Will return the corners in the proper order (clockwise)
75
- # 0 => Top Left
76
- # 1 => Top Right
77
- # 2 => Bottom Right
78
- # 3 => Bottom Left
79
- @board.play_at(0,0,'a')
80
- @board.play_at(2,0,'b')
81
- @board.play_at(2,2,'c')
82
- @board.play_at(0,2,'d')
83
- answers = %w( a b c d )
84
- @board.corners.each_with_index { |corner, i| assert_equal answers[i], corner }
85
- end
86
-
87
- def test_play_at
88
- # Cells are proper set and override nil values
89
- assert_nil @board.get_cell(1,1)
90
- @board.play_at(1, 1, 'x')
91
- assert_equal 'x', @board.get_cell(1,1)
92
- end
93
-
94
- def test_can_not_overide_values
71
+ def test_can_not_override_values
95
72
  assert_nil @board.get_cell(1,1)
96
73
  @board.play_at(1, 1, 'x')
97
74
  assert_equal 'x', @board.get_cell(1,1)
@@ -99,7 +76,7 @@ class BoardTest < MiniTest::Unit::TestCase
99
76
  assert_equal 'x', @board.get_cell(1,1)
100
77
  end
101
78
 
102
- def test_solved_acoss
79
+ def test_solved_across
103
80
  refute @board.solved?
104
81
 
105
82
  @board.grid = [ %w(x x x), [nil, nil, nil], [nil, nil, nil]]
@@ -138,4 +115,15 @@ class BoardTest < MiniTest::Unit::TestCase
138
115
  refute @board.solved?
139
116
  end
140
117
 
118
+ def test_displays_correctly
119
+ assert_equal (<<-HEREDOC), @board.to_s
120
+ -----------
121
+ 1 | 2 | 3 |
122
+ 4 | 5 | 6 |
123
+ 7 | 8 | 9 |
124
+ -----------
125
+
126
+ HEREDOC
127
+ end
128
+
141
129
  end