tictactoe-core 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|