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.
@@ -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