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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0c6951ba1348109e58534d413b08206c18b0fa94
|
4
|
+
data.tar.gz: 8c33ce5f64a2e0215f41f6c682d8773509002e19
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 31b53621e6afa8e9aaf7f2bcd57d667ac10056f7c1d5944a6359e646a9e7a9404b0af2b11dbb23db04040a4caf17d9c3080eb252cd4c1552259f2098d0b864f5
|
7
|
+
data.tar.gz: a275b3c67c86b11195fe1e86baf70c4fdbf944c036a67e61b6976e32c0a9ae5e3bf2e6adbb14274718c83490e8a6e1c1f1830964af669c6ff9a9242f11285f7c
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,2 @@
|
|
1
|
+
# tictactoe-core.rb
|
2
|
+
 [](https://codeclimate.com/github/demonh3x/tictactoe.rb) [](https://codeclimate.com/github/demonh3x/tictactoe.rb)
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
task :default => ["spec:develop"]
|
2
|
+
|
3
|
+
namespace :spec do
|
4
|
+
require './spec/rake_rspec'
|
5
|
+
|
6
|
+
rspec_task(:unit) do
|
7
|
+
exclude_tags :integration, :regression, :properties
|
8
|
+
end
|
9
|
+
|
10
|
+
rspec_task(:develop) do
|
11
|
+
exclude_tags :regression, :properties
|
12
|
+
end
|
13
|
+
|
14
|
+
rspec_task(:ci) do
|
15
|
+
add_opts "--color -fd"
|
16
|
+
exclude_tags :properties
|
17
|
+
end
|
18
|
+
|
19
|
+
rspec_task(:properties) do
|
20
|
+
include_tags :properties
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
task :profile do
|
25
|
+
require 'ruby-prof'
|
26
|
+
require './spec/test_run'
|
27
|
+
|
28
|
+
RubyProf.start
|
29
|
+
|
30
|
+
TestRun.new(4).game_winner
|
31
|
+
|
32
|
+
result = RubyProf.stop
|
33
|
+
RubyProf::GraphHtmlPrinter.new(result).print(STDOUT)
|
34
|
+
end
|
data/lib/tictactoe.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'tictactoe/game'
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Tictactoe
|
2
|
+
module Ai
|
3
|
+
class ABMinimax
|
4
|
+
def initialize(min_score, heuristic_score, depth_limit)
|
5
|
+
@min_score_possible = min_score
|
6
|
+
@heuristic_score = heuristic_score
|
7
|
+
@depth_limit = depth_limit
|
8
|
+
end
|
9
|
+
|
10
|
+
def best_nodes(tree)
|
11
|
+
most_beneficial_strategy(tree, nil, depth_limit)[:nodes]
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
attr_reader :min_score_possible, :heuristic_score, :depth_limit
|
16
|
+
|
17
|
+
def most_beneficial_strategy(tree, previous_most_damaging_score, depth)
|
18
|
+
my_best_score = min_score_possible
|
19
|
+
best_nodes = []
|
20
|
+
|
21
|
+
tree.children.each do |child|
|
22
|
+
if child.is_final?
|
23
|
+
score = child.score
|
24
|
+
elsif depth == 0
|
25
|
+
score = heuristic_score
|
26
|
+
else
|
27
|
+
score = most_damaging_score(child, my_best_score, depth-1)
|
28
|
+
end
|
29
|
+
|
30
|
+
my_best_score ||= score
|
31
|
+
|
32
|
+
if score == my_best_score
|
33
|
+
best_nodes << child
|
34
|
+
end
|
35
|
+
|
36
|
+
if score > my_best_score
|
37
|
+
my_best_score = score
|
38
|
+
best_nodes = [child]
|
39
|
+
end
|
40
|
+
|
41
|
+
break if previous_most_damaging_score && previous_most_damaging_score <= score
|
42
|
+
end
|
43
|
+
|
44
|
+
return {
|
45
|
+
:score => my_best_score,
|
46
|
+
:nodes => best_nodes
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def most_damaging_score(child, my_best_score, depth)
|
51
|
+
most_damaging_score = nil
|
52
|
+
|
53
|
+
child.children.each do |grandchild|
|
54
|
+
if grandchild.is_final?
|
55
|
+
minimizing_score = grandchild.score
|
56
|
+
elsif depth == 0
|
57
|
+
minimizing_score = heuristic_score
|
58
|
+
else
|
59
|
+
res = most_beneficial_strategy(grandchild, most_damaging_score, depth-1)
|
60
|
+
minimizing_score = res[:score]
|
61
|
+
end
|
62
|
+
|
63
|
+
most_damaging_score ||= minimizing_score
|
64
|
+
|
65
|
+
if minimizing_score < most_damaging_score
|
66
|
+
most_damaging_score = minimizing_score
|
67
|
+
end
|
68
|
+
|
69
|
+
it_cannot_be_worse = min_score_possible && most_damaging_score == min_score_possible
|
70
|
+
there_is_a_better_option = my_best_score && most_damaging_score < my_best_score
|
71
|
+
break if it_cannot_be_worse || there_is_a_better_option
|
72
|
+
end
|
73
|
+
|
74
|
+
most_damaging_score
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Tictactoe
|
2
|
+
module Ai
|
3
|
+
class ABNegamax
|
4
|
+
COLOR_SELF = 1
|
5
|
+
COLOR_OPPONENT = -1
|
6
|
+
|
7
|
+
def initialize(depth_limit, depth_reached_score)
|
8
|
+
@depth_limit = depth_limit
|
9
|
+
@depth_reached_score = depth_reached_score
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :depth_reached_score, :depth_limit
|
13
|
+
|
14
|
+
def score(tree)
|
15
|
+
negamax(tree)[:score]
|
16
|
+
end
|
17
|
+
|
18
|
+
def best_nodes(tree)
|
19
|
+
negamax(tree)[:nodes]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def negamax(node, depth = depth_limit, a = -1000, b = 1000, color = COLOR_SELF)
|
24
|
+
return {:score => color * node.score, :nodes => []} if node.is_final?
|
25
|
+
return {:score => color * depth_reached_score, :nodes => node.children} if depth == 0
|
26
|
+
|
27
|
+
best_score = -1000
|
28
|
+
best_nodes = []
|
29
|
+
node.children.each do |child|
|
30
|
+
score = -negamax(child, depth-1, -b, -a, -color)[:score]
|
31
|
+
if score > best_score
|
32
|
+
best_score = score
|
33
|
+
best_nodes = [child]
|
34
|
+
elsif score == best_score
|
35
|
+
best_nodes << child
|
36
|
+
end
|
37
|
+
a = [a, score].max
|
38
|
+
break if a > b
|
39
|
+
end
|
40
|
+
|
41
|
+
{:score => best_score, :nodes => best_nodes}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'tictactoe/ai/ab_minimax'
|
2
|
+
require 'tictactoe/ai/tree'
|
3
|
+
|
4
|
+
module Tictactoe
|
5
|
+
module Ai
|
6
|
+
class PerfectIntelligence
|
7
|
+
SCORE_FOR_UNKNOWN_FUTURE = -1
|
8
|
+
|
9
|
+
def desired_moves(state, player)
|
10
|
+
find_best_moves(state, player)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def find_best_moves(state, player)
|
15
|
+
depth = dynamic_depth_for(state)
|
16
|
+
root = Tree.new(state, player)
|
17
|
+
ai = ABMinimax.new(-1000, SCORE_FOR_UNKNOWN_FUTURE, depth)
|
18
|
+
ai.best_nodes(root).map(&:move)
|
19
|
+
end
|
20
|
+
|
21
|
+
def dynamic_depth_for(state)
|
22
|
+
is_4_by_4 = state.board.locations.length == 16
|
23
|
+
if is_4_by_4
|
24
|
+
initial_depth_to_stay_out_of_trouble = 0
|
25
|
+
minimum_depth_to_avoid_lethal_moves = 7
|
26
|
+
else
|
27
|
+
initial_depth_to_stay_out_of_trouble = 4
|
28
|
+
minimum_depth_to_avoid_lethal_moves = 5
|
29
|
+
end
|
30
|
+
|
31
|
+
[minimum_depth_to_avoid_lethal_moves, state.played_moves + initial_depth_to_stay_out_of_trouble].min
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Tictactoe
|
2
|
+
module Ai
|
3
|
+
class RandomChooser
|
4
|
+
def initialize(random)
|
5
|
+
@random = random
|
6
|
+
end
|
7
|
+
|
8
|
+
def choose_one(list)
|
9
|
+
index = bounded_random list.size
|
10
|
+
list[index]
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
attr_accessor :random
|
15
|
+
|
16
|
+
def bounded_random(outer_bound)
|
17
|
+
(random.rand * outer_bound).to_i
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Tictactoe
|
2
|
+
module Ai
|
3
|
+
class Tree
|
4
|
+
MAXIMUM_SCORE = 2
|
5
|
+
MINIMUM_SCORE = -2
|
6
|
+
NEUTRAL_SCORE = 0
|
7
|
+
|
8
|
+
def initialize(state, mark, current_mark = mark, move_to_arrive_here=nil)
|
9
|
+
@state = state
|
10
|
+
@mark = mark
|
11
|
+
@current_mark = current_mark
|
12
|
+
@move = move_to_arrive_here
|
13
|
+
end
|
14
|
+
attr_reader :state, :mark, :current_mark, :move
|
15
|
+
|
16
|
+
def is_final?
|
17
|
+
state.is_finished?
|
18
|
+
end
|
19
|
+
|
20
|
+
def children
|
21
|
+
state.available_moves.map do |move|
|
22
|
+
next_state = state.make_move(move, current_mark.value)
|
23
|
+
Tree.new(next_state, mark, current_mark.next, move)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def score
|
28
|
+
height = state.available_moves.length + 1
|
29
|
+
base_score * height
|
30
|
+
end
|
31
|
+
|
32
|
+
def base_score
|
33
|
+
case state.winner
|
34
|
+
when mark.value
|
35
|
+
MAXIMUM_SCORE
|
36
|
+
when nil
|
37
|
+
NEUTRAL_SCORE
|
38
|
+
else
|
39
|
+
MINIMUM_SCORE
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'tictactoe/boards/three_by_three_board'
|
2
|
+
require 'tictactoe/boards/four_by_four_board'
|
3
|
+
|
4
|
+
module Tictactoe
|
5
|
+
module Boards
|
6
|
+
class BoardTypeFactory
|
7
|
+
def create(side_size)
|
8
|
+
case side_size
|
9
|
+
when 3 then ThreeByThreeBoard.new
|
10
|
+
when 4 then FourByFourBoard.new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Tictactoe
|
2
|
+
module Boards
|
3
|
+
class FourByFourBoard
|
4
|
+
LOCATIONS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
|
5
|
+
|
6
|
+
HORIZONTAL_LINES = [
|
7
|
+
[0, 1, 2, 3],
|
8
|
+
[4, 5, 6, 7],
|
9
|
+
[8, 9, 10, 11],
|
10
|
+
[12, 13, 14, 15],
|
11
|
+
]
|
12
|
+
VERTICAL_LINES = HORIZONTAL_LINES.transpose
|
13
|
+
DIAGONALS = [
|
14
|
+
[0, 5, 10, 15],
|
15
|
+
[3, 6, 9, 12],
|
16
|
+
]
|
17
|
+
LINES = HORIZONTAL_LINES + VERTICAL_LINES + DIAGONALS
|
18
|
+
|
19
|
+
def locations
|
20
|
+
LOCATIONS
|
21
|
+
end
|
22
|
+
|
23
|
+
def lines
|
24
|
+
LINES
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Tictactoe
|
2
|
+
module Boards
|
3
|
+
class ThreeByThreeBoard
|
4
|
+
LOCATIONS = [0, 1, 2, 3, 4, 5, 6, 7, 8]
|
5
|
+
|
6
|
+
HORIZONTAL_LINES = [
|
7
|
+
[0, 1, 2],
|
8
|
+
[3, 4, 5],
|
9
|
+
[6, 7, 8],
|
10
|
+
]
|
11
|
+
VERTICAL_LINES = HORIZONTAL_LINES.transpose
|
12
|
+
DIAGONALS = [
|
13
|
+
[0, 4, 8],
|
14
|
+
[2, 4, 6],
|
15
|
+
]
|
16
|
+
LINES = HORIZONTAL_LINES + VERTICAL_LINES + DIAGONALS
|
17
|
+
|
18
|
+
def locations
|
19
|
+
LOCATIONS
|
20
|
+
end
|
21
|
+
|
22
|
+
def lines
|
23
|
+
LINES
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'tictactoe/state'
|
2
|
+
require 'tictactoe/sequence'
|
3
|
+
require 'tictactoe/boards/board_type_factory'
|
4
|
+
require 'tictactoe/ai/perfect_intelligence'
|
5
|
+
require 'tictactoe/ai/random_chooser'
|
6
|
+
|
7
|
+
require 'tictactoe/players/factory'
|
8
|
+
require 'tictactoe/players/computer'
|
9
|
+
|
10
|
+
module Tictactoe
|
11
|
+
class Game
|
12
|
+
attr_accessor :board_size, :x_type, :o_type, :random
|
13
|
+
attr_accessor :current_player, :state
|
14
|
+
|
15
|
+
def initialize(board_size, x_type, o_type, random=Random.new)
|
16
|
+
@board_size = board_size
|
17
|
+
@x_type = x_type
|
18
|
+
@o_type = o_type
|
19
|
+
@random = random
|
20
|
+
end
|
21
|
+
|
22
|
+
def register_human_factory(factory)
|
23
|
+
players_factory.register(:human, factory)
|
24
|
+
reset
|
25
|
+
end
|
26
|
+
|
27
|
+
def tick
|
28
|
+
move = get_move
|
29
|
+
if is_valid?(move) && !is_finished?
|
30
|
+
update_state(move)
|
31
|
+
advance_player
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def is_finished?
|
36
|
+
state.is_finished?
|
37
|
+
end
|
38
|
+
|
39
|
+
def winner
|
40
|
+
state.winner
|
41
|
+
end
|
42
|
+
|
43
|
+
def marks
|
44
|
+
state.marks
|
45
|
+
end
|
46
|
+
|
47
|
+
def available
|
48
|
+
state.available_moves
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def is_valid?(move)
|
53
|
+
move && state.available_moves.include?(move)
|
54
|
+
end
|
55
|
+
|
56
|
+
def update_state(move)
|
57
|
+
self.state = state.make_move(move, current_player.value.mark.value)
|
58
|
+
end
|
59
|
+
|
60
|
+
def advance_player
|
61
|
+
self.current_player = current_player.next
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_move
|
65
|
+
current_player.value.get_move(state)
|
66
|
+
end
|
67
|
+
|
68
|
+
def reset
|
69
|
+
reset_players
|
70
|
+
reset_state
|
71
|
+
end
|
72
|
+
|
73
|
+
def reset_players
|
74
|
+
first_mark = Sequence.new([:x, :o]).first
|
75
|
+
|
76
|
+
x_player = players_factory.create(x_type, first_mark)
|
77
|
+
o_player = players_factory.create(o_type, first_mark.next)
|
78
|
+
|
79
|
+
self.current_player = Sequence.new([x_player, o_player]).first
|
80
|
+
end
|
81
|
+
|
82
|
+
def players_factory
|
83
|
+
@players_factory ||= create_players_factory
|
84
|
+
end
|
85
|
+
|
86
|
+
def create_players_factory
|
87
|
+
factory = Players::Factory.new()
|
88
|
+
factory.register(:computer, computer_factory)
|
89
|
+
factory
|
90
|
+
end
|
91
|
+
|
92
|
+
def computer_factory
|
93
|
+
intelligence = Ai::PerfectIntelligence.new
|
94
|
+
chooser = Ai::RandomChooser.new(random)
|
95
|
+
lambda {|mark| Players::Computer.new(mark, intelligence, chooser)}
|
96
|
+
end
|
97
|
+
|
98
|
+
def reset_state
|
99
|
+
self.state = State.new(Boards::BoardTypeFactory.new.create(board_size))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|