tictactoe-core 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +3 -0
  4. data/README.md +2 -0
  5. data/Rakefile +34 -0
  6. data/lib/tictactoe.rb +1 -0
  7. data/lib/tictactoe/ai/ab_minimax.rb +78 -0
  8. data/lib/tictactoe/ai/ab_negamax.rb +45 -0
  9. data/lib/tictactoe/ai/perfect_intelligence.rb +35 -0
  10. data/lib/tictactoe/ai/random_chooser.rb +21 -0
  11. data/lib/tictactoe/ai/tree.rb +44 -0
  12. data/lib/tictactoe/boards/board_type_factory.rb +15 -0
  13. data/lib/tictactoe/boards/four_by_four_board.rb +28 -0
  14. data/lib/tictactoe/boards/three_by_three_board.rb +27 -0
  15. data/lib/tictactoe/game.rb +102 -0
  16. data/lib/tictactoe/players/computer.rb +21 -0
  17. data/lib/tictactoe/players/factory.rb +21 -0
  18. data/lib/tictactoe/sequence.rb +24 -0
  19. data/lib/tictactoe/state.rb +64 -0
  20. data/runtests.sh +1 -0
  21. data/spec/performance_spec.rb +16 -0
  22. data/spec/properties_spec.rb +27 -0
  23. data/spec/rake_rspec.rb +42 -0
  24. data/spec/regression_spec.rb +29 -0
  25. data/spec/reproducible_random.rb +16 -0
  26. data/spec/spec_helper.rb +6 -0
  27. data/spec/test_run.rb +19 -0
  28. data/spec/tictactoe/ai/ab_minimax_spec.rb +511 -0
  29. data/spec/tictactoe/ai/ab_negamax_spec.rb +199 -0
  30. data/spec/tictactoe/ai/perfect_intelligence_spec.rb +122 -0
  31. data/spec/tictactoe/ai/random_choser_spec.rb +50 -0
  32. data/spec/tictactoe/boards/board_type_factory_spec.rb +16 -0
  33. data/spec/tictactoe/boards/four_by_four_board_spec.rb +30 -0
  34. data/spec/tictactoe/boards/three_by_three_board_spec.rb +30 -0
  35. data/spec/tictactoe/game_spec.rb +288 -0
  36. data/spec/tictactoe/players/computer_spec.rb +42 -0
  37. data/spec/tictactoe/players/factory_spec.rb +48 -0
  38. data/spec/tictactoe/sequence_spec.rb +20 -0
  39. data/spec/tictactoe/state_spec.rb +136 -0
  40. data/tictactoe-core-0.1.0.gem +0 -0
  41. data/tictactoe-core.gemspec +15 -0
  42. 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