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.
- 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
|