erics_tic_tac_toe 0.1.0 → 0.5.0

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