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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +36 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +2 -0
  7. data/Gemfile +8 -0
  8. data/Gemfile.lock +61 -0
  9. data/README.md +29 -0
  10. data/Rakefile +8 -0
  11. data/bin/run.sh +21 -0
  12. data/lib/ai_player.rb +117 -0
  13. data/lib/board.rb +87 -0
  14. data/lib/board_factory.rb +8 -0
  15. data/lib/game.rb +27 -0
  16. data/lib/player.rb +16 -0
  17. data/lib/player_factory.rb +38 -0
  18. data/lib/player_options.rb +48 -0
  19. data/lib/player_symbols.rb +16 -0
  20. data/lib/replay_option.rb +3 -0
  21. data/lib/ttt-core/version.rb +3 -0
  22. data/spec/ai_player_spec.rb +143 -0
  23. data/spec/board_factory_spec.rb +10 -0
  24. data/spec/board_spec.rb +125 -0
  25. data/spec/game_spec.rb +58 -0
  26. data/spec/player_options_spec.rb +33 -0
  27. data/spec/player_symbols_spec.rb +32 -0
  28. data/spec/replay_option_spec.rb +8 -0
  29. data/spec/spec_helper.rb +107 -0
  30. data/ttt-core-0.0.1/.gitignore +36 -0
  31. data/ttt-core-0.0.1/.rspec +2 -0
  32. data/ttt-core-0.0.1/.ruby-gemset +1 -0
  33. data/ttt-core-0.0.1/.ruby-version +1 -0
  34. data/ttt-core-0.0.1/.travis.yml +2 -0
  35. data/ttt-core-0.0.1/Gemfile +10 -0
  36. data/ttt-core-0.0.1/README.md +21 -0
  37. data/ttt-core-0.0.1/Rakefile +7 -0
  38. data/ttt-core-0.0.1/bin/run.sh +21 -0
  39. data/ttt-core-0.0.1/lib/ai_player.rb +117 -0
  40. data/ttt-core-0.0.1/lib/board.rb +87 -0
  41. data/ttt-core-0.0.1/lib/board_factory.rb +8 -0
  42. data/ttt-core-0.0.1/lib/game.rb +27 -0
  43. data/ttt-core-0.0.1/lib/player.rb +16 -0
  44. data/ttt-core-0.0.1/lib/player_factory.rb +38 -0
  45. data/ttt-core-0.0.1/lib/player_options.rb +48 -0
  46. data/ttt-core-0.0.1/lib/player_symbols.rb +16 -0
  47. data/ttt-core-0.0.1/lib/replay_option.rb +3 -0
  48. data/ttt-core-0.0.1/spec/ai_player_spec.rb +143 -0
  49. data/ttt-core-0.0.1/spec/board_factory_spec.rb +10 -0
  50. data/ttt-core-0.0.1/spec/board_spec.rb +125 -0
  51. data/ttt-core-0.0.1/spec/game_spec.rb +58 -0
  52. data/ttt-core-0.0.1/spec/player_options_spec.rb +33 -0
  53. data/ttt-core-0.0.1/spec/player_symbols_spec.rb +32 -0
  54. data/ttt-core-0.0.1/spec/replay_option_spec.rb +8 -0
  55. data/ttt-core-0.0.1/spec/spec_helper.rb +107 -0
  56. data/ttt-core.gemspec +22 -0
  57. 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,8 @@
1
+ require 'board'
2
+
3
+ class BoardFactory
4
+
5
+ def create_board
6
+ Board.new
7
+ end
8
+ 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,16 @@
1
+ module Player
2
+
3
+ def initialize(symbol)
4
+ @symbol = symbol
5
+ end
6
+
7
+ def game_symbol
8
+ symbol
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :symbol
14
+
15
+ end
16
+
@@ -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,16 @@
1
+ class PlayerSymbols
2
+ X = :X
3
+ O = :O
4
+
5
+ def self.opponent(symbol)
6
+ symbol == X ? O : X
7
+ end
8
+
9
+ def self.all
10
+ [X.to_s, O.to_s]
11
+ end
12
+
13
+ def self.to_symbol(value)
14
+ value == X.to_s ? X : O
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ class ReplayOption
2
+ Y = "Y"
3
+ 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,10 @@
1
+ require 'board_factory'
2
+
3
+ RSpec.describe BoardFactory do
4
+ let(:board_factory) { BoardFactory.new }
5
+
6
+ it "creates 3x3 board " do
7
+ board = board_factory.create_board
8
+ expect(board.grid_for_display.size).to be 3
9
+ end
10
+ 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