tic_tac_toes 0.0.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/.gitignore +23 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +8 -0
- data/Rakefile +25 -0
- data/bin/tic_tac_toes +23 -0
- data/lib/command_line/io.rb +27 -0
- data/lib/command_line/menu.rb +54 -0
- data/lib/command_line/runner.rb +37 -0
- data/lib/database/pg_wrapper.rb +72 -0
- data/lib/tic_tac_toes/board.rb +62 -0
- data/lib/tic_tac_toes/easy_ai.rb +9 -0
- data/lib/tic_tac_toes/hard_ai.rb +57 -0
- data/lib/tic_tac_toes/history.rb +27 -0
- data/lib/tic_tac_toes/io_interface.rb +96 -0
- data/lib/tic_tac_toes/medium_ai.rb +11 -0
- data/lib/tic_tac_toes/player.rb +20 -0
- data/lib/tic_tac_toes/player_factory.rb +24 -0
- data/lib/tic_tac_toes/rules.rb +64 -0
- data/lib/tic_tac_toes/strings.rb +103 -0
- data/lib/tic_tac_toes/version.rb +3 -0
- data/spec/command_line/menu_spec.rb +103 -0
- data/spec/command_line/runner_spec.rb +119 -0
- data/spec/database/pg_wrapper_spec.rb +56 -0
- data/spec/tic_tac_toes/board_spec.rb +104 -0
- data/spec/tic_tac_toes/easy_ai_spec.rb +20 -0
- data/spec/tic_tac_toes/hard_ai_spec.rb +103 -0
- data/spec/tic_tac_toes/history_spec.rb +41 -0
- data/spec/tic_tac_toes/io_interface_spec.rb +175 -0
- data/spec/tic_tac_toes/medium_ai_spec.rb +22 -0
- data/spec/tic_tac_toes/player_factory_spec.rb +34 -0
- data/spec/tic_tac_toes/player_spec.rb +34 -0
- data/spec/tic_tac_toes/rules_spec.rb +159 -0
- data/spec/tic_tac_toes/spec_helper.rb +13 -0
- data/spec/tic_tac_toes/strings_spec.rb +50 -0
- data/tic_tac_toes.gemspec +24 -0
- metadata +151 -0
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'tic_tac_toes/rules'
|
2
|
+
|
3
|
+
module TicTacToes
|
4
|
+
module Strings
|
5
|
+
smallest_row_size = Rules::ROW_SIZE_RANGE.min
|
6
|
+
largest_row_size = Rules::ROW_SIZE_RANGE.max
|
7
|
+
|
8
|
+
NOT_AN_INTEGER = "Input must be an integer"
|
9
|
+
INVALID_ROW_SIZE = "Input must be between #{smallest_row_size} and #{largest_row_size}"
|
10
|
+
INVALID_TOKEN = "Input must be a single, untaken character"
|
11
|
+
INVALID_DIFFICULTY = "Input must be a valid difficulty"
|
12
|
+
INVALID_MOVE = "Input must be a space that is on the board and untaken"
|
13
|
+
ROW_SIZE_SOLICITATION = "Pick row size of board:"
|
14
|
+
DIFFICULTY_SOLICITATION = "Pick difficulty (easy, medium, hard):"
|
15
|
+
MOVE_SOLICITATION = "Pick a space:"
|
16
|
+
THINKING = "Thinking..."
|
17
|
+
|
18
|
+
def self.token_solicitation(player)
|
19
|
+
"Pick #{player} token:"
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.game_over_notification(winner)
|
23
|
+
"#{winner} wins!"
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.board(board)
|
27
|
+
board_string = ""
|
28
|
+
board.rows.each_with_index do |row, index|
|
29
|
+
row_start_index = (index * board.row_size).to_i
|
30
|
+
at_last_row = index == board.row_size - 1
|
31
|
+
board_string << "\n"
|
32
|
+
board_string << row(row, row_start_index, board.size)
|
33
|
+
board_string << "\n"
|
34
|
+
board_string << horizontal_divider(board.row_size, board.size) unless at_last_row
|
35
|
+
end
|
36
|
+
board_string << "\n"
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def self.row(row, row_start_index, board_size)
|
42
|
+
row_array = []
|
43
|
+
row.each_with_index do |space, index|
|
44
|
+
if space.nil?
|
45
|
+
board_index = index + row_start_index
|
46
|
+
row_array << empty_space(board_index, board_size)
|
47
|
+
else
|
48
|
+
row_array << token(space, board_size)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
row_array.join("|")
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.empty_space(board_index, board_size)
|
55
|
+
if space_needs_buffer?(board_index, board_size)
|
56
|
+
"[ #{board_index}]"
|
57
|
+
else
|
58
|
+
"[#{board_index}]"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.token(space, board_size)
|
63
|
+
token = get_colored_token(space)
|
64
|
+
|
65
|
+
if double_digit_board?(board_size)
|
66
|
+
" #{token} "
|
67
|
+
else
|
68
|
+
" #{token} "
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.get_colored_token(player)
|
73
|
+
if player.needs_to_think
|
74
|
+
return CommandLine::IO.red(player.token)
|
75
|
+
else
|
76
|
+
return CommandLine::IO.blue(player.token)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.space_needs_buffer?(board_index, board_size)
|
81
|
+
is_double_digit_board = double_digit_board?(board_size)
|
82
|
+
is_single_digit_space = board_index < 10
|
83
|
+
|
84
|
+
is_double_digit_board && is_single_digit_space
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.double_digit_board?(board_size)
|
88
|
+
board_size > 10
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.horizontal_divider(row_size, board_size)
|
92
|
+
horizontal_divider = ""
|
93
|
+
divider_unit = "-"
|
94
|
+
divider_units_per_space = double_digit_board?(board_size) ? 5 : 4
|
95
|
+
extra_units_per_row = 1
|
96
|
+
|
97
|
+
raw_length = (row_size * divider_units_per_space).to_i
|
98
|
+
truncated_length = raw_length - extra_units_per_row
|
99
|
+
truncated_length.times { horizontal_divider << divider_unit }
|
100
|
+
horizontal_divider
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'tic_tac_toes/io_interface'
|
2
|
+
require 'command_line/menu'
|
3
|
+
require 'tic_tac_toes/medium_ai'
|
4
|
+
require 'tic_tac_toes/spec_helper'
|
5
|
+
|
6
|
+
describe CommandLine::Menu do
|
7
|
+
let(:io) { double("io") }
|
8
|
+
let(:io_interface) { TicTacToes::IOInterface.new(io) }
|
9
|
+
let(:menu) { CommandLine::Menu.new(io_interface) }
|
10
|
+
|
11
|
+
|
12
|
+
describe '#get_board' do
|
13
|
+
let(:invalid_row_size) { 11 }
|
14
|
+
let(:valid_row_size) { 7 }
|
15
|
+
|
16
|
+
it "displays an invalid row size error when given an invalid (outside 2-10) row size" do
|
17
|
+
allow(io_interface).to receive(:get_row_size).and_return(invalid_row_size, valid_row_size)
|
18
|
+
|
19
|
+
expect(io_interface).to receive(:invalid_row_size_error)
|
20
|
+
menu.get_board
|
21
|
+
end
|
22
|
+
|
23
|
+
it "only returns a row size once it receives a valid (2-10) row size" do
|
24
|
+
allow(io_interface).to receive(:invalid_row_size_error)
|
25
|
+
allow(io_interface).to receive(:get_row_size).and_return(invalid_row_size, valid_row_size)
|
26
|
+
|
27
|
+
board = menu.get_board
|
28
|
+
expect(board.row_size).to eq(valid_row_size)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
describe '#get_players' do
|
34
|
+
before(:each) do
|
35
|
+
allow(io).to receive(:blue) { |argument| argument }
|
36
|
+
allow(io).to receive(:red) { |argument| argument }
|
37
|
+
end
|
38
|
+
|
39
|
+
it "returns an array with a human (IO decider) player and a computer (AI decider) player" do
|
40
|
+
first_token, second_token = "X", "O"
|
41
|
+
difficulty = :medium
|
42
|
+
|
43
|
+
allow(io_interface).to receive(:get_token).and_return(first_token, second_token)
|
44
|
+
allow(io_interface).to receive(:get_difficulty).and_return(difficulty)
|
45
|
+
|
46
|
+
human_player, computer_player = menu.get_players
|
47
|
+
expect(human_player.decider).to be_a TicTacToes::IOInterface
|
48
|
+
expect(computer_player.decider).to eq(TicTacToes::MediumAI)
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'when given an invalid (non-single-character) token' do
|
52
|
+
let(:invalid_token) { "invalid" }
|
53
|
+
let(:first_token) { "X" }
|
54
|
+
let(:second_token) { "O" }
|
55
|
+
|
56
|
+
before(:each) do
|
57
|
+
valid_difficulty = :medium
|
58
|
+
allow(io_interface).to receive(:get_difficulty) { valid_difficulty }
|
59
|
+
end
|
60
|
+
|
61
|
+
it "displays an invalid token error" do
|
62
|
+
allow(io_interface).to receive(:get_token).and_return(invalid_token, first_token, second_token)
|
63
|
+
|
64
|
+
expect(io_interface).to receive(:invalid_token_error)
|
65
|
+
menu.get_players
|
66
|
+
end
|
67
|
+
|
68
|
+
it "only returns an array of players (with correct tokens) once it receives a valid token" do
|
69
|
+
allow(io_interface).to receive(:invalid_token_error)
|
70
|
+
allow(io_interface).to receive(:get_token).and_return(invalid_token, first_token, second_token)
|
71
|
+
|
72
|
+
first_player, second_player = menu.get_players
|
73
|
+
expect(first_player.token).to eq(first_token)
|
74
|
+
expect(second_player.token).to eq(second_token)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'when given an invalid (not in the list) difficulty' do
|
79
|
+
let(:invalid_difficulty) { :invalid }
|
80
|
+
let(:valid_difficulty) { :medium }
|
81
|
+
|
82
|
+
before(:each) do
|
83
|
+
first_token, second_token = "X", "O"
|
84
|
+
allow(io_interface).to receive(:get_token).and_return(first_token, second_token)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "displays an invalid difficulty error" do
|
88
|
+
allow(io_interface).to receive(:get_difficulty).and_return(invalid_difficulty, valid_difficulty)
|
89
|
+
|
90
|
+
expect(io_interface).to receive(:invalid_difficulty_error)
|
91
|
+
menu.get_players
|
92
|
+
end
|
93
|
+
|
94
|
+
it "only returns an array of players (with correct difficulty) once it receives a valid difficulty" do
|
95
|
+
allow(io_interface).to receive(:invalid_difficulty_error)
|
96
|
+
allow(io_interface).to receive(:get_difficulty).and_return(invalid_difficulty, valid_difficulty)
|
97
|
+
|
98
|
+
computer_player = menu.get_players.last
|
99
|
+
expect(computer_player.decider).to eq(TicTacToes::MediumAI)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'command_line/runner'
|
2
|
+
require 'tic_tac_toes/board'
|
3
|
+
require 'tic_tac_toes/history'
|
4
|
+
require 'tic_tac_toes/spec_helper'
|
5
|
+
|
6
|
+
describe CommandLine::Runner do
|
7
|
+
let(:io_interface) { double("io interface",
|
8
|
+
:draw_board => true,
|
9
|
+
:thinking_notification => true,
|
10
|
+
:game_over_notification => true) }
|
11
|
+
let(:board) { TicTacToes::Board.new(row_size: 3) }
|
12
|
+
let(:menu) { double("menu",
|
13
|
+
:get_board => board,
|
14
|
+
:get_players => true) }
|
15
|
+
let(:rules) { double("rules",
|
16
|
+
:game_over? => true,
|
17
|
+
:determine_winner => "X") }
|
18
|
+
let(:database_interface) { double("database interface", :record_game_history => true) }
|
19
|
+
let(:history) { TicTacToes::History.new(database_interface) }
|
20
|
+
|
21
|
+
let(:runner) { CommandLine::Runner.new(io_interface, menu, rules, history) }
|
22
|
+
|
23
|
+
|
24
|
+
describe '#run' do
|
25
|
+
it "gets a board and players" do
|
26
|
+
expect(menu).to receive(:get_board)
|
27
|
+
expect(menu).to receive(:get_players)
|
28
|
+
runner.run
|
29
|
+
end
|
30
|
+
|
31
|
+
it "has its history object record the board size" do
|
32
|
+
runner.run
|
33
|
+
expect(history.board_size).to eq(board.size)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "takes turns until the game is over" do
|
37
|
+
allow(rules).to receive(:game_over?).and_return(false, true)
|
38
|
+
|
39
|
+
expect(runner).to receive(:take_turn).once
|
40
|
+
runner.run
|
41
|
+
end
|
42
|
+
|
43
|
+
it "ends the game when the game is over" do
|
44
|
+
expect(runner).to receive(:end_game)
|
45
|
+
runner.run
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
describe '#take_turn' do
|
51
|
+
move = ["X", 0]
|
52
|
+
let(:board) { double("board") }
|
53
|
+
let(:first_player) { double("first player", make_move: move, needs_to_think: true) }
|
54
|
+
let(:second_player) { double("second player", make_move: move, needs_to_think: false) }
|
55
|
+
let(:players) { [first_player, second_player] }
|
56
|
+
|
57
|
+
it "draws the board" do
|
58
|
+
expect(io_interface).to receive(:draw_board)
|
59
|
+
runner.take_turn(board, players)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "displays a thinking notification if the current player needs to think" do
|
63
|
+
expect(io_interface).to receive(:thinking_notification)
|
64
|
+
runner.take_turn(board, players)
|
65
|
+
|
66
|
+
expect(io_interface).not_to receive(:thinking_notification)
|
67
|
+
runner.take_turn(board, players)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "asks the first player to make a move" do
|
71
|
+
expect(first_player).to receive(:make_move)
|
72
|
+
runner.take_turn(board, players)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "has its history object record the move" do
|
76
|
+
runner.take_turn(board, players)
|
77
|
+
expect(history.moves.first).to eql(move)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "keeps track of the current player by rotating the players" do
|
81
|
+
runner.take_turn(board, players)
|
82
|
+
expect(second_player).to receive(:make_move)
|
83
|
+
runner.take_turn(board, players)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
describe '#end_game' do
|
89
|
+
let(:board) { double("board") }
|
90
|
+
let(:players) { double("players") }
|
91
|
+
|
92
|
+
it "draws the board" do
|
93
|
+
expect(io_interface).to receive(:draw_board)
|
94
|
+
runner.end_game(board, players)
|
95
|
+
end
|
96
|
+
|
97
|
+
it "determines the winner" do
|
98
|
+
expect(rules).to receive(:determine_winner)
|
99
|
+
runner.end_game(board, players)
|
100
|
+
end
|
101
|
+
|
102
|
+
it "displays the winner" do
|
103
|
+
allow(rules).to receive(:determine_winner) { :winner }
|
104
|
+
|
105
|
+
expect(io_interface).to receive(:game_over_notification).with(:winner)
|
106
|
+
runner.end_game(board, players)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "has its history object record the winner" do
|
110
|
+
runner.end_game(board, players)
|
111
|
+
expect(history.winner).to eq("X")
|
112
|
+
end
|
113
|
+
|
114
|
+
it "has its history object persist the game history" do
|
115
|
+
expect(history).to receive(:persist)
|
116
|
+
runner.end_game(board, players)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'database/pg_wrapper'
|
2
|
+
require 'pg'
|
3
|
+
require 'tic_tac_toes/history'
|
4
|
+
require 'tic_tac_toes/spec_helper'
|
5
|
+
|
6
|
+
describe Database::PGWrapper do
|
7
|
+
database = "test"
|
8
|
+
let(:pg_wrapper) { Database::PGWrapper.new(database) }
|
9
|
+
let(:history1) { double("history 1",
|
10
|
+
:board_size => 9,
|
11
|
+
:moves => [["X", 1], ["O", 4]],
|
12
|
+
:winner => "X") }
|
13
|
+
let(:history2) { double("history 2",
|
14
|
+
:board_size => 16,
|
15
|
+
:moves => [["&", 14]],
|
16
|
+
:winner => "*") }
|
17
|
+
|
18
|
+
before do
|
19
|
+
connection = PG.connect(dbname: database)
|
20
|
+
connection.exec("CREATE TABLE games (
|
21
|
+
id serial primary key,
|
22
|
+
board_size integer,
|
23
|
+
winner varchar)")
|
24
|
+
connection.exec("CREATE TABLE moves (
|
25
|
+
game integer REFERENCES games (id),
|
26
|
+
number integer,
|
27
|
+
token varchar,
|
28
|
+
space integer)")
|
29
|
+
end
|
30
|
+
|
31
|
+
describe '#record_game_history and #read_games' do
|
32
|
+
it "records and reads a history object to and from the database" do
|
33
|
+
pg_wrapper.record_game_history(history1)
|
34
|
+
|
35
|
+
history_from_database = pg_wrapper.read_game_histories.first
|
36
|
+
|
37
|
+
expect(history_from_database.board_size).to eq(9)
|
38
|
+
expect(history_from_database.moves[0]).to eq(["X", 1])
|
39
|
+
expect(history_from_database.moves[1]).to eq(["O", 4])
|
40
|
+
expect(history_from_database.winner).to eq("X")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "records and reads multiple history objects to and from the database" do
|
44
|
+
pg_wrapper.record_game_history(history1)
|
45
|
+
pg_wrapper.record_game_history(history2)
|
46
|
+
histories_from_database = pg_wrapper.read_game_histories
|
47
|
+
expect(histories_from_database).to have(2).histories
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
after do
|
52
|
+
connection = PG.connect(dbname: database)
|
53
|
+
connection.exec("DROP TABLE moves")
|
54
|
+
connection.exec("DROP TABLE games")
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'tic_tac_toes/spec_helper'
|
2
|
+
require 'tic_tac_toes/board'
|
3
|
+
|
4
|
+
describe TicTacToes::Board do
|
5
|
+
describe '#place (and #space)' do
|
6
|
+
let(:board) { TicTacToes::Board.new(row_size: 3) }
|
7
|
+
|
8
|
+
it "returns nil if the space is not nil" do
|
9
|
+
first_token, second_token = :X, :O
|
10
|
+
space = 0
|
11
|
+
|
12
|
+
board.place(first_token, space)
|
13
|
+
expect(board.place(second_token, space)).to be_nil
|
14
|
+
end
|
15
|
+
|
16
|
+
it "returns nil if it isn't in the board's spaces range" do
|
17
|
+
expect(board.place(:X, 9)).to be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it "places a token at a space if the space is nil and in the board's spaces range" do
|
21
|
+
token, valid_space = :X, 0
|
22
|
+
|
23
|
+
board.place(token, valid_space)
|
24
|
+
expect(board.space(valid_space)).to eql(token)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
describe '#open_spaces' do
|
30
|
+
it "returns an array of the board's nil spaces" do
|
31
|
+
structure = [:X, :O, nil,
|
32
|
+
:O, :O, :X,
|
33
|
+
:X, :X, nil]
|
34
|
+
board = generate_board(structure)
|
35
|
+
open_spaces = [2, 8]
|
36
|
+
|
37
|
+
expect(board.open_spaces).to eql(open_spaces)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
describe '#rows' do
|
43
|
+
it "returns an array of row arrays based on the board's spaces" do
|
44
|
+
structure = [ :X, :X, :X, :X,
|
45
|
+
nil, nil, nil, nil,
|
46
|
+
nil, nil, nil, nil,
|
47
|
+
nil, nil, nil, nil]
|
48
|
+
board = generate_board(structure)
|
49
|
+
first_row = [:X, :X, :X, :X]
|
50
|
+
|
51
|
+
expect(board.rows).to have(4).rows
|
52
|
+
expect(board.rows.first).to eql(first_row)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
describe '#columns' do
|
58
|
+
it "returns an array of column arrays based on the board's spaces" do
|
59
|
+
structure = [:X, nil, nil, nil,
|
60
|
+
:X, nil, nil, nil,
|
61
|
+
:X, nil, nil, nil,
|
62
|
+
:X, nil, nil, nil]
|
63
|
+
board = generate_board(structure)
|
64
|
+
first_column = [:X, :X, :X, :X]
|
65
|
+
|
66
|
+
expect(board.columns).to have(4).columns
|
67
|
+
expect(board.columns.first).to eql(first_column)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
describe '#diagonals' do
|
73
|
+
it "returns an array of diagonal arrays based on the board's spaces" do
|
74
|
+
structure = [ :X, nil, nil, :O,
|
75
|
+
nil, :X, :O, nil,
|
76
|
+
nil, :O, :X, nil,
|
77
|
+
:O, nil, nil, :X]
|
78
|
+
board = generate_board(structure)
|
79
|
+
back_diagonal = [:X, :X, :X, :X]
|
80
|
+
front_diagonal = [:O, :O, :O, :O]
|
81
|
+
|
82
|
+
expect(board.diagonals).to have(2).diagonals
|
83
|
+
expect(board.diagonals.first).to eql(back_diagonal)
|
84
|
+
expect(board.diagonals.last).to eql(front_diagonal)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
describe '#full?' do
|
90
|
+
it "returns false if any spaces are still nil" do
|
91
|
+
board = TicTacToes::Board.new
|
92
|
+
expect(board.full?).to be false
|
93
|
+
end
|
94
|
+
|
95
|
+
it "returns true if all spaces are non-nil" do
|
96
|
+
structure = [:X, :O, :X,
|
97
|
+
:O, :X, :O,
|
98
|
+
:X, :O, :X]
|
99
|
+
board = generate_board(structure)
|
100
|
+
|
101
|
+
expect(board.full?).to be true
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'tic_tac_toes/spec_helper'
|
2
|
+
require 'tic_tac_toes/easy_ai'
|
3
|
+
|
4
|
+
describe TicTacToes::EasyAI do
|
5
|
+
describe '#make_move' do
|
6
|
+
let(:players) { double("players") }
|
7
|
+
let(:ai) { TicTacToes::EasyAI }
|
8
|
+
|
9
|
+
it "returns a randomly-selected valid move" do
|
10
|
+
structure = [ :O, nil, nil,
|
11
|
+
nil, :X, nil,
|
12
|
+
nil, :X, nil]
|
13
|
+
board = generate_board(structure)
|
14
|
+
valid_moves = [1, 2, 3, 5, 6, 8]
|
15
|
+
|
16
|
+
move = ai.make_move(board, players)
|
17
|
+
expect(valid_moves).to include(move)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'tic_tac_toes/hard_ai'
|
2
|
+
require 'tic_tac_toes/player'
|
3
|
+
require 'tic_tac_toes/spec_helper'
|
4
|
+
|
5
|
+
describe TicTacToes::HardAI do
|
6
|
+
let(:ai) { TicTacToes::HardAI }
|
7
|
+
let(:x) { TicTacToes::Player.new("decider", "x", false, "interface") }
|
8
|
+
let(:o) { TicTacToes::Player.new(ai, "o", true, "interface") }
|
9
|
+
let(:players) { [o, x] }
|
10
|
+
|
11
|
+
|
12
|
+
describe '#make_move' do
|
13
|
+
it "returns the best move" do
|
14
|
+
structure = [x, nil, nil,
|
15
|
+
o, o, nil,
|
16
|
+
x, nil, x]
|
17
|
+
board = generate_board(structure)
|
18
|
+
best_move = 5
|
19
|
+
|
20
|
+
expect(ai.make_move(board, players)).to eql(best_move)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
describe '#minimax' do
|
26
|
+
it "returns the correct score for a pre-win board" do
|
27
|
+
structure = [x, nil, nil,
|
28
|
+
o, o, nil,
|
29
|
+
x, nil, x]
|
30
|
+
board = generate_board(structure)
|
31
|
+
win_score = 1
|
32
|
+
|
33
|
+
expect(ai.minimax(board, :max, players)).to eql(win_score)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "returns the correct score for a pre-loss board" do
|
37
|
+
structure = [ o, o, x,
|
38
|
+
nil, nil, nil,
|
39
|
+
x, nil, x]
|
40
|
+
board = generate_board(structure)
|
41
|
+
loss_score = -1
|
42
|
+
|
43
|
+
expect(ai.minimax(board, :max, players)).to eql(loss_score)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns the correct score for a pre-draw board" do
|
47
|
+
structure = [x, x, o,
|
48
|
+
o, nil, x,
|
49
|
+
x, o, x]
|
50
|
+
board = generate_board(structure)
|
51
|
+
draw_score = 0
|
52
|
+
|
53
|
+
expect(ai.minimax(board, :max, players)).to eql(draw_score)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
describe '#generate_board' do
|
59
|
+
it "returns a board based on a token, a space, and an existing board" do
|
60
|
+
token, space = o, 3
|
61
|
+
structure = [ x, nil, nil,
|
62
|
+
nil, o, nil,
|
63
|
+
x, nil, nil]
|
64
|
+
board = generate_board(structure)
|
65
|
+
|
66
|
+
new_board = ai.generate_board(token, space, board)
|
67
|
+
expect(new_board.space(space)).to eql(token)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
describe '#score' do
|
73
|
+
it "returns the correct score when HardAI has won" do
|
74
|
+
structure = [ o, nil, nil,
|
75
|
+
nil, o, nil,
|
76
|
+
nil, nil, o]
|
77
|
+
board = generate_board(structure)
|
78
|
+
win_score = 1
|
79
|
+
|
80
|
+
expect(ai.score(board, players)).to eql(win_score)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "returns the correct score when no one has won" do
|
84
|
+
structure = [o, o, x,
|
85
|
+
x, x, o,
|
86
|
+
o, x, o]
|
87
|
+
board = generate_board(structure)
|
88
|
+
draw_score = 0
|
89
|
+
|
90
|
+
expect(ai.score(board, players)).to eql(draw_score)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "returns the correct score when the opponent has won" do
|
94
|
+
structure = [ x, nil, nil,
|
95
|
+
nil, x, nil,
|
96
|
+
nil, nil, x]
|
97
|
+
board = generate_board(structure)
|
98
|
+
loss_score = -1
|
99
|
+
|
100
|
+
expect(ai.score(board, players)).to eql(loss_score)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'tic_tac_toes/spec_helper'
|
2
|
+
require 'tic_tac_toes/history'
|
3
|
+
|
4
|
+
describe TicTacToes::History do
|
5
|
+
let(:database_interface) { double("database interface", :record_game_history => true) }
|
6
|
+
let(:history) { TicTacToes::History.new(database_interface) }
|
7
|
+
|
8
|
+
describe '#record_board_size' do
|
9
|
+
it "records the passed board size" do
|
10
|
+
board_size = 9
|
11
|
+
|
12
|
+
history.record_board_size(board_size)
|
13
|
+
expect(history.board_size).to eq(board_size)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe '#record_move' do
|
18
|
+
it "records the passed token and space" do
|
19
|
+
move = ["X", 0]
|
20
|
+
|
21
|
+
history.record_move(move)
|
22
|
+
expect(history.moves.first).to eql(move)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#record_winner' do
|
27
|
+
it "records the passed winner" do
|
28
|
+
token = "O"
|
29
|
+
|
30
|
+
history.record_winner(token)
|
31
|
+
expect(history.winner).to eq(token)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '#persist' do
|
36
|
+
it "sends the history instance to its database interface for storage" do
|
37
|
+
expect(database_interface).to receive(:record_game_history).with history
|
38
|
+
history.persist
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|