tactical_tic_tac_toe 0.1.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/bin/ttt +5 -0
- data/lib/available_player_types.rb +8 -0
- data/lib/board.rb +118 -0
- data/lib/command_line_interface.rb +96 -0
- data/lib/computer_player.rb +86 -0
- data/lib/game.rb +88 -0
- data/lib/human_player.rb +14 -0
- data/lib/negamax.rb +39 -0
- data/lib/old_computer_player.rb +89 -0
- data/lib/player_factory.rb +35 -0
- data/lib/tactical_tic_tac_toe.rb +1 -0
- data/spec/board_spec.rb +374 -0
- data/spec/command_line_interface_spec.rb +167 -0
- data/spec/computer_player_spec.rb +222 -0
- data/spec/game_spec.rb +279 -0
- data/spec/human_player_spec.rb +23 -0
- data/spec/negamax_spec.rb +90 -0
- data/spec/old_computer_player_spec.rb +230 -0
- data/spec/player_factory_spec.rb +49 -0
- data/spec/spec_helper.rb +107 -0
- metadata +64 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "computer_player"
|
|
3
|
+
|
|
4
|
+
require "board"
|
|
5
|
+
|
|
6
|
+
module TicTacToe
|
|
7
|
+
describe ComputerPlayer do
|
|
8
|
+
include_context "default_values"
|
|
9
|
+
include_context "helper_methods"
|
|
10
|
+
|
|
11
|
+
let(:_) { Board.blank_mark }
|
|
12
|
+
let(:x) { @default_first_player }
|
|
13
|
+
let(:o) { @default_second_player }
|
|
14
|
+
|
|
15
|
+
def new_computer_player(parameters)
|
|
16
|
+
ComputerPlayer.new(parameters)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def x_player(board)
|
|
20
|
+
parameters = {
|
|
21
|
+
board: board,
|
|
22
|
+
player_mark: x,
|
|
23
|
+
opponent_mark: o
|
|
24
|
+
}
|
|
25
|
+
new_computer_player(parameters)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def o_player(board)
|
|
29
|
+
parameters = {
|
|
30
|
+
board: board,
|
|
31
|
+
player_mark: o,
|
|
32
|
+
opponent_mark: x
|
|
33
|
+
}
|
|
34
|
+
new_computer_player(parameters)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe "#move" do
|
|
38
|
+
it "returns the coordinates of a valid move" do
|
|
39
|
+
board_config = [
|
|
40
|
+
x, o, x,
|
|
41
|
+
x, x, o,
|
|
42
|
+
o, _, _
|
|
43
|
+
]
|
|
44
|
+
board = build_board(board_config)
|
|
45
|
+
board.mark_cell(o, *[2, 1])
|
|
46
|
+
computer_player = x_player(board)
|
|
47
|
+
coordinates = computer_player.move
|
|
48
|
+
|
|
49
|
+
expect(board.out_of_bounds?(coordinates)).to be false
|
|
50
|
+
expect(board.blank?(coordinates)).to be true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
context "when game board has a center space and it is blank" do
|
|
54
|
+
it "returns the center coordinates" do
|
|
55
|
+
board_size = @default_board_size.odd? ? @default_board_size : 3
|
|
56
|
+
computer_player = x_player(blank_board(board_size))
|
|
57
|
+
|
|
58
|
+
expect(computer_player.move).to eq [board_size / 2, board_size / 2]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context "when computer player can make a horizontal winning move" do
|
|
63
|
+
it "returns the coordinates of the winning move" do
|
|
64
|
+
board_configs = [
|
|
65
|
+
[ x, _, x,
|
|
66
|
+
_, o, _,
|
|
67
|
+
o, x, o ],
|
|
68
|
+
[ o, _, _,
|
|
69
|
+
x, x, _,
|
|
70
|
+
o, o, x ],
|
|
71
|
+
[ _, o, _,
|
|
72
|
+
x, o, o,
|
|
73
|
+
_, x, x ]
|
|
74
|
+
]
|
|
75
|
+
winning_moves = [[0, 1], [1, 2], [2, 0]]
|
|
76
|
+
board_configs.zip(winning_moves).each do |board_config, winning_move|
|
|
77
|
+
board = build_board(board_config)
|
|
78
|
+
computer_player = x_player(board)
|
|
79
|
+
|
|
80
|
+
expect(computer_player.move).to eq winning_move
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
context "when computer player can make a vertical winning move" do
|
|
86
|
+
it "returns the coordinates of the winning move" do
|
|
87
|
+
board_configs = [
|
|
88
|
+
[ x, _, o,
|
|
89
|
+
_, o, x,
|
|
90
|
+
x, _, o ],
|
|
91
|
+
[ o, x, o,
|
|
92
|
+
o, x, _,
|
|
93
|
+
x, _, _ ],
|
|
94
|
+
[ _, o, _,
|
|
95
|
+
_, o, x,
|
|
96
|
+
o, x, x ]
|
|
97
|
+
]
|
|
98
|
+
winning_moves = [[1, 0], [2, 1], [0, 2]]
|
|
99
|
+
board_configs.zip(winning_moves).each do |board_config, winning_move|
|
|
100
|
+
board = build_board(board_config)
|
|
101
|
+
computer_player = x_player(board)
|
|
102
|
+
|
|
103
|
+
expect(computer_player.move).to eq winning_move
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
context "when computer player can make a diagonal winning move" do
|
|
109
|
+
it "returns the coordinates of the winning move" do
|
|
110
|
+
board_configs = [
|
|
111
|
+
[ x, _, o,
|
|
112
|
+
o, x, _,
|
|
113
|
+
x, o, _ ],
|
|
114
|
+
[ o, x, _,
|
|
115
|
+
o, x, _,
|
|
116
|
+
x, o, _ ]
|
|
117
|
+
]
|
|
118
|
+
winning_moves = [[2, 2], [0, 2]]
|
|
119
|
+
board_configs.zip(winning_moves).each do |board_config, winning_move|
|
|
120
|
+
board = build_board(board_config)
|
|
121
|
+
computer_player = x_player(board)
|
|
122
|
+
|
|
123
|
+
expect(computer_player.move).to eq winning_move
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
context "when opponent can make a horizontal winning move next turn" do
|
|
129
|
+
it "returns the coordinates of the move that blocks the opponent from winning" do
|
|
130
|
+
board_configs = [
|
|
131
|
+
[ x, _, x,
|
|
132
|
+
_, o, _,
|
|
133
|
+
o, x, o ],
|
|
134
|
+
[ o, _, _,
|
|
135
|
+
x, x, _,
|
|
136
|
+
o, o, x ],
|
|
137
|
+
[ _, o, _,
|
|
138
|
+
x, o, o,
|
|
139
|
+
_, x, x ]
|
|
140
|
+
]
|
|
141
|
+
winning_moves = [[0, 1], [1, 2], [2, 0]]
|
|
142
|
+
board_configs.zip(winning_moves).each do |board_config, winning_move|
|
|
143
|
+
board = build_board(board_config)
|
|
144
|
+
computer_player = o_player(board)
|
|
145
|
+
|
|
146
|
+
expect(computer_player.move).to eq winning_move
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
context "when opponent can make a vertical winning move next turn" do
|
|
152
|
+
it "returns the coordinates of the move that blocks the opponent from winning" do
|
|
153
|
+
board_configs = [
|
|
154
|
+
[ x, _, o,
|
|
155
|
+
_, o, x,
|
|
156
|
+
x, _, o ],
|
|
157
|
+
[ o, x, o,
|
|
158
|
+
o, x, _,
|
|
159
|
+
x, _, _ ],
|
|
160
|
+
[ _, o, _,
|
|
161
|
+
_, o, x,
|
|
162
|
+
o, x, x ]
|
|
163
|
+
]
|
|
164
|
+
winning_moves = [[1, 0], [2, 1], [0, 2]]
|
|
165
|
+
board_configs.zip(winning_moves).each do |board_config, winning_move|
|
|
166
|
+
board = build_board(board_config)
|
|
167
|
+
computer_player = o_player(board)
|
|
168
|
+
|
|
169
|
+
expect(computer_player.move).to eq winning_move
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
context "when opponent can make a diagonal winning move next turn" do
|
|
175
|
+
it "returns the coordinates of the move that blocks the opponent from winning" do
|
|
176
|
+
board_configs = [
|
|
177
|
+
[ x, _, o,
|
|
178
|
+
o, x, _,
|
|
179
|
+
x, o, _ ],
|
|
180
|
+
[ o, x, _,
|
|
181
|
+
o, x, _,
|
|
182
|
+
x, o, _ ]
|
|
183
|
+
]
|
|
184
|
+
winning_moves = [[2, 2], [0, 2]]
|
|
185
|
+
board_configs.zip(winning_moves).each do |board_config, winning_move|
|
|
186
|
+
board = build_board(board_config)
|
|
187
|
+
computer_player = o_player(board)
|
|
188
|
+
|
|
189
|
+
expect(computer_player.move).to eq winning_move
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
context "when opponent can make a fork next turn" do
|
|
195
|
+
it "returns the coordinates of a move that prevents that fork" do
|
|
196
|
+
board_configs = [
|
|
197
|
+
[ x, _, _,
|
|
198
|
+
_, x, _,
|
|
199
|
+
_, _, o ],
|
|
200
|
+
[ x, _, _,
|
|
201
|
+
_, o, _,
|
|
202
|
+
_, _, x ],
|
|
203
|
+
[ _, x, _,
|
|
204
|
+
_, x, _,
|
|
205
|
+
_, o, _ ]
|
|
206
|
+
]
|
|
207
|
+
good_move_sets = [
|
|
208
|
+
[[2, 0], [0, 2]],
|
|
209
|
+
[[0, 1], [1, 0], [1, 2], [2, 1]],
|
|
210
|
+
[[0, 0], [0, 2], [2, 0], [2, 2]]
|
|
211
|
+
]
|
|
212
|
+
board_configs.zip(good_move_sets).each do |board_config, good_moves|
|
|
213
|
+
board = build_board(board_config)
|
|
214
|
+
computer_player = o_player(board)
|
|
215
|
+
|
|
216
|
+
expect(good_moves).to include computer_player.move
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
data/spec/game_spec.rb
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "game"
|
|
3
|
+
|
|
4
|
+
module TicTacToe
|
|
5
|
+
describe Game do
|
|
6
|
+
include_context "default_values"
|
|
7
|
+
|
|
8
|
+
def new_game(parameters)
|
|
9
|
+
Game.new(parameters)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
let(:game) { new_game({}) }
|
|
13
|
+
|
|
14
|
+
describe "#initialize" do
|
|
15
|
+
it "initializes a board and stores it in an instance variable" do
|
|
16
|
+
expect(game.board).to be_a Board
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
context "when given parameters" do
|
|
20
|
+
it "uses the given board" do
|
|
21
|
+
board = Board.new(size: 4)
|
|
22
|
+
custom_game = new_game(board: board)
|
|
23
|
+
|
|
24
|
+
expect(custom_game.board).to eq board
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "uses player marks of the given type" do
|
|
28
|
+
custom_game = new_game(player_marks: [:F, :B])
|
|
29
|
+
|
|
30
|
+
expect(custom_game.player_marks).to eq [:F, :B]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "uses the given interface" do
|
|
34
|
+
interface = double("CommandLineInterface")
|
|
35
|
+
custom_game = new_game(interface: interface)
|
|
36
|
+
|
|
37
|
+
expect(custom_game.interface).to eq interface
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
context "when a given parameter is not provided" do
|
|
42
|
+
it "creates a board with the default size" do
|
|
43
|
+
expect(game.board.size).to eq @default_board_size
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "uses a default set of player marks" do
|
|
47
|
+
game.player_marks.each do |default_mark|
|
|
48
|
+
expect(@default_player_marks).to include default_mark
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "creates and uses a command line interface by default" do
|
|
53
|
+
expect(game.interface).to be_a CommandLineInterface
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe "#run" do
|
|
59
|
+
it "sets up the game" do
|
|
60
|
+
allow(game).to receive(:handle_turns)
|
|
61
|
+
allow(game).to receive(:handle_game_over)
|
|
62
|
+
|
|
63
|
+
expect(game).to receive(:set_up)
|
|
64
|
+
game.run
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "handles each turn" do
|
|
68
|
+
allow(game).to receive(:handle_game_over)
|
|
69
|
+
allow(game).to receive(:set_up)
|
|
70
|
+
|
|
71
|
+
expect(game).to receive(:handle_turns)
|
|
72
|
+
game.run
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "handles the end of the game" do
|
|
76
|
+
allow(game).to receive(:set_up)
|
|
77
|
+
allow(game).to receive(:handle_turns)
|
|
78
|
+
|
|
79
|
+
expect(game).to receive(:handle_game_over)
|
|
80
|
+
game.run
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe "#set_up" do
|
|
85
|
+
it "uses information from the interface to create the players" do
|
|
86
|
+
allow(game.interface).to receive(:game_setup_interaction).and_return([:human, :computer])
|
|
87
|
+
game.set_up
|
|
88
|
+
|
|
89
|
+
game.players.each_with_index do |player, index|
|
|
90
|
+
expect(player).to respond_to :move
|
|
91
|
+
expect(player).to be_a index == 0 ? HumanPlayer : ComputerPlayer
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe "#handle_turns" do
|
|
97
|
+
it "executes turns until the game is over" do
|
|
98
|
+
allow(game.interface).to receive(:game_setup_interaction).and_return([:human, :computer])
|
|
99
|
+
game.set_up
|
|
100
|
+
allow(game).to receive(:over?).and_return(false, false, false, false, true)
|
|
101
|
+
allow(game).to receive(:handle_one_turn)
|
|
102
|
+
|
|
103
|
+
expect(game).to receive(:handle_one_turn).exactly(5).times
|
|
104
|
+
game.handle_turns
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe "#handle_one_turn" do
|
|
109
|
+
let(:player_stub) { Object.new }
|
|
110
|
+
|
|
111
|
+
before do
|
|
112
|
+
allow(player_stub).to receive(:player_mark).and_return(@default_first_player)
|
|
113
|
+
allow(player_stub).to receive(:move).and_return([0, 0])
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "displays the game board via the interface" do
|
|
117
|
+
allow(game.interface).to receive(:report_move)
|
|
118
|
+
|
|
119
|
+
expect(game.interface).to receive(:show_game_board)
|
|
120
|
+
game.handle_one_turn(player_stub)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "gets valid move coordinates from given player" do
|
|
124
|
+
allow(game.interface).to receive(:show_game_board)
|
|
125
|
+
allow(game.interface).to receive(:report_move)
|
|
126
|
+
|
|
127
|
+
expect(game).to receive(:get_valid_move).and_return([0, 0])
|
|
128
|
+
game.handle_one_turn(player_stub)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "marks the game board with player's mark at the coordinates given by player" do
|
|
132
|
+
allow(game.interface).to receive(:show_game_board)
|
|
133
|
+
allow(game.interface).to receive(:report_move)
|
|
134
|
+
game.handle_one_turn(player_stub)
|
|
135
|
+
|
|
136
|
+
expect(game.board.read_cell(0, 0)).to eq @default_first_player
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "reports the move that was just made through the interface" do
|
|
140
|
+
allow(game.interface).to receive(:show_game_board)
|
|
141
|
+
|
|
142
|
+
expect(game.interface).to receive(:report_move).with(@default_first_player, [0, 0])
|
|
143
|
+
game.handle_one_turn(player_stub)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
describe "#get_valid_move" do
|
|
148
|
+
let(:player_stub) { Object.new }
|
|
149
|
+
|
|
150
|
+
before do
|
|
151
|
+
allow(player_stub).to receive(:player_mark)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
context "when it receives valid move coordinates from given player" do
|
|
155
|
+
it "returns those coordinates" do
|
|
156
|
+
allow(player_stub).to receive(:move).and_return([0, 0])
|
|
157
|
+
|
|
158
|
+
expect(game.get_valid_move(player_stub)).to eq [0, 0]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
context "when it receives invalid move coordinates from given player" do
|
|
163
|
+
let(:occupied_coordinates) { [0, 0] }
|
|
164
|
+
let(:out_of_bounds_coordinates) { [0, game.board.size] }
|
|
165
|
+
let(:valid_coordinates) { [0, game.board.size - 1] }
|
|
166
|
+
|
|
167
|
+
before do
|
|
168
|
+
game.board.mark_cell(@default_first_player, 0, 0)
|
|
169
|
+
allow(player_stub).to receive(:move).and_return(
|
|
170
|
+
occupied_coordinates,
|
|
171
|
+
out_of_bounds_coordinates,
|
|
172
|
+
valid_coordinates)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "complains through the interface" do
|
|
176
|
+
expect(game.interface).to receive(:report_invalid_move).exactly(2).times
|
|
177
|
+
|
|
178
|
+
game.get_valid_move(player_stub)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it "gets another move from player until it receives valid coordinates" do
|
|
182
|
+
allow(game.interface).to receive(:report_invalid_move)
|
|
183
|
+
|
|
184
|
+
expect(game.get_valid_move(player_stub)).to eq valid_coordinates
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
describe "#handle_game_over" do
|
|
190
|
+
it "shows the final state of the game board via the interface" do
|
|
191
|
+
allow(game.interface).to receive(:report_game_over)
|
|
192
|
+
|
|
193
|
+
expect(game.interface).to receive(:show_game_board)
|
|
194
|
+
game.handle_game_over
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
context "when game has been won" do
|
|
198
|
+
it "reports that the given last player to move has won via the interface" do
|
|
199
|
+
allow(game.interface).to receive(:show_game_board)
|
|
200
|
+
allow(game.board).to receive(:has_winning_line?).and_return(true)
|
|
201
|
+
allow(game.board).to receive(:last_mark_made).and_return(@default_first_player)
|
|
202
|
+
|
|
203
|
+
expect(game.interface).to receive(:report_game_over).with(@default_first_player)
|
|
204
|
+
game.handle_game_over
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
context "when game has no winner" do
|
|
209
|
+
it "reports that game ended in a draw via the interface" do
|
|
210
|
+
allow(game.interface).to receive(:show_game_board)
|
|
211
|
+
allow(game.board).to receive(:last_mark_made).and_return(@default_first_player)
|
|
212
|
+
|
|
213
|
+
expect(game.interface).to receive(:report_game_over).with(:none)
|
|
214
|
+
game.handle_game_over
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
describe "#over?" do
|
|
220
|
+
it "returns true when the board has a winning line" do
|
|
221
|
+
allow(game.board).to receive(:has_winning_line?).and_return(true)
|
|
222
|
+
|
|
223
|
+
expect(game.over?).to be true
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "returns true if the board is filled" do
|
|
227
|
+
allow(game.board).to receive(:all_marked?).and_return(true)
|
|
228
|
+
|
|
229
|
+
expect(game.over?).to be true
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it "returns false when the game is not over" do
|
|
233
|
+
expect(game.over?).to be false
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
describe "integration tests" do
|
|
238
|
+
let(:game) { new_game({}) }
|
|
239
|
+
|
|
240
|
+
before do
|
|
241
|
+
allow(game.interface).to receive(:show_game_board)
|
|
242
|
+
allow(game.interface).to receive(:solicit_move)
|
|
243
|
+
allow(game.interface).to receive(:report_move)
|
|
244
|
+
allow(game.interface).to receive(:report_invalid_move)
|
|
245
|
+
allow(game.interface).to receive(:report_game_over)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
context "in a game with two computer players" do
|
|
249
|
+
before do
|
|
250
|
+
allow(game.interface).to receive(:game_setup_interaction).and_return([:computer, :computer])
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "should end the game in a draw" do
|
|
254
|
+
game.run
|
|
255
|
+
|
|
256
|
+
expect(game.board.has_winning_line?).to be false
|
|
257
|
+
expect(game.board.all_marked?).to be true
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
context "in a game with a human player and a computer player" do
|
|
262
|
+
before do
|
|
263
|
+
allow(game.interface).to receive(:game_setup_interaction).and_return([:human, :computer])
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "should end in a win for the computer player against an unskilled human player" do
|
|
267
|
+
allow(game.interface).to receive(:solicit_move) do
|
|
268
|
+
game.board.blank_cell_coordinates.first
|
|
269
|
+
end
|
|
270
|
+
computer_player_mark = :o
|
|
271
|
+
game.run
|
|
272
|
+
|
|
273
|
+
expect(game.board.has_winning_line?).to be true
|
|
274
|
+
expect(game.board.last_mark_made).to be computer_player_mark
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "human_player"
|
|
3
|
+
|
|
4
|
+
module TicTacToe
|
|
5
|
+
describe HumanPlayer do
|
|
6
|
+
include_context "default_values"
|
|
7
|
+
|
|
8
|
+
let(:interface) { double("CommandLineInterface") }
|
|
9
|
+
let(:human_player) do
|
|
10
|
+
HumanPlayer.new(
|
|
11
|
+
interface: interface,
|
|
12
|
+
player_mark: @default_first_player)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe "#move" do
|
|
16
|
+
it "returns the coordinates of the move selected by the player" do
|
|
17
|
+
allow(interface).to receive(:solicit_move).and_return([0, 0])
|
|
18
|
+
|
|
19
|
+
expect(human_player.move).to eq [0, 0]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "negamax"
|
|
3
|
+
|
|
4
|
+
module TicTacToe
|
|
5
|
+
describe Negamax do
|
|
6
|
+
def new_negamax(parameters)
|
|
7
|
+
Negamax.new(parameters)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe "#apply" do
|
|
11
|
+
context "when given node meets the end-node criterion" do
|
|
12
|
+
it "returns the given node" do
|
|
13
|
+
parameters = {
|
|
14
|
+
child_node_generator: nil,
|
|
15
|
+
terminal_node_criterion: ->(_) { true },
|
|
16
|
+
evaluation_heuristic: nil
|
|
17
|
+
}
|
|
18
|
+
negamax = new_negamax(parameters)
|
|
19
|
+
|
|
20
|
+
expect(negamax.apply(:terminal_node)).to eq :terminal_node
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
context "when given node does not meet the given end-node criterion" do
|
|
25
|
+
it "returns the highest scoring child node" do
|
|
26
|
+
child_nodes = [{id: 1, score: 1}, {id: 2, score: 0}, {id: 3, score: -1}]
|
|
27
|
+
parameters = {
|
|
28
|
+
child_node_generator: ->(_) { child_nodes },
|
|
29
|
+
terminal_node_criterion: ->(node) { node[:id] > 0 },
|
|
30
|
+
evaluation_heuristic: ->(node) { node[:score] }
|
|
31
|
+
}
|
|
32
|
+
negamax = new_negamax(parameters)
|
|
33
|
+
|
|
34
|
+
expect(negamax.apply({id: 0})).to eq child_nodes.max_by { |n| n[:score] }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "applies the child node generator to obtain child nodes to score" do
|
|
38
|
+
parameters = {
|
|
39
|
+
child_node_generator: ->(_) { [:child_node] },
|
|
40
|
+
terminal_node_criterion: ->(node) { node == :child_node },
|
|
41
|
+
evaluation_heuristic: ->(_) { 0 }
|
|
42
|
+
}
|
|
43
|
+
negamax = new_negamax(parameters)
|
|
44
|
+
|
|
45
|
+
expect(parameters[:child_node_generator]).to receive(:call).and_call_original
|
|
46
|
+
negamax.apply(:initial_node)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "applies the evaluation scheme to obtain a score for child nodes" do
|
|
50
|
+
parameters = {
|
|
51
|
+
child_node_generator: ->(_) { [:child_node] },
|
|
52
|
+
terminal_node_criterion: ->(node) { node == :child_node },
|
|
53
|
+
evaluation_heuristic: ->(_) { 0 }
|
|
54
|
+
}
|
|
55
|
+
negamax = new_negamax(parameters)
|
|
56
|
+
|
|
57
|
+
heuristic = parameters[:evaluation_heuristic]
|
|
58
|
+
expect(heuristic).to receive(:call).with(:child_node).and_call_original
|
|
59
|
+
negamax.apply(:initial_node)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
context "when given a root node with a depth of 3" do
|
|
63
|
+
let(:root_node) do
|
|
64
|
+
{id: 0, score: nil, children: [
|
|
65
|
+
{id: 1, score: nil, children: [
|
|
66
|
+
{id: 3, score: 1, children: nil},
|
|
67
|
+
{id: 4, score: -1, children: nil}
|
|
68
|
+
]},
|
|
69
|
+
{id: 2, score: nil, children: [
|
|
70
|
+
{id: 5, score: 0, children: nil},
|
|
71
|
+
{id: 6, score: 0, children: nil}
|
|
72
|
+
]}
|
|
73
|
+
]}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "returns the node whose children have the highest minimum score" do
|
|
77
|
+
parameters = {
|
|
78
|
+
child_node_generator: ->(node) { node[:children] },
|
|
79
|
+
terminal_node_criterion: ->(node) { node[:children].nil? },
|
|
80
|
+
evaluation_heuristic: ->(node) { node[:score] }
|
|
81
|
+
}
|
|
82
|
+
negamax = new_negamax(parameters)
|
|
83
|
+
|
|
84
|
+
expect(negamax.apply(root_node)[:id]).to eq 2
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|