erics_tic_tac_toe 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/LICENSE +7 -0
- data/README.markdown +15 -0
- data/Rakefile +12 -0
- data/bin/tic_tac_toe +18 -0
- data/lib/tic-tac-toe/board.rb +148 -0
- data/lib/tic-tac-toe/game.rb +62 -0
- data/lib/tic-tac-toe/game_types/terminal_game.rb +63 -0
- data/lib/tic-tac-toe/solver.rb +16 -0
- data/lib/tic-tac-toe/strategies/threebythree_implementations/brute_force_implementation.rb +197 -0
- data/lib/tic-tac-toe/strategies/threebythree_stategy.rb +30 -0
- data/lib/tic-tac-toe/version.rb +3 -0
- data/lib/tic_tac_toe.rb +13 -0
- data/test/board_test.rb +141 -0
- data/test/brute_force_implementation_test.rb +25 -0
- data/test/solver_test.rb +149 -0
- data/test/test_helper.rb +3 -0
- data/tic-tac-toe.gemspec +23 -0
- metadata +98 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pkg/
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2012 Eric Koslow
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
data/Rakefile
ADDED
data/bin/tic_tac_toe
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
libdir = File.expand_path('../../lib', __FILE__).sub(/^#{Dir.pwd}\//, '')
|
4
|
+
begin
|
5
|
+
require 'tic_tac_toe'
|
6
|
+
rescue LoadError
|
7
|
+
case $!.to_s
|
8
|
+
when /tic_tac_toe/
|
9
|
+
if !$:.include?(libdir)
|
10
|
+
warn "warn: #$!. trying again with #{libdir} on load path"
|
11
|
+
$:.unshift(libdir)
|
12
|
+
retry
|
13
|
+
end
|
14
|
+
end
|
15
|
+
raise
|
16
|
+
end
|
17
|
+
|
18
|
+
TicTacToe::Game.new.run
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module TicTacToe
|
2
|
+
class Board
|
3
|
+
|
4
|
+
SIZE = 3.freeze
|
5
|
+
|
6
|
+
def initialize(grid=nil)
|
7
|
+
#[[ nil, nil, nil],
|
8
|
+
# [ nil, nil, nil],
|
9
|
+
# [ nil, nil, nil]]
|
10
|
+
@grid = grid || [ [nil] * SIZE ] * SIZE
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_cell(x, y)
|
14
|
+
@grid[y][x]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns the closest middle cell
|
18
|
+
def center_cell
|
19
|
+
mid = @grid.size/2
|
20
|
+
get_cell(mid, mid)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sets the closet middle cell if its not already set
|
24
|
+
def center_cell=(letter)
|
25
|
+
mid = @grid.size/2
|
26
|
+
play_at(mid, mid, letter)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the corners of the grid
|
30
|
+
def corners
|
31
|
+
[@grid[0][0],
|
32
|
+
@grid[0][SIZE-1],
|
33
|
+
@grid[SIZE-1][SIZE-1],
|
34
|
+
@grid[SIZE-1][0]]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns the corners of the empty cells
|
38
|
+
def any_empty_position(&block)
|
39
|
+
SIZE.times do |y|
|
40
|
+
SIZE.times do |x|
|
41
|
+
next if get_cell(x, y)
|
42
|
+
yield(x, y)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Plays a letter at a position, unless that position has already been taken
|
48
|
+
def play_at(x, y, letter)
|
49
|
+
# Weird bug I found in ruby 1.9.3-p0
|
50
|
+
# Given a @grid of [ [nil,nil,nil], [nil,nil,nil], [nil,nil,nil] ]
|
51
|
+
# If you call @grid[0][0] = 'x'
|
52
|
+
# I aspect @grid to be [ ['x',nil,nil], [nil,nil,nil], [nil,nil,nil] ]
|
53
|
+
# What happens is that @grid is [ ['x',nil,nil], ['x',nil,nil], ['x',nil,nil] ]
|
54
|
+
# This is a workaround for that
|
55
|
+
inner = @grid[y].clone
|
56
|
+
inner[x] ||= letter
|
57
|
+
@grid[y] = inner
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns true if the grid is empty
|
61
|
+
def empty?
|
62
|
+
each_cell do |cell|
|
63
|
+
return false if cell
|
64
|
+
end
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns true if the grid only has one element
|
69
|
+
def only_one?
|
70
|
+
counter = 0
|
71
|
+
each_cell do |cell|
|
72
|
+
counter += 1 if cell
|
73
|
+
return false if counter >= 2
|
74
|
+
end
|
75
|
+
counter == 1
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns true if every cell is set to a value
|
79
|
+
def full?
|
80
|
+
each_cell do |cell|
|
81
|
+
return false unless cell
|
82
|
+
end
|
83
|
+
true
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns true if the board has a wining pattern
|
87
|
+
def solved?
|
88
|
+
return true if won_across?
|
89
|
+
return true if won_up_and_down?
|
90
|
+
return true if won_diagonally?
|
91
|
+
false
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_s
|
95
|
+
output = ["-----------\n"]
|
96
|
+
@grid.each_with_index do |row, i|
|
97
|
+
row.each_with_index do |cell, j|
|
98
|
+
output << "#{cell || ((i*3)+j+1)} | #{"\n" if j==2}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
output << ["-----------\n\n"]
|
102
|
+
output.join
|
103
|
+
end
|
104
|
+
|
105
|
+
# Preform a deep clone of the board
|
106
|
+
# FIXME: This only works for SIZE=3
|
107
|
+
def clone
|
108
|
+
Board.new([[@grid[0][0], @grid[0][1], @grid[0][2] ],
|
109
|
+
[@grid[1][0], @grid[1][1], @grid[1][2] ],
|
110
|
+
[@grid[2][0], @grid[2][1], @grid[2][2] ]])
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def each_cell(&block)
|
116
|
+
@grid.each do |row|
|
117
|
+
row.each do |cell|
|
118
|
+
yield(cell)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def won_across?(grid = @grid)
|
124
|
+
grid.any? do |row|
|
125
|
+
winning_group?(row)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def won_up_and_down?
|
130
|
+
won_across?(@grid.transpose)
|
131
|
+
end
|
132
|
+
|
133
|
+
def won_diagonally?
|
134
|
+
base = (0..@grid.size-1)
|
135
|
+
left_to_right = base.collect { |i| @grid[i][i]}
|
136
|
+
right_to_left = base.collect { |i| @grid[i][@grid.size-1-i]}
|
137
|
+
return true if winning_group?(left_to_right)
|
138
|
+
return true if winning_group?(right_to_left)
|
139
|
+
false
|
140
|
+
end
|
141
|
+
|
142
|
+
def winning_group?(group)
|
143
|
+
# Won when none of the cells are nil and they are all the same value
|
144
|
+
!group.any? { |cell| cell.nil? } && group.uniq.size == 1
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
#This is a computer that will play a perfect game of tic-tac-toe
|
2
|
+
#Author: Eric Koslow
|
3
|
+
|
4
|
+
require_relative 'game_types/terminal_game'
|
5
|
+
|
6
|
+
module TicTacToe
|
7
|
+
class Game
|
8
|
+
def initialize(type=TerminalGame)
|
9
|
+
@board = Board.new
|
10
|
+
@game_type = type.new(@board)
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
if @game_type.computer_goes_first?
|
15
|
+
@computer_letter = O
|
16
|
+
get_move_from_computer!
|
17
|
+
else
|
18
|
+
@computer_letter = X
|
19
|
+
end
|
20
|
+
|
21
|
+
@game_type.update_board
|
22
|
+
|
23
|
+
loop do
|
24
|
+
@game_type.get_move_from_user!
|
25
|
+
@game_type.update_board
|
26
|
+
break if game_over?
|
27
|
+
|
28
|
+
get_move_from_computer!
|
29
|
+
@game_type.update_board
|
30
|
+
break if game_over?
|
31
|
+
end
|
32
|
+
|
33
|
+
if @game_type.play_again?
|
34
|
+
new_game
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def new_game
|
41
|
+
Game.new.run
|
42
|
+
end
|
43
|
+
|
44
|
+
def game_over?
|
45
|
+
if @board.solved?
|
46
|
+
@game_type.display_text("You lost!")
|
47
|
+
true
|
48
|
+
elsif @board.full?
|
49
|
+
@game_type.display_text("Cats game!")
|
50
|
+
true
|
51
|
+
else
|
52
|
+
false
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_move_from_computer!
|
57
|
+
Solver.new(@board, @computer_letter).next_move!
|
58
|
+
@game_type.display_text("Computer's move (#{@computer_letter}):")
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module TicTacToe
|
2
|
+
class TerminalGame
|
3
|
+
class IllegalMove < RuntimeError
|
4
|
+
end
|
5
|
+
|
6
|
+
def initialize(board)
|
7
|
+
@board = board
|
8
|
+
end
|
9
|
+
|
10
|
+
def computer_goes_first?
|
11
|
+
print "Would you like to play first or second? (f/s) "
|
12
|
+
input = gets.chomp
|
13
|
+
unless input =~ /^(f|first)$/i
|
14
|
+
@human_letter = X
|
15
|
+
true
|
16
|
+
else
|
17
|
+
@human_letter = O
|
18
|
+
false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_move_from_user!
|
23
|
+
print "Your move (#{@human_letter}) (1-9): "
|
24
|
+
input = gets.chomp
|
25
|
+
|
26
|
+
if input =~ /^\d$/
|
27
|
+
cords = cord_from_num(input.to_i)
|
28
|
+
if !@board.get_cell(*cords)
|
29
|
+
@board.play_at(*(cords+[@human_letter]))
|
30
|
+
else
|
31
|
+
raise IllegalMove.new("That cell is already taken")
|
32
|
+
end
|
33
|
+
else
|
34
|
+
raise IllegalMove.new("Must be a single number")
|
35
|
+
end
|
36
|
+
rescue IllegalMove => e
|
37
|
+
display_text "Illegal Move: #{e.message}. Please try again"
|
38
|
+
get_move_from_user!
|
39
|
+
end
|
40
|
+
|
41
|
+
def play_again?
|
42
|
+
print "Play again? (y/n) "
|
43
|
+
input = gets.chomp
|
44
|
+
input =~ /^(y|yes)$/i
|
45
|
+
end
|
46
|
+
|
47
|
+
def update_board
|
48
|
+
puts @board
|
49
|
+
end
|
50
|
+
|
51
|
+
def display_text(text)
|
52
|
+
puts text
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def cord_from_num(num)
|
58
|
+
num -= 1
|
59
|
+
[num % Board::SIZE, num/Board::SIZE]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative 'strategies/threebythree_stategy'
|
2
|
+
|
3
|
+
module TicTacToe
|
4
|
+
class Solver
|
5
|
+
|
6
|
+
def initialize(board, letter, strategy=ThreebythreeStrategy)
|
7
|
+
@strategy = strategy.new(board, letter)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Performs the next perfect move and updates the board as a side effect
|
11
|
+
def next_move!
|
12
|
+
@strategy.solve!
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# Brute Force Implementation for the Three by Three Strategy
|
2
|
+
# This implementation uses loops to try a change all of the board values and
|
3
|
+
# checking the result
|
4
|
+
#
|
5
|
+
# For example, win! is implemented by trying every cell, and checking if
|
6
|
+
# it was a winning solution
|
7
|
+
module TicTacToe
|
8
|
+
class BruteForceImplementation
|
9
|
+
|
10
|
+
def initialize(board, letter)
|
11
|
+
@board, @letter = board, letter
|
12
|
+
end
|
13
|
+
|
14
|
+
# Try placing letter at every available position
|
15
|
+
# If the board is solved, do that
|
16
|
+
def win!
|
17
|
+
each_position do |x, y|
|
18
|
+
temp_board = @board.clone
|
19
|
+
temp_board.play_at(x, y, @letter)
|
20
|
+
if temp_board.solved?
|
21
|
+
@board.play_at(x, y, @letter)
|
22
|
+
return true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
# Try placing the opponent's letter at every available position
|
29
|
+
# If the board is solved, block them at that position
|
30
|
+
def block!(board = @board, letter = @letter)
|
31
|
+
each_position do |x, y|
|
32
|
+
temp_board = board.clone
|
33
|
+
temp_board.play_at(x, y, other_player(letter))
|
34
|
+
if temp_board.solved?
|
35
|
+
board.play_at(x, y, letter)
|
36
|
+
return true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
# Try placing the letter at every position.
|
43
|
+
# If there are now two winning solutions for next turn, go there
|
44
|
+
def fork!
|
45
|
+
each_forking_position do |x, y|
|
46
|
+
@board.play_at(x, y, @letter)
|
47
|
+
return true
|
48
|
+
end
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
# Try placing the opponent's letter at every position.
|
53
|
+
# If there are now two winning solutions for next turn, block them there
|
54
|
+
def block_fork!(board = @board)
|
55
|
+
each_forking_position(other_player) do |x, y|
|
56
|
+
temp_board = board.clone
|
57
|
+
temp_board.play_at(x,y,@letter)
|
58
|
+
# Search for the elusive double fork
|
59
|
+
each_forking_position(other_player, temp_board) do |x, y|
|
60
|
+
return force_a_block
|
61
|
+
end
|
62
|
+
board.play_at(x, y, @letter)
|
63
|
+
return true
|
64
|
+
end
|
65
|
+
false
|
66
|
+
end
|
67
|
+
|
68
|
+
def center!
|
69
|
+
return false if @board.center_cell
|
70
|
+
|
71
|
+
@board.center_cell = @letter
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
# Cycle through all of the corners looking for the opponent's letter
|
76
|
+
# If one is found, place letter at the opposite corner
|
77
|
+
def oposite_corner!
|
78
|
+
@board.corners.each_with_index do |corner, index|
|
79
|
+
if corner == other_player
|
80
|
+
case index
|
81
|
+
when 0 # Top Left
|
82
|
+
next if @board.get_cell(Board::SIZE-1, Board::SIZE-1)
|
83
|
+
@board.play_at(Board::SIZE-1, Board::SIZE-1, @letter)
|
84
|
+
return true
|
85
|
+
when 1 # Top Right
|
86
|
+
next if @board.get_cell(0, Board::SIZE-1)
|
87
|
+
@board.play_at(0, Board::SIZE-1, @letter)
|
88
|
+
return true
|
89
|
+
when 2 # Bottom Right
|
90
|
+
next if @board.get_cell(0, 0)
|
91
|
+
@board.play_at(0, 0, @letter)
|
92
|
+
return true
|
93
|
+
when 3 # Bottom Left
|
94
|
+
next if @board.get_cell(Board::SIZE-1, 0)
|
95
|
+
@board.play_at(Board::SIZE-1, 0, @letter)
|
96
|
+
return true
|
97
|
+
else
|
98
|
+
raise Exception.new("Board#corners returned more than 4")
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
false
|
103
|
+
end
|
104
|
+
|
105
|
+
# Cycle though all of the corners, until one is found that is empty
|
106
|
+
def empty_corner!
|
107
|
+
@board.corners.each_with_index do |corner, index|
|
108
|
+
unless corner
|
109
|
+
case index
|
110
|
+
when 0 # Top Left
|
111
|
+
@board.play_at(0, 0, @letter)
|
112
|
+
return true
|
113
|
+
when 1 # Top Right
|
114
|
+
@board.play_at(Board::SIZE-1, 0, @letter)
|
115
|
+
return true
|
116
|
+
when 2 # Bottom Right
|
117
|
+
@board.play_at(Board::SIZE-1, Board::SIZE-1, @letter)
|
118
|
+
return true
|
119
|
+
when 3 # Bottom Left
|
120
|
+
@board.play_at(0, Board::SIZE-1, @letter)
|
121
|
+
return true
|
122
|
+
else
|
123
|
+
raise Exception.new("Board#corners returned more than 4")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
false
|
128
|
+
end
|
129
|
+
|
130
|
+
# Place letter at a random empty cell, at this point it should only be sides left
|
131
|
+
def empty_side!
|
132
|
+
@board.any_empty_position do |x, y|
|
133
|
+
@board.play_at(x, y, @letter)
|
134
|
+
return true
|
135
|
+
end
|
136
|
+
false
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def other_player(letter = @letter)
|
142
|
+
letter == X ? O : X
|
143
|
+
end
|
144
|
+
|
145
|
+
def fork_exsits?(x, y, letter = @letter, board = @board)
|
146
|
+
(count = can_win_next_turn?(x, y, letter, board)) && count >= 2
|
147
|
+
end
|
148
|
+
|
149
|
+
def each_position(&block)
|
150
|
+
Board::SIZE.times do |y|
|
151
|
+
Board::SIZE.times do |x|
|
152
|
+
yield(x, y)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def each_forking_position(letter = @letter, board = @board, &block)
|
158
|
+
each_position do |x, y|
|
159
|
+
if fork_exsits?(x, y, letter, board)
|
160
|
+
yield(x, y)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def can_win_next_turn?(x, y, letter = @letter, board = @board)
|
166
|
+
count = 0
|
167
|
+
temp_board = board.clone
|
168
|
+
temp_board.play_at(x,y,letter)
|
169
|
+
each_position do |x, y|
|
170
|
+
inner_loop_board = temp_board.clone
|
171
|
+
inner_loop_board.play_at(x, y, letter)
|
172
|
+
count += 1 if inner_loop_board.solved?
|
173
|
+
end
|
174
|
+
return count == 0 ? false : count
|
175
|
+
end
|
176
|
+
|
177
|
+
def force_a_block
|
178
|
+
# Force them to block without creating another fork
|
179
|
+
each_position do |x, y|
|
180
|
+
if can_win_next_turn?(x, y)
|
181
|
+
temp_board = @board.clone
|
182
|
+
temp_board.play_at(x, y, @letter)
|
183
|
+
raise Exception.new("Couldn't force a block") unless block!(temp_board, other_player)
|
184
|
+
# Did I just create another fork with that block?
|
185
|
+
if fork_exsits?(x, y, other_player, temp_board)
|
186
|
+
next
|
187
|
+
else
|
188
|
+
@board.play_at(x, y, @letter)
|
189
|
+
return true
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
raise Exception.new("No position found to block the fork")
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative 'threebythree_implementations/brute_force_implementation'
|
2
|
+
|
3
|
+
module TicTacToe
|
4
|
+
class ThreebythreeStrategy
|
5
|
+
def initialize(board, letter, implementation=BruteForceImplementation)
|
6
|
+
@implementation = implementation.new(board, letter)
|
7
|
+
end
|
8
|
+
|
9
|
+
# The strategy is from the Wikipedia article on Tic-Tac-Toe
|
10
|
+
# 1) Try to win
|
11
|
+
# 2) Try to block if they're about to win
|
12
|
+
# 3) Try to fork so you'll win next turn
|
13
|
+
# 4) Try to block their fork so they will not win next turn
|
14
|
+
# 5) Take the center if its not already taken
|
15
|
+
# 6) Play the opposite corner of your opponent
|
16
|
+
# 7) Play in an empty corner
|
17
|
+
# 8) Play in an empty side
|
18
|
+
def solve!
|
19
|
+
return if @implementation.win!
|
20
|
+
return if @implementation.block!
|
21
|
+
return if @implementation.fork!
|
22
|
+
return if @implementation.block_fork!
|
23
|
+
return if @implementation.center!
|
24
|
+
return if @implementation.oposite_corner!
|
25
|
+
return if @implementation.empty_corner!
|
26
|
+
return if @implementation.empty_side!
|
27
|
+
raise Exception.new("No possible moves to play!")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/tic_tac_toe.rb
ADDED
data/test/board_test.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class BoardTest < MiniTest::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
TicTacToe::Board.instance_eval { attr_accessor :grid } # For testing purposes
|
6
|
+
@board = TicTacToe::Board.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_empty
|
10
|
+
# Board empty when created
|
11
|
+
assert @board.empty?
|
12
|
+
|
13
|
+
# Board not longer empty after letter placed
|
14
|
+
@board.play_at(0,0,'o')
|
15
|
+
refute @board.empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_full
|
19
|
+
# Board not full when empty
|
20
|
+
refute @board.full?
|
21
|
+
|
22
|
+
# Board full when no cells are nil
|
23
|
+
@board.grid = [ %w(x o x), %w(o x o), %w(x o x)]
|
24
|
+
assert @board.full?
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_only_one
|
28
|
+
# False when empty
|
29
|
+
refute @board.only_one?
|
30
|
+
|
31
|
+
# True when one
|
32
|
+
@board.grid = [ ['x',nil,nil], [nil,nil,nil], [nil,nil,nil] ]
|
33
|
+
assert @board.only_one?
|
34
|
+
|
35
|
+
# False when more than one
|
36
|
+
@board.grid = [ ['x','x',nil], [nil,nil,nil], [nil,nil,nil] ]
|
37
|
+
refute @board.only_one?
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_get_cell
|
41
|
+
# Returns nil when nothing in cell
|
42
|
+
assert_nil @board.get_cell(0, 0)
|
43
|
+
|
44
|
+
# Returns the right letter after they've been played
|
45
|
+
@board.play_at(0,0,"o")
|
46
|
+
assert_equal "o", @board.get_cell(0, 0)
|
47
|
+
|
48
|
+
@board.play_at(1,2,"o")
|
49
|
+
assert_equal "o", @board.get_cell(1, 2)
|
50
|
+
end
|
51
|
+
|
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
|
95
|
+
assert_nil @board.get_cell(1,1)
|
96
|
+
@board.play_at(1, 1, 'x')
|
97
|
+
assert_equal 'x', @board.get_cell(1,1)
|
98
|
+
@board.play_at(1, 1, 'o')
|
99
|
+
assert_equal 'x', @board.get_cell(1,1)
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_solved_acoss
|
103
|
+
refute @board.solved?
|
104
|
+
|
105
|
+
@board.grid = [ %w(x x x), [nil, nil, nil], [nil, nil, nil]]
|
106
|
+
assert @board.solved?
|
107
|
+
|
108
|
+
@board.grid = [ [nil,nil,'x'], [nil,'o',nil], ['x','x','x'] ]
|
109
|
+
assert @board.solved?
|
110
|
+
|
111
|
+
@board.grid = [ %w(x o x), [nil, nil, nil], [nil, nil, nil]]
|
112
|
+
refute @board.solved?
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_solved_up_and_down
|
116
|
+
refute @board.solved?
|
117
|
+
|
118
|
+
@board.grid = [ %w(x o o), ['x', nil, nil], ['x', nil, nil]]
|
119
|
+
assert @board.solved?
|
120
|
+
|
121
|
+
@board.grid = [ [nil, nil, 'x'], [nil, nil, 'x'], [nil, nil, 'x']]
|
122
|
+
assert @board.solved?
|
123
|
+
|
124
|
+
@board.grid = [ %w(x o o), ['x', nil, nil], ['o', nil, nil]]
|
125
|
+
refute @board.solved?
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_won_diagonally
|
129
|
+
refute @board.solved?
|
130
|
+
|
131
|
+
@board.grid = [ ['x', 'o', 'x'], ['x', 'x', nil], ['o', nil, 'x']]
|
132
|
+
assert @board.solved?
|
133
|
+
|
134
|
+
@board.grid = [ [nil, nil, 'x'], [nil, 'x', nil], ['x', nil, nil]]
|
135
|
+
assert @board.solved?
|
136
|
+
|
137
|
+
@board.grid = [ [nil, nil, 'x'], [nil, 'x', nil], ['o', nil, nil]]
|
138
|
+
refute @board.solved?
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class BruteForceImplementationTest < MiniTest::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
TicTacToe::BruteForceImplementation.instance_eval { attr_accessor :board }
|
6
|
+
@implementation = TicTacToe::BruteForceImplementation.new(TicTacToe::Board.new, 'x')
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_can_win_next_turn
|
10
|
+
# Test private method that it returns the correct amount of winning moves
|
11
|
+
|
12
|
+
set_grid([[ 'x', nil, nil], [nil, nil, nil], [nil, nil, nil] ])
|
13
|
+
assert_equal 1, @implementation.send(:can_win_next_turn?, 0, 1)
|
14
|
+
|
15
|
+
# 1 | 2 | x
|
16
|
+
# 4 | o | 6
|
17
|
+
# 7 | x | 9
|
18
|
+
set_grid([[ nil, nil, 'x'], [nil, 'o', nil], [nil, 'x', nil] ])
|
19
|
+
assert_equal 2, @implementation.send(:can_win_next_turn?, 2, 2)
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_grid(grid)
|
23
|
+
@implementation.board.instance_variable_set("@grid", grid)
|
24
|
+
end
|
25
|
+
end
|
data/test/solver_test.rb
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class SolverTest < MiniTest::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
TicTacToe::Solver.instance_eval { attr_accessor :strategy } # For testing
|
6
|
+
@solver = TicTacToe::Solver.new(TicTacToe::Board.new, 'x')
|
7
|
+
@solver.strategy.class.instance_eval { attr_accessor :implementation } # For testing
|
8
|
+
@solver.strategy.implementation.class.instance_eval { attr_accessor :board } # For testing
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_win_next_move
|
12
|
+
refute board.solved?
|
13
|
+
|
14
|
+
# Will win, when there is a winning move
|
15
|
+
set_grid([[ 'x', 'x', nil], [nil, nil, nil], [nil, nil, nil] ])
|
16
|
+
refute board.solved? # Is not solved yet
|
17
|
+
@solver.next_move!
|
18
|
+
assert board.solved? # Solved it
|
19
|
+
assert_equal 'x', board.get_cell(2, 0) # Placed the right letter
|
20
|
+
|
21
|
+
set_grid([[ 'x', nil, nil], [nil, 'x', nil], [nil, nil, nil] ])
|
22
|
+
refute board.solved?
|
23
|
+
@solver.next_move!
|
24
|
+
assert board.solved?
|
25
|
+
assert_equal 'x', board.get_cell(2, 2)
|
26
|
+
|
27
|
+
set_grid([[ 'x', nil, nil], [nil, nil, nil], ['x', nil, nil] ])
|
28
|
+
refute board.solved?
|
29
|
+
@solver.next_move!
|
30
|
+
assert board.solved?
|
31
|
+
assert_equal 'x', board.get_cell(0, 1)
|
32
|
+
|
33
|
+
# Can not win when there is no winning move
|
34
|
+
set_grid([[ 'o', nil, nil], [nil, nil, nil], ['o', nil, nil] ])
|
35
|
+
refute board.solved?
|
36
|
+
@solver.next_move!
|
37
|
+
refute board.solved?
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_block_next_move
|
41
|
+
refute board.solved?
|
42
|
+
|
43
|
+
# Will block when the opponent is about to win
|
44
|
+
set_grid([ ['o', 'o', nil], [nil, nil,nil], [nil,nil,nil]])
|
45
|
+
refute board.solved?
|
46
|
+
@solver.next_move!
|
47
|
+
refute board.solved?
|
48
|
+
# Placed the right letter at the right place
|
49
|
+
assert_equal 'x', board.get_cell(2, 0)
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def test_fork_next_move
|
54
|
+
refute board.solved?
|
55
|
+
|
56
|
+
# Place letter in place where next turn is an automatic win
|
57
|
+
set_grid([ ['x', nil, nil], [nil, 'o',nil], [nil,nil,'x']])
|
58
|
+
refute board.solved?
|
59
|
+
@solver.next_move!
|
60
|
+
refute board.solved?
|
61
|
+
assert_equal 'x', board.get_cell(2, 0)
|
62
|
+
|
63
|
+
# 1 | 2 | x
|
64
|
+
# 4 | o | 6
|
65
|
+
# 7 | x | 9 <--
|
66
|
+
set_grid([ [nil, nil, 'x'], [nil, 'o',nil], [nil,'x',nil]])
|
67
|
+
refute board.solved?
|
68
|
+
@solver.next_move!
|
69
|
+
refute board.solved?
|
70
|
+
assert_equal 'x', board.get_cell(2, 2)
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_block_fork_next_move
|
74
|
+
# Will block an opponent if they will have a change to fork next turn
|
75
|
+
|
76
|
+
# 1 | 2 | o
|
77
|
+
# 4 | x | 6
|
78
|
+
# 7 | o | 9 <--
|
79
|
+
set_grid([ [nil, nil, 'o'], [nil, 'x',nil], [nil,'o',nil]])
|
80
|
+
refute board.solved?
|
81
|
+
@solver.next_move!
|
82
|
+
refute board.solved?
|
83
|
+
assert_equal 'x', board.get_cell(2, 2)
|
84
|
+
|
85
|
+
# 1 | 2 | o
|
86
|
+
# 4 | x | 6
|
87
|
+
# o | 8 | 9
|
88
|
+
set_grid([[nil,nil,'o'],[nil,'x',nil],['o',nil,nil]])
|
89
|
+
@solver.next_move!
|
90
|
+
assert_nil board.get_cell(2,2)
|
91
|
+
assert_nil board.get_cell(0,0)
|
92
|
+
|
93
|
+
# x | 2 | 3
|
94
|
+
# 4 | o | 6
|
95
|
+
# 7 | 8 | o
|
96
|
+
set_grid([ ['x',nil,nil], [nil,'o',nil], [nil,nil,'o'] ])
|
97
|
+
@solver.next_move!
|
98
|
+
assert_nil board.get_cell(1, 0)
|
99
|
+
assert_nil board.get_cell(0, 1)
|
100
|
+
assert_nil board.get_cell(2, 1)
|
101
|
+
assert_nil board.get_cell(1, 2)
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_center
|
105
|
+
# Will play in the center if its an empty board
|
106
|
+
assert board.empty?
|
107
|
+
@solver.next_move!
|
108
|
+
assert_equal 'x', board.center_cell
|
109
|
+
assert board.only_one?
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_oposite_corner
|
113
|
+
# 1 | 2 | o
|
114
|
+
# 4 | x | 6
|
115
|
+
# 7 | 8 | 9
|
116
|
+
set_grid([[nil,nil,'o'],[nil,'x',nil],[nil,nil,nil]])
|
117
|
+
@solver.next_move!
|
118
|
+
assert_equal 'x', board.get_cell(0, 2)
|
119
|
+
|
120
|
+
set_grid([['o',nil,nil],[nil,'x',nil],[nil,nil,nil]])
|
121
|
+
@solver.next_move!
|
122
|
+
assert_equal 'x', board.get_cell(2, 2)
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_empty_corner
|
126
|
+
set_grid([[nil,nil,nil],[nil,'o',nil],[nil,nil,nil]])
|
127
|
+
@solver.next_move!
|
128
|
+
assert_equal 'x', board.get_cell(0,0)
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_any_empty_position
|
132
|
+
# o | x | o
|
133
|
+
# o | x | 6
|
134
|
+
# x | o | x
|
135
|
+
set_grid([['o', 'x', 'o'],['o','x',nil],['x','o','x']])
|
136
|
+
@solver.next_move!
|
137
|
+
assert_equal 'x', board.get_cell(2, 1)
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
|
142
|
+
def set_grid(grid)
|
143
|
+
board.instance_variable_set("@grid", grid)
|
144
|
+
end
|
145
|
+
|
146
|
+
def board
|
147
|
+
@solver.strategy.implementation.board
|
148
|
+
end
|
149
|
+
end
|
data/test/test_helper.rb
ADDED
data/tic-tac-toe.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "tic-tac-toe/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "erics_tic_tac_toe"
|
7
|
+
s.version = TicTacToe::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Eric Koslow"]
|
10
|
+
s.email = ["ekoslow@gmail.com"]
|
11
|
+
s.homepage = "https://github.com/ekosz/Erics-Tic-Tac-Toe"
|
12
|
+
s.summary = %q{A game of Tic Tac Toe}
|
13
|
+
s.description = %q{Plays the perfect game of Tic Tac Toe everytime. This computer can not lose.}
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_development_dependency 'minitest'
|
21
|
+
s.add_development_dependency "rake"
|
22
|
+
end
|
23
|
+
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: erics_tic_tac_toe
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.1.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Eric Koslow
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2012-04-09 00:00:00 -04:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: minitest
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "0"
|
25
|
+
type: :development
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: "0"
|
36
|
+
type: :development
|
37
|
+
version_requirements: *id002
|
38
|
+
description: Plays the perfect game of Tic Tac Toe everytime. This computer can not lose.
|
39
|
+
email:
|
40
|
+
- ekoslow@gmail.com
|
41
|
+
executables:
|
42
|
+
- tic_tac_toe
|
43
|
+
extensions: []
|
44
|
+
|
45
|
+
extra_rdoc_files: []
|
46
|
+
|
47
|
+
files:
|
48
|
+
- .gitignore
|
49
|
+
- LICENSE
|
50
|
+
- README.markdown
|
51
|
+
- Rakefile
|
52
|
+
- bin/tic_tac_toe
|
53
|
+
- lib/tic-tac-toe/board.rb
|
54
|
+
- lib/tic-tac-toe/game.rb
|
55
|
+
- lib/tic-tac-toe/game_types/terminal_game.rb
|
56
|
+
- lib/tic-tac-toe/solver.rb
|
57
|
+
- lib/tic-tac-toe/strategies/threebythree_implementations/brute_force_implementation.rb
|
58
|
+
- lib/tic-tac-toe/strategies/threebythree_stategy.rb
|
59
|
+
- lib/tic-tac-toe/version.rb
|
60
|
+
- lib/tic_tac_toe.rb
|
61
|
+
- test/board_test.rb
|
62
|
+
- test/brute_force_implementation_test.rb
|
63
|
+
- test/solver_test.rb
|
64
|
+
- test/test_helper.rb
|
65
|
+
- tic-tac-toe.gemspec
|
66
|
+
has_rdoc: true
|
67
|
+
homepage: https://github.com/ekosz/Erics-Tic-Tac-Toe
|
68
|
+
licenses: []
|
69
|
+
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
|
73
|
+
require_paths:
|
74
|
+
- lib
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
76
|
+
none: false
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: "0"
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: "0"
|
87
|
+
requirements: []
|
88
|
+
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 1.6.2
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: A game of Tic Tac Toe
|
94
|
+
test_files:
|
95
|
+
- test/board_test.rb
|
96
|
+
- test/brute_force_implementation_test.rb
|
97
|
+
- test/solver_test.rb
|
98
|
+
- test/test_helper.rb
|