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.
- data/.gitignore +1 -0
- data/README.markdown +6 -7
- data/Rakefile +2 -1
- data/bin/tic_tac_toe +41 -2
- data/lib/tic_tac_toe.rb +5 -4
- data/lib/tic_tac_toe/board.rb +153 -0
- data/lib/tic_tac_toe/game.rb +70 -0
- data/lib/tic_tac_toe/game_types/terminal_game.rb +86 -0
- data/lib/tic_tac_toe/player.rb +13 -0
- data/lib/tic_tac_toe/players/computer_player.rb +26 -0
- data/lib/tic_tac_toe/players/human_player.rb +28 -0
- data/lib/tic_tac_toe/presentors/game_presenter.rb +16 -0
- data/lib/tic_tac_toe/presentors/player_presenter.rb +13 -0
- data/lib/tic_tac_toe/strategies/minimax_strategy.rb +91 -0
- data/lib/tic_tac_toe/strategies/three_by_three_strategy.rb +260 -0
- data/lib/{tic-tac-toe → tic_tac_toe}/version.rb +1 -1
- data/test/board_test.rb +40 -52
- data/test/game_presentor_test.rb +19 -0
- data/test/game_test.rb +79 -0
- data/test/player_presenter_test.rb +12 -0
- data/test/player_test.rb +57 -0
- data/test/potential_state_test.rb +53 -0
- data/test/solver_test.rb +169 -93
- data/test/terminal_game_test.rb +78 -0
- data/test/test_helper.rb +4 -0
- data/tic-tac-toe.gemspec +1 -1
- metadata +25 -11
- data/lib/tic-tac-toe/board.rb +0 -148
- data/lib/tic-tac-toe/game.rb +0 -62
- data/lib/tic-tac-toe/game_types/terminal_game.rb +0 -63
- data/lib/tic-tac-toe/solver.rb +0 -16
- data/lib/tic-tac-toe/strategies/threebythree_implementations/brute_force_implementation.rb +0 -197
- data/lib/tic-tac-toe/strategies/threebythree_stategy.rb +0 -30
- data/test/brute_force_implementation_test.rb +0 -25
data/.gitignore
CHANGED
data/README.markdown
CHANGED
@@ -4,12 +4,11 @@ Tic Tac Toe
|
|
4
4
|
How to play
|
5
5
|
----
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
Game.new.run
|
7
|
+
gem install erics_tic_tac_toe
|
8
|
+
tic_tac_toe
|
10
9
|
|
11
|
-
|
12
|
-
|
10
|
+
TODO
|
11
|
+
---
|
13
12
|
|
14
|
-
|
15
|
-
|
13
|
+
* Use Curses for a nicer playing experience
|
14
|
+
* Colorize diffs so that its easier to see what move was just played
|
data/Rakefile
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'rake'
|
2
|
+
include Rake::DSL
|
2
3
|
require 'bundler'
|
3
4
|
require 'rake/testtask'
|
4
5
|
|
@@ -6,7 +7,7 @@ Bundler::GemHelper.install_tasks
|
|
6
7
|
|
7
8
|
Rake::TestTask.new do |t|
|
8
9
|
t.libs << 'test'
|
9
|
-
t.pattern = '
|
10
|
+
t.pattern = '**/*_test.rb'
|
10
11
|
end
|
11
12
|
|
12
13
|
task :default => :test
|
data/bin/tic_tac_toe
CHANGED
@@ -6,7 +6,7 @@ begin
|
|
6
6
|
rescue LoadError
|
7
7
|
case $!.to_s
|
8
8
|
when /tic_tac_toe/
|
9
|
-
|
9
|
+
if !$:.include?(libdir)
|
10
10
|
warn "warn: #$!. trying again with #{libdir} on load path"
|
11
11
|
$:.unshift(libdir)
|
12
12
|
retry
|
@@ -15,4 +15,43 @@ rescue LoadError
|
|
15
15
|
raise
|
16
16
|
end
|
17
17
|
|
18
|
-
|
18
|
+
def setup
|
19
|
+
game = TicTacToe::Game.new
|
20
|
+
terminal = TicTacToe::TerminalGame.new(game.board)
|
21
|
+
|
22
|
+
if terminal.computer_goes_first?
|
23
|
+
@player_1 = TicTacToe::ComputerPlayer.new(letter: 'o')
|
24
|
+
@player_2 = TicTacToe::HumanPlayer.new(letter: 'x')
|
25
|
+
else
|
26
|
+
@player_1 = TicTacToe::HumanPlayer.new(letter: 'o')
|
27
|
+
@player_2 = TicTacToe::HumanPlayer.new(letter: 'x')
|
28
|
+
end
|
29
|
+
|
30
|
+
game = TicTacToe::Game.new(nil, @player_1, @player_2)
|
31
|
+
|
32
|
+
[game, terminal]
|
33
|
+
end
|
34
|
+
|
35
|
+
game, terminal = setup
|
36
|
+
|
37
|
+
loop do
|
38
|
+
game.start
|
39
|
+
terminal = TicTacToe::TerminalGame.new(game.board)
|
40
|
+
|
41
|
+
if game.solved? || game.cats?
|
42
|
+
terminal.update_board
|
43
|
+
break unless terminal.play_again?
|
44
|
+
game, terminal = setup
|
45
|
+
end
|
46
|
+
|
47
|
+
terminal.update_board
|
48
|
+
|
49
|
+
move = terminal.get_move_from_user
|
50
|
+
@player_1 = TicTacToe::HumanPlayer.new(letter: @player_1.letter, move: move)
|
51
|
+
@player_2 = TicTacToe::ComputerPlayer.new(letter: @player_2.letter)
|
52
|
+
|
53
|
+
grid = game.grid
|
54
|
+
game = TicTacToe::Game.new(grid, @player_1, @player_2)
|
55
|
+
|
56
|
+
end
|
57
|
+
|
data/lib/tic_tac_toe.rb
CHANGED
@@ -7,7 +7,8 @@ module TicTacToe
|
|
7
7
|
end
|
8
8
|
|
9
9
|
# Internal Project Requires
|
10
|
-
require '
|
11
|
-
require '
|
12
|
-
require '
|
13
|
-
|
10
|
+
require 'tic_tac_toe/board'
|
11
|
+
require 'tic_tac_toe/game'
|
12
|
+
require 'tic_tac_toe/player'
|
13
|
+
require 'tic_tac_toe/presentors/game_presenter'
|
14
|
+
require 'tic_tac_toe/presentors/player_presenter'
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module TicTacToe
|
2
|
+
# The main class for managing the state of the game board
|
3
|
+
# The board data is represented at a two dimensional array
|
4
|
+
# It provides helper methods for access the data
|
5
|
+
class Board
|
6
|
+
|
7
|
+
attr_accessor :grid
|
8
|
+
|
9
|
+
def initialize(size=3)
|
10
|
+
#[[ nil, nil, nil],
|
11
|
+
# [ nil, nil, nil],
|
12
|
+
# [ nil, nil, nil]]
|
13
|
+
@grid = Array.new(size) { Array.new(size) { nil } }
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_cell(row, column)
|
17
|
+
@grid[column][row]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the corners of the empty cells
|
21
|
+
def empty_positions(&block)
|
22
|
+
positions = []
|
23
|
+
each_position do |row, column|
|
24
|
+
next if get_cell(row, column)
|
25
|
+
yield(row, column) if block_given?
|
26
|
+
positions << [row, column]
|
27
|
+
end
|
28
|
+
positions
|
29
|
+
end
|
30
|
+
|
31
|
+
# Plays a letter at a position, unless that position has already been taken
|
32
|
+
def play_at(row, column, letter)
|
33
|
+
@grid[column][row] ||= letter
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns true if the grid is empty
|
38
|
+
def empty?
|
39
|
+
each_cell do |cell|
|
40
|
+
return false if cell
|
41
|
+
end
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns true if the grid only has one element
|
46
|
+
def only_one?
|
47
|
+
counter = 0
|
48
|
+
each_cell do |cell|
|
49
|
+
counter += 1 if cell
|
50
|
+
return false if counter >= 2
|
51
|
+
end
|
52
|
+
counter == 1
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns true if every cell is set to a value
|
56
|
+
def full?
|
57
|
+
each_cell do |cell|
|
58
|
+
return false unless cell
|
59
|
+
end
|
60
|
+
true
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns true if the board has a wining pattern
|
64
|
+
def solved?
|
65
|
+
letter = won_across?
|
66
|
+
return letter if letter
|
67
|
+
letter = won_up_and_down?
|
68
|
+
return letter if letter
|
69
|
+
letter = won_diagonally?
|
70
|
+
return letter if letter
|
71
|
+
false
|
72
|
+
end
|
73
|
+
|
74
|
+
def winner
|
75
|
+
solved?
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_s
|
79
|
+
output = ["-----------\n"]
|
80
|
+
@grid.each_with_index do |row, i|
|
81
|
+
row.each_with_index do |cell, j|
|
82
|
+
output << "#{cell || ((i*@grid.size)+j+1)} | #{"\n" if j==@grid.size-1}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
output << ["-----------\n\n"]
|
86
|
+
output.join
|
87
|
+
end
|
88
|
+
|
89
|
+
# Preform a deep clone of the board
|
90
|
+
def clone
|
91
|
+
board = Board.new
|
92
|
+
board.grid = @grid.map { |row| row.map { |cell| cell } }
|
93
|
+
board
|
94
|
+
end
|
95
|
+
|
96
|
+
def size
|
97
|
+
@grid.size
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def each_cell(&block)
|
103
|
+
@grid.each do |row|
|
104
|
+
row.each do |cell|
|
105
|
+
yield(cell)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def each_position(&block)
|
111
|
+
@grid.each_with_index do |row, y|
|
112
|
+
row.each_with_index do |cell, x|
|
113
|
+
yield(y, x)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def won_across?(grid = @grid)
|
119
|
+
winning_letter = false
|
120
|
+
grid.each do |row|
|
121
|
+
winning_letter = row[0] if winning_group?(row)
|
122
|
+
end
|
123
|
+
winning_letter
|
124
|
+
end
|
125
|
+
|
126
|
+
def won_up_and_down?
|
127
|
+
won_across?(@grid.transpose)
|
128
|
+
end
|
129
|
+
|
130
|
+
def won_diagonally?
|
131
|
+
right_limit = @grid.size-1
|
132
|
+
base = (0..right_limit)
|
133
|
+
|
134
|
+
letter = create_and_check_group(base) { |i| @grid[i][i] }
|
135
|
+
return letter if letter
|
136
|
+
letter = create_and_check_group(base) { |i| @grid[i][right_limit-i] }
|
137
|
+
return letter if letter
|
138
|
+
|
139
|
+
false
|
140
|
+
end
|
141
|
+
|
142
|
+
def create_and_check_group(base, &block)
|
143
|
+
group = base.collect { |i| yield(i) }
|
144
|
+
return group[0] if winning_group?(group)
|
145
|
+
false
|
146
|
+
end
|
147
|
+
|
148
|
+
def winning_group?(group)
|
149
|
+
!group.any? { |cell| cell.nil? } && group.uniq.size == 1
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,70 @@
|
|
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
|
+
require_relative 'strategies/three_by_three_strategy'
|
6
|
+
require_relative 'strategies/minimax_strategy'
|
7
|
+
require_relative 'players/human_player'
|
8
|
+
require_relative 'players/computer_player'
|
9
|
+
|
10
|
+
module TicTacToe
|
11
|
+
# The main director of the program
|
12
|
+
# Directs its gametype when to retrieve information from the user
|
13
|
+
class Game
|
14
|
+
attr_reader :board
|
15
|
+
|
16
|
+
attr_reader :player_moves
|
17
|
+
|
18
|
+
def initialize(board=nil, player_1=nil, player_2=nil)
|
19
|
+
@board = Board.new
|
20
|
+
@board.grid = board || [[nil,nil,nil],[nil,nil,nil],[nil,nil,nil]]
|
21
|
+
@player_1 = player_1
|
22
|
+
@player_2 = player_2
|
23
|
+
@current_player = @player_1 && @player_1.has_next_move? ? @player_1 : @player_2
|
24
|
+
end
|
25
|
+
|
26
|
+
def start
|
27
|
+
while @current_player && (move = @current_player.get_move(@board))
|
28
|
+
move = number_to_cords(move) unless move.is_a?(Array)
|
29
|
+
|
30
|
+
@board.play_at(*move, @current_player.letter)
|
31
|
+
break if over?
|
32
|
+
|
33
|
+
switch_player
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def grid
|
38
|
+
@board.grid
|
39
|
+
end
|
40
|
+
|
41
|
+
def solved?
|
42
|
+
@board.solved?
|
43
|
+
end
|
44
|
+
|
45
|
+
def cats?
|
46
|
+
@board.full? && !solved?
|
47
|
+
end
|
48
|
+
|
49
|
+
def winner
|
50
|
+
@board.winner
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def over?
|
56
|
+
solved? || cats?
|
57
|
+
end
|
58
|
+
|
59
|
+
def number_to_cords(num)
|
60
|
+
num = num.to_i
|
61
|
+
num -= 1
|
62
|
+
[num % @board.size, num/@board.size]
|
63
|
+
end
|
64
|
+
|
65
|
+
def switch_player
|
66
|
+
@current_player = @current_player == @player_1 ? @player_2 : @player_1
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module TicTacToe
|
2
|
+
# Terminal GameType
|
3
|
+
# Interacts with the user through the terminal
|
4
|
+
# Uses puts for output, and gets for input
|
5
|
+
class TerminalGame
|
6
|
+
# Internal Error used when a user tries pulling an illegal move
|
7
|
+
class IllegalMove < RuntimeError
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(board, io=Kernel)
|
11
|
+
@board = board
|
12
|
+
@io = io
|
13
|
+
end
|
14
|
+
|
15
|
+
def computer_goes_first?
|
16
|
+
input = get_input("Would you like to play first or second? (f/s)")
|
17
|
+
|
18
|
+
return true unless input =~ /^(f|first)$/i
|
19
|
+
false
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_move_from_user
|
23
|
+
cords = get_cords_from_user
|
24
|
+
|
25
|
+
raise IllegalMove.new("That cell is already taken") unless empty_cell?(cords)
|
26
|
+
|
27
|
+
cords
|
28
|
+
rescue IllegalMove => error
|
29
|
+
display_text "Illegal Move: #{error.message}. Please try again"
|
30
|
+
retry
|
31
|
+
end
|
32
|
+
|
33
|
+
def play_again?
|
34
|
+
input = get_input "Play again? (y/n)"
|
35
|
+
input =~ /^(y|yes)$/i
|
36
|
+
end
|
37
|
+
|
38
|
+
def select(choices)
|
39
|
+
loop do
|
40
|
+
output = "Select the solver:\n"
|
41
|
+
choices.each_with_index { |choice, i| output += "#{i+1}: #{choice}\n" }
|
42
|
+
|
43
|
+
input = get_input(output)
|
44
|
+
|
45
|
+
if input =~ /^\d+$/ and input.to_i <= choices.size
|
46
|
+
return choices[input.to_i-1]
|
47
|
+
end
|
48
|
+
|
49
|
+
display_text("Not a valid choice")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def update_board
|
54
|
+
@io.puts @board
|
55
|
+
end
|
56
|
+
|
57
|
+
def display_text(text)
|
58
|
+
@io.puts text
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def get_input(text)
|
64
|
+
@io.print text + ' '
|
65
|
+
@io.gets.chomp
|
66
|
+
end
|
67
|
+
|
68
|
+
def empty_cell?(cords)
|
69
|
+
!@board.get_cell(*cords)
|
70
|
+
end
|
71
|
+
|
72
|
+
def get_cords_from_user
|
73
|
+
input = get_input("Your move (1-#{@board.size**2}):")
|
74
|
+
|
75
|
+
raise IllegalMove.new("That is not a real location") unless input =~ /^\d+$/
|
76
|
+
|
77
|
+
cord_from_num(input.to_i)
|
78
|
+
end
|
79
|
+
|
80
|
+
def cord_from_num(num)
|
81
|
+
num -= 1
|
82
|
+
[num % @board.size, num/@board.size]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module TicTacToe
|
2
|
+
|
3
|
+
class ComputerPlayer
|
4
|
+
attr_reader :letter
|
5
|
+
|
6
|
+
def initialize(params, solver=MinimaxStrategy)
|
7
|
+
@letter = params['letter'] || params[:letter]
|
8
|
+
@solver = solver
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_move(board)
|
12
|
+
@solver.new(board, @letter).solve
|
13
|
+
end
|
14
|
+
|
15
|
+
def has_next_move?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
def type
|
20
|
+
Player::COMPUTER
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
end
|