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