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,230 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "old_computer_player"
|
|
3
|
+
|
|
4
|
+
require "board"
|
|
5
|
+
|
|
6
|
+
module TicTacToe
|
|
7
|
+
describe OldComputerPlayer do
|
|
8
|
+
include_context "default_values"
|
|
9
|
+
include_context "helper_methods"
|
|
10
|
+
|
|
11
|
+
let(:ai) do
|
|
12
|
+
described_class.new(
|
|
13
|
+
board: blank_board(@default_board_size),
|
|
14
|
+
player_mark: @default_first_player,
|
|
15
|
+
opponent_mark: @default_second_player)
|
|
16
|
+
end
|
|
17
|
+
let(:infinity) { Float::INFINITY }
|
|
18
|
+
let(:neg_infinity) { -Float::INFINITY }
|
|
19
|
+
|
|
20
|
+
describe "#initialize" do
|
|
21
|
+
it "takes a parameters hash with the player's mark, opponent's mark, and the game board" do
|
|
22
|
+
params = {
|
|
23
|
+
board: blank_board(@default_board_size),
|
|
24
|
+
player_mark: @default_first_player,
|
|
25
|
+
opponent_mark: @default_second_player
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
expect { described_class.new(params) }.not_to raise_error
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "#move" do
|
|
33
|
+
context "when a center space is available" do
|
|
34
|
+
it "returns the move coordinate for centermost space" do
|
|
35
|
+
[3, 5, 7].each do |odd_size|
|
|
36
|
+
custom_ai = described_class.new(
|
|
37
|
+
board: blank_board(odd_size),
|
|
38
|
+
player_mark: @default_first_player,
|
|
39
|
+
opponent_mark: @default_second_player)
|
|
40
|
+
|
|
41
|
+
expect(custom_ai.move).to eq [odd_size / 2, odd_size / 2]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
context "when a center space is not available" do
|
|
47
|
+
let(:_) { Board.blank_mark }
|
|
48
|
+
let(:x) { @default_first_player }
|
|
49
|
+
let(:o) { @default_second_player }
|
|
50
|
+
|
|
51
|
+
def x_player(board)
|
|
52
|
+
parameters = {
|
|
53
|
+
board: board,
|
|
54
|
+
player_mark: x,
|
|
55
|
+
opponent_mark: o
|
|
56
|
+
}
|
|
57
|
+
OldComputerPlayer.new(parameters)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def o_player(board)
|
|
61
|
+
parameters = {
|
|
62
|
+
board: board,
|
|
63
|
+
player_mark: o,
|
|
64
|
+
opponent_mark: x
|
|
65
|
+
}
|
|
66
|
+
OldComputerPlayer.new(parameters)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context "when computer player can make a horizontal winning move" do
|
|
70
|
+
it "returns the coordinates of the winning move" do
|
|
71
|
+
board_configs = [
|
|
72
|
+
[ x, _, x,
|
|
73
|
+
_, o, _,
|
|
74
|
+
o, x, o ],
|
|
75
|
+
[ o, _, _,
|
|
76
|
+
x, x, _,
|
|
77
|
+
o, o, x ],
|
|
78
|
+
[ _, o, _,
|
|
79
|
+
x, o, o,
|
|
80
|
+
_, x, x ]
|
|
81
|
+
]
|
|
82
|
+
winning_moves = [[0, 1], [1, 2], [2, 0]]
|
|
83
|
+
board_configs.zip(winning_moves).each do |board_config, winning_move|
|
|
84
|
+
board = build_board(board_config)
|
|
85
|
+
computer_player = x_player(board)
|
|
86
|
+
|
|
87
|
+
expect(computer_player.move).to eq winning_move
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
context "when computer player can make a vertical winning move" do
|
|
93
|
+
it "returns the coordinates of the winning move" do
|
|
94
|
+
board_configs = [
|
|
95
|
+
[ x, _, o,
|
|
96
|
+
_, o, x,
|
|
97
|
+
x, _, o ],
|
|
98
|
+
[ o, x, o,
|
|
99
|
+
o, x, _,
|
|
100
|
+
x, _, _ ],
|
|
101
|
+
[ _, o, _,
|
|
102
|
+
_, o, x,
|
|
103
|
+
o, x, x ]
|
|
104
|
+
]
|
|
105
|
+
winning_moves = [[1, 0], [2, 1], [0, 2]]
|
|
106
|
+
board_configs.zip(winning_moves).each do |board_config, winning_move|
|
|
107
|
+
board = build_board(board_config)
|
|
108
|
+
computer_player = x_player(board)
|
|
109
|
+
|
|
110
|
+
expect(computer_player.move).to eq winning_move
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
context "when computer player can make a diagonal winning move" do
|
|
116
|
+
it "returns the coordinates of the winning move" do
|
|
117
|
+
board_configs = [
|
|
118
|
+
[ x, _, o,
|
|
119
|
+
o, x, _,
|
|
120
|
+
x, o, _ ],
|
|
121
|
+
[ o, x, _,
|
|
122
|
+
o, x, _,
|
|
123
|
+
x, o, _ ]
|
|
124
|
+
]
|
|
125
|
+
winning_moves = [[2, 2], [0, 2]]
|
|
126
|
+
board_configs.zip(winning_moves).each do |board_config, winning_move|
|
|
127
|
+
board = build_board(board_config)
|
|
128
|
+
computer_player = x_player(board)
|
|
129
|
+
|
|
130
|
+
expect(computer_player.move).to eq winning_move
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
context "when opponent can make a horizontal winning move next turn" do
|
|
136
|
+
it "returns the coordinates of the move that blocks the opponent from winning" do
|
|
137
|
+
board_configs = [
|
|
138
|
+
[ x, _, x,
|
|
139
|
+
_, o, _,
|
|
140
|
+
o, x, o ],
|
|
141
|
+
[ o, _, _,
|
|
142
|
+
x, x, _,
|
|
143
|
+
o, o, x ],
|
|
144
|
+
[ _, o, _,
|
|
145
|
+
x, o, o,
|
|
146
|
+
_, x, x ]
|
|
147
|
+
]
|
|
148
|
+
winning_moves = [[0, 1], [1, 2], [2, 0]]
|
|
149
|
+
board_configs.zip(winning_moves).each do |board_config, winning_move|
|
|
150
|
+
board = build_board(board_config)
|
|
151
|
+
computer_player = o_player(board)
|
|
152
|
+
|
|
153
|
+
expect(computer_player.move).to eq winning_move
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
context "when opponent can make a vertical winning move next turn" do
|
|
159
|
+
it "returns the coordinates of the move that blocks the opponent from winning" do
|
|
160
|
+
board_configs = [
|
|
161
|
+
[ x, _, o,
|
|
162
|
+
_, o, x,
|
|
163
|
+
x, _, o ],
|
|
164
|
+
[ o, x, o,
|
|
165
|
+
o, x, _,
|
|
166
|
+
x, _, _ ],
|
|
167
|
+
[ _, o, _,
|
|
168
|
+
_, o, x,
|
|
169
|
+
o, x, x ]
|
|
170
|
+
]
|
|
171
|
+
winning_moves = [[1, 0], [2, 1], [0, 2]]
|
|
172
|
+
board_configs.zip(winning_moves).each do |board_config, winning_move|
|
|
173
|
+
board = build_board(board_config)
|
|
174
|
+
computer_player = o_player(board)
|
|
175
|
+
|
|
176
|
+
expect(computer_player.move).to eq winning_move
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
context "when opponent can make a diagonal winning move next turn" do
|
|
182
|
+
it "returns the coordinates of the move that blocks the opponent from winning" do
|
|
183
|
+
board_configs = [
|
|
184
|
+
[ x, _, o,
|
|
185
|
+
o, x, _,
|
|
186
|
+
x, o, _ ],
|
|
187
|
+
[ o, x, _,
|
|
188
|
+
o, x, _,
|
|
189
|
+
x, o, _ ]
|
|
190
|
+
]
|
|
191
|
+
winning_moves = [[2, 2], [0, 2]]
|
|
192
|
+
board_configs.zip(winning_moves).each do |board_config, winning_move|
|
|
193
|
+
board = build_board(board_config)
|
|
194
|
+
computer_player = o_player(board)
|
|
195
|
+
|
|
196
|
+
expect(computer_player.move).to eq winning_move
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
context "when opponent can make a fork next turn" do
|
|
202
|
+
it "returns the coordinates of a move that prevents that fork" do
|
|
203
|
+
board_configs = [
|
|
204
|
+
[ x, _, _,
|
|
205
|
+
_, x, _,
|
|
206
|
+
_, _, o ],
|
|
207
|
+
[ x, _, _,
|
|
208
|
+
_, o, _,
|
|
209
|
+
_, _, x ],
|
|
210
|
+
[ _, x, _,
|
|
211
|
+
_, x, _,
|
|
212
|
+
_, o, _ ]
|
|
213
|
+
]
|
|
214
|
+
good_move_sets = [
|
|
215
|
+
[[2, 0], [0, 2]],
|
|
216
|
+
[[0, 1], [1, 0], [1, 2], [2, 1]],
|
|
217
|
+
[[0, 0], [0, 2], [2, 0], [2, 2]]
|
|
218
|
+
]
|
|
219
|
+
board_configs.zip(good_move_sets).each do |board_config, good_moves|
|
|
220
|
+
board = build_board(board_config)
|
|
221
|
+
computer_player = o_player(board)
|
|
222
|
+
|
|
223
|
+
expect(good_moves).to include computer_player.move
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "player_factory"
|
|
3
|
+
|
|
4
|
+
require "game"
|
|
5
|
+
|
|
6
|
+
module TicTacToe
|
|
7
|
+
describe PlayerFactory do
|
|
8
|
+
include_context "default_values"
|
|
9
|
+
|
|
10
|
+
describe ".build" do
|
|
11
|
+
let(:player_types) { [AvailablePlayerTypes::HUMAN, AvailablePlayerTypes::COMPUTER] }
|
|
12
|
+
let(:player_classes) { [HumanPlayer, ComputerPlayer] }
|
|
13
|
+
let(:player_config) do
|
|
14
|
+
{
|
|
15
|
+
game: Game.new({}),
|
|
16
|
+
player_mark: @default_player_marks.first
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def build_player(config)
|
|
21
|
+
PlayerFactory.build(config)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "creates a player of the given type" do
|
|
25
|
+
player_types.zip(player_classes).each do |type, player_class|
|
|
26
|
+
player_config[:type] = type
|
|
27
|
+
|
|
28
|
+
expect(build_player(player_config)).to be_a player_class
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "creates players that respond to #move" do
|
|
33
|
+
player_types.each do |type|
|
|
34
|
+
player_config[:type] = type
|
|
35
|
+
|
|
36
|
+
expect(build_player(player_config)).to respond_to :move
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "creates players that use the given mark" do
|
|
41
|
+
player_types.each do |type|
|
|
42
|
+
player_config[:type] = type
|
|
43
|
+
|
|
44
|
+
expect(build_player(player_config).player_mark).to eq player_config[:player_mark]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
require "rspec"
|
|
2
|
+
require "rspec/collection_matchers"
|
|
3
|
+
|
|
4
|
+
RSpec.shared_context "default_values" do
|
|
5
|
+
before :all do
|
|
6
|
+
@default_board_size = 3
|
|
7
|
+
@default_first_player = :x
|
|
8
|
+
@default_second_player = :o
|
|
9
|
+
@default_player_marks = [:x, :o]
|
|
10
|
+
@default_player_types = [:human, :computer]
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
RSpec.shared_context "helper_methods" do
|
|
15
|
+
def blank_board_configuration(board_size)
|
|
16
|
+
(0...board_size**2).map { TicTacToe::Board.blank_mark }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def at_least_one_repeated_line?(string)
|
|
20
|
+
lines = string.split("\n")
|
|
21
|
+
lines.uniq.length < lines.length
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def random_coordinates(board_size)
|
|
25
|
+
[rand(board_size), rand(board_size)]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def new_board(parameters)
|
|
29
|
+
TicTacToe::Board.new(parameters)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build_board(config)
|
|
33
|
+
size = Math.sqrt(config.size).to_i
|
|
34
|
+
new_board(size: size, config: config)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def blank_board(board_size)
|
|
38
|
+
new_board(size: board_size)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def board_with_potential_win_loss_or_draw(board_size, player_marks)
|
|
42
|
+
first_mark, second_mark = player_marks
|
|
43
|
+
board = blank_board(board_size)
|
|
44
|
+
(0...board_size - 1).each { |col| board.mark_cell(first_mark, 0, col) }
|
|
45
|
+
(0...board_size - 1).each { |col| board.mark_cell(second_mark, 1, col) }
|
|
46
|
+
(2...board_size).each do |row|
|
|
47
|
+
(0...board_size - 1).each do |col|
|
|
48
|
+
mark = col.even? ? first_mark : second_mark
|
|
49
|
+
board.mark_cell(mark, row, col)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
board
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def board_with_draw(board_size, player_marks)
|
|
56
|
+
first_mark, second_mark = player_marks
|
|
57
|
+
board = blank_board(board_size)
|
|
58
|
+
(0...board_size).each do |row|
|
|
59
|
+
(0...board_size).each do |col|
|
|
60
|
+
if row == 0
|
|
61
|
+
mark = col.odd? ? first_mark : second_mark
|
|
62
|
+
board.mark_cell(mark, row, col)
|
|
63
|
+
else
|
|
64
|
+
mark = col.even? ? first_mark : second_mark
|
|
65
|
+
board.mark_cell(mark, row, col)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
board
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def all_wins(board_size, winning_mark)
|
|
73
|
+
winning_boards = horizontal_wins(board_size, winning_mark)
|
|
74
|
+
winning_boards.concat vertical_wins(board_size, winning_mark)
|
|
75
|
+
winning_boards.concat diagonal_wins(board_size, winning_mark)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def horizontal_wins(board_size, winning_mark)
|
|
79
|
+
(0...board_size).each_with_object([]) do |row, winning_boards|
|
|
80
|
+
board = blank_board(board_size)
|
|
81
|
+
(0...board_size).each { |col| board.mark_cell(winning_mark, row, col) }
|
|
82
|
+
winning_boards << board
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def vertical_wins(board_size, winning_mark)
|
|
87
|
+
(0...board_size).each_with_object([]) do |col, winning_boards|
|
|
88
|
+
board = blank_board(board_size)
|
|
89
|
+
(0...board_size).each { |row| board.mark_cell(winning_mark, row, col) }
|
|
90
|
+
winning_boards << board
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def diagonal_wins(board_size, winning_mark)
|
|
95
|
+
ldiag_board = blank_board(board_size)
|
|
96
|
+
(0...board_size).each do |index|
|
|
97
|
+
ldiag_board.mark_cell(winning_mark, index, index)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
rdiag_board = blank_board(board_size)
|
|
101
|
+
(0...board_size).each do |index|
|
|
102
|
+
rdiag_board.mark_cell(winning_mark, index, rdiag_board.size - index - 1)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
[ldiag_board, rdiag_board]
|
|
106
|
+
end
|
|
107
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: tactical_tic_tac_toe
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Batman
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2015-10-01 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Command line tic tac toe! Run with command 'ttt'
|
|
14
|
+
email: no@no.no
|
|
15
|
+
executables:
|
|
16
|
+
- ttt
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- bin/ttt
|
|
21
|
+
- lib/available_player_types.rb
|
|
22
|
+
- lib/board.rb
|
|
23
|
+
- lib/command_line_interface.rb
|
|
24
|
+
- lib/computer_player.rb
|
|
25
|
+
- lib/game.rb
|
|
26
|
+
- lib/human_player.rb
|
|
27
|
+
- lib/negamax.rb
|
|
28
|
+
- lib/old_computer_player.rb
|
|
29
|
+
- lib/player_factory.rb
|
|
30
|
+
- lib/tactical_tic_tac_toe.rb
|
|
31
|
+
- spec/board_spec.rb
|
|
32
|
+
- spec/command_line_interface_spec.rb
|
|
33
|
+
- spec/computer_player_spec.rb
|
|
34
|
+
- spec/game_spec.rb
|
|
35
|
+
- spec/human_player_spec.rb
|
|
36
|
+
- spec/negamax_spec.rb
|
|
37
|
+
- spec/old_computer_player_spec.rb
|
|
38
|
+
- spec/player_factory_spec.rb
|
|
39
|
+
- spec/spec_helper.rb
|
|
40
|
+
homepage: http://rubygems.org/gems/tactical_tic_tac_toe
|
|
41
|
+
licenses:
|
|
42
|
+
- MIT
|
|
43
|
+
metadata: {}
|
|
44
|
+
post_install_message:
|
|
45
|
+
rdoc_options: []
|
|
46
|
+
require_paths:
|
|
47
|
+
- lib
|
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0'
|
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '0'
|
|
58
|
+
requirements: []
|
|
59
|
+
rubyforge_project:
|
|
60
|
+
rubygems_version: 2.4.6
|
|
61
|
+
signing_key:
|
|
62
|
+
specification_version: 4
|
|
63
|
+
summary: Command line tic tac toe.
|
|
64
|
+
test_files: []
|