ttt-core 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +36 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +2 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +61 -0
- data/README.md +29 -0
- data/Rakefile +8 -0
- data/bin/run.sh +21 -0
- data/lib/ai_player.rb +117 -0
- data/lib/board.rb +87 -0
- data/lib/board_factory.rb +8 -0
- data/lib/game.rb +27 -0
- data/lib/player.rb +16 -0
- data/lib/player_factory.rb +38 -0
- data/lib/player_options.rb +48 -0
- data/lib/player_symbols.rb +16 -0
- data/lib/replay_option.rb +3 -0
- data/lib/ttt-core/version.rb +3 -0
- data/spec/ai_player_spec.rb +143 -0
- data/spec/board_factory_spec.rb +10 -0
- data/spec/board_spec.rb +125 -0
- data/spec/game_spec.rb +58 -0
- data/spec/player_options_spec.rb +33 -0
- data/spec/player_symbols_spec.rb +32 -0
- data/spec/replay_option_spec.rb +8 -0
- data/spec/spec_helper.rb +107 -0
- data/ttt-core-0.0.1/.gitignore +36 -0
- data/ttt-core-0.0.1/.rspec +2 -0
- data/ttt-core-0.0.1/.ruby-gemset +1 -0
- data/ttt-core-0.0.1/.ruby-version +1 -0
- data/ttt-core-0.0.1/.travis.yml +2 -0
- data/ttt-core-0.0.1/Gemfile +10 -0
- data/ttt-core-0.0.1/README.md +21 -0
- data/ttt-core-0.0.1/Rakefile +7 -0
- data/ttt-core-0.0.1/bin/run.sh +21 -0
- data/ttt-core-0.0.1/lib/ai_player.rb +117 -0
- data/ttt-core-0.0.1/lib/board.rb +87 -0
- data/ttt-core-0.0.1/lib/board_factory.rb +8 -0
- data/ttt-core-0.0.1/lib/game.rb +27 -0
- data/ttt-core-0.0.1/lib/player.rb +16 -0
- data/ttt-core-0.0.1/lib/player_factory.rb +38 -0
- data/ttt-core-0.0.1/lib/player_options.rb +48 -0
- data/ttt-core-0.0.1/lib/player_symbols.rb +16 -0
- data/ttt-core-0.0.1/lib/replay_option.rb +3 -0
- data/ttt-core-0.0.1/spec/ai_player_spec.rb +143 -0
- data/ttt-core-0.0.1/spec/board_factory_spec.rb +10 -0
- data/ttt-core-0.0.1/spec/board_spec.rb +125 -0
- data/ttt-core-0.0.1/spec/game_spec.rb +58 -0
- data/ttt-core-0.0.1/spec/player_options_spec.rb +33 -0
- data/ttt-core-0.0.1/spec/player_symbols_spec.rb +32 -0
- data/ttt-core-0.0.1/spec/replay_option_spec.rb +8 -0
- data/ttt-core-0.0.1/spec/spec_helper.rb +107 -0
- data/ttt-core.gemspec +22 -0
- metadata +150 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
require 'player_symbols'
|
|
2
|
+
require 'player'
|
|
3
|
+
|
|
4
|
+
class AiPlayer
|
|
5
|
+
include Player
|
|
6
|
+
|
|
7
|
+
def initialize(symbol)
|
|
8
|
+
super(symbol)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def choose_move(board)
|
|
12
|
+
minimax(board, true, board.vacant_indices.size, ALPHA, BETA).first[1]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ready?
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def minimax(board, is_max_player, depth, alpha, beta)
|
|
20
|
+
best_score_so_far = initial_score(is_max_player)
|
|
21
|
+
|
|
22
|
+
if round_is_over(board, depth)
|
|
23
|
+
return score(board, depth)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
board.vacant_indices.each do |i|
|
|
27
|
+
new_board = board.make_move(i, current_players_symbol(is_max_player))
|
|
28
|
+
result = minimax(new_board, !is_max_player, depth - 1, alpha, beta)
|
|
29
|
+
best_score_so_far = update_score(is_max_player, i, best_score_so_far, score_from(result))
|
|
30
|
+
|
|
31
|
+
alpha = update_alpha(is_max_player, best_score_so_far, alpha)
|
|
32
|
+
beta = update_beta(is_max_player, best_score_so_far, beta)
|
|
33
|
+
|
|
34
|
+
if alpha > beta
|
|
35
|
+
break
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
best_score_so_far
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
ALPHA = -2
|
|
46
|
+
BETA = 2
|
|
47
|
+
|
|
48
|
+
def update_alpha(is_max_player, best_score_so_far, alpha)
|
|
49
|
+
if is_max_player && score_from(best_score_so_far) > alpha
|
|
50
|
+
alpha = score_from(best_score_so_far)
|
|
51
|
+
end
|
|
52
|
+
alpha
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def score_from(score)
|
|
56
|
+
score.first.first
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def update_beta(is_max_player, best_score_so_far, beta)
|
|
60
|
+
if !is_max_player && score_from(best_score_so_far) < beta
|
|
61
|
+
beta = score_from(best_score_so_far)
|
|
62
|
+
end
|
|
63
|
+
beta
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def round_is_over(board, depth)
|
|
67
|
+
@winner = board.winning_symbol
|
|
68
|
+
!@winner.nil? || depth == 0
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def score(board, depth)
|
|
72
|
+
if @winner.nil?
|
|
73
|
+
return {0 => nil}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
if maximizing_player_won?(@winner)
|
|
77
|
+
return {(1 + depth) => nil}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if minimizing_player_won?(@winner)
|
|
81
|
+
return {(-1 - depth) => nil}
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def maximizing_player_won?(winners_symbol)
|
|
86
|
+
symbols_are_equal?(winners_symbol, game_symbol)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def minimizing_player_won?(winners_symbol)
|
|
90
|
+
symbols_are_equal?(winners_symbol, PlayerSymbols::opponent(game_symbol))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def symbols_are_equal?(symbol1, symbol2)
|
|
94
|
+
symbol1 == symbol2
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def initial_score(is_max_player)
|
|
98
|
+
if is_max_player
|
|
99
|
+
best_score_so_far = {ALPHA => nil}
|
|
100
|
+
else
|
|
101
|
+
best_score_so_far = {BETA => nil}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def current_players_symbol(is_max_player)
|
|
106
|
+
is_max_player == true ? game_symbol : PlayerSymbols::opponent(game_symbol)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def update_score(is_max_player, move, score, result_score)
|
|
110
|
+
if is_max_player && (result_score >= score_from(score))
|
|
111
|
+
return {result_score => move}
|
|
112
|
+
elsif !is_max_player && (result_score < score_from(score))
|
|
113
|
+
return {result_score => move}
|
|
114
|
+
end
|
|
115
|
+
return score
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
class Board
|
|
2
|
+
|
|
3
|
+
def initialize(symbols = Array.new(9))
|
|
4
|
+
@grid = symbols
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def empty?
|
|
8
|
+
grid.all?(&:nil?)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def make_move(index, symbol)
|
|
12
|
+
copy_of_grid = grid.dup
|
|
13
|
+
copy_of_grid[index] = symbol
|
|
14
|
+
Board.new(copy_of_grid)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def free_spaces?
|
|
18
|
+
grid.include?(nil)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def get_symbol_at(position)
|
|
22
|
+
grid.at(position)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def winning_combination?
|
|
26
|
+
not_nil_row(find_winning_row_from(all_rows))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def winning_symbol
|
|
30
|
+
@winning_row = find_winning_row_from(all_rows)
|
|
31
|
+
@winning_row.nil? ? nil : @winning_row.first
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def vacant_indices
|
|
35
|
+
grid.each_index.select{|v| grid[v].nil?}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def grid_for_display
|
|
39
|
+
rows
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader :grid
|
|
45
|
+
|
|
46
|
+
def find_winning_row_from(rows)
|
|
47
|
+
@all_rows ||= rows.find do |row|
|
|
48
|
+
all_cells_match = row.all? {|cell| cell == row.first}
|
|
49
|
+
all_cells_match && not_nil_symbol(row.first)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def not_nil_row(row)
|
|
54
|
+
!row.nil?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def all_rows
|
|
58
|
+
@all ||= rows + columns + diagonals
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def not_nil_symbol(cell)
|
|
62
|
+
!cell.nil?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def rows
|
|
66
|
+
[
|
|
67
|
+
[grid.at(0), grid.at(1), grid.at(2)],
|
|
68
|
+
[grid.at(3), grid.at(4), grid.at(5)],
|
|
69
|
+
[grid.at(6), grid.at(7), grid.at(8)]
|
|
70
|
+
]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def columns
|
|
74
|
+
[
|
|
75
|
+
[grid.at(0), grid.at(3), grid.at(6)],
|
|
76
|
+
[grid.at(1), grid.at(4), grid.at(7)],
|
|
77
|
+
[grid.at(2), grid.at(5), grid.at(8)]
|
|
78
|
+
]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def diagonals
|
|
82
|
+
[
|
|
83
|
+
[grid.at(0),grid.at(4), grid.at(8)],
|
|
84
|
+
[grid.at(2), grid.at(4), grid.at(6)]
|
|
85
|
+
]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
class Game
|
|
2
|
+
|
|
3
|
+
def initialize(board, players)
|
|
4
|
+
@board = board
|
|
5
|
+
@players = players
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def play
|
|
9
|
+
while game_in_progress?
|
|
10
|
+
@board = board.make_move(current_player.choose_move(board), current_player.game_symbol)
|
|
11
|
+
players.reverse!
|
|
12
|
+
end
|
|
13
|
+
board
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
attr_reader :board, :players
|
|
19
|
+
|
|
20
|
+
def game_in_progress?
|
|
21
|
+
current_player.ready? && board.free_spaces? && !board.winning_combination?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def current_player
|
|
25
|
+
players.first
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require 'player_symbols'
|
|
2
|
+
require 'player_options'
|
|
3
|
+
require 'ai_player'
|
|
4
|
+
|
|
5
|
+
class PlayerFactory
|
|
6
|
+
def create_players(player_option, command_line_ui)
|
|
7
|
+
if player_option == PlayerOptions::HUMAN_VS_HUMAN
|
|
8
|
+
human_vs_human(command_line_ui)
|
|
9
|
+
elsif player_option == PlayerOptions::HUMAN_VS_AI
|
|
10
|
+
human_vs_ai(command_line_ui)
|
|
11
|
+
else
|
|
12
|
+
ai_vs_human(command_line_ui)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def human_vs_human(command_line_ui)
|
|
19
|
+
[
|
|
20
|
+
create_human(command_line_ui, PlayerSymbols::X),
|
|
21
|
+
create_human(command_line_ui, PlayerSymbols::O)
|
|
22
|
+
]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def human_vs_ai(command_line_ui)
|
|
26
|
+
[
|
|
27
|
+
create_human(command_line_ui, PlayerSymbols::X),
|
|
28
|
+
AiPlayer.new(PlayerSymbols::O)
|
|
29
|
+
]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ai_vs_human(command_line_ui)
|
|
33
|
+
[
|
|
34
|
+
AiPlayer.new(PlayerSymbols::X),
|
|
35
|
+
create_human(command_line_ui, PlayerSymbols::O)
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
class PlayerOptions
|
|
2
|
+
HUMAN_VS_HUMAN = "Human vs Human"
|
|
3
|
+
HUMAN_VS_AI = "Human vs Ai"
|
|
4
|
+
AI_VS_HUMAN = "Ai vs Human"
|
|
5
|
+
|
|
6
|
+
ID_TO_PLAYER_TYPE = {
|
|
7
|
+
1 => HUMAN_VS_HUMAN,
|
|
8
|
+
2 => HUMAN_VS_AI,
|
|
9
|
+
3 => AI_VS_HUMAN
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def self.valid_ids
|
|
13
|
+
ID_TO_PLAYER_TYPE.keys
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.get_player_type_for_id(id)
|
|
17
|
+
game_value_of_player = ID_TO_PLAYER_TYPE.fetch(id)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.display_player_options
|
|
21
|
+
player_options_for_display = ID_TO_PLAYER_TYPE.each_pair.map do |id, option|
|
|
22
|
+
open_bracket + id.to_s + close_bracket + space + option
|
|
23
|
+
end
|
|
24
|
+
player_options_for_display.join(comma + space)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.all
|
|
28
|
+
ID_TO_PLAYER_TYPE
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def self.open_bracket
|
|
34
|
+
"("
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.close_bracket
|
|
38
|
+
")"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.space
|
|
42
|
+
" "
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.comma
|
|
46
|
+
","
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
require 'ai_player'
|
|
2
|
+
require 'board'
|
|
3
|
+
|
|
4
|
+
RSpec.describe AiPlayer do
|
|
5
|
+
let(:ai_player) { AiPlayer.new(PlayerSymbols::X) }
|
|
6
|
+
|
|
7
|
+
it "has a player symbol" do
|
|
8
|
+
expect(ai_player.game_symbol).to eq(PlayerSymbols::X)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "has ready state" do
|
|
12
|
+
expect(ai_player.ready?).to be true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "scores zero when a draw is made" do
|
|
16
|
+
draw_board = Board.new([PlayerSymbols::X, PlayerSymbols::O, PlayerSymbols::X, PlayerSymbols::X, PlayerSymbols::O, PlayerSymbols::O, PlayerSymbols::O, PlayerSymbols::X, PlayerSymbols::X])
|
|
17
|
+
expect(ai_player.minimax(draw_board, true, draw_board.vacant_indices.size, -2, 2).first.first).to be (0 + draw_board.vacant_indices.size)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "scores one if computer wins" do
|
|
21
|
+
winning_board = Board.new([PlayerSymbols::X, PlayerSymbols::X, PlayerSymbols::X, nil, nil, PlayerSymbols::O, PlayerSymbols::O, nil, nil])
|
|
22
|
+
|
|
23
|
+
expect(ai_player.minimax(winning_board, true, winning_board.vacant_indices.size, -2, 2).first.first).to be(1 + winning_board.vacant_indices.size)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "scores negative one if opponent wins" do
|
|
27
|
+
winning_board = Board.new([PlayerSymbols::O, PlayerSymbols::O, PlayerSymbols::O, nil, nil, PlayerSymbols::X, PlayerSymbols::X, nil, nil])
|
|
28
|
+
|
|
29
|
+
expect(ai_player.minimax(winning_board, true, winning_board.vacant_indices.size, -2, 2).first.first).to be(-1 - winning_board.vacant_indices.size)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "takes a winning move on top row" do
|
|
33
|
+
winning_move_on_top_row = Board.new([PlayerSymbols::X, nil, PlayerSymbols::X, nil, nil, PlayerSymbols::O, PlayerSymbols::O, nil, nil])
|
|
34
|
+
|
|
35
|
+
move = ai_player.choose_move(winning_move_on_top_row)
|
|
36
|
+
expect(move).to eq(1)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "takes a winning move on middle row" do
|
|
40
|
+
winning_move_on_middle_row = Board.new([PlayerSymbols::O, nil, nil, PlayerSymbols::X, nil, PlayerSymbols::X, PlayerSymbols::O, nil, PlayerSymbols::O])
|
|
41
|
+
|
|
42
|
+
move = ai_player.choose_move(winning_move_on_middle_row)
|
|
43
|
+
expect(move).to eq(4)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "takes a winning move on bottom row" do
|
|
47
|
+
winning_move_on_bottom_row = Board.new([PlayerSymbols::O, nil, PlayerSymbols::O, PlayerSymbols::O, nil, PlayerSymbols::X, nil, PlayerSymbols::X, PlayerSymbols::X])
|
|
48
|
+
|
|
49
|
+
move = ai_player.choose_move(winning_move_on_bottom_row)
|
|
50
|
+
expect(move).to eq(6)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "takes a winning move on left column" do
|
|
54
|
+
winning_move_on_left_column = Board.new([PlayerSymbols::X, PlayerSymbols::O, nil, nil, nil, nil, PlayerSymbols::X, nil, PlayerSymbols::O])
|
|
55
|
+
|
|
56
|
+
move = ai_player.choose_move(winning_move_on_left_column)
|
|
57
|
+
expect(move).to eq(3)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "takes a winning move on middle column" do
|
|
61
|
+
winning_move_on_middle_column = Board.new([nil, PlayerSymbols::X, PlayerSymbols::O, PlayerSymbols::O, PlayerSymbols::X, nil, nil, nil, nil])
|
|
62
|
+
|
|
63
|
+
move = ai_player.choose_move(winning_move_on_middle_column)
|
|
64
|
+
expect(move).to eq(7)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "takes a winning move on right column" do
|
|
68
|
+
winning_move_on_right_column = Board.new([nil, PlayerSymbols::O, PlayerSymbols::X, nil, PlayerSymbols::O, nil, nil, nil, PlayerSymbols::X])
|
|
69
|
+
|
|
70
|
+
move = ai_player.choose_move(winning_move_on_right_column)
|
|
71
|
+
expect(move).to eq(5)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "takes a winning move on first diagonal" do
|
|
75
|
+
winning_move_on_first_diagonal = Board.new([PlayerSymbols::X, nil, PlayerSymbols::O, nil, nil, PlayerSymbols::O, nil, nil, PlayerSymbols::X])
|
|
76
|
+
|
|
77
|
+
move = ai_player.choose_move(winning_move_on_first_diagonal)
|
|
78
|
+
expect(move).to eq(4)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "takes a winning move on second diagonal" do
|
|
82
|
+
winning_move_on_second_diagonal = Board.new([PlayerSymbols::O, nil, nil, nil, PlayerSymbols::X, nil, PlayerSymbols::X, PlayerSymbols::O, nil])
|
|
83
|
+
|
|
84
|
+
move = ai_player.choose_move(winning_move_on_second_diagonal)
|
|
85
|
+
expect(move).to eq(2)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "takes a blocking move on top row" do
|
|
89
|
+
blocking_move_on_top_row = Board.new([PlayerSymbols::O, nil, PlayerSymbols::O, PlayerSymbols::X, nil, nil, nil, nil, PlayerSymbols::X])
|
|
90
|
+
|
|
91
|
+
move = ai_player.choose_move(blocking_move_on_top_row)
|
|
92
|
+
expect(move).to eq(1)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "takes a blocking move on middle row" do
|
|
96
|
+
blocking_move_on_middle_row = Board.new([PlayerSymbols::X, PlayerSymbols::O, PlayerSymbols::X, PlayerSymbols::O, PlayerSymbols::O, nil, nil, PlayerSymbols::X, nil])
|
|
97
|
+
|
|
98
|
+
move = ai_player.choose_move(blocking_move_on_middle_row)
|
|
99
|
+
expect(move).to eq(5)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "takes a blocking move on bottom row" do
|
|
103
|
+
blocking_move_on_bottom_row = Board.new([PlayerSymbols::X, PlayerSymbols::O, PlayerSymbols::X, nil, nil, PlayerSymbols::X, nil, PlayerSymbols::O, PlayerSymbols::O])
|
|
104
|
+
|
|
105
|
+
move = ai_player.choose_move(blocking_move_on_bottom_row)
|
|
106
|
+
expect(move).to eq(6)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "takes a blocking move on left column" do
|
|
110
|
+
blocking_move_on_left_column = Board.new([PlayerSymbols::O, nil, nil, nil, nil, PlayerSymbols::X, PlayerSymbols::O, PlayerSymbols::X, nil])
|
|
111
|
+
|
|
112
|
+
move = ai_player.choose_move(blocking_move_on_left_column)
|
|
113
|
+
expect(move).to eq(3)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "takes a blocking move on middle column" do
|
|
117
|
+
blocking_move_on_middle_column = Board.new([nil, PlayerSymbols::O, nil, PlayerSymbols::X,nil, nil, nil, PlayerSymbols::O, nil])
|
|
118
|
+
|
|
119
|
+
move = ai_player.choose_move(blocking_move_on_middle_column)
|
|
120
|
+
expect(move).to eq(4)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "takes a blocking move on right column" do
|
|
124
|
+
blocking_move_on_right_column = Board.new([nil, nil, PlayerSymbols::O, nil, nil, PlayerSymbols::O, nil, PlayerSymbols::X, nil])
|
|
125
|
+
|
|
126
|
+
move = ai_player.choose_move(blocking_move_on_right_column)
|
|
127
|
+
expect(move).to eq(8)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "takes a blocking move on first diagonal" do
|
|
131
|
+
blocking_move_on_first_diagonal = Board.new([PlayerSymbols::O, PlayerSymbols::X, nil, nil,nil, nil, nil, nil, PlayerSymbols::O])
|
|
132
|
+
|
|
133
|
+
move = ai_player.choose_move(blocking_move_on_first_diagonal)
|
|
134
|
+
expect(move).to eq(4)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "takes a blocking move on second diagonal" do
|
|
138
|
+
blocking_move_on_second_diagonal = Board.new([nil, nil, PlayerSymbols::O, nil, nil, nil, PlayerSymbols::O, PlayerSymbols::X, nil])
|
|
139
|
+
|
|
140
|
+
move = ai_player.choose_move(blocking_move_on_second_diagonal)
|
|
141
|
+
expect(move).to eq(4)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
require 'board'
|
|
2
|
+
require 'player_symbols'
|
|
3
|
+
|
|
4
|
+
RSpec.describe Board do
|
|
5
|
+
X = PlayerSymbols::X
|
|
6
|
+
O = PlayerSymbols::O
|
|
7
|
+
|
|
8
|
+
let (:board) {Board.new}
|
|
9
|
+
|
|
10
|
+
it "is empty on initialisation" do
|
|
11
|
+
expect(board.empty?).to be true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "is not empty when all slots are occupied" do
|
|
15
|
+
full_board = Board.new([X, O, X, O, X, O, X, X, O])
|
|
16
|
+
expect(full_board.empty?).to be false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "returns the current grid formation" do
|
|
20
|
+
board = Board.new([nil, nil, nil, nil, X, O, nil, nil, nil])
|
|
21
|
+
expect(board.grid_for_display).to eq([[nil, nil, nil], [nil, X, O], [nil, nil, nil]])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "can get symbol at given position" do
|
|
25
|
+
board = Board.new([nil, nil, nil, nil, X, O, nil, nil, nil])
|
|
26
|
+
expect(board.get_symbol_at(4)).to be :X
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "can be updated at a given position" do
|
|
30
|
+
updated_board = board.make_move(1, X)
|
|
31
|
+
expect(updated_board.get_symbol_at(1)).to eq(X)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "has free spaces" do
|
|
35
|
+
expect(board.free_spaces?).to be true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "has no free spaces when all slots are occupied" do
|
|
39
|
+
full_board = Board.new([X, O, X, O, X, O, X, X, O])
|
|
40
|
+
expect(full_board.free_spaces?).to be false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "has no winning combination" do
|
|
44
|
+
winning_board = Board.new([O, X, X, nil, nil, nil, nil, nil, nil])
|
|
45
|
+
expect(winning_board.winning_combination?).to be false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "has winning combination of X in top row" do
|
|
49
|
+
winning_board = Board.new([X, X, X, nil, nil, nil, nil, nil, nil])
|
|
50
|
+
expect(winning_board.winning_combination?).to be true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "has winning combination of X in middle row" do
|
|
54
|
+
winning_board = Board.new([nil, nil, nil, X, X, X, nil, nil, nil])
|
|
55
|
+
expect(winning_board.winning_combination?).to be true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "has winning combination of X in bottom row" do
|
|
59
|
+
winning_board = Board.new([nil, nil, nil, nil, nil, nil, X, X, X])
|
|
60
|
+
expect(winning_board.winning_combination?).to be true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "has winning combination of X in left column" do
|
|
64
|
+
winning_board = Board.new([X, nil, nil, X, nil, nil, X, nil, nil])
|
|
65
|
+
expect(winning_board.winning_combination?).to be true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "has winning combination of X in middle column" do
|
|
69
|
+
winning_board = Board.new([nil, X, nil, nil, X, nil, nil, X, nil])
|
|
70
|
+
expect(winning_board.winning_combination?).to be true
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "has winning combination of X in right column" do
|
|
74
|
+
winning_board = Board.new([nil, nil, X, nil, nil, X, nil, nil, X])
|
|
75
|
+
expect(winning_board.winning_combination?).to be true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "has winning combination of X in first diagonal" do
|
|
79
|
+
winning_board = Board.new([X, nil, nil, nil, X, nil, nil, nil, X])
|
|
80
|
+
expect(winning_board.winning_combination?).to be true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "has winning combination of X in second diagonal" do
|
|
84
|
+
winning_board = Board.new([nil, nil, X, nil, X, nil, X, nil, nil])
|
|
85
|
+
expect(winning_board.winning_combination?).to be true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "has winning combination of O in top row" do
|
|
89
|
+
winning_board = Board.new([O, O, O, nil, nil, nil, nil, nil, nil])
|
|
90
|
+
expect(winning_board.winning_combination?).to be true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "has winning combination of O in left column" do
|
|
94
|
+
winning_board = Board.new([O, nil, nil, O, nil, nil, O, nil, nil])
|
|
95
|
+
expect(winning_board.winning_combination?).to be true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "has winning combination of O in first diagonal" do
|
|
99
|
+
winning_board = Board.new([O, nil, nil, nil, O, nil, nil, nil, O])
|
|
100
|
+
expect(winning_board.winning_combination?).to be true
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "has winning symbol X" do
|
|
104
|
+
winning_board = Board.new([X, X, X, nil, nil, nil, nil, nil, nil])
|
|
105
|
+
expect(winning_board.winning_symbol).to be X
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "has winning symbol O" do
|
|
109
|
+
winning_board = Board.new([O, O, O, nil, nil, nil, nil, nil, nil])
|
|
110
|
+
expect(winning_board.winning_symbol).to be O
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "has no winning symbol" do
|
|
114
|
+
expect(board.winning_symbol).to be nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "gives all vacant indices on an empty board" do
|
|
118
|
+
expect(board.vacant_indices).to include(0, 1, 2, 3, 4, 5, 6, 7, 8)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "gives all vacant indices on a board with moves" do
|
|
122
|
+
board = Board.new([X, nil, O, nil, nil, nil, nil, nil, nil])
|
|
123
|
+
expect(board.vacant_indices).not_to include(0, 2)
|
|
124
|
+
end
|
|
125
|
+
end
|