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
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
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - "2.2.0"
4
+ - "2.1.0"
5
+ - "2.0.0-p598"
6
+ script: "rake spec:ci"
7
+ addons:
8
+ code_climate:
9
+ repo_token: f7b977ac9c2d1d7678f076881c00b89610e14a86dee04df3066081ff6fd6b8dc
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+ gem "rspec" , "~> 3.1.0", :group => :test
3
+ gem "codeclimate-test-reporter", :group => :test, :require => nil
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