tic_tac_toes 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,3 @@
1
+ module TicTacToes
2
+ VERSION = "0.0.1"
3
+ 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