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