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
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://travis-ci.org/demonh3x/tictactoe-core.rb.svg?branch=master) [![Code Climate](https://codeclimate.com/github/demonh3x/tictactoe.rb/badges/gpa.svg)](https://codeclimate.com/github/demonh3x/tictactoe.rb) [![Test Coverage](https://codeclimate.com/github/demonh3x/tictactoe.rb/badges/coverage.svg)](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
|