tictactoe-core 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/.travis.yml +9 -0
- data/Gemfile +3 -0
- data/README.md +2 -0
- data/Rakefile +34 -0
- data/lib/tictactoe.rb +1 -0
- data/lib/tictactoe/ai/ab_minimax.rb +78 -0
- data/lib/tictactoe/ai/ab_negamax.rb +45 -0
- data/lib/tictactoe/ai/perfect_intelligence.rb +35 -0
- data/lib/tictactoe/ai/random_chooser.rb +21 -0
- data/lib/tictactoe/ai/tree.rb +44 -0
- data/lib/tictactoe/boards/board_type_factory.rb +15 -0
- data/lib/tictactoe/boards/four_by_four_board.rb +28 -0
- data/lib/tictactoe/boards/three_by_three_board.rb +27 -0
- data/lib/tictactoe/game.rb +102 -0
- data/lib/tictactoe/players/computer.rb +21 -0
- data/lib/tictactoe/players/factory.rb +21 -0
- data/lib/tictactoe/sequence.rb +24 -0
- data/lib/tictactoe/state.rb +64 -0
- data/runtests.sh +1 -0
- data/spec/performance_spec.rb +16 -0
- data/spec/properties_spec.rb +27 -0
- data/spec/rake_rspec.rb +42 -0
- data/spec/regression_spec.rb +29 -0
- data/spec/reproducible_random.rb +16 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/test_run.rb +19 -0
- data/spec/tictactoe/ai/ab_minimax_spec.rb +511 -0
- data/spec/tictactoe/ai/ab_negamax_spec.rb +199 -0
- data/spec/tictactoe/ai/perfect_intelligence_spec.rb +122 -0
- data/spec/tictactoe/ai/random_choser_spec.rb +50 -0
- data/spec/tictactoe/boards/board_type_factory_spec.rb +16 -0
- data/spec/tictactoe/boards/four_by_four_board_spec.rb +30 -0
- data/spec/tictactoe/boards/three_by_three_board_spec.rb +30 -0
- data/spec/tictactoe/game_spec.rb +288 -0
- data/spec/tictactoe/players/computer_spec.rb +42 -0
- data/spec/tictactoe/players/factory_spec.rb +48 -0
- data/spec/tictactoe/sequence_spec.rb +20 -0
- data/spec/tictactoe/state_spec.rb +136 -0
- data/tictactoe-core-0.1.0.gem +0 -0
- data/tictactoe-core.gemspec +15 -0
- metadata +84 -0
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tictactoe/ai/ab_negamax'
|
3
|
+
|
4
|
+
RSpec.describe Tictactoe::Ai::ABNegamax do
|
5
|
+
def preferred_nodes(tree, depth = 10, depth_reached_score = -10)
|
6
|
+
negamax = described_class.new(depth, depth_reached_score)
|
7
|
+
strategy = negamax.best_nodes(tree)
|
8
|
+
strategy
|
9
|
+
end
|
10
|
+
|
11
|
+
def score(tree, depth = 10, depth_reached_score = -10)
|
12
|
+
negamax = described_class.new(depth, depth_reached_score)
|
13
|
+
negamax.score(tree)
|
14
|
+
end
|
15
|
+
|
16
|
+
def leaf(score)
|
17
|
+
spy "leaf scored: #{score}", :is_final? => true, :score => score
|
18
|
+
end
|
19
|
+
|
20
|
+
def tree(children)
|
21
|
+
spy "tree, children: #{children.to_s}", :is_final? => false, :children => children
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'with one player choice of score 0, the resulting score is 0' do
|
25
|
+
root = tree([
|
26
|
+
#player choice
|
27
|
+
leaf(0)
|
28
|
+
])
|
29
|
+
|
30
|
+
expect(score(root)).to eq(0)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'with one player choice of score -1 for player, the resulting score is -1' do
|
34
|
+
root = tree([
|
35
|
+
#player choice
|
36
|
+
leaf(-1)
|
37
|
+
])
|
38
|
+
|
39
|
+
expect(score(root)).to eq(-1)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'with two player choices of score 0 and 1 for player, the resulting score is 1 (the best for player)' do
|
43
|
+
root = tree([
|
44
|
+
#player choice
|
45
|
+
leaf(1),
|
46
|
+
leaf(0),
|
47
|
+
])
|
48
|
+
|
49
|
+
expect(score(root)).to eq(1)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'with one opponnent choice of score 1 for player, the resulting score is 1' do
|
53
|
+
root = tree([
|
54
|
+
#player choice
|
55
|
+
tree([
|
56
|
+
#opponent choice
|
57
|
+
leaf(1),
|
58
|
+
])
|
59
|
+
])
|
60
|
+
|
61
|
+
expect(score(root)).to eq(1)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'with two opponnent choices of score 0 and 1 for player, it will chose 0 (the worst for player)' do
|
65
|
+
root = tree([
|
66
|
+
#player choice
|
67
|
+
tree([
|
68
|
+
#opponent choice
|
69
|
+
leaf(1),
|
70
|
+
leaf(0),
|
71
|
+
])
|
72
|
+
])
|
73
|
+
|
74
|
+
expect(score(root)).to eq(0)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'with a depth limit of 0 and a score of -10 for the deeper nodes, when provided a single node at depth 1 returns -10' do
|
78
|
+
depth = 0
|
79
|
+
depth_reached_score = -10
|
80
|
+
|
81
|
+
all_children = [
|
82
|
+
#player choice
|
83
|
+
leaf(1),
|
84
|
+
]
|
85
|
+
root = tree(all_children)
|
86
|
+
|
87
|
+
expect(score(root, depth, depth_reached_score)).to eq(-10)
|
88
|
+
expect(preferred_nodes(root, depth, depth_reached_score)).to eq(all_children)
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'with a depth limit of 1 and a score of -10 for the deeper nodes, when provided a single node at depth 2 returns -10' do
|
92
|
+
depth_limit = 1
|
93
|
+
depth_reached_score = -10
|
94
|
+
|
95
|
+
root = tree([
|
96
|
+
#player choice
|
97
|
+
tree([
|
98
|
+
#opponent choice
|
99
|
+
leaf(1),
|
100
|
+
])
|
101
|
+
])
|
102
|
+
|
103
|
+
expect(score(root, depth_limit, depth_reached_score)).to eq(-10)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'with a depth limit of 2 and a score of -10 for the deeper nodes, when provided a single node of score 1 at depth 2 returns 1' do
|
107
|
+
depth_limit = 2
|
108
|
+
depth_reached_score = -10
|
109
|
+
|
110
|
+
root = tree([
|
111
|
+
#player choice
|
112
|
+
tree([
|
113
|
+
#opponent choice
|
114
|
+
leaf(1),
|
115
|
+
])
|
116
|
+
])
|
117
|
+
|
118
|
+
expect(score(root, depth_limit, depth_reached_score)).to eq(1)
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'with a branch that is going to be worse than a previous choice, stops evaluating once it knows' do
|
122
|
+
not_evaluated_node = leaf(100)
|
123
|
+
root = tree([
|
124
|
+
#player choice
|
125
|
+
leaf(1),
|
126
|
+
tree([
|
127
|
+
#opponent choice
|
128
|
+
leaf(0),
|
129
|
+
not_evaluated_node,
|
130
|
+
])
|
131
|
+
])
|
132
|
+
|
133
|
+
root_score = score(root)
|
134
|
+
|
135
|
+
expect(root_score).to eq(1)
|
136
|
+
expect(not_evaluated_node).not_to have_received(:score)
|
137
|
+
expect(not_evaluated_node).not_to have_received(:is_leaf?)
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'with a branch that is going to be discarded by the opponents choice, stops evaluating once it knows' do
|
141
|
+
not_evaluated_node = leaf(-1)
|
142
|
+
root = tree([
|
143
|
+
#player choice
|
144
|
+
tree([
|
145
|
+
#opponent choice
|
146
|
+
leaf(-1),
|
147
|
+
tree([
|
148
|
+
#player choice
|
149
|
+
leaf(0),
|
150
|
+
not_evaluated_node,
|
151
|
+
]),
|
152
|
+
]),
|
153
|
+
])
|
154
|
+
|
155
|
+
score(root)
|
156
|
+
expect(not_evaluated_node).not_to have_received(:score)
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'with one player choice of score 0, that is the only preferred node' do
|
160
|
+
only_option = leaf(0)
|
161
|
+
root = tree([
|
162
|
+
#player choice
|
163
|
+
only_option
|
164
|
+
])
|
165
|
+
|
166
|
+
expect(preferred_nodes(root)).to eq([only_option])
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'with two player choices of score 0, those two are the prefered nodes' do
|
170
|
+
option1 = leaf(0)
|
171
|
+
option2 = leaf(0)
|
172
|
+
root = tree([
|
173
|
+
#player choice
|
174
|
+
option1,
|
175
|
+
option2
|
176
|
+
])
|
177
|
+
|
178
|
+
expect(preferred_nodes(root)).to eq([option1, option2])
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'with equivalent branches, those are the preferred nodes' do
|
182
|
+
option1 = tree([
|
183
|
+
#opponent choice
|
184
|
+
leaf(0)
|
185
|
+
])
|
186
|
+
option2 = tree([
|
187
|
+
#opponent choice
|
188
|
+
leaf(0)
|
189
|
+
])
|
190
|
+
|
191
|
+
root = tree([
|
192
|
+
#player choice
|
193
|
+
option1,
|
194
|
+
option2
|
195
|
+
])
|
196
|
+
|
197
|
+
expect(preferred_nodes(root)).to eq([option1, option2])
|
198
|
+
end
|
199
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tictactoe/ai/perfect_intelligence'
|
3
|
+
require 'tictactoe/state'
|
4
|
+
require 'tictactoe/sequence'
|
5
|
+
require 'tictactoe/boards/three_by_three_board'
|
6
|
+
require 'tictactoe/boards/four_by_four_board'
|
7
|
+
|
8
|
+
RSpec.describe Tictactoe::Ai::PerfectIntelligence do
|
9
|
+
def board(*marks)
|
10
|
+
state = Tictactoe::State.new(Tictactoe::Boards::ThreeByThreeBoard.new)
|
11
|
+
marks.each_with_index do |mark, location|
|
12
|
+
state = state.make_move(location, mark)
|
13
|
+
end
|
14
|
+
state
|
15
|
+
end
|
16
|
+
|
17
|
+
def play(state)
|
18
|
+
player = Tictactoe::Sequence.new([:x, :o]).first
|
19
|
+
described_class.new().desired_moves(state, player)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'given only one possible play, should do it' do
|
23
|
+
state = board(
|
24
|
+
:x, :o, nil,
|
25
|
+
:o, :x, :x,
|
26
|
+
:x, :o, :o
|
27
|
+
)
|
28
|
+
expect(play(state)).to eq [2]
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'given the possibility to lose, should block' do
|
32
|
+
state = board(
|
33
|
+
:x, nil, nil,
|
34
|
+
:o, nil, :o,
|
35
|
+
:x, nil, nil
|
36
|
+
)
|
37
|
+
expect(play(state)).to eq [4]
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'given the possibility to lose, should block, not matter if there is a fork comming' do
|
41
|
+
state = board(
|
42
|
+
:x, nil, nil,
|
43
|
+
:x, nil, nil,
|
44
|
+
:o, nil, :o,
|
45
|
+
)
|
46
|
+
expect(play(state)).to eq [7]
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'given the possibility to win, should prefer it' do
|
50
|
+
state = board(
|
51
|
+
:x, :o, :x,
|
52
|
+
:o, :x, :o,
|
53
|
+
nil, nil, :o
|
54
|
+
)
|
55
|
+
expect(play(state)).to eq [6]
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'given the possibility to win, should prefer it over blocking or forking' do
|
59
|
+
state = board(
|
60
|
+
:x, nil, :o,
|
61
|
+
nil, nil, :o,
|
62
|
+
:x, nil, nil
|
63
|
+
)
|
64
|
+
expect(play(state)).to eq [3]
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'given the possibility to fork, should prefer it' do
|
68
|
+
state = board(
|
69
|
+
:x, :o, :x,
|
70
|
+
nil, nil, :o,
|
71
|
+
nil, nil, nil
|
72
|
+
)
|
73
|
+
expect(play(state)).to eq [4, 6]
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'given the possibility to block, should prefer it over forking' do
|
77
|
+
state = board(
|
78
|
+
nil, nil, :x,
|
79
|
+
nil, :o, nil,
|
80
|
+
:x, :o, nil
|
81
|
+
)
|
82
|
+
expect(play(state)).to eq [1]
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'given the possibility to block a fork, should do it' do
|
86
|
+
#:o started the game
|
87
|
+
state = board(
|
88
|
+
:o, :x, :o,
|
89
|
+
nil, nil, nil,
|
90
|
+
nil, nil, nil,
|
91
|
+
)
|
92
|
+
expect(play(state)).to eq [4]
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'given two possible forks for the opponent, should attack avoiding the creation of the fork' do
|
96
|
+
#:o started the game
|
97
|
+
state = board(
|
98
|
+
nil, nil, :o,
|
99
|
+
nil, :x, nil,
|
100
|
+
:o, nil, nil,
|
101
|
+
)
|
102
|
+
expect(play(state)).to eq [1, 3, 5, 7]
|
103
|
+
end
|
104
|
+
|
105
|
+
def board4(*marks)
|
106
|
+
state = Tictactoe::State.new(Tictactoe::Boards::FourByFourBoard.new)
|
107
|
+
marks.each_with_index do |mark, location|
|
108
|
+
state = state.make_move(location, mark)
|
109
|
+
end
|
110
|
+
state
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'given initial state, any option is valid' do
|
114
|
+
state = board4(
|
115
|
+
nil, nil, nil, nil,
|
116
|
+
nil, nil, nil, nil,
|
117
|
+
nil, nil, nil, nil,
|
118
|
+
nil, nil, nil, nil,
|
119
|
+
)
|
120
|
+
expect(play(state)).to eq((0..15).to_a)
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tictactoe/ai/random_chooser'
|
3
|
+
|
4
|
+
RSpec.describe Tictactoe::Ai::RandomChooser do
|
5
|
+
def choose_with(random_value, list)
|
6
|
+
random = spy :rand => random_value
|
7
|
+
described_class.new(random).choose_one list
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'given only one option' do
|
11
|
+
describe 'should choose it no matter the random value' do
|
12
|
+
it 'for example: minimum random value' do
|
13
|
+
expect(choose_with 0.0, [1]).to eq 1
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'for example: maximum random value' do
|
17
|
+
expect(choose_with 0.99, [1]).to eq 1
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe 'given two options' do
|
23
|
+
it 'chooses the first one when the random is less than a half' do
|
24
|
+
expect(choose_with 0.0, [1, 2]).to eq 1
|
25
|
+
expect(choose_with 0.4, [1, 2]).to eq 1
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'chooses the second one when the random is more than a half' do
|
29
|
+
expect(choose_with 0.5, [1, 2]).to eq 2
|
30
|
+
expect(choose_with 0.99, [1, 2]).to eq 2
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'given three options' do
|
35
|
+
it 'chooses the first one when the random is less than a third' do
|
36
|
+
expect(choose_with 0.0, [1, 2, 3]).to eq 1
|
37
|
+
expect(choose_with 0.3, [1, 2, 3]).to eq 1
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'chooses the second one when the random is between a third and two thirds' do
|
41
|
+
expect(choose_with 0.4, [1, 2, 3]).to eq 2
|
42
|
+
expect(choose_with 0.6, [1, 2, 3]).to eq 2
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'chooses the third one when the random is more than two thirds' do
|
46
|
+
expect(choose_with 0.7, [1, 2, 3]).to eq 3
|
47
|
+
expect(choose_with 0.9, [1, 2, 3]).to eq 3
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tictactoe/boards/board_type_factory'
|
3
|
+
|
4
|
+
RSpec.describe Tictactoe::Boards::BoardTypeFactory do
|
5
|
+
def create(side_size)
|
6
|
+
described_class.new.create(side_size)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'given 3 returns a ThreeByThreeBoard' do
|
10
|
+
expect(create(3)).to be_an_instance_of(Tictactoe::Boards::ThreeByThreeBoard)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'given 4 returns a FourByFourBoard' do
|
14
|
+
expect(create(4)).to be_an_instance_of(Tictactoe::Boards::FourByFourBoard)
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tictactoe/boards/four_by_four_board'
|
3
|
+
|
4
|
+
RSpec.describe Tictactoe::Boards::FourByFourBoard do
|
5
|
+
it 'has the possible locations' do
|
6
|
+
expect(described_class.new.locations).to eq([
|
7
|
+
0, 1, 2, 3,
|
8
|
+
4, 5, 6, 7,
|
9
|
+
8, 9, 10, 11,
|
10
|
+
12, 13, 14, 15
|
11
|
+
])
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should know the lines' do
|
15
|
+
expect(described_class.new.lines).to eq([
|
16
|
+
[0, 1, 2, 3],
|
17
|
+
[4, 5, 6, 7],
|
18
|
+
[8, 9, 10, 11],
|
19
|
+
[12, 13, 14, 15],
|
20
|
+
|
21
|
+
[0, 4, 8, 12],
|
22
|
+
[1, 5, 9, 13],
|
23
|
+
[2, 6, 10, 14],
|
24
|
+
[3, 7, 11, 15],
|
25
|
+
|
26
|
+
[0, 5, 10, 15],
|
27
|
+
[3, 6, 9, 12]
|
28
|
+
])
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'tictactoe/boards/three_by_three_board'
|
3
|
+
|
4
|
+
RSpec.describe Tictactoe::Boards::ThreeByThreeBoard do
|
5
|
+
before(:each) do
|
6
|
+
@board = described_class.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should know the available locations" do
|
10
|
+
expect(@board.locations).to eq([0, 1, 2, 3, 4, 5, 6, 7, 8])
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should know the lines" do
|
14
|
+
expected_lines = [
|
15
|
+
[0, 1, 2],
|
16
|
+
[3, 4, 5],
|
17
|
+
[6, 7, 8],
|
18
|
+
|
19
|
+
[0, 3, 6],
|
20
|
+
[1, 4, 7],
|
21
|
+
[2, 5, 8],
|
22
|
+
|
23
|
+
[0, 4, 8],
|
24
|
+
[2, 4, 6],
|
25
|
+
]
|
26
|
+
actual_lines = @board.lines
|
27
|
+
|
28
|
+
expect(actual_lines).to match_array(expected_lines)
|
29
|
+
end
|
30
|
+
end
|