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