erics_tic_tac_toe 0.5.0 → 0.5.1
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/bin/tic_tac_toe +4 -2
- data/lib/tic_tac_toe.rb +9 -2
- data/lib/tic_tac_toe/game.rb +1 -7
- data/lib/tic_tac_toe/game_types/terminal_game.rb +47 -65
- data/lib/tic_tac_toe/player.rb +3 -3
- data/lib/tic_tac_toe/players/computer_player.rb +19 -15
- data/lib/tic_tac_toe/players/human_player.rb +20 -17
- data/lib/tic_tac_toe/presenters/game_presenter.rb +21 -0
- data/lib/tic_tac_toe/presenters/player_presenter.rb +19 -0
- data/lib/tic_tac_toe/strategies/minimax_strategy.rb +65 -67
- data/lib/tic_tac_toe/strategies/three_by_three_strategy.rb +199 -197
- data/lib/tic_tac_toe/version.rb +1 -1
- data/test/game_presentor_test.rb +2 -2
- data/test/player_presenter_test.rb +1 -1
- data/test/player_test.rb +4 -4
- data/test/potential_state_test.rb +1 -1
- data/test/solver_test.rb +2 -2
- data/test/terminal_game_test.rb +9 -24
- metadata +4 -4
- data/lib/tic_tac_toe/presentors/game_presenter.rb +0 -16
- data/lib/tic_tac_toe/presentors/player_presenter.rb +0 -13
data/bin/tic_tac_toe
CHANGED
@@ -20,8 +20,8 @@ def setup
|
|
20
20
|
terminal = TicTacToe::TerminalGame.new(game.board)
|
21
21
|
|
22
22
|
if terminal.computer_goes_first?
|
23
|
-
@player_1 = TicTacToe::
|
24
|
-
@player_2 = TicTacToe::
|
23
|
+
@player_1 = TicTacToe::HumanPlayer.new(letter: 'x')
|
24
|
+
@player_2 = TicTacToe::ComputerPlayer.new(letter: 'o')
|
25
25
|
else
|
26
26
|
@player_1 = TicTacToe::HumanPlayer.new(letter: 'o')
|
27
27
|
@player_2 = TicTacToe::HumanPlayer.new(letter: 'x')
|
@@ -35,6 +35,7 @@ end
|
|
35
35
|
game, terminal = setup
|
36
36
|
|
37
37
|
loop do
|
38
|
+
|
38
39
|
game.start
|
39
40
|
terminal = TicTacToe::TerminalGame.new(game.board)
|
40
41
|
|
@@ -42,6 +43,7 @@ loop do
|
|
42
43
|
terminal.update_board
|
43
44
|
break unless terminal.play_again?
|
44
45
|
game, terminal = setup
|
46
|
+
next
|
45
47
|
end
|
46
48
|
|
47
49
|
terminal.update_board
|
data/lib/tic_tac_toe.rb
CHANGED
@@ -4,11 +4,18 @@
|
|
4
4
|
module TicTacToe
|
5
5
|
X = 'x'.freeze
|
6
6
|
O = 'o'.freeze
|
7
|
+
|
8
|
+
def self.number_to_cords(num, size)
|
9
|
+
num = num.to_i
|
10
|
+
num -= 1
|
11
|
+
[num % size, num/size]
|
12
|
+
end
|
13
|
+
|
7
14
|
end
|
8
15
|
|
9
16
|
# Internal Project Requires
|
10
17
|
require 'tic_tac_toe/board'
|
11
18
|
require 'tic_tac_toe/game'
|
12
19
|
require 'tic_tac_toe/player'
|
13
|
-
require 'tic_tac_toe/
|
14
|
-
require 'tic_tac_toe/
|
20
|
+
require 'tic_tac_toe/presenters/game_presenter'
|
21
|
+
require 'tic_tac_toe/presenters/player_presenter'
|
data/lib/tic_tac_toe/game.rb
CHANGED
@@ -25,7 +25,7 @@ module TicTacToe
|
|
25
25
|
|
26
26
|
def start
|
27
27
|
while @current_player && (move = @current_player.get_move(@board))
|
28
|
-
move = number_to_cords(move) unless move.is_a?(Array)
|
28
|
+
move = TicTacToe::number_to_cords(move, @board.size) unless move.is_a?(Array)
|
29
29
|
|
30
30
|
@board.play_at(*move, @current_player.letter)
|
31
31
|
break if over?
|
@@ -56,12 +56,6 @@ module TicTacToe
|
|
56
56
|
solved? || cats?
|
57
57
|
end
|
58
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
59
|
def switch_player
|
66
60
|
@current_player = @current_player == @player_1 ? @player_2 : @player_1
|
67
61
|
end
|
@@ -1,86 +1,68 @@
|
|
1
1
|
module TicTacToe
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
2
|
+
module GameType
|
3
|
+
# Terminal GameType
|
4
|
+
# Interacts with the user through the terminal
|
5
|
+
# Uses puts for output, and gets for input
|
6
|
+
class Terminal
|
7
|
+
# Internal Error used when a user tries pulling an illegal move
|
8
|
+
class IllegalMove < RuntimeError
|
9
|
+
end
|
24
10
|
|
25
|
-
|
11
|
+
def initialize(board, io=Kernel)
|
12
|
+
@board = board
|
13
|
+
@io = io
|
14
|
+
end
|
26
15
|
|
27
|
-
|
28
|
-
|
29
|
-
display_text "Illegal Move: #{error.message}. Please try again"
|
30
|
-
retry
|
31
|
-
end
|
16
|
+
def computer_goes_first?
|
17
|
+
input = get_input("Would you like to play first or second? (f/s)")
|
32
18
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
19
|
+
return true unless input =~ /^(f|first)$/i
|
20
|
+
false
|
21
|
+
end
|
37
22
|
|
38
|
-
|
39
|
-
|
40
|
-
output = "Select the solver:\n"
|
41
|
-
choices.each_with_index { |choice, i| output += "#{i+1}: #{choice}\n" }
|
23
|
+
def get_move_from_user
|
24
|
+
cords = get_cords_from_user
|
42
25
|
|
43
|
-
|
26
|
+
raise IllegalMove.new("That cell is already taken") unless empty_cell?(cords)
|
44
27
|
|
45
|
-
|
46
|
-
|
47
|
-
|
28
|
+
cords
|
29
|
+
rescue IllegalMove => error
|
30
|
+
display_text "Illegal Move: #{error.message}. Please try again"
|
31
|
+
retry
|
32
|
+
end
|
48
33
|
|
49
|
-
|
34
|
+
def play_again?
|
35
|
+
input = get_input "Play again? (y/n)"
|
36
|
+
input =~ /^(y|yes)$/i
|
50
37
|
end
|
51
|
-
end
|
52
38
|
|
53
|
-
|
54
|
-
|
55
|
-
|
39
|
+
def update_board
|
40
|
+
@io.puts @board
|
41
|
+
end
|
56
42
|
|
57
|
-
|
58
|
-
|
59
|
-
|
43
|
+
def display_text(text)
|
44
|
+
@io.puts text
|
45
|
+
end
|
60
46
|
|
61
|
-
|
47
|
+
private
|
62
48
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
49
|
+
def get_input(text)
|
50
|
+
@io.print text + ' '
|
51
|
+
@io.gets.chomp
|
52
|
+
end
|
67
53
|
|
68
|
-
|
69
|
-
|
70
|
-
|
54
|
+
def empty_cell?(cords)
|
55
|
+
!@board.get_cell(*cords)
|
56
|
+
end
|
71
57
|
|
72
|
-
|
73
|
-
|
58
|
+
def get_cords_from_user
|
59
|
+
input = get_input("Your move (1-#{@board.size**2}):")
|
74
60
|
|
75
|
-
|
61
|
+
raise IllegalMove.new("That is not a real location") unless input =~ /^\d+$/
|
76
62
|
|
77
|
-
|
78
|
-
|
63
|
+
TicTacToe::number_to_cords(input, @board.size)
|
64
|
+
end
|
79
65
|
|
80
|
-
def cord_from_num(num)
|
81
|
-
num -= 1
|
82
|
-
[num % @board.size, num/@board.size]
|
83
66
|
end
|
84
67
|
end
|
85
|
-
|
86
68
|
end
|
data/lib/tic_tac_toe/player.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
module TicTacToe
|
2
|
-
|
2
|
+
module Player
|
3
3
|
HUMAN = 'human'
|
4
4
|
COMPUTER = 'computer'
|
5
5
|
|
6
6
|
def self.build(params)
|
7
|
-
return
|
7
|
+
return Player::Human.new(params) if params['type'] == HUMAN
|
8
8
|
|
9
|
-
|
9
|
+
Player::Computer.new(params)
|
10
10
|
end
|
11
11
|
|
12
12
|
end
|
@@ -1,23 +1,27 @@
|
|
1
1
|
module TicTacToe
|
2
2
|
|
3
|
-
|
4
|
-
attr_reader :letter
|
3
|
+
module Player
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
@solver = solver
|
9
|
-
end
|
5
|
+
class Computer
|
6
|
+
attr_reader :letter
|
10
7
|
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
def initialize(params, solver=Strategy::MinimaxStrategy)
|
9
|
+
@letter = params['letter'] || params[:letter]
|
10
|
+
@solver = solver
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_move(board)
|
14
|
+
@solver.new(board, @letter).solve
|
15
|
+
end
|
16
|
+
|
17
|
+
def has_next_move?
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def type
|
22
|
+
Player::COMPUTER
|
23
|
+
end
|
14
24
|
|
15
|
-
def has_next_move?
|
16
|
-
true
|
17
|
-
end
|
18
|
-
|
19
|
-
def type
|
20
|
-
Player::COMPUTER
|
21
25
|
end
|
22
26
|
|
23
27
|
end
|
@@ -1,26 +1,29 @@
|
|
1
1
|
module TicTacToe
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
module Player
|
4
|
+
class Human
|
5
|
+
attr_reader :letter
|
6
|
+
attr_writer :move
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
def initialize(params)
|
9
|
+
@letter, @move = (params['letter'] || params[:letter]),
|
10
|
+
(params['move'] || params[:move])
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
def get_move(_board)
|
14
|
+
move = @move
|
15
|
+
@move = nil
|
16
|
+
move
|
17
|
+
end
|
17
18
|
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
def has_next_move?
|
20
|
+
!!@move
|
21
|
+
end
|
22
|
+
|
23
|
+
def type
|
24
|
+
Player::HUMAN
|
25
|
+
end
|
21
26
|
|
22
|
-
def type
|
23
|
-
Player::HUMAN
|
24
27
|
end
|
25
28
|
|
26
29
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module TicTacToe
|
2
|
+
|
3
|
+
module Presenter
|
4
|
+
|
5
|
+
class Game
|
6
|
+
def initialize(game)
|
7
|
+
@game = game
|
8
|
+
end
|
9
|
+
|
10
|
+
def grid
|
11
|
+
@game.grid.each_with_index.map do |row, i|
|
12
|
+
row.each_with_index.map do |cell, j|
|
13
|
+
cell || ((i*3)+(j+1)).to_s
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module TicTacToe
|
4
|
+
|
5
|
+
module Presenter
|
6
|
+
|
7
|
+
class Player
|
8
|
+
def initialize(player)
|
9
|
+
@player = player
|
10
|
+
end
|
11
|
+
|
12
|
+
def move_json(move=nil)
|
13
|
+
{letter: @player.letter, type: @player.type, move: move}.to_json
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
@@ -1,89 +1,87 @@
|
|
1
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
2
|
|
7
|
-
|
3
|
+
module Strategy
|
4
|
+
# This implements the Minimax algorithum with AlphaBeta Prunning
|
5
|
+
# http://en.wikipedia.org/wiki/Minimax
|
6
|
+
# http://en.wikipedia.org/wiki/Alpha-beta_pruning
|
7
|
+
class MinimaxStrategy
|
8
8
|
|
9
|
-
|
10
|
-
@board, @letter = board, letter
|
11
|
-
end
|
9
|
+
attr_reader :board
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
11
|
+
def initialize(board, letter)
|
12
|
+
@board, @letter = board, letter
|
13
|
+
end
|
37
14
|
|
38
|
-
|
39
|
-
|
40
|
-
|
15
|
+
def solve
|
16
|
+
raise "Can not solve a full board" if @board.full?
|
17
|
+
Minimax.new(@board, @letter).best_move
|
41
18
|
end
|
42
19
|
end
|
43
20
|
|
44
|
-
|
21
|
+
# The game tree needs a evaluator for generating rankings,
|
22
|
+
# an initial game state,
|
23
|
+
# and a player
|
24
|
+
class Minimax
|
45
25
|
|
46
|
-
|
47
|
-
|
26
|
+
MAXDEPTH = 6
|
27
|
+
PositiveInfinity = +1.0/0.0
|
28
|
+
NegativeInfinity = -1.0/0.0
|
48
29
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
return 0
|
30
|
+
def initialize(board, player)
|
31
|
+
@start_board = board
|
32
|
+
@player = player
|
53
33
|
end
|
54
34
|
|
55
|
-
|
35
|
+
def best_move
|
36
|
+
@start_board.empty_positions.max_by do |column, row|
|
37
|
+
score(@start_board.clone.play_at(column, row, @player), next_turn(@player))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def score(board, whos_turn, depth=1,
|
44
|
+
alpha=NegativeInfinity, beta=PositiveInfinity)
|
45
|
+
|
46
|
+
if board.full? || board.solved?
|
47
|
+
return 1.0 / depth if board.winner == @player
|
48
|
+
return -1.0 if board.solved?
|
49
|
+
return 0
|
50
|
+
end
|
51
|
+
|
52
|
+
if whos_turn == @player
|
53
|
+
board.empty_positions.each do |column, row|
|
54
|
+
alpha = [
|
55
|
+
alpha,
|
56
|
+
next_score(board, column, row, whos_turn, depth, alpha, beta)
|
57
|
+
].max
|
58
|
+
break if beta <= alpha || depth >= MAXDEPTH
|
59
|
+
end
|
60
|
+
return alpha
|
61
|
+
end
|
62
|
+
|
56
63
|
board.empty_positions.each do |column, row|
|
57
|
-
|
58
|
-
|
64
|
+
beta = [
|
65
|
+
beta,
|
59
66
|
next_score(board, column, row, whos_turn, depth, alpha, beta)
|
60
|
-
].
|
61
|
-
break if
|
67
|
+
].min
|
68
|
+
break if alpha >= beta || depth >= MAXDEPTH
|
62
69
|
end
|
63
|
-
|
70
|
+
beta
|
64
71
|
end
|
65
72
|
|
66
|
-
board
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
break if alpha >= beta || depth >= MAXDEPTH
|
73
|
+
def next_score(board, column, row, whos_turn, depth, alpha, beta)
|
74
|
+
score( board.clone.play_at(column, row, whos_turn),
|
75
|
+
next_turn(whos_turn),
|
76
|
+
depth+1, alpha, beta
|
77
|
+
)
|
72
78
|
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
79
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
80
|
+
def next_turn(player)
|
81
|
+
case player
|
82
|
+
when X then O
|
83
|
+
when O then X
|
84
|
+
end
|
87
85
|
end
|
88
86
|
end
|
89
87
|
end
|
@@ -1,257 +1,259 @@
|
|
1
1
|
module TicTacToe
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
2
|
+
module Strategy
|
3
|
+
# Strategy used when playing on a 3x3 board
|
4
|
+
class ThreeByThreeStrategy
|
5
|
+
def initialize(board, letter)
|
6
|
+
@heuristic = ThreeByThree::Heuristic.new(board, letter)
|
7
|
+
end
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
+
[:win, :block, :fork, :block_fork,
|
20
|
+
:center, :opposite_corner, :empty_corner, :empty_side].each do |step|
|
21
|
+
move = @heuristic.send(step)
|
22
|
+
return move if move
|
23
|
+
end
|
23
24
|
|
24
|
-
|
25
|
+
raise "No possible moves to play!"
|
26
|
+
end
|
25
27
|
end
|
26
|
-
end
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
29
|
+
module ThreeByThree
|
30
|
+
# Brute Force Implementation for the Three by Three Strategy
|
31
|
+
# This implementation uses loops to try a change all of the board values and
|
32
|
+
# checking the result
|
33
|
+
#
|
34
|
+
# For example, win! is implemented by trying every cell, and checking if
|
35
|
+
# it was a winning solution
|
36
|
+
class Heuristic
|
36
37
|
|
37
|
-
|
38
|
+
attr_reader :board
|
38
39
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
40
|
+
def initialize(board, letter)
|
41
|
+
@board, @letter, @state = board, letter, PotentialState.new(board, letter)
|
42
|
+
@other_player = other_player
|
43
|
+
end
|
43
44
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
45
|
+
# Try placing letter at every available position
|
46
|
+
# If the board is solved, do that
|
47
|
+
def win
|
48
|
+
each_position do |row, column|
|
49
|
+
if @state.at(row, column).solved?
|
50
|
+
return [row, column]
|
51
|
+
end
|
50
52
|
end
|
53
|
+
false
|
51
54
|
end
|
52
|
-
false
|
53
|
-
end
|
54
55
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
56
|
+
# Try placing the opponent's letter at every available position
|
57
|
+
# If the board is solved, block them at that position
|
58
|
+
def block(board = @board, letter = @letter)
|
59
|
+
state = PotentialState.new(board, other_player(letter))
|
60
|
+
each_position do |row, column|
|
61
|
+
if state.at(row, column).solved?
|
62
|
+
return [row, column]
|
63
|
+
end
|
62
64
|
end
|
65
|
+
false
|
63
66
|
end
|
64
|
-
false
|
65
|
-
end
|
66
67
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
68
|
+
# Try placing the letter at every position.
|
69
|
+
# If there are now two winning solutions for next turn, go there
|
70
|
+
def fork
|
71
|
+
@state.each_forking_position do |row, column|
|
72
|
+
return [row, column]
|
73
|
+
end
|
74
|
+
false
|
72
75
|
end
|
73
|
-
false
|
74
|
-
end
|
75
76
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
77
|
+
# Try placing the opponent's letter at every position.
|
78
|
+
# If there are now two winning solutions for next turn, block them there
|
79
|
+
def block_fork
|
80
|
+
PotentialState.new(@board, @other_player).each_forking_position do |row, column|
|
81
|
+
|
82
|
+
# Simulate blocking the fork
|
83
|
+
temp_board = @board.clone
|
84
|
+
temp_board.play_at(row, column, @letter)
|
80
85
|
|
81
|
-
|
82
|
-
|
83
|
-
|
86
|
+
# Search for the elusive double fork
|
87
|
+
if PotentialState.new(temp_board, @other_player).forking_positions.any?
|
88
|
+
return force_a_block
|
89
|
+
end
|
84
90
|
|
85
|
-
|
86
|
-
if PotentialState.new(temp_board, @other_player).forking_positions.any?
|
87
|
-
return force_a_block
|
91
|
+
return [row, column]
|
88
92
|
end
|
89
|
-
|
90
|
-
return [row, column]
|
93
|
+
false
|
91
94
|
end
|
92
|
-
false
|
93
|
-
end
|
94
95
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
96
|
+
def center
|
97
|
+
return false if @board.get_cell(@board.size/2, @board.size/2)
|
98
|
+
[@board.size/2, @board.size/2]
|
99
|
+
end
|
99
100
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
101
|
+
# Cycle through all of the corners looking for the opponent's letter
|
102
|
+
# If one is found, place letter at the opposite corner
|
103
|
+
def opposite_corner
|
104
|
+
first = 0
|
105
|
+
last = @board.size - 1
|
106
|
+
corners.each_with_index do |corner, index|
|
107
|
+
if corner == @other_player
|
108
|
+
next if @board.get_cell(*opposite_corner_from_index(index).compact)
|
109
|
+
return opposite_corner_from_index(index)
|
110
|
+
end
|
109
111
|
end
|
112
|
+
false
|
110
113
|
end
|
111
|
-
false
|
112
|
-
end
|
113
114
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
115
|
+
# Cycle though all of the corners, until one is found that is empty
|
116
|
+
def empty_corner
|
117
|
+
corners.each_with_index do |corner, index|
|
118
|
+
next if corner
|
119
|
+
return corner_from_index(index)
|
120
|
+
end
|
121
|
+
false
|
119
122
|
end
|
120
|
-
false
|
121
|
-
end
|
122
123
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
124
|
+
# Place letter at a random empty cell, at this point it should only be sides left
|
125
|
+
def empty_side
|
126
|
+
@board.empty_positions do |row, column|
|
127
|
+
return [row, column]
|
128
|
+
end
|
129
|
+
false
|
127
130
|
end
|
128
|
-
false
|
129
|
-
end
|
130
131
|
|
131
|
-
|
132
|
+
private
|
132
133
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
134
|
+
def corners
|
135
|
+
[@board.get_cell(0, 0), # Top Left
|
136
|
+
@board.get_cell(@board.size-1, 0), # Top Right
|
137
|
+
@board.get_cell(@board.size-1, @board.size-1), # Bottom Right
|
138
|
+
@board.get_cell(0, @board.size-1)] # Bottom Left
|
139
|
+
end
|
139
140
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
141
|
+
def corner_from_index(index)
|
142
|
+
first = 0
|
143
|
+
last = @board.size - 1
|
144
|
+
case index
|
145
|
+
when 0 # Top Left
|
146
|
+
[first, first]
|
147
|
+
when 1 # Top Right
|
148
|
+
[last, first]
|
149
|
+
when 2 # Bottom Right
|
150
|
+
[last, last]
|
151
|
+
when 3 # Bottom Left
|
152
|
+
[first, last]
|
153
|
+
end
|
154
|
+
end
|
154
155
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
156
|
+
def opposite_corner_from_index(index)
|
157
|
+
first = 0
|
158
|
+
last = @board.size - 1
|
159
|
+
case index
|
160
|
+
when 0 # Top Left
|
161
|
+
[last, last]
|
162
|
+
when 1 # Top Right
|
163
|
+
[first, last]
|
164
|
+
when 2 # Bottom Right
|
165
|
+
[first, first]
|
166
|
+
when 3 # Bottom Left
|
167
|
+
[last, first]
|
168
|
+
end
|
167
169
|
end
|
168
|
-
end
|
169
170
|
|
170
|
-
|
171
|
-
|
172
|
-
|
171
|
+
def other_player(letter = @letter)
|
172
|
+
letter == X ? O : X
|
173
|
+
end
|
173
174
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
175
|
+
def each_position(&block)
|
176
|
+
@board.size.times do |column|
|
177
|
+
@board.size.times do |row|
|
178
|
+
yield(row, column)
|
179
|
+
end
|
178
180
|
end
|
179
181
|
end
|
180
|
-
end
|
181
182
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
183
|
+
def force_a_block
|
184
|
+
# Force them to block without creating another fork
|
185
|
+
each_position do |row, column|
|
186
|
+
if @state.at(row, column).can_win_next_turn?
|
186
187
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
188
|
+
# Simulate forcing them to block
|
189
|
+
temp_board = @board.clone
|
190
|
+
temp_board.play_at(row, column, @letter)
|
191
|
+
temp_board.play_at(*block(temp_board, @other_player), @other_player)
|
191
192
|
|
192
|
-
|
193
|
-
|
193
|
+
# Did I just create another fork with that block?
|
194
|
+
next if PotentialState.new(temp_board, @other_player).fork_exists?
|
194
195
|
|
195
|
-
|
196
|
+
return [row, column]
|
196
197
|
|
198
|
+
end
|
197
199
|
end
|
198
200
|
end
|
199
201
|
end
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
end
|
202
|
+
# Represents a state of players move
|
203
|
+
# This is comprised of a board and letter (player)
|
204
|
+
class PotentialState
|
205
|
+
def initialize(board, letter)
|
206
|
+
@board, @letter = board, letter
|
207
|
+
end
|
207
208
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
209
|
+
def at(row, column)
|
210
|
+
new_board = @board.clone
|
211
|
+
new_board.play_at(row, column, @letter)
|
212
|
+
PotentialState.new(new_board, @letter)
|
213
|
+
end
|
213
214
|
|
214
|
-
|
215
|
-
|
216
|
-
|
215
|
+
def solved?
|
216
|
+
@board.solved?
|
217
|
+
end
|
217
218
|
|
218
|
-
|
219
|
-
|
220
|
-
|
219
|
+
def each_forking_position(&block)
|
220
|
+
forking_positions.each { |position| yield(*position) }
|
221
|
+
end
|
221
222
|
|
222
|
-
|
223
|
-
|
224
|
-
|
223
|
+
def fork_exists?
|
224
|
+
winning_positions_count >= 2
|
225
|
+
end
|
225
226
|
|
226
|
-
|
227
|
-
|
228
|
-
|
227
|
+
def can_win_next_turn?
|
228
|
+
each_position do |row, column|
|
229
|
+
return true if at(row, column).solved?
|
230
|
+
end
|
231
|
+
false
|
229
232
|
end
|
230
|
-
false
|
231
|
-
end
|
232
233
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
234
|
+
def forking_positions
|
235
|
+
positions = []
|
236
|
+
each_position do |row, column|
|
237
|
+
positions << [row, column] if at(row, column).fork_exists?
|
238
|
+
end
|
239
|
+
positions
|
237
240
|
end
|
238
|
-
positions
|
239
|
-
end
|
240
241
|
|
241
|
-
|
242
|
+
private
|
242
243
|
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
244
|
+
def winning_positions_count
|
245
|
+
count = 0
|
246
|
+
each_position do |row, column|
|
247
|
+
count += 1 if at(row, column).solved?
|
248
|
+
end
|
249
|
+
count
|
247
250
|
end
|
248
|
-
count
|
249
|
-
end
|
250
251
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
252
|
+
def each_position(&block)
|
253
|
+
@board.size.times do |column|
|
254
|
+
@board.size.times do |row|
|
255
|
+
yield(row, column)
|
256
|
+
end
|
255
257
|
end
|
256
258
|
end
|
257
259
|
end
|
data/lib/tic_tac_toe/version.rb
CHANGED
data/test/game_presentor_test.rb
CHANGED
@@ -3,7 +3,7 @@ require 'test_helper'
|
|
3
3
|
class GamePresentorTest < MiniTest::Unit::TestCase
|
4
4
|
def setup
|
5
5
|
@game = TicTacToe::Game.new
|
6
|
-
@presentor = TicTacToe::
|
6
|
+
@presentor = TicTacToe::Presenter::Game.new(@game)
|
7
7
|
end
|
8
8
|
|
9
9
|
def test_grid
|
@@ -12,7 +12,7 @@ class GamePresentorTest < MiniTest::Unit::TestCase
|
|
12
12
|
|
13
13
|
def test_grid_with_moves
|
14
14
|
@game = TicTacToe::Game.new([['x', nil, nil], [nil, nil, nil], [nil, nil, nil]])
|
15
|
-
@presentor = TicTacToe::
|
15
|
+
@presentor = TicTacToe::Presenter::Game.new(@game)
|
16
16
|
|
17
17
|
assert_equal [%w(x 2 3), %w(4 5 6), %w(7 8 9)], @presentor.grid
|
18
18
|
end
|
@@ -7,6 +7,6 @@ class PlayerPresenterTest < MiniTest::Unit::TestCase
|
|
7
7
|
|
8
8
|
def test_move_json
|
9
9
|
assert_equal({letter: 'x', type: 'mock', move: '1'}.to_json,
|
10
|
-
TicTacToe::
|
10
|
+
TicTacToe::Presenter::Player.new(@player_mock).move_json('1'))
|
11
11
|
end
|
12
12
|
end
|
data/test/player_test.rb
CHANGED
@@ -5,12 +5,12 @@ class BoardMock; end
|
|
5
5
|
class PlayerTest < MiniTest::Unit::TestCase
|
6
6
|
|
7
7
|
def test_build_human_player
|
8
|
-
assert_equal TicTacToe::
|
8
|
+
assert_equal TicTacToe::Player::Human,
|
9
9
|
TicTacToe::Player.build('type' => 'human').class
|
10
10
|
end
|
11
11
|
|
12
12
|
def test_build_computer_player
|
13
|
-
assert_equal TicTacToe::
|
13
|
+
assert_equal TicTacToe::Player::Computer,
|
14
14
|
TicTacToe::Player.build('type' => 'computer').class
|
15
15
|
end
|
16
16
|
end
|
@@ -36,7 +36,7 @@ end
|
|
36
36
|
|
37
37
|
class HumanPlayerTest < MiniTest::Unit::TestCase
|
38
38
|
def setup
|
39
|
-
@player = TicTacToe::
|
39
|
+
@player = TicTacToe::Player::Human.new('letter' => "x", 'move' => [0,0])
|
40
40
|
end
|
41
41
|
|
42
42
|
include SharedPlayerTests
|
@@ -50,7 +50,7 @@ class ComputerPlayerTest < MiniTest::Unit::TestCase
|
|
50
50
|
end
|
51
51
|
end
|
52
52
|
def setup
|
53
|
-
@player = TicTacToe::
|
53
|
+
@player = TicTacToe::Player::Computer.new({'letter' => "x"}, SolverMock)
|
54
54
|
end
|
55
55
|
|
56
56
|
include SharedPlayerTests
|
data/test/solver_test.rb
CHANGED
@@ -165,7 +165,7 @@ end
|
|
165
165
|
class MinimaxSolverTest < MiniTest::Unit::TestCase
|
166
166
|
def setup
|
167
167
|
@board = TicTacToe::Board.new
|
168
|
-
@solver = TicTacToe::MinimaxStrategy.new(@board, 'x')
|
168
|
+
@solver = TicTacToe::Strategy::MinimaxStrategy.new(@board, 'x')
|
169
169
|
end
|
170
170
|
|
171
171
|
include SharedSolverTests
|
@@ -174,7 +174,7 @@ end
|
|
174
174
|
class ThreeByThreeSolverTest < MiniTest::Unit::TestCase
|
175
175
|
def setup
|
176
176
|
@board = TicTacToe::Board.new
|
177
|
-
@solver = TicTacToe::ThreeByThreeStrategy.new(@board, 'x')
|
177
|
+
@solver = TicTacToe::Strategy::ThreeByThreeStrategy.new(@board, 'x')
|
178
178
|
end
|
179
179
|
|
180
180
|
include SharedSolverTests
|
data/test/terminal_game_test.rb
CHANGED
@@ -19,34 +19,34 @@ class TerminalGameTest < MiniTest::Unit::TestCase
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def test_computer_goes_first
|
22
|
-
assert TicTacToe::
|
23
|
-
refute TicTacToe::
|
22
|
+
assert TicTacToe::GameType::Terminal.new(nil, IoMock.new("s")).computer_goes_first?
|
23
|
+
refute TicTacToe::GameType::Terminal.new(nil, IoMock.new("f")).computer_goes_first?
|
24
24
|
end
|
25
25
|
|
26
26
|
def test_get_move_from_user
|
27
|
-
game = TicTacToe::
|
27
|
+
game = TicTacToe::GameType::Terminal.new(TicTacToe::Board.new, IoMock.new("1"))
|
28
28
|
assert_equal [0, 0], game.get_move_from_user
|
29
29
|
|
30
|
-
game = TicTacToe::
|
30
|
+
game = TicTacToe::GameType::Terminal.new(TicTacToe::Board.new, IoMock.new("9"))
|
31
31
|
assert_equal [2, 2], game.get_move_from_user
|
32
32
|
end
|
33
33
|
|
34
34
|
def test_handle_bad_input
|
35
|
-
game = TicTacToe::
|
35
|
+
game = TicTacToe::GameType::Terminal.new(TicTacToe::Board.new, IoMock.new(["1", "a"]))
|
36
36
|
assert_equal [0, 0], game.get_move_from_user
|
37
37
|
end
|
38
38
|
|
39
39
|
def test_play_again
|
40
|
-
game = TicTacToe::
|
40
|
+
game = TicTacToe::GameType::Terminal.new(nil, IoMock.new("y"))
|
41
41
|
assert game.play_again?
|
42
42
|
|
43
|
-
game = TicTacToe::
|
43
|
+
game = TicTacToe::GameType::Terminal.new(nil, IoMock.new("n"))
|
44
44
|
refute game.play_again?
|
45
45
|
end
|
46
46
|
|
47
47
|
def test_update_board
|
48
48
|
mock = IoMock.new
|
49
|
-
game = TicTacToe::
|
49
|
+
game = TicTacToe::GameType::Terminal.new("FooBar", mock)
|
50
50
|
game.update_board
|
51
51
|
|
52
52
|
assert_equal "FooBar", mock.output
|
@@ -54,25 +54,10 @@ class TerminalGameTest < MiniTest::Unit::TestCase
|
|
54
54
|
|
55
55
|
def test_display_text
|
56
56
|
mock = IoMock.new
|
57
|
-
game = TicTacToe::
|
57
|
+
game = TicTacToe::GameType::Terminal.new(nil, mock)
|
58
58
|
game.display_text("FooBar")
|
59
59
|
|
60
60
|
assert_equal "FooBar", mock.output
|
61
61
|
end
|
62
62
|
|
63
|
-
def test_select_solver
|
64
|
-
mock = IoMock.new("1")
|
65
|
-
game = TicTacToe::TerminalGame.new(nil, mock)
|
66
|
-
assert_equal "FooBar", game.select(["FooBar", "BarFoo"])
|
67
|
-
|
68
|
-
mock = IoMock.new("2")
|
69
|
-
game = TicTacToe::TerminalGame.new(nil, mock)
|
70
|
-
assert_equal "BarFoo", game.select(["FooBar", "BarFoo"])
|
71
|
-
end
|
72
|
-
|
73
|
-
def test_select_bad_choice
|
74
|
-
mock = IoMock.new(["1", 'x'])
|
75
|
-
game = TicTacToe::TerminalGame.new(nil, mock)
|
76
|
-
assert_equal "FooBar", game.select(["FooBar", "BarFoo"])
|
77
|
-
end
|
78
63
|
end
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: erics_tic_tac_toe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.5.
|
5
|
+
version: 0.5.1
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Eric Koslow
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2012-08-
|
13
|
+
date: 2012-08-27 00:00:00 -05:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -57,8 +57,8 @@ files:
|
|
57
57
|
- lib/tic_tac_toe/player.rb
|
58
58
|
- lib/tic_tac_toe/players/computer_player.rb
|
59
59
|
- lib/tic_tac_toe/players/human_player.rb
|
60
|
-
- lib/tic_tac_toe/
|
61
|
-
- lib/tic_tac_toe/
|
60
|
+
- lib/tic_tac_toe/presenters/game_presenter.rb
|
61
|
+
- lib/tic_tac_toe/presenters/player_presenter.rb
|
62
62
|
- lib/tic_tac_toe/strategies/minimax_strategy.rb
|
63
63
|
- lib/tic_tac_toe/strategies/three_by_three_strategy.rb
|
64
64
|
- lib/tic_tac_toe/version.rb
|