tactical_tic_tac_toe 0.1.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.
- checksums.yaml +7 -0
- data/bin/ttt +5 -0
- data/lib/available_player_types.rb +8 -0
- data/lib/board.rb +118 -0
- data/lib/command_line_interface.rb +96 -0
- data/lib/computer_player.rb +86 -0
- data/lib/game.rb +88 -0
- data/lib/human_player.rb +14 -0
- data/lib/negamax.rb +39 -0
- data/lib/old_computer_player.rb +89 -0
- data/lib/player_factory.rb +35 -0
- data/lib/tactical_tic_tac_toe.rb +1 -0
- data/spec/board_spec.rb +374 -0
- data/spec/command_line_interface_spec.rb +167 -0
- data/spec/computer_player_spec.rb +222 -0
- data/spec/game_spec.rb +279 -0
- data/spec/human_player_spec.rb +23 -0
- data/spec/negamax_spec.rb +90 -0
- data/spec/old_computer_player_spec.rb +230 -0
- data/spec/player_factory_spec.rb +49 -0
- data/spec/spec_helper.rb +107 -0
- metadata +64 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 01bb765823d823d69d962bdcf3cd43e8bf7fc48f
|
|
4
|
+
data.tar.gz: d2215f338e0b37e2fe3e6fc2d7b17fb4c731e3c1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f44cc813fba53dec3622cb8051a8551578e7ff941c12d11539e4d48b38eb118641abb19c8b2439aa8e26b6b8b09cd41c30513979363eb06417c8684d32be6772
|
|
7
|
+
data.tar.gz: bda0c00122f5e46f4234725dd4045f3541d757b30de32ec6f159c496bf1bb04fe7eb8927153bed5a42fd49204461fdc293d1ebfbd69ab486b879e45fe4a65396
|
data/bin/ttt
ADDED
data/lib/board.rb
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module TicTacToe
|
|
2
|
+
class Board
|
|
3
|
+
BoardError = Class.new(StandardError)
|
|
4
|
+
BLANK_MARK = nil
|
|
5
|
+
|
|
6
|
+
attr_reader :size, :last_move_made
|
|
7
|
+
|
|
8
|
+
def self.blank_mark
|
|
9
|
+
BLANK_MARK
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(parameters)
|
|
13
|
+
fail BoardError, "Given size is too small, must be 3 or greater" if parameters[:size] < 3
|
|
14
|
+
|
|
15
|
+
@size = parameters[:size]
|
|
16
|
+
config = parameters[:config] || blank_board_configuration
|
|
17
|
+
@cells = map_configuration_to_cells(config)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def read_cell(row, col)
|
|
21
|
+
fail BoardError, "Cell coordinates are out of bounds" if out_of_bounds?([row, col])
|
|
22
|
+
|
|
23
|
+
@cells[row][col]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def mark_cell(mark, row, col)
|
|
27
|
+
fail BoardError, "Cell coordinates are out of bounds" if out_of_bounds?([row, col])
|
|
28
|
+
fail BoardError, "Cannot alter a marked cell" if marked?([row, col])
|
|
29
|
+
|
|
30
|
+
@last_move_made = [row, col]
|
|
31
|
+
@cells[row][col] = mark
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def lines
|
|
35
|
+
(0...@size).each_with_object([left_diag, right_diag]) do |index, lines|
|
|
36
|
+
lines << row_at(index) << col_at(index)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def all_coordinates
|
|
41
|
+
(0...@size).to_a.repeated_permutation(2).to_a
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def blank_cell_coordinates
|
|
45
|
+
all_coordinates.reject { |coordinates| marked?(coordinates) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def last_mark_made
|
|
49
|
+
return if @last_move_made.nil?
|
|
50
|
+
self.read_cell(*last_move_made)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def deep_copy
|
|
54
|
+
Board.new(size: @size, config: map_cells_to_configuration(@cells))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def blank?(coordinates)
|
|
58
|
+
self.read_cell(*coordinates) == BLANK_MARK
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def marked?(coordinates)
|
|
62
|
+
!blank?(coordinates)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def out_of_bounds?(coordinates)
|
|
66
|
+
coordinates.any? { |i| !i.between?(0, @size - 1) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def all_blank?
|
|
70
|
+
all_coordinates.all? { |coordinates| blank?(coordinates) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def all_marked?
|
|
74
|
+
all_coordinates.all? { |coordinates| marked?(coordinates) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def has_winning_line?
|
|
78
|
+
lines.each do |line|
|
|
79
|
+
return true if line.first != BLANK_MARK && line.all? { |cell| cell == line.first }
|
|
80
|
+
end
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def map_configuration_to_cells(config)
|
|
87
|
+
if Math.sqrt(config.size).to_i != @size
|
|
88
|
+
fail BoardError, "Given size does not reconcile with given configuration"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
config.each_slice(@size).to_a
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def map_cells_to_configuration(cells)
|
|
95
|
+
cells.flatten
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def blank_board_configuration
|
|
99
|
+
(0...@size**2).map { BLANK_MARK }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def row_at(row)
|
|
103
|
+
(0...@size).map { |col| self.read_cell(row, col) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def col_at(col)
|
|
107
|
+
(0...@size).map { |row| self.read_cell(row, col) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def left_diag
|
|
111
|
+
(0...@size).map { |index| self.read_cell(index, index) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def right_diag
|
|
115
|
+
(0...@size).map { |row| self.read_cell(row, @size - row - 1) }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module TicTacToe
|
|
2
|
+
class CommandLineInterface
|
|
3
|
+
|
|
4
|
+
def initialize(parameters)
|
|
5
|
+
@input = parameters[:input]
|
|
6
|
+
@output = parameters[:output]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def game_setup_interaction(player_marks)
|
|
10
|
+
@output.puts instructions
|
|
11
|
+
player_marks.map { |mark| solicit_player_type(mark) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def solicit_player_type(player_mark)
|
|
15
|
+
@output.puts "Is player #{player_mark} a human or computer?"
|
|
16
|
+
@output.puts "Enter 'computer' or 'human'."
|
|
17
|
+
|
|
18
|
+
cleaned_input = get_valid_input(/^(computer|human)$/)
|
|
19
|
+
cleaned_input.to_sym
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def show_game_board(board)
|
|
23
|
+
row_separator = "----" * board.size + "-\n"
|
|
24
|
+
col_separator = "|"
|
|
25
|
+
|
|
26
|
+
@output.print assemble_board_string(board, row_separator, col_separator)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def solicit_move(player_mark)
|
|
30
|
+
@output.puts "Player #{player_mark}: select your move."
|
|
31
|
+
@output.puts "Enter your move coordinates in the format 'row, col' - eg. '0, 0'."
|
|
32
|
+
|
|
33
|
+
cleaned_input = get_valid_input(/^\s*\d+\s*,\s*\d+\s*$/)
|
|
34
|
+
cleaned_input.split(",").map(&:to_i)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get_valid_input(valid_input_pattern)
|
|
38
|
+
catch(:success) do
|
|
39
|
+
loop do
|
|
40
|
+
cleaned_input = @input.gets.strip.downcase
|
|
41
|
+
throw(:success, cleaned_input) if valid_input_pattern =~ cleaned_input
|
|
42
|
+
@output.puts "Sorry, '#{cleaned_input}' is not valid input. Please try again."
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def report_invalid_move(move_coordinates)
|
|
48
|
+
row, col = move_coordinates
|
|
49
|
+
@output.puts "Couldn't move at row: #{row}, column: #{col}. Please try again."
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def report_move(player_mark, move_coordinates)
|
|
53
|
+
row, col = move_coordinates
|
|
54
|
+
@output.puts "Player #{player_mark} moved at row: #{row}, column: #{col}."
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def report_game_over(winning_player)
|
|
58
|
+
if winning_player == :none
|
|
59
|
+
report_draw
|
|
60
|
+
else
|
|
61
|
+
report_win(winning_player)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def report_win(player_mark)
|
|
66
|
+
@output.puts "Player #{player_mark} wins!"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def report_draw
|
|
70
|
+
@output.puts "The game ended in a draw."
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def instructions
|
|
74
|
+
<<-EOS
|
|
75
|
+
~*~ Welcome to TIC TAC TOE ~*~
|
|
76
|
+
You've probably done this a million times before.
|
|
77
|
+
You don't need me to tell you what to do.
|
|
78
|
+
Go get 'em killer~
|
|
79
|
+
EOS
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def assemble_board_string(board, row_separator, col_separator)
|
|
85
|
+
output_string = row_separator
|
|
86
|
+
(0...board.size).each do |row|
|
|
87
|
+
output_string += col_separator
|
|
88
|
+
(0...board.size).each do |col|
|
|
89
|
+
output_string += " #{board.read_cell(row, col) || " "} " + col_separator
|
|
90
|
+
end
|
|
91
|
+
output_string += "\n" + row_separator
|
|
92
|
+
end
|
|
93
|
+
output_string
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require "negamax"
|
|
2
|
+
|
|
3
|
+
module TicTacToe
|
|
4
|
+
class ComputerPlayer
|
|
5
|
+
attr_reader :player_mark
|
|
6
|
+
|
|
7
|
+
def initialize(parameters)
|
|
8
|
+
@board = parameters[:board]
|
|
9
|
+
@player_mark = parameters[:player_mark]
|
|
10
|
+
@opponent_mark = parameters[:opponent_mark]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def move
|
|
14
|
+
if @board.all_blank? && @board.size.odd?
|
|
15
|
+
[:row, :col].map { @board.size / 2 }
|
|
16
|
+
else
|
|
17
|
+
choose_best_move
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def choose_best_move
|
|
24
|
+
@negamax ||= create_negamax
|
|
25
|
+
@negamax.apply(initial_node).fetch(:last_move_made)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def create_negamax
|
|
29
|
+
parameters = {
|
|
30
|
+
child_node_generator: create_child_node_generator,
|
|
31
|
+
terminal_node_criterion: create_terminal_node_criterion,
|
|
32
|
+
evaluation_heuristic: create_evaluation_heuristic
|
|
33
|
+
}
|
|
34
|
+
Negamax.new(parameters)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def initial_node
|
|
38
|
+
{
|
|
39
|
+
board: @board,
|
|
40
|
+
current_player_mark: @player_mark,
|
|
41
|
+
last_move_made: nil
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def create_child_node_generator
|
|
46
|
+
lambda do |node|
|
|
47
|
+
board = node.fetch(:board)
|
|
48
|
+
current_player_mark = node.fetch(:current_player_mark)
|
|
49
|
+
|
|
50
|
+
board.blank_cell_coordinates.map do |coordinates|
|
|
51
|
+
child_node = {
|
|
52
|
+
board: board.deep_copy,
|
|
53
|
+
current_player_mark: toggle_mark(current_player_mark),
|
|
54
|
+
last_move_made: coordinates
|
|
55
|
+
}
|
|
56
|
+
child_node[:board].mark_cell(current_player_mark, *coordinates)
|
|
57
|
+
child_node
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def create_terminal_node_criterion
|
|
63
|
+
lambda do |node|
|
|
64
|
+
board = node.fetch(:board)
|
|
65
|
+
|
|
66
|
+
board.has_winning_line? || board.all_marked?
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def create_evaluation_heuristic
|
|
71
|
+
lambda do |node|
|
|
72
|
+
board = node.fetch(:board)
|
|
73
|
+
|
|
74
|
+
board.lines.each do |line|
|
|
75
|
+
return 1 if line.all? { |cell| cell == @player_mark }
|
|
76
|
+
return -1 if line.all? { |cell| cell == @opponent_mark }
|
|
77
|
+
end
|
|
78
|
+
0
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def toggle_mark(mark)
|
|
83
|
+
mark == @player_mark ? @opponent_mark : @player_mark
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/game.rb
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require "board"
|
|
2
|
+
require "player_factory"
|
|
3
|
+
require "command_line_interface"
|
|
4
|
+
|
|
5
|
+
module TicTacToe
|
|
6
|
+
class Game
|
|
7
|
+
attr_reader :interface, :board, :player_marks, :players
|
|
8
|
+
|
|
9
|
+
def initialize(parameters)
|
|
10
|
+
@player_marks = parameters[:player_marks] || [:x, :o]
|
|
11
|
+
@board = parameters[:board] || create_default_board
|
|
12
|
+
@interface = parameters[:interface] || create_default_interface
|
|
13
|
+
@players = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run
|
|
17
|
+
set_up
|
|
18
|
+
handle_turns
|
|
19
|
+
handle_game_over
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def set_up
|
|
23
|
+
player_types = @interface.game_setup_interaction(@player_marks)
|
|
24
|
+
|
|
25
|
+
@players = @player_marks.zip(player_types).map { |mark, type| create_player(mark, type) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def handle_turns
|
|
29
|
+
catch(:game_over) do
|
|
30
|
+
@players.cycle do |current_player|
|
|
31
|
+
handle_one_turn(current_player)
|
|
32
|
+
throw :game_over if over?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def handle_one_turn(current_player)
|
|
38
|
+
@interface.show_game_board(@board)
|
|
39
|
+
coordinates = get_valid_move(current_player)
|
|
40
|
+
@board.mark_cell(current_player.player_mark, *coordinates)
|
|
41
|
+
@interface.report_move(current_player.player_mark, coordinates)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def get_valid_move(player)
|
|
45
|
+
loop do
|
|
46
|
+
coordinates = player.move
|
|
47
|
+
if @board.out_of_bounds?(coordinates) || @board.marked?(coordinates)
|
|
48
|
+
@interface.report_invalid_move(coordinates)
|
|
49
|
+
else
|
|
50
|
+
return coordinates
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def handle_game_over
|
|
56
|
+
@interface.show_game_board(@board)
|
|
57
|
+
winning_player_mark = @board.has_winning_line? ? @board.last_mark_made : :none
|
|
58
|
+
@interface.report_game_over(winning_player_mark)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def over?
|
|
62
|
+
@board.has_winning_line? || @board.all_marked?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def create_default_board
|
|
68
|
+
Board.new(size: 3)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def create_default_interface
|
|
72
|
+
parameters = {
|
|
73
|
+
input: $stdin,
|
|
74
|
+
output: $stdout
|
|
75
|
+
}
|
|
76
|
+
CommandLineInterface.new(parameters)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def create_player(mark, type)
|
|
80
|
+
player_config = {
|
|
81
|
+
type: type,
|
|
82
|
+
game: self,
|
|
83
|
+
player_mark: mark
|
|
84
|
+
}
|
|
85
|
+
PlayerFactory.build(player_config)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/human_player.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module TicTacToe
|
|
2
|
+
class HumanPlayer
|
|
3
|
+
attr_reader :player_mark
|
|
4
|
+
|
|
5
|
+
def initialize(parameters)
|
|
6
|
+
@player_mark = parameters[:player_mark]
|
|
7
|
+
@interface = parameters[:interface]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def move
|
|
11
|
+
@interface.solicit_move(@player_mark)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/negamax.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module TicTacToe
|
|
2
|
+
class Negamax
|
|
3
|
+
def initialize(parameters)
|
|
4
|
+
@child_node_generator = parameters[:child_node_generator]
|
|
5
|
+
@terminal_node_criterion = parameters[:terminal_node_criterion]
|
|
6
|
+
@evaluation_heuristic = parameters[:evaluation_heuristic]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def apply(node)
|
|
10
|
+
if terminal_node?(node)
|
|
11
|
+
node
|
|
12
|
+
else
|
|
13
|
+
generate_child_nodes(node).max_by { |child| -score(child, -1) }
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def score(node, color)
|
|
20
|
+
if terminal_node?(node)
|
|
21
|
+
evaluate(node, color)
|
|
22
|
+
else
|
|
23
|
+
generate_child_nodes(node).map { |child| -score(child, -color) }.max
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def generate_child_nodes(node)
|
|
28
|
+
@child_node_generator.call(node)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def terminal_node?(node)
|
|
32
|
+
@terminal_node_criterion.call(node)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def evaluate(node, color)
|
|
36
|
+
@evaluation_heuristic.call(node) * color
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
module TicTacToe
|
|
2
|
+
class OldComputerPlayer
|
|
3
|
+
attr_reader :player_mark
|
|
4
|
+
|
|
5
|
+
def initialize(parameters)
|
|
6
|
+
@player_mark = parameters[:player_mark]
|
|
7
|
+
@opponent_mark = parameters[:opponent_mark]
|
|
8
|
+
@board = parameters[:board]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def move
|
|
12
|
+
center_coordinate = [@board.size / 2, @board.size / 2]
|
|
13
|
+
if !@board.marked?(center_coordinate) && @board.size.odd?
|
|
14
|
+
center_coordinate
|
|
15
|
+
else
|
|
16
|
+
select_best_move
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def select_best_move
|
|
23
|
+
best_score_so_far = {
|
|
24
|
+
player: -Float::INFINITY, # highest score is best for player
|
|
25
|
+
opponent: Float::INFINITY # lowest score is best for opponent
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
successor_boards = generate_possible_successor_boards(@board, @player_mark)
|
|
29
|
+
best_board = successor_boards.max_by { |board| minimax(board, false, best_score_so_far) }
|
|
30
|
+
best_board.last_move_made
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def generate_possible_successor_boards(board, mark)
|
|
34
|
+
board.blank_cell_coordinates.map do |coordinates|
|
|
35
|
+
successor_board = board.deep_copy
|
|
36
|
+
successor_board.mark_cell(mark, *coordinates)
|
|
37
|
+
successor_board
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def minimax(board, my_turn, best_score_so_far)
|
|
42
|
+
if board.has_winning_line? || board.all_marked?
|
|
43
|
+
evaluate(board)
|
|
44
|
+
else
|
|
45
|
+
select_score_of_best_successor_board(board, my_turn, best_score_so_far.dup)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def evaluate(board)
|
|
50
|
+
board.lines.each do |line|
|
|
51
|
+
return Float::INFINITY if line.all? { |cell| cell == @player_mark }
|
|
52
|
+
return -Float::INFINITY if line.all? { |cell| cell == @opponent_mark }
|
|
53
|
+
end
|
|
54
|
+
0
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def select_score_of_best_successor_board(board, my_turn, best_score_so_far)
|
|
58
|
+
current_player_mark = my_turn ? @player_mark : @opponent_mark
|
|
59
|
+
|
|
60
|
+
successor_boards = generate_possible_successor_boards(board, current_player_mark)
|
|
61
|
+
scores = score_successor_boards(successor_boards, my_turn, best_score_so_far)
|
|
62
|
+
|
|
63
|
+
my_turn ? scores.max : scores.min
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def score_successor_boards(successor_boards, my_turn, best_score_so_far)
|
|
67
|
+
successor_boards.each_with_object([]) do |board, scores|
|
|
68
|
+
score = minimax(board, !my_turn, best_score_so_far.dup)
|
|
69
|
+
scores << score
|
|
70
|
+
|
|
71
|
+
update_best_score_so_far(my_turn, best_score_so_far, score)
|
|
72
|
+
return scores if best_score_guaranteed_elsewhere?(best_score_so_far)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def update_best_score_so_far(my_turn, best_score_so_far, score)
|
|
77
|
+
if score > best_score_so_far[:player] && my_turn
|
|
78
|
+
best_score_so_far[:player] = score
|
|
79
|
+
end
|
|
80
|
+
if score < best_score_so_far[:opponent] && !my_turn
|
|
81
|
+
best_score_so_far[:opponent] = score
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def best_score_guaranteed_elsewhere?(best_score_so_far)
|
|
86
|
+
best_score_so_far[:player] >= best_score_so_far[:opponent]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require "human_player"
|
|
2
|
+
require "computer_player"
|
|
3
|
+
require "available_player_types"
|
|
4
|
+
|
|
5
|
+
module TicTacToe
|
|
6
|
+
module PlayerFactory
|
|
7
|
+
def self.build(config)
|
|
8
|
+
case config[:type]
|
|
9
|
+
when AvailablePlayerTypes::HUMAN
|
|
10
|
+
create_human_player(config)
|
|
11
|
+
when AvailablePlayerTypes::COMPUTER
|
|
12
|
+
create_computer_player(config)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def self.create_human_player(config)
|
|
19
|
+
human_parameters = {
|
|
20
|
+
player_mark: config[:player_mark],
|
|
21
|
+
interface: config[:game].interface
|
|
22
|
+
}
|
|
23
|
+
HumanPlayer.new(human_parameters)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.create_computer_player(config)
|
|
27
|
+
computer_parameters = {
|
|
28
|
+
player_mark: config[:player_mark],
|
|
29
|
+
opponent_mark: (config[:game].player_marks - [config[:player_mark]]).pop,
|
|
30
|
+
board: config[:game].board
|
|
31
|
+
}
|
|
32
|
+
ComputerPlayer.new(computer_parameters)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "game"
|