tic_tac_toes 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|