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,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
|
data/spec/rake_rspec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
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
|