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.
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