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,21 @@
1
+ module Tictactoe
2
+ module Players
3
+ class Computer
4
+ attr_reader :mark
5
+
6
+ def initialize(mark, intelligence, chooser)
7
+ @intelligence = intelligence
8
+ @chooser = chooser
9
+ @mark = mark
10
+ end
11
+
12
+ def get_move(state)
13
+ moves = intelligence.desired_moves(state, mark)
14
+ chooser.choose_one(moves)
15
+ end
16
+
17
+ private
18
+ attr_reader :intelligence, :chooser
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ module Tictactoe
2
+ module Players
3
+ class Factory
4
+ def initialize()
5
+ @factories = {}
6
+ end
7
+
8
+ def create(type, mark)
9
+ raise "No factory has been defined for type: #{type}" unless factories.has_key?(type)
10
+ factories[type].call(mark)
11
+ end
12
+
13
+ def register(type, factory)
14
+ factories[type] = factory
15
+ end
16
+
17
+ private
18
+ attr_accessor :factories
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,24 @@
1
+ module Tictactoe
2
+ class Sequence
3
+ class Node
4
+ attr_reader :value
5
+ attr_accessor :next
6
+
7
+ def initialize(value)
8
+ @value = value
9
+ end
10
+ end
11
+
12
+ attr_reader :first
13
+
14
+ def initialize(values)
15
+ nodes = values.map{|value| Node.new(value)}
16
+ nodes.each_with_index do |node, index|
17
+ next_index = (index +1) % nodes.length
18
+ node.next = nodes[next_index]
19
+ end
20
+
21
+ @first = nodes.first
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,64 @@
1
+ module Tictactoe
2
+ class State
3
+ attr_reader :board, :marks
4
+
5
+ def initialize(board, marks=[nil] * board.locations.length)
6
+ @board = board
7
+ @marks = marks
8
+ end
9
+
10
+ def available_moves
11
+ @available ||= board.locations.select{|location| marks[location].nil?}
12
+ end
13
+
14
+ def played_moves
15
+ @played_moves ||= board.locations.length - available_moves.length
16
+ end
17
+
18
+ def make_move(location, mark)
19
+ new_marks = marks.clone
20
+ new_marks[location] = mark
21
+ self.class.new(board, new_marks)
22
+ end
23
+
24
+ def when_finished(&block)
25
+ yield winner if is_finished?
26
+ end
27
+
28
+ def is_finished?
29
+ is_full? || has_winner?
30
+ end
31
+
32
+ def winner
33
+ @winner ||= find_winner
34
+ end
35
+
36
+ private
37
+ def is_full?
38
+ available_moves.empty?
39
+ end
40
+
41
+ def has_winner?
42
+ winner != nil
43
+ end
44
+
45
+ def find_winner
46
+ for line in board.lines
47
+ first_mark_in_line = nil
48
+ is_winner = true
49
+
50
+ for location in line
51
+ mark = marks[location]
52
+ first_mark_in_line ||= mark
53
+
54
+ is_winner = mark && mark == first_mark_in_line
55
+ break if !is_winner
56
+ end
57
+
58
+ return first_mark_in_line if is_winner
59
+ end
60
+
61
+ nil
62
+ end
63
+ end
64
+ end
data/runtests.sh ADDED
@@ -0,0 +1 @@
1
+ rake spec:develop
@@ -0,0 +1,16 @@
1
+ require 'timeout'
2
+ require 'test_run'
3
+
4
+ RSpec.describe 'Performance', :integration => true do
5
+ it 'runs a full 3x3 game in less than 1 second' do
6
+ Timeout::timeout(1) {
7
+ TestRun.new(3).game_winner
8
+ }
9
+ end
10
+
11
+ it 'runs a full 4x4 game in less than 10 seconds' do
12
+ Timeout::timeout(10) {
13
+ TestRun.new(4).game_winner
14
+ }
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+ require 'reproducible_random'
3
+ require 'test_run'
4
+
5
+ RSpec.describe "Properties", :properties => true do
6
+ 10.times do |n|
7
+ it 'two perfect players in a 4x4 board ends up in a draw' do
8
+ random = ReproducibleRandom.new
9
+
10
+ random.print
11
+ puts n
12
+
13
+ expect(TestRun.new(4, random).game_winner).to eq(nil)
14
+ end
15
+ end
16
+
17
+ 10.times do |n|
18
+ it 'two perfect players in a 3x3 board ends up in a draw' do
19
+ random = ReproducibleRandom.new
20
+
21
+ random.print
22
+ puts n
23
+
24
+ expect(TestRun.new(3, random).game_winner).to eq(nil)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,42 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ class RSpecTask
4
+ def initialize(task)
5
+ @task = task
6
+ end
7
+
8
+ def add_opts(opts)
9
+ @task.rspec_opts ||= ""
10
+ @task.rspec_opts += " #{opts}"
11
+ end
12
+
13
+ def include_tag(tag)
14
+ add_opts "--tag #{tag.to_s}"
15
+ end
16
+
17
+ def exclude_tag(tag)
18
+ add_opts "--tag ~#{tag.to_s}"
19
+ end
20
+
21
+ def include_tags(*tags)
22
+ tags.flatten.each {|t| include_tag t}
23
+ end
24
+
25
+ def exclude_tags(*tags)
26
+ tags.flatten.each {|t| exclude_tag t}
27
+ end
28
+
29
+ def eval(&block)
30
+ instance_eval &block
31
+ end
32
+
33
+ def method_missing(sym, *args, &block)
34
+ @task.send sym, *args, &block
35
+ end
36
+ end
37
+
38
+ def rspec_task(task_name, &block)
39
+ RSpec::Core::RakeTask.new(task_name) do |task|
40
+ RSpecTask.new(task).eval(&block)
41
+ end
42
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+ require 'reproducible_random'
3
+ require 'test_run'
4
+
5
+ RSpec.describe "Regression", :regression => true do
6
+ def game_winner(board_size, random)
7
+ TestRun.new(board_size, random).game_winner
8
+ end
9
+
10
+ it '4x4 failure with depth 4' do
11
+ sequence = [0.6626694797262954, 0.30448981659546004, 0.26798910592351544, 0.2830459900271195, 0.8895551951203527, 0.9046657862565063, 0.7899802463007816, 0.9098809266633705, 0.5677509599820714, 0.18716424973282353, 0.6105498878101738, 0.2681744677257635, 0.30826022341252124, 0.053929729073694754, 0.7765776002726397, 0.21077253937369878, 0.8753238832455099, 0.9254492923943836, 0.29696052495963243, 0.10534603754755134]
12
+ expect(game_winner 4, ReproducibleRandom.new(sequence)).to eq(nil)
13
+ end
14
+
15
+ it '4x4 failure with depth 4' do
16
+ sequence = [0.1752952614642579, 0.8821241018038767, 0.6794926188368996, 0.8518302762222564, 0.4458811493973551, 0.9306345357423046, 0.3123799482994526, 0.5924524099570939, 0.6450816940352238, 0.8825832762954303, 0.23750013882693732, 0.12379845558667857, 0.7600372531198462, 0.9208682981404592, 0.4524751171155964, 0.31049852102768605, 0.7718096983015857, 0.6281565115385799, 0.9986801091827466, 0.18577634176797098]
17
+ expect(game_winner 4, ReproducibleRandom.new(sequence)).to eq(nil)
18
+ end
19
+
20
+ it '4x4 failure with depth 6' do
21
+ sequence = [0.6137695984061624, 0.2695201125662491, 0.0008415175788497598, 0.832189486355373, 0.6430165225443919, 0.8156207919820028, 0.09507654190749693, 0.8769632565382544, 0.05710300982358729, 0.2448707731542331, 0.8227686892057651, 0.1670967918329198, 0.14015244123427695, 0.5902007740200433, 0.1892333599454875, 0.16358163954350124, 0.8366990526943789, 0.3834904620942655, 0.6036916112731134, 0.35463854154420804]
22
+ expect(game_winner 4, ReproducibleRandom.new(sequence)).to eq(nil)
23
+ end
24
+
25
+ it '3x3 failure with depth 4' do
26
+ sequence = [0.26761252406241565, 0.3290653539136804, 0.2922656712698549, 0.6264650506684584, 0.8348217536107002, 0.6663705177073628, 0.4028423873660856, 0.8250949303519487, 0.2619108550896686, 0.9881311048809777, 0.17014068693848805, 0.9661804312253627, 0.8518758632715853, 0.4047533345563624, 0.8064386913142052, 0.8619449038153968, 0.7895923865428888, 0.19974944649456394, 0.3203368624001046, 0.08129744891781843]
27
+ expect(game_winner 3, ReproducibleRandom.new(sequence)).to eq(nil)
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ class ReproducibleRandom
2
+ attr_accessor :sequence, :progress
3
+
4
+ def initialize(sequence = 20.times.map{Random.new.rand})
5
+ @sequence = sequence
6
+ @progress = sequence.cycle
7
+ end
8
+
9
+ def rand
10
+ progress.next
11
+ end
12
+
13
+ def print
14
+ puts "ReproducibleRandom sequence: #{sequence.to_s}"
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ require "codeclimate-test-reporter"
2
+ CodeClimate::TestReporter.start
3
+
4
+ RSpec.configure do |config|
5
+ config.order = :random
6
+ end
data/spec/test_run.rb ADDED
@@ -0,0 +1,19 @@
1
+ dirname = File.expand_path(File.dirname(File.dirname(__FILE__))) + "/lib"
2
+ $LOAD_PATH.unshift(dirname) unless $LOAD_PATH.include?(dirname)
3
+
4
+ require 'tictactoe/game'
5
+
6
+ class TestRun
7
+ attr_reader :board_size, :random
8
+ def initialize(board_size, random = Random.new)
9
+ @board_size = board_size
10
+ @random = random
11
+ end
12
+
13
+ def game_winner
14
+ game = Tictactoe::Game.new(board_size, :computer, :computer, random)
15
+ game.register_human_factory(nil)
16
+ game.tick() until game.is_finished?()
17
+ game.winner
18
+ end
19
+ end
@@ -0,0 +1,511 @@
1
+ require 'spec_helper'
2
+ require 'tictactoe/ai/ab_minimax'
3
+
4
+ RSpec.describe Tictactoe::Ai::ABMinimax do
5
+ def strategy(tree)
6
+ minimax = described_class.new(-1, -1, 10)
7
+ strategy = minimax.best_nodes(tree)
8
+ strategy
9
+ end
10
+
11
+ def leaf(score)
12
+ spy "leaf scored: #{score}", :is_final? => true, :score => score
13
+ end
14
+
15
+ describe 'given a leaf node' do
16
+ describe 'there is no strategy possible' do
17
+ it do
18
+ expect(strategy leaf 1)
19
+ .to eq([])
20
+ end
21
+
22
+ it do
23
+ expect(strategy leaf(-1))
24
+ .to eq([])
25
+ end
26
+
27
+ it do
28
+ expect(strategy leaf 0)
29
+ .to eq([])
30
+ end
31
+ end
32
+ end
33
+
34
+ def tree(children)
35
+ spy "tree, children: #{children.to_s}", :is_final? => false, :children => children
36
+ end
37
+
38
+ describe 'given a one-leaf one-level tree' do
39
+ it 'does not ask the root for the score' do
40
+ root = tree [leaf(1)]
41
+ strategy root
42
+ expect(root).not_to have_received(:score)
43
+ end
44
+
45
+ describe 'has that leaf as the only strategy' do
46
+ it do
47
+ leaf = leaf(1)
48
+ expect(strategy tree [leaf])
49
+ .to eq([leaf])
50
+ end
51
+
52
+ it do
53
+ leaf = leaf(-1)
54
+ expect(strategy tree [leaf])
55
+ .to eq([leaf])
56
+ end
57
+
58
+ it do
59
+ leaf = leaf(0)
60
+ expect(strategy tree [leaf])
61
+ .to eq([leaf])
62
+ end
63
+ end
64
+ end
65
+
66
+ describe 'given a multiple-leaves one-level tree' do
67
+ describe 'chooses the best leaves' do
68
+ it do
69
+ best_leaf = leaf(1)
70
+ expect(strategy tree [best_leaf, leaf(0)])
71
+ .to eq([best_leaf])
72
+ end
73
+
74
+ it do
75
+ best_leaf = leaf(0)
76
+ expect(strategy tree [best_leaf, leaf(-1)])
77
+ .to eq([best_leaf])
78
+ end
79
+
80
+ it do
81
+ best_leaf = leaf(1)
82
+ expect(strategy tree [leaf(0), best_leaf])
83
+ .to eq([best_leaf])
84
+ end
85
+
86
+ it do
87
+ best_leaf = leaf(0)
88
+ expect(strategy tree [leaf(-1), best_leaf])
89
+ .to eq([best_leaf])
90
+ end
91
+
92
+ it do
93
+ leaf1 = leaf(1)
94
+ leaf2 = leaf(1)
95
+ expect(strategy tree [leaf1, leaf2])
96
+ .to eq([leaf1, leaf2])
97
+ end
98
+
99
+ it do
100
+ leaf2 = leaf(1)
101
+ leaf3 = leaf(1)
102
+ expect(strategy tree [leaf(0), leaf2, leaf3])
103
+ .to eq([leaf2, leaf3])
104
+ end
105
+
106
+ it do
107
+ leaf1 = leaf(1)
108
+ leaf3 = leaf(1)
109
+ expect(strategy tree [leaf1, leaf(0), leaf3])
110
+ .to eq([leaf1, leaf3])
111
+ end
112
+ end
113
+ end
114
+
115
+ describe 'given a two-level one-leaf tree' do
116
+ it 'does not ask the subtree for the score' do
117
+ subtree = tree [leaf(1)]
118
+ strategy tree [subtree]
119
+ expect(subtree).not_to have_received(:score)
120
+ end
121
+
122
+ it do
123
+ subtree = tree [leaf(1)]
124
+ expect(strategy tree [subtree])
125
+ .to eq([subtree])
126
+ end
127
+ end
128
+
129
+ describe 'given complex tree' do
130
+ describe 'chooses the best option even if it is immediate' do
131
+ it do
132
+ best_option = leaf(1)
133
+ root = tree([
134
+ #my choice
135
+ best_option,
136
+ tree([
137
+ #other's choice
138
+ leaf(0)
139
+ ]),
140
+ ])
141
+
142
+ expect(strategy root)
143
+ .to eq([best_option])
144
+ end
145
+
146
+ it do
147
+ best_option = leaf(1)
148
+ root = tree([
149
+ #my choice
150
+ tree([
151
+ #other's choice
152
+ leaf(0)
153
+ ]),
154
+ best_option,
155
+ ])
156
+
157
+ expect(strategy root)
158
+ .to eq([best_option])
159
+ end
160
+ end
161
+
162
+ describe 'chooses the best option even if it is one-level deep' do
163
+ it do
164
+ best_option = tree([
165
+ #other's choice
166
+ leaf(1)
167
+ ])
168
+ root = tree([
169
+ #my choice
170
+ leaf(0),
171
+ best_option,
172
+ ])
173
+
174
+ expect(strategy root)
175
+ .to eq([best_option])
176
+ end
177
+
178
+ it do
179
+ best_option = tree([
180
+ #other's choice
181
+ leaf(1)
182
+ ])
183
+ root = tree([
184
+ #my choice
185
+ best_option,
186
+ leaf(0),
187
+ ])
188
+
189
+ expect(strategy root)
190
+ .to eq([best_option])
191
+ end
192
+ end
193
+
194
+ describe 'chooses equivalent options even if they are at different levels' do
195
+ it do
196
+ option1 = tree([
197
+ #other's choice
198
+ leaf(1)
199
+ ])
200
+ option2 = leaf(1)
201
+ root = tree([
202
+ #my choice
203
+ option1,
204
+ option2,
205
+ ])
206
+
207
+ expect(strategy root)
208
+ .to eq([option1, option2])
209
+ end
210
+
211
+ it do
212
+ option1 = tree([
213
+ #other's choice
214
+ leaf(0)
215
+ ])
216
+ option2 = leaf(0)
217
+ root = tree([
218
+ #my choice
219
+ option1,
220
+ option2,
221
+ ])
222
+
223
+ expect(strategy root)
224
+ .to eq([option1, option2])
225
+ end
226
+ end
227
+
228
+ describe 'assumes the opponent chooses the worst option' do
229
+ it do
230
+ best_option = leaf(0)
231
+ root = tree([
232
+ #my choice
233
+ best_option,
234
+ tree([
235
+ #other's choice
236
+ leaf(1),
237
+ leaf(-1),
238
+ ]),
239
+ ])
240
+
241
+ expect(strategy root)
242
+ .to eq([best_option])
243
+ end
244
+
245
+ it do
246
+ best_option = tree([
247
+ #other's choice
248
+ leaf(0),
249
+ leaf(1),
250
+ ])
251
+ root = tree([
252
+ #my choice
253
+ leaf(-1),
254
+ best_option,
255
+ ])
256
+
257
+ expect(strategy root)
258
+ .to eq([best_option])
259
+ end
260
+
261
+ it do
262
+ best_option = leaf(0)
263
+ root = tree([
264
+ #my choice
265
+ best_option,
266
+ tree([
267
+ #other's choice
268
+ leaf(1),
269
+ leaf(1),
270
+ leaf(-1),
271
+ ]),
272
+ ])
273
+
274
+ expect(strategy root)
275
+ .to eq([best_option])
276
+ end
277
+ end
278
+
279
+ describe 'stops evaluating when the branch is going to be discarded by my decision' do
280
+ it do
281
+ not_evaluated_node = leaf(-1)
282
+ root = tree([
283
+ #my choice
284
+ leaf(1),
285
+ tree([
286
+ #other's choice
287
+ leaf(0),
288
+ not_evaluated_node,
289
+ ]),
290
+ ])
291
+
292
+ strategy root
293
+ expect(not_evaluated_node).not_to have_received(:score)
294
+ end
295
+
296
+ it do
297
+ not_evaluated_node = leaf(1)
298
+ root = tree([
299
+ #my choice
300
+ leaf(0),
301
+ tree([
302
+ #other's choice
303
+ leaf(-1),
304
+ not_evaluated_node,
305
+ ]),
306
+ ])
307
+
308
+ strategy root
309
+ expect(not_evaluated_node).not_to have_received(:score)
310
+ end
311
+
312
+ it do
313
+ evaluated_node = leaf(-1)
314
+ root = tree([
315
+ #my choice
316
+ leaf(1),
317
+ tree([
318
+ #other's choice
319
+ leaf(1),
320
+ evaluated_node,
321
+ ]),
322
+ ])
323
+
324
+ strategy root
325
+ expect(evaluated_node).to have_received(:score)
326
+ end
327
+ end
328
+
329
+ describe 'stops evaluating when the branch is going to be discarded by the opponents decision' do
330
+ it do
331
+ not_evaluated_node = leaf(-1)
332
+ root = tree([
333
+ #my choice
334
+ tree([
335
+ #other's choice
336
+ leaf(-1),
337
+ tree([
338
+ #my choice
339
+ leaf(0),
340
+ not_evaluated_node,
341
+ ]),
342
+ ]),
343
+ ])
344
+
345
+ strategy root
346
+ expect(not_evaluated_node).not_to have_received(:score)
347
+ end
348
+ end
349
+
350
+ describe 'stops evaluating when the opponent has the possibility to choose the minimum score' do
351
+ it do
352
+ not_evaluated_node = leaf(0)
353
+ root = tree([
354
+ #my choice
355
+ leaf(-1),
356
+ tree([
357
+ #other's choice
358
+ leaf(-1),
359
+ not_evaluated_node,
360
+ ]),
361
+ ])
362
+
363
+ strategy root
364
+ expect(not_evaluated_node).not_to have_received(:score)
365
+ end
366
+ end
367
+
368
+ describe 'given tree-level deep tree with only maximizing choices' do
369
+ it 'goes in depth until the leaves' do
370
+ last_leaf = leaf(-1)
371
+ root = tree([
372
+ # my choice
373
+ leaf(0),
374
+ tree([
375
+ #other's choice
376
+ tree([
377
+ #my choice
378
+ leaf(1),
379
+ last_leaf,
380
+ ])
381
+ ]),
382
+ ])
383
+
384
+ strategy root
385
+ expect(last_leaf).to have_received(:score)
386
+ end
387
+
388
+ it 'and a better option at the last level, chooses it' do
389
+ best_option = tree([
390
+ #other's choice
391
+ tree([
392
+ #my choice
393
+ leaf(1)
394
+ ])
395
+ ])
396
+
397
+ root = tree([
398
+ #my choice
399
+ leaf(-1),
400
+ best_option,
401
+ ])
402
+
403
+ expect(strategy root).to eq([best_option])
404
+ end
405
+
406
+ it 'assumes the opponent is going to minimize my winnings' do
407
+ best_option = leaf(0)
408
+
409
+ root = tree([
410
+ #my choice
411
+ tree([
412
+ #other's choice
413
+ tree([
414
+ #my choice
415
+ leaf(-1),
416
+ ]),
417
+ tree([
418
+ #my choice
419
+ leaf(1),
420
+ ])
421
+ ]),
422
+ best_option,
423
+ ])
424
+
425
+ expect(strategy root).to eq([best_option])
426
+ end
427
+ end
428
+
429
+ describe 'given a depth limit' do
430
+ describe 'supposes that the deeper trees have the minimum score possible' do
431
+ it do
432
+ evaluated_leaf = leaf(-1)
433
+ not_evaluated_leaf = leaf(1)
434
+ options_perceived_as_equivalent = [
435
+ #depth 0
436
+ evaluated_leaf,
437
+ tree([
438
+ #depth 1
439
+ not_evaluated_leaf
440
+ ])
441
+ ]
442
+
443
+ root = tree(options_perceived_as_equivalent)
444
+
445
+ minimax = described_class.new(-1, -1, 0)
446
+ expect(minimax.best_nodes root).to eq(options_perceived_as_equivalent)
447
+ expect(evaluated_leaf).to have_received(:score)
448
+ expect(not_evaluated_leaf).not_to have_received(:score)
449
+ end
450
+
451
+ it do
452
+ evaluated_leaf = leaf(0)
453
+ not_evaluated_leaf = leaf(1)
454
+ option_perceived_as_best = tree([
455
+ #depth 1
456
+ evaluated_leaf,
457
+ ])
458
+
459
+ root = tree([
460
+ #depth 0
461
+ option_perceived_as_best,
462
+ tree([
463
+ #depth 1
464
+ tree([
465
+ #depth 2
466
+ not_evaluated_leaf
467
+ ])
468
+ ])
469
+ ])
470
+
471
+ minimax = described_class.new(-1, -1, 1)
472
+ expect(minimax.best_nodes root).to eq([option_perceived_as_best])
473
+ expect(evaluated_leaf).to have_received(:score)
474
+ expect(not_evaluated_leaf).not_to have_received(:score)
475
+ end
476
+
477
+ it do
478
+ evaluated_leaf = leaf(0)
479
+ not_evaluated_leaf = leaf(1)
480
+ option_perceived_as_best = tree([
481
+ #depth 1
482
+ tree([
483
+ #depth 2
484
+ evaluated_leaf,
485
+ ]),
486
+ ])
487
+
488
+ root = tree([
489
+ #depth 0
490
+ option_perceived_as_best,
491
+ tree([
492
+ #depth 1
493
+ tree([
494
+ #depth 2
495
+ tree([
496
+ #depth 3
497
+ not_evaluated_leaf
498
+ ])
499
+ ])
500
+ ])
501
+ ])
502
+
503
+ minimax = described_class.new(-1, -1, 2)
504
+ expect(minimax.best_nodes root).to eq([option_perceived_as_best])
505
+ expect(evaluated_leaf).to have_received(:score)
506
+ expect(not_evaluated_leaf).not_to have_received(:score)
507
+ end
508
+ end
509
+ end
510
+ end
511
+ end