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,374 @@
1
+ require "spec_helper"
2
+ require "board"
3
+
4
+ module TicTacToe
5
+ describe Board do
6
+ include_context "default_values"
7
+ include_context "helper_methods"
8
+
9
+ let(:board_error) { Board::BoardError }
10
+ let(:_) { Board.blank_mark }
11
+ let(:x) { @default_player_marks.first }
12
+ let(:o) { @default_player_marks.last }
13
+
14
+ describe ".blank_mark" do
15
+ it "returns the mark used by board to indicate a blank cell" do
16
+ board = blank_board(@default_board_size)
17
+
18
+ board.all_coordinates.each do |coordinates|
19
+ expect(board.read_cell(*coordinates)).to eq Board.blank_mark
20
+ end
21
+ end
22
+ end
23
+
24
+ describe "#initialize" do
25
+ it "raises error if given size is less than 3" do
26
+ error_info = [board_error, "Given size is too small, must be 3 or greater"]
27
+
28
+ expect { new_board(size: 2) }.to raise_error(*error_info)
29
+ end
30
+
31
+ context "given the configuration of a board with preexisting marks" do
32
+ let(:config) do
33
+ [ x, _, _,
34
+ _, o, _,
35
+ _, _, x ]
36
+ end
37
+
38
+ it "generates a board with the specified marks in each cell" do
39
+ board = new_board(size: Math.sqrt(config.size).to_i, config: config)
40
+
41
+ board.all_coordinates.zip(config).each do |coordinates, mark|
42
+ expect(board.read_cell(*coordinates)).to eq mark
43
+ end
44
+ end
45
+
46
+ it "raises error if given configuration does not reconcile with given size" do
47
+ error_info = [board_error, "Given size does not reconcile with given configuration"]
48
+ params = {
49
+ size: Math.sqrt(config.size).to_i + 1,
50
+ config: config
51
+ }
52
+
53
+ expect { new_board(params) }.to raise_error(*error_info)
54
+ end
55
+ end
56
+
57
+ context "when not given a configuration of a board with preexisting marks" do
58
+ it "generates a NxN board of the given size" do
59
+ (3..5).each do |size|
60
+ custom_board = new_board(size: size)
61
+
62
+ expect(custom_board.size).to eq size
63
+ expect(custom_board.all_coordinates.count).to eq size**2
64
+ end
65
+ end
66
+
67
+ it "leaves the generated board blank" do
68
+ expect(new_board(size: @default_board_size).all_blank?).to be true
69
+ end
70
+ end
71
+ end
72
+
73
+ describe "#read_cell" do
74
+ it "gets the contents of cell at given row and column" do
75
+ mark = @default_player_marks.sample
76
+ config = blank_board_configuration(@default_board_size)
77
+ config[0] = mark
78
+ board = build_board(config)
79
+
80
+ expect(board.read_cell(0, 0)).to eq mark
81
+ end
82
+
83
+ it "raises error if cell coordinates are out of bounds" do
84
+ error_info = [board_error, "Cell coordinates are out of bounds"]
85
+ board = blank_board(@default_board_size)
86
+ oob_coordinates = [board.size, board.size]
87
+
88
+ expect { board.read_cell(*oob_coordinates) }.to raise_error(*error_info)
89
+ end
90
+ end
91
+
92
+ describe "#mark_cell" do
93
+ let(:board) { blank_board(@default_board_size) }
94
+ let(:coordinates) { random_coordinates(board.size) }
95
+ let(:mark) { @default_player_marks.sample }
96
+
97
+ it "sets the contents of an empty cell at the given row and column" do
98
+ board.mark_cell(mark, *coordinates)
99
+
100
+ expect(board.read_cell(*coordinates)).to eq mark
101
+ end
102
+
103
+ it "records the coordinates of the last mark made" do
104
+ board.mark_cell(mark, *coordinates)
105
+
106
+ expect(board.last_move_made).to eq coordinates
107
+ end
108
+
109
+ it "raises error if cell coordinates are out of bounds" do
110
+ error_info = [board_error, "Cell coordinates are out of bounds"]
111
+ oob_coordinates = [board.size, board.size]
112
+
113
+ expect { board.mark_cell(mark, *oob_coordinates)}.to raise_error(*error_info)
114
+ end
115
+
116
+ it "raises error when attempting to change contents of non-empty cell" do
117
+ error_info = [board_error, "Cannot alter a marked cell"]
118
+ board.mark_cell(mark, *coordinates)
119
+
120
+ expect { board.mark_cell(mark, *coordinates) }.to raise_error(*error_info)
121
+ end
122
+ end
123
+
124
+ describe "#lines" do
125
+ let(:board) { blank_board(@default_board_size) }
126
+
127
+ it "returns the contents of every board-length line (rows, cols, and diagonals)" do
128
+ lines = board.lines
129
+
130
+ lines.each do |line|
131
+ expect(line.count).to eq board.size
132
+ end
133
+ expect(lines.count).to eq board.size * 2 + 2
134
+ end
135
+ end
136
+
137
+ describe "#all_coordinates" do
138
+ let(:board) { blank_board(@default_board_size) }
139
+
140
+ it "returns the coordinates of every cell in board" do
141
+ all_coordinates = (0...board.size).to_a.repeated_permutation(2).to_a
142
+
143
+ expect(board.all_coordinates).to match_array all_coordinates
144
+ end
145
+ end
146
+
147
+ describe "#blank_cell_coordinates" do
148
+ it "returns all cell coordinates when board is blank" do
149
+ board = blank_board(@default_board_size)
150
+
151
+ expect(board.blank_cell_coordinates).to match_array board.all_coordinates
152
+ end
153
+
154
+ it "returns the coordinates of all blank cells in board" do
155
+ config = [x, o, _, _, _, _, _, _, _].shuffle
156
+ board = build_board(config)
157
+ unmarked_coordinates = board.all_coordinates.reject { |coords| board.marked?(coords) }
158
+
159
+ expect(board.blank_cell_coordinates).to match_array unmarked_coordinates
160
+ end
161
+
162
+ it "returns no coordinates when no cells are blank" do
163
+ config = [x, o, x, o, x, o, x, o, x].shuffle
164
+ board = build_board(config)
165
+
166
+ expect(board.blank_cell_coordinates.count).to eq 0
167
+ end
168
+ end
169
+
170
+ describe "#last_mark_made" do
171
+ context "when board is blank" do
172
+ it "returns nil" do
173
+ expect(blank_board(@default_board_size).last_mark_made).to be nil
174
+ end
175
+ end
176
+
177
+ context "when board is not blank" do
178
+ it "returns the last mark made on the board" do
179
+ board = blank_board(@default_board_size)
180
+ mark = @default_player_marks.sample
181
+ board.mark_cell(mark, *random_coordinates(board.size))
182
+
183
+ expect(board.last_mark_made).to eq mark
184
+ end
185
+ end
186
+ end
187
+
188
+ describe "#deep_copy" do
189
+ it "returns a new board" do
190
+ board = blank_board(@default_board_size)
191
+ board_copy = board.deep_copy
192
+
193
+ expect(board).not_to eq board_copy
194
+ end
195
+
196
+ it "returns a board that has the same marks as called board" do
197
+ config = [x, o, x, o, x, o, _, _, _].shuffle
198
+ board = build_board(config)
199
+ board_copy = board.deep_copy
200
+
201
+ board.all_coordinates.each do |coordinates|
202
+ expect(board.read_cell(*coordinates)).to eq board_copy.read_cell(*coordinates)
203
+ end
204
+ end
205
+
206
+ it "returns a board which references cells that are not those of called board" do
207
+ board = blank_board(@default_board_size)
208
+ board_copy = board.deep_copy
209
+ coordinates = random_coordinates(board.size)
210
+ board.mark_cell(@default_player_marks.first, *coordinates)
211
+
212
+ expect(board.read_cell(*coordinates)).not_to eq board_copy.read_cell(*coordinates)
213
+ end
214
+ end
215
+
216
+ describe "#blank?" do
217
+ it "returns true if cell at given coordinates is blank" do
218
+ board = blank_board(@default_board_size)
219
+
220
+ expect(board.blank?(random_coordinates(board.size))).to be true
221
+ end
222
+
223
+ it "returns false if cell at given coordinates is not blank" do
224
+ board = blank_board(@default_board_size)
225
+ coordinates = random_coordinates(board.size)
226
+ board.mark_cell(@default_player_marks.sample, *coordinates)
227
+
228
+ expect(board.blank?(coordinates)).to be false
229
+ end
230
+ end
231
+
232
+ describe "#marked?" do
233
+ it "returns true if cell at given coordinates has any player's mark" do
234
+ board = blank_board(@default_board_size)
235
+ coordinates = random_coordinates(board.size)
236
+ board.mark_cell(@default_player_marks.sample, *coordinates)
237
+
238
+ expect(board.marked?(coordinates)).to be true
239
+ end
240
+
241
+ it "returns false if cell at given coordinates does not have a player's mark" do
242
+ blank_board = blank_board(@default_board_size)
243
+ coordinates = random_coordinates(blank_board.size)
244
+
245
+ expect(blank_board.marked?(coordinates)).to be false
246
+ end
247
+ end
248
+
249
+ describe "#out_of_bounds?" do
250
+ it "returns true if given coordinates are not within bounds of board" do
251
+ board = blank_board(@default_board_size)
252
+ coordinates = [0, board.size]
253
+
254
+ expect(board.out_of_bounds?(coordinates)).to be true
255
+ end
256
+
257
+ it "returns false if given coordinates are within bounds of board" do
258
+ board = blank_board(@default_board_size)
259
+
260
+ board.all_coordinates.each do |coordinates|
261
+ expect(board.out_of_bounds?(coordinates)).to be false
262
+ end
263
+ end
264
+ end
265
+
266
+ describe "#all_blank?" do
267
+ it "returns true if no cell in board is marked" do
268
+ board = blank_board(@default_board_size)
269
+
270
+ expect(board.all_blank?).to be true
271
+ end
272
+
273
+ it "returns false if any cell in board is marked" do
274
+ config = [x, _, _, _, _, _, _, _, _].shuffle
275
+ board = build_board(config)
276
+
277
+ expect(board.all_blank?).to be false
278
+ end
279
+ end
280
+
281
+ describe "#all_marked?" do
282
+ it "returns true if all cells in board are marked" do
283
+ config = [x, x, x, x, x, o, o, o, o].shuffle
284
+ board = build_board(config)
285
+
286
+ expect(board.all_marked?).to be true
287
+ end
288
+
289
+ it "returns false if any cell in board is blank" do
290
+ config = [x, x, x, x, _, o, o, o, o].shuffle
291
+ board = build_board(config)
292
+
293
+ expect(board.all_marked?).to be false
294
+ end
295
+ end
296
+
297
+ describe "#has_winning_line?" do
298
+ it "returns true if board has a horizontal winning line" do
299
+ winning_configs = [
300
+ [ x, x, x,
301
+ _, _, _,
302
+ _, _, _ ],
303
+ [ _, _, _,
304
+ x, x, x,
305
+ _, _, _ ],
306
+ [ _, _, _,
307
+ _, _, _,
308
+ x, x, x ]
309
+ ]
310
+
311
+ winning_configs.each do |config|
312
+ expect(build_board(config).has_winning_line?).to be true
313
+ end
314
+ end
315
+
316
+ it "returns true if board has a vertical winning line" do
317
+ winning_configs = [
318
+ [ x, _, _,
319
+ x, _, _,
320
+ x, _, _ ],
321
+ [ _, x, _,
322
+ _, x, _,
323
+ _, x, _ ],
324
+ [ _, _, x,
325
+ _, _, x,
326
+ _, _, x ]
327
+ ]
328
+
329
+ winning_configs.each do |config|
330
+ expect(build_board(config).has_winning_line?).to be true
331
+ end
332
+ end
333
+
334
+ it "returns true if board has a diagonal winning line" do
335
+ winning_configs = [
336
+ [ x, _, _,
337
+ _, x, _,
338
+ _, _, x ],
339
+ [ _, _, x,
340
+ _, x, _,
341
+ x, _, _ ]
342
+ ]
343
+
344
+ winning_configs.each do |config|
345
+ expect(build_board(config).has_winning_line?).to be true
346
+ end
347
+ end
348
+
349
+ it "returns false if board has no winning line" do
350
+ non_winning_configs = [
351
+ [ _, _, _,
352
+ _, _, _,
353
+ _, _, _ ],
354
+ [ _, _, _,
355
+ x, x, o,
356
+ _, _, _ ],
357
+ [ x, _, _,
358
+ x, _, _,
359
+ o, _, _ ],
360
+ [ x, _, _,
361
+ _, x, _,
362
+ _, _, o ],
363
+ [ x, o, x,
364
+ x, o, x,
365
+ o, x, o ]
366
+ ]
367
+
368
+ non_winning_configs.each do |config|
369
+ expect(build_board(config).has_winning_line?).to be false
370
+ end
371
+ end
372
+ end
373
+ end
374
+ end
@@ -0,0 +1,167 @@
1
+ require "spec_helper"
2
+ require "command_line_interface"
3
+
4
+ require "board"
5
+
6
+ module TicTacToe
7
+ describe CommandLineInterface do
8
+ include_context "default_values"
9
+ include_context "helper_methods"
10
+
11
+ let(:input_stream) { StringIO.new }
12
+ let(:output_stream) { StringIO.new }
13
+ let(:interface) do
14
+ parameters = { input: input_stream, output: output_stream }
15
+ CommandLineInterface.new(parameters)
16
+ end
17
+
18
+ describe "#game_setup_interaction" do
19
+ before do
20
+ input_stream.string = %w(human computer).join("\n")
21
+ end
22
+
23
+ it "prints an introductory message to the command line" do
24
+ interface.game_setup_interaction(@default_player_marks)
25
+
26
+ expect(output_stream.string).not_to eq ""
27
+ end
28
+
29
+ it "returns the player type associated with each of the given player marks" do
30
+ player_types = interface.game_setup_interaction(@default_player_marks)
31
+
32
+ expect(player_types).to have(2).things
33
+ player_types.each do |type|
34
+ expect(@default_player_types).to include type
35
+ end
36
+ end
37
+ end
38
+
39
+ describe "#solicit_player_type" do
40
+ let(:valid_input) { "human" }
41
+
42
+ it "prints a prompt to the command line" do
43
+ input_stream.string = valid_input
44
+ interface.solicit_player_type(@default_first_player)
45
+
46
+ expect(output_stream.string).not_to eq ""
47
+ end
48
+
49
+ context "when given valid input" do
50
+ it "returns the player type input by user" do
51
+ input_stream.string = valid_input
52
+
53
+ expect(interface.solicit_player_type(@default_first_player)).to eq valid_input.to_sym
54
+ end
55
+ end
56
+
57
+ context "when given invalid input" do
58
+ let(:invalid_input) { "foo" }
59
+
60
+ it "prompts the user until given valid input" do
61
+ input_stream.string = invalid_input + "\n" + invalid_input + "\n" + valid_input
62
+ interface.solicit_player_type(@default_first_player)
63
+
64
+ expect(at_least_one_repeated_line?(output_stream.string)).to be true
65
+ end
66
+ end
67
+ end
68
+
69
+ describe "#show_game_board" do
70
+ it "prints a respresentation of the given board to the command line" do
71
+ board = board_with_draw(@default_board_size, @default_player_marks)
72
+ interface.show_game_board(board)
73
+
74
+ board_characters = output_stream.string.split("")
75
+ cell_count = board.size**2
76
+ expected_first_mark_count = cell_count.odd? ? cell_count / 2 + 1 : cell_count / 2
77
+ expected_second_mark_count = cell_count / 2
78
+
79
+ expect(board_characters.count(@default_first_player.to_s)).to eq expected_first_mark_count
80
+ expect(board_characters.count(@default_second_player.to_s)).to eq expected_second_mark_count
81
+ end
82
+ end
83
+
84
+ describe "solicit_move" do
85
+ let(:valid_input) { "0, 0" }
86
+
87
+ it "prints a prompt to the command line" do
88
+ input_stream.string = valid_input
89
+ interface.solicit_move(@default_first_player)
90
+
91
+ expect(output_stream.string).not_to eq ""
92
+ end
93
+
94
+ context "when given valid input" do
95
+ it "returns the move coordinates input by user" do
96
+ input_stream.string = valid_input
97
+ valid_coordinate = valid_input.split(",").map(&:to_i)
98
+
99
+ expect(interface.solicit_move(@default_first_player)).to eq valid_coordinate
100
+ end
101
+ end
102
+
103
+ context "when given invalid input" do
104
+ let(:invalid_input) { "foo" }
105
+
106
+ it "prompts the user until given valid input" do
107
+ input_stream.string = invalid_input + "\n" + invalid_input + "\n" + valid_input
108
+ interface.solicit_move(@default_first_player)
109
+
110
+ expect(at_least_one_repeated_line?(output_stream.string)).to be true
111
+ end
112
+ end
113
+ end
114
+
115
+ describe "#report_invalid_move" do
116
+ it "prints a notification that a move cannot be made at the given coordinates" do
117
+ interface.report_invalid_move([0, 0])
118
+
119
+ expect(output_stream.string.split("").count("0")).to eq 2
120
+ end
121
+ end
122
+
123
+ describe "#report_move" do
124
+ it "prints a notification of the move made by the given mark at the given coordinates" do
125
+ interface.report_move(@default_first_player, [0, 0])
126
+ interface_output = output_stream.string
127
+
128
+ expect(interface_output).to include @default_first_player.to_s
129
+ expect(interface_output.split("").count("0")).to eq 2
130
+ end
131
+ end
132
+
133
+ describe "#report_game_over" do
134
+ context "when given the mark of a winning player" do
135
+ it "prints a notification that the player has won" do
136
+ expect(interface).to receive(:report_win).with(@default_first_player)
137
+ interface.report_game_over(@default_first_player)
138
+ end
139
+ end
140
+
141
+ context "when winning player is ':none'" do
142
+ it "prints a notification that the game has ended in a draw" do
143
+ expect(interface).to receive(:report_draw)
144
+ interface.report_game_over(:none)
145
+ end
146
+ end
147
+ end
148
+
149
+ describe "#report_win" do
150
+ it "prints a notification that the player using the given mark has won" do
151
+ interface.report_win(@default_first_player)
152
+ interface_output = output_stream.string
153
+
154
+ expect(interface_output).to include @default_first_player.to_s
155
+ expect(interface_output).to include "wins"
156
+ end
157
+ end
158
+
159
+ describe "#report_draw" do
160
+ it "prints a notification that the game has ended in a draw" do
161
+ interface.report_draw
162
+
163
+ expect(output_stream.string).to include "draw"
164
+ end
165
+ end
166
+ end
167
+ end