xo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f4e9916b4a489801329e2b8713a883a06ebef55f
4
+ data.tar.gz: 83ea35a8950526c5ab4a10587ac151ce3523f6d6
5
+ SHA512:
6
+ metadata.gz: ff7f19ed461e17028efaeda45010b6faf967103640d004b80da343f179dd4dbf307577aeebfb38bcfeda61f33c01b45657ea5ceadd911c7ca134496d6ff532b5
7
+ data.tar.gz: e31c7c082b239cd9e3efc10edb36e9e92197e383b4da15f84f8a9d6520fa55c72ccbaa743d3e0aa1fe224f4260bf2c8e4327b57d564eaa1aea8148dd310aa06b
@@ -0,0 +1 @@
1
+ *.gem
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Dwayne R. Crooks
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,55 @@
1
+ # xo
2
+
3
+ A [Ruby](http://www.ruby-lang.org/en/) library for [Tic-tac-toe](http://en.wikipedia.org/wiki/Tic-tac-toe).
4
+
5
+ # Performance of the Minimax Algorithm
6
+
7
+ ```ruby
8
+ require 'benchmark'
9
+ require 'xo'
10
+
11
+ # Empty grid
12
+ g = XO::Grid.new
13
+
14
+ puts Benchmark.measure { XO::AI.minimax(g, :x) }
15
+ # => 0.000000 0.000000 0.000000 ( 0.000463)
16
+ # => O(1) time due to caching
17
+
18
+ # One spot taken
19
+ g[1, 1] = :x
20
+
21
+ puts Benchmark.measure { XO::AI.minimax(g, :o) }
22
+ # => 0.000000 0.000000 0.000000 ( 0.000216)
23
+ # => O(1) time due to caching
24
+
25
+ # Two spots taken
26
+ g[1, 3] = :o
27
+
28
+ puts Benchmark.measure { XO::AI.minimax(g, :x) }
29
+ # => 0.690000 0.000000 0.690000 ( 0.695095)
30
+ # => Worst-case time, performance only improves from here on as the grid gets filled
31
+ ```
32
+
33
+ # Testing
34
+
35
+ You can run:
36
+
37
+ - All tests: `rake test`
38
+ - One test: `ruby -Ilib -Ispec spec/path_to_spec_file.rb`
39
+
40
+ # TODO
41
+
42
+ 1. Write documentation.
43
+ 2. Show example usage.
44
+ 3. Write an example Tic-tac-toe command-line game client.
45
+
46
+ # Contributing
47
+
48
+ If you'd like to contribute a feature or bugfix: Thanks! To make sure your fix/feature has a high chance of being included, please read the following guidelines:
49
+
50
+ 1. Post a [pull request](https://github.com/dwayne/xo/compare/).
51
+ 2. Make sure there are tests! I will not accept any patch that is not tested. It's a rare time when explicit tests aren't needed. If you have questions about writing tests for xo, please open a [GitHub issue](https://github.com/dwayne/xo/issues/new).
52
+
53
+ # License
54
+
55
+ xo is Copyright © 2014 Dwayne R. Crooks. It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.
@@ -0,0 +1,7 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.pattern = 'spec/**/*_spec.rb'
6
+ t.libs.push 'spec'
7
+ end
@@ -0,0 +1,25 @@
1
+ module XO
2
+
3
+ X = :x
4
+ O = :o
5
+
6
+ def self.is_token?(val)
7
+ [X, O].include?(val)
8
+ end
9
+
10
+ def self.other_token(token)
11
+ token == X ? O : (token == O ? X : token)
12
+ end
13
+
14
+ class << self
15
+ alias_method :is_player?, :is_token?
16
+ alias_method :other_player, :other_token
17
+ end
18
+
19
+ class Position < Struct.new(:row, :column); end
20
+ end
21
+
22
+ require 'xo/grid'
23
+ require 'xo/evaluator'
24
+ require 'xo/engine'
25
+ require 'xo/ai'
@@ -0,0 +1,4 @@
1
+ require 'xo/ai/minimax'
2
+ require 'xo/ai/expert'
3
+ require 'xo/ai/novice'
4
+ require 'xo/ai/advanced_beginner'
@@ -0,0 +1,17 @@
1
+ require 'xo/ai/expert'
2
+
3
+ module XO::AI
4
+
5
+ class AdvancedBeginner < Expert
6
+
7
+ def self.get_moves(grid, player)
8
+ smart_moves = super(grid, player)
9
+ dumb_moves = all_moves - smart_moves
10
+
11
+ # if there are no dumb moves then we have no choice but to make a smart move
12
+ # otherwise, 75% of the time we'll make a smart move and the other 25% of the
13
+ # time we'll make a dumb move
14
+ dumb_moves.empty? ? smart_moves : (rand < 0.75 ? smart_moves : dumb_moves)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,64 @@
1
+ require 'xo/grid'
2
+ require 'xo/evaluator'
3
+ require 'xo/ai'
4
+
5
+ module XO::AI
6
+
7
+ class Expert
8
+
9
+ def self.suggest_moves(grid, player)
10
+ result = XO::Evaluator.analyze(grid, player)
11
+
12
+ case result[:status]
13
+ when :ok
14
+ get_moves(grid, player)
15
+ when :game_over
16
+ []
17
+ else
18
+ raise IllegalGridStatusError
19
+ end
20
+ end
21
+
22
+ def self.get_moves(grid, player)
23
+ if moves = MOVES_CACHE[grid] || MOVES_CACHE[invert_grid(grid)]
24
+ moves.map { |pos| XO::Position.new(*pos) }
25
+ else
26
+ XO::AI.minimax(grid, player).moves
27
+ end
28
+ end
29
+
30
+ def self.all_moves(grid)
31
+ grid.enum_for(:each_free).map { |r, c| XO::Position.new(r, c) }
32
+ end
33
+
34
+ private
35
+
36
+ def self.invert_grid(grid)
37
+ (new_grid = grid.dup).each do |r, c, val|
38
+ new_grid[r, c] = XO::other_token(val)
39
+ end
40
+ end
41
+
42
+ def self.one_x_grid(r, c)
43
+ XO::Grid.new.tap do |grid|
44
+ grid[r, c] = :x
45
+ end
46
+ end
47
+
48
+ MOVES_CACHE = {
49
+ XO::Grid.new => [[1, 1], [1, 2], [1, 3], [2, 1], [2, 2], [2, 3], [3, 1], [3, 2], [3, 3]],
50
+
51
+ one_x_grid(1, 1) => [[2, 2]],
52
+ one_x_grid(1, 3) => [[2, 2]],
53
+ one_x_grid(3, 1) => [[2, 2]],
54
+ one_x_grid(3, 3) => [[2, 2]],
55
+
56
+ one_x_grid(1, 2) => [[1, 1], [1, 3], [2, 2], [3, 2]],
57
+ one_x_grid(2, 1) => [[1, 1], [2, 2], [2, 3], [3, 1]],
58
+ one_x_grid(2, 3) => [[1, 3], [2, 1], [2, 2], [3, 3]],
59
+ one_x_grid(3, 2) => [[1, 2], [2, 2], [3, 1], [3, 3]],
60
+
61
+ one_x_grid(2, 2) => [[1, 1], [1, 3], [3, 1], [3, 3]]
62
+ }
63
+ end
64
+ end
@@ -0,0 +1,125 @@
1
+ require 'ostruct'
2
+ require 'xo/evaluator'
3
+
4
+ module XO::AI
5
+
6
+ def self.minimax(grid, player)
7
+ state = MaxGameState.new(grid, player)
8
+ moves = state.next_states.select { |next_state| state.score == next_state.score }.map(&:move)
9
+
10
+ OpenStruct.new(start_state: state, moves: moves)
11
+ end
12
+
13
+ class GameState
14
+
15
+ attr_reader :grid, :player, :move, :next_states
16
+
17
+ def initialize(grid, player, move = nil)
18
+ @grid = grid.dup
19
+ @player = player
20
+ @move = move
21
+
22
+ generate_next_states
23
+ end
24
+
25
+ def result
26
+ @result ||= XO::Evaluator.analyze(grid, player)
27
+ end
28
+
29
+ def is_terminal?
30
+ case result[:status]
31
+ when :ok
32
+ false
33
+ when :game_over
34
+ true
35
+ else
36
+ raise IllegalGridStatusError
37
+ end
38
+ end
39
+
40
+ def scores
41
+ next_states.map(&:score)
42
+ end
43
+
44
+ def score
45
+ if is_terminal?
46
+ terminal_score
47
+ else
48
+ non_terminal_score
49
+ end
50
+ end
51
+
52
+ def terminal_score
53
+ raise NotImplementedError
54
+ end
55
+
56
+ def non_terminal_score
57
+ raise NotImplementedError
58
+ end
59
+
60
+ def next_game_state(next_grid, other_player, move)
61
+ raise NotImplementedError
62
+ end
63
+
64
+ private
65
+
66
+ def generate_next_states
67
+ @next_states = []
68
+
69
+ unless is_terminal?
70
+ grid.each_free do |r, c|
71
+ next_grid = grid.dup
72
+ next_grid[r, c] = player
73
+
74
+ @next_states << next_game_state(next_grid, XO.other_player(player), XO::Position.new(r, c))
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ class MaxGameState < GameState
81
+
82
+ def next_game_state(next_grid, other_player, move)
83
+ MinGameState.new(next_grid, other_player, move)
84
+ end
85
+
86
+ def terminal_score
87
+ case result[:type]
88
+ when :winner
89
+ 1
90
+ when :loser
91
+ -1
92
+ when :squashed
93
+ 0
94
+ end
95
+ end
96
+
97
+ def non_terminal_score
98
+ scores.max
99
+ end
100
+ end
101
+
102
+ class MinGameState < GameState
103
+
104
+ def next_game_state(next_grid, other_player, move)
105
+ MaxGameState.new(next_grid, other_player, move)
106
+ end
107
+
108
+ def terminal_score
109
+ case result[:type]
110
+ when :winner
111
+ -1
112
+ when :loser
113
+ 1
114
+ when :squashed
115
+ 0
116
+ end
117
+ end
118
+
119
+ def non_terminal_score
120
+ scores.min
121
+ end
122
+ end
123
+
124
+ class IllegalGridStatusError < StandardError; end
125
+ end
@@ -0,0 +1,11 @@
1
+ require 'xo/ai/expert'
2
+
3
+ module XO::AI
4
+
5
+ class Novice < Expert
6
+
7
+ def self.get_moves(grid, player)
8
+ all_moves
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,137 @@
1
+ require 'observer'
2
+ require 'ostruct'
3
+
4
+ require 'xo/grid'
5
+ require 'xo/evaluator'
6
+
7
+ module XO
8
+
9
+ class Engine
10
+ include Observable
11
+
12
+ attr_reader :turn, :state
13
+
14
+ def initialize
15
+ @grid = Grid.new
16
+ @turn = :nobody
17
+ @state = :idle
18
+ end
19
+
20
+ def grid
21
+ @grid.dup
22
+ end
23
+
24
+ def next_turn
25
+ XO.other_player(turn)
26
+ end
27
+
28
+ def start(player)
29
+ raise ArgumentError, "unknown player #{player}" unless XO.is_player?(player)
30
+
31
+ case state
32
+ when :idle
33
+ handle_start(player)
34
+ else
35
+ raise NotImplementedError
36
+ end
37
+
38
+ self
39
+ end
40
+
41
+ def stop
42
+ case state
43
+ when :playing, :game_over
44
+ handle_stop
45
+ else
46
+ raise NotImplementedError
47
+ end
48
+
49
+ self
50
+ end
51
+
52
+ def play(r, c)
53
+ case state
54
+ when :playing
55
+ handle_play(r.to_i, c.to_i)
56
+ else
57
+ raise NotImplementedError
58
+ end
59
+
60
+ self
61
+ end
62
+
63
+ def continue_playing(player)
64
+ raise ArgumentError, "unknown player #{player}" unless XO.is_player?(player)
65
+
66
+ case state
67
+ when :game_over
68
+ handle_continue_playing(player)
69
+ else
70
+ raise NotImplementedError
71
+ end
72
+
73
+ self
74
+ end
75
+
76
+ private
77
+
78
+ attr_writer :turn, :state
79
+
80
+ def handle_start(player)
81
+ self.turn = player
82
+ self.state = :playing
83
+ @grid.clear
84
+
85
+ send_event(:game_started, who: player)
86
+ end
87
+
88
+ def handle_stop
89
+ self.state = :idle
90
+
91
+ send_event(:game_stopped)
92
+ end
93
+
94
+ def handle_play(r, c)
95
+ if Grid.contains?(r, c)
96
+ if @grid.free?(r, c)
97
+ @grid[r, c] = turn
98
+ last_played_at = OpenStruct.new(row: r, col: c)
99
+
100
+ result = Evaluator.analyze(@grid, turn)
101
+
102
+ case result[:status]
103
+ when :ok
104
+ self.turn = next_turn
105
+ send_event(:next_turn, who: turn, last_played_at: last_played_at)
106
+ when :game_over
107
+ self.state = :game_over
108
+
109
+ case result[:type]
110
+ when :winner
111
+ send_event(:game_over, type: :winner, who: turn, last_played_at: last_played_at, details: result[:details])
112
+ when :squashed
113
+ send_event(:game_over, type: :squashed, who: turn, last_played_at: last_played_at)
114
+ end
115
+ end
116
+ else
117
+ send_event(:invalid_move, type: :occupied)
118
+ end
119
+ else
120
+ send_event(:invalid_move, type: :out_of_bounds)
121
+ end
122
+ end
123
+
124
+ def handle_continue_playing(player)
125
+ self.turn = player
126
+ self.state = :playing
127
+ @grid.clear
128
+
129
+ send_event(:continue_playing, who: player)
130
+ end
131
+
132
+ def send_event(name, message = {})
133
+ changed
134
+ notify_observers({ event: name }.merge(message))
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,108 @@
1
+ module XO
2
+
3
+ module Evaluator
4
+
5
+ def self.analyze(grid, player)
6
+ @grid = grid
7
+ @player = player
8
+
9
+ perform_analysis
10
+ end
11
+
12
+ private
13
+
14
+ class << self
15
+ attr_reader :grid, :player, :winners
16
+ end
17
+
18
+ def self.perform_analysis
19
+ return { status: :error, type: :too_many_moves_ahead } if two_or_more_moves_ahead?
20
+
21
+ find_winners
22
+
23
+ if two_winners?
24
+ { status: :error, type: :two_winners }
25
+ elsif winners[player]
26
+ { status: :game_over, type: :winner, details: winners[player] }
27
+ elsif winners[other_player]
28
+ { status: :game_over, type: :loser, details: winners[other_player] }
29
+ else
30
+ if grid.full?
31
+ { status: :game_over, type: :squashed }
32
+ else
33
+ { status: :ok }
34
+ end
35
+ end
36
+ end
37
+
38
+ def self.two_or_more_moves_ahead?
39
+ moves_ahead >= 2
40
+ end
41
+
42
+ def self.moves_ahead
43
+ xs = os = 0
44
+
45
+ grid.each do |_, _, val|
46
+ xs += 1 if val == XO::X
47
+ os += 1 if val == XO::O
48
+ end
49
+
50
+ (xs - os).abs
51
+ end
52
+
53
+ def self.find_winners
54
+ @winners = {}
55
+
56
+ # check rows
57
+ if XO.is_token?(grid[1, 1]) && grid[1, 1] == grid[1, 2] && grid[1, 2] == grid[1, 3]
58
+ add_winner(grid[1, 1], { where: :row, index: 1, positions: [[1, 1], [1, 2], [1, 3]] })
59
+ end
60
+
61
+ if XO.is_token?(grid[2, 1]) && grid[2, 1] == grid[2, 2] && grid[2, 2] == grid[2, 3]
62
+ add_winner(grid[2, 1], { where: :row, index: 2, positions: [[2, 1], [2, 2], [2, 3]] })
63
+ end
64
+
65
+ if XO.is_token?(grid[3, 1]) && grid[3, 1] == grid[3, 2] && grid[3, 2] == grid[3, 3]
66
+ add_winner(grid[3, 1], { where: :row, index: 3, positions: [[3, 1], [3, 2], [3, 3]] })
67
+ end
68
+
69
+ # check columns
70
+ if XO.is_token?(grid[1, 1]) && grid[1, 1] == grid[2, 1] && grid[2, 1] == grid[3, 1]
71
+ add_winner(grid[1, 1], { where: :column, index: 1, positions: [[1, 1], [2, 1], [3, 1]] })
72
+ end
73
+
74
+ if XO.is_token?(grid[1, 2]) && grid[1, 2] == grid[2, 2] && grid[2, 2] == grid[3, 2]
75
+ add_winner(grid[1, 2], { where: :column, index: 2, positions: [[1, 2], [2, 2], [3, 2]] })
76
+ end
77
+
78
+ if XO.is_token?(grid[1, 3]) && grid[1, 3] == grid[2, 3] && grid[2, 3] == grid[3, 3]
79
+ add_winner(grid[1, 3], { where: :column, index: 3, positions: [[1, 3], [2, 3], [3, 3]] })
80
+ end
81
+
82
+ # check diagonals
83
+ if XO.is_token?(grid[1, 1]) && grid[1, 1] == grid[2, 2] && grid[2, 2] == grid[3, 3]
84
+ add_winner(grid[1, 1], { where: :diagonal, index: 1, positions: [[1, 1], [2, 2], [3, 3]] })
85
+ end
86
+
87
+ if XO.is_token?(grid[1, 3]) && grid[1, 3] == grid[2, 2] && grid[2, 2] == grid[3, 1]
88
+ add_winner(grid[1, 3], { where: :diagonal, index: 2, positions: [[1, 3], [2, 2], [3, 1]] })
89
+ end
90
+ end
91
+
92
+ def self.add_winner(player, details)
93
+ if winners.key?(player)
94
+ winners[player] << details
95
+ else
96
+ winners[player] = [details]
97
+ end
98
+ end
99
+
100
+ def self.two_winners?
101
+ winners[XO::X] && winners[XO::O]
102
+ end
103
+
104
+ def self.other_player
105
+ XO.other_player(player)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,95 @@
1
+ module XO
2
+
3
+ class Grid
4
+
5
+ ROWS = 3
6
+ COLS = 3
7
+
8
+ def self.contains?(r, c)
9
+ r.between?(1, ROWS) && c.between?(1, COLS)
10
+ end
11
+
12
+ def initialize
13
+ @grid = Array.new(ROWS * COLS, :e)
14
+ end
15
+
16
+ def initialize_copy(orig)
17
+ @grid = orig.instance_variable_get(:@grid).dup
18
+ end
19
+
20
+ def []=(r, c, val)
21
+ if self.class.contains?(r, c)
22
+ grid[idx(r, c)] = val
23
+ else
24
+ raise IndexError, "position (#{r}, #{c}) is off the grid"
25
+ end
26
+ end
27
+
28
+ def [](r, c)
29
+ if self.class.contains?(r, c)
30
+ grid[idx(r, c)]
31
+ else
32
+ raise IndexError, "position (#{r}, #{c}) is off the grid"
33
+ end
34
+ end
35
+
36
+ def empty?
37
+ grid.all? { |val| !XO.is_token?(val) }
38
+ end
39
+
40
+ def full?
41
+ grid.all? { |val| XO.is_token?(val) }
42
+ end
43
+
44
+ def free?(r, c)
45
+ !XO.is_token?(self[r, c])
46
+ end
47
+
48
+ def clear
49
+ grid.fill(:e)
50
+ end
51
+
52
+ def each
53
+ (1..ROWS).each do |r|
54
+ (1..COLS).each do |c|
55
+ yield(r, c, self[r, c])
56
+ end
57
+ end
58
+
59
+ self
60
+ end
61
+
62
+ def each_free
63
+ self.each { |r, c, _| yield(r, c) if free?(r, c) }
64
+ end
65
+
66
+ def ==(other)
67
+ return false unless other.instance_of?(self.class)
68
+ grid == other.instance_variable_get(:@grid)
69
+ end
70
+ alias_method :eql?, :==
71
+
72
+ def hash
73
+ grid.hash
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :grid
79
+
80
+ # Computes the 0-based index of position (r, c) on a 3x3 grid.
81
+ #
82
+ # c 1 2 3
83
+ # r
84
+ # 1 0 | 1 | 2
85
+ # ---+---+---
86
+ # 2 3 | 4 | 5
87
+ # ---+---+---
88
+ # 3 6 | 7 | 8
89
+ #
90
+ # For e.g. idx(2, 3) is 5.
91
+ def idx(r, c)
92
+ COLS * (r - 1) + (c - 1)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,3 @@
1
+ module XO
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,4 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/spec'
3
+
4
+ require 'xo'
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+
3
+ module XO
4
+
5
+ describe AI do
6
+
7
+ describe 'minimax' do
8
+
9
+ let(:grid) { Grid.new }
10
+
11
+ describe 'immediate wins' do
12
+
13
+ it 'should return (1, 3)' do
14
+ grid[1, 1] = grid[1, 2] = :x
15
+ grid[2, 1] = grid[2, 2] = :o
16
+
17
+ moves = AI.minimax(grid, :x).moves
18
+
19
+ moves.size.must_equal 1
20
+ [moves[0].row, moves[0].column].must_equal [1, 3]
21
+ end
22
+
23
+ it 'should return (1, 3), (3, 2) and (3, 3)' do
24
+ grid[2, 1] = grid[2, 3] = grid[3, 1] = :x
25
+ grid[1, 1] = grid[1, 2] = grid[2, 2] = :o
26
+
27
+ moves = AI.minimax(grid, :o).moves
28
+
29
+ moves.size.must_equal 3
30
+ [moves[0].row, moves[0].column].must_equal [1, 3]
31
+ [moves[1].row, moves[1].column].must_equal [3, 2]
32
+ [moves[2].row, moves[2].column].must_equal [3, 3]
33
+ end
34
+ end
35
+
36
+ describe 'blocking moves' do
37
+
38
+ it 'should return (2, 1)' do
39
+ grid[1, 1] = grid[3, 1] = :x
40
+ grid[2, 2] = :o
41
+
42
+ moves = AI.minimax(grid, :o).moves
43
+
44
+ moves.size.must_equal 1
45
+ [moves[0].row, moves[0].column].must_equal [2, 1]
46
+ end
47
+ end
48
+
49
+ describe 'smart moves' do
50
+
51
+ it 'should return (1, 3)' do
52
+ grid[1, 1] = grid[3, 1] = :x
53
+ grid[2, 1] = grid[3, 3] = :o
54
+
55
+ moves = AI.minimax(grid, :x).moves
56
+
57
+ moves.size.must_equal 1
58
+ [moves[0].row, moves[0].column].must_equal [1, 3]
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ module XO
4
+
5
+ describe Engine do
6
+
7
+ let (:engine) { Engine.new }
8
+
9
+ describe 'initial state' do
10
+
11
+ it 'has an empty grid' do
12
+ engine.grid.empty?.must_equal true
13
+ end
14
+
15
+ it "is nobody's turn" do
16
+ engine.turn.must_equal :nobody
17
+ end
18
+
19
+ it 'is in the idle state' do
20
+ engine.state.must_equal :idle
21
+ end
22
+ end
23
+
24
+ describe '#grid' do
25
+
26
+ it 'returns a copy' do
27
+ grid = engine.grid
28
+ grid[1, 1] = X
29
+
30
+ # FIXME: How else can I test this requirement? I don't like that the test
31
+ # depends on knowing the name of the internal private instance variable.
32
+ engine.instance_variable_get(:@grid).empty?.must_equal true
33
+ end
34
+ end
35
+
36
+ describe 'a single round of play' do
37
+
38
+ it 'works as follows' do
39
+ observer = Object.new
40
+
41
+ def observer.handle_event(e)
42
+ if e[:event] == :game_over && e[:type] == :winner
43
+ @winner = e[:who]
44
+ end
45
+ end
46
+
47
+ engine.add_observer(observer, :handle_event)
48
+
49
+ engine.start(X).play(1, 1).play(2, 1).play(1, 2).play(2, 2).play(1, 3)
50
+
51
+ observer.instance_variable_get(:@winner).must_equal X
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+
3
+ module XO
4
+
5
+ describe Evaluator do
6
+
7
+ describe 'analyze' do
8
+
9
+ let (:grid) { Grid.new }
10
+
11
+ describe 'error statuses' do
12
+
13
+ it 'returns too many moves ahead' do
14
+ grid[1, 1] = grid[1, 2] = grid[1, 3] = X
15
+ grid[2, 1] = O
16
+
17
+ result = { status: :error, type: :too_many_moves_ahead }
18
+
19
+ Evaluator.analyze(grid, X).must_equal result
20
+ Evaluator.analyze(grid, O).must_equal result
21
+ end
22
+
23
+ it 'returns two winners' do
24
+ grid[1, 1] = grid[1, 2] = grid[1, 3] = X
25
+ grid[2, 1] = grid[2, 2] = grid[2, 3] = O
26
+
27
+ result = { status: :error, type: :two_winners }
28
+
29
+ Evaluator.analyze(grid, X).must_equal result
30
+ Evaluator.analyze(grid, O).must_equal result
31
+ end
32
+ end
33
+
34
+ describe 'game over statuses' do
35
+
36
+ describe 'wins and losses' do
37
+
38
+ it 'returns a win/loss in the first row' do
39
+ grid[1, 1] = grid[1, 2] = grid[1, 3] = X
40
+ grid[2, 1] = grid[2, 2] = O
41
+
42
+ result = {
43
+ status: :game_over,
44
+ type: :winner,
45
+ details: [{
46
+ where: :row,
47
+ index: 1,
48
+ positions: [[1, 1], [1, 2], [1, 3]]
49
+ }]
50
+ }
51
+
52
+ Evaluator.analyze(grid, X).must_equal result
53
+
54
+ result[:type] = :loser
55
+ Evaluator.analyze(grid, O).must_equal result
56
+ end
57
+
58
+ # TODO: Test the winners/losers in the other rows, the columns and the diagonals.
59
+ end
60
+
61
+ describe 'squashed' do
62
+
63
+ it 'returns squashed' do
64
+ grid[1, 1] = grid[1, 2] = grid[2, 3] = grid[3, 1] = grid[3, 3] = X
65
+ grid[1, 3] = grid[2, 1] = grid[2, 2] = grid[3, 2] = O
66
+
67
+ result = { status: :game_over, type: :squashed }
68
+
69
+ Evaluator.analyze(grid, X).must_equal result
70
+ Evaluator.analyze(grid, O).must_equal result
71
+ end
72
+ end
73
+ end
74
+
75
+ describe 'ok status' do
76
+
77
+ it 'returns ok' do
78
+ result = { status: :ok }
79
+
80
+ Evaluator.analyze(grid, X).must_equal result
81
+ Evaluator.analyze(grid, O).must_equal result
82
+
83
+ grid[1, 1] = X
84
+ Evaluator.analyze(grid, X).must_equal result
85
+ Evaluator.analyze(grid, O).must_equal result
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,146 @@
1
+ require 'spec_helper'
2
+
3
+ module XO
4
+
5
+ describe Grid do
6
+
7
+ it 'has 3 rows' do
8
+ Grid::ROWS.must_equal 3
9
+ end
10
+
11
+ it 'has 3 columns' do
12
+ Grid::COLS.must_equal 3
13
+ end
14
+
15
+ it 'contains all r, c where r is in {1, 2, 3} and c is in {1, 2, 3}' do
16
+ (1..3).each do |r|
17
+ (1..3).each do |c|
18
+ Grid.contains?(r, c).must_equal true
19
+ end
20
+ end
21
+ end
22
+
23
+ it 'does not contain any r, c where either r is not in {1, 2, 3} or c is not in {1, 2, 3}' do
24
+ [[0, 0], [0, 1], [0, 2], [0, 3], [0, 4],
25
+ [1, 0], [1, 4],
26
+ [2, 0], [2, 4],
27
+ [3, 0], [3, 4],
28
+ [4, 0], [4, 1], [4, 2], [4, 3], [4, 4]].each do |pos|
29
+ Grid.contains?(*pos).must_equal false
30
+ end
31
+ end
32
+
33
+ let(:grid) { Grid.new }
34
+
35
+ describe 'a new grid' do
36
+
37
+ it 'is empty' do
38
+ grid.empty?.must_equal true
39
+ end
40
+ end
41
+
42
+ describe '#dup' do
43
+
44
+ it 'creates a copy' do
45
+ grid[1, 1] = X
46
+
47
+ grid_copy = grid.dup
48
+ grid_copy[1, 1] = O
49
+
50
+ grid[1, 1].must_equal X
51
+ end
52
+ end
53
+
54
+ describe '#empty?' do
55
+
56
+ it 'returns false when at least one position has a token' do
57
+ grid[1, 1] = X
58
+ grid.empty?.must_equal false
59
+ end
60
+ end
61
+
62
+ describe '#[]=' do
63
+
64
+ it 'raises IndexError when a token is placed at a position it does not contain' do
65
+ proc { grid[0, 0] = X }.must_raise IndexError
66
+ end
67
+ end
68
+
69
+ describe '#[]' do
70
+
71
+ it 'raises IndexError when given a position it does not contain' do
72
+ proc { grid[4, 4] }.must_raise IndexError
73
+ end
74
+ end
75
+
76
+ describe '#full?' do
77
+
78
+ before do
79
+ (1..3).each do |r|
80
+ (1..3).each do |c|
81
+ grid[r, c] = O
82
+ end
83
+ end
84
+ end
85
+
86
+ it 'returns true when every position has a token' do
87
+ grid.full?.must_equal true
88
+ end
89
+
90
+ it 'returns false when at least one position does not have a token' do
91
+ grid[1, 1] = :e
92
+ grid.full?.must_equal false
93
+ end
94
+ end
95
+
96
+ describe '#free?' do
97
+
98
+ it 'returns true when no token is at the given position' do
99
+ grid.free?(2, 2).must_equal true
100
+ end
101
+
102
+ it 'returns false when a token is at the given position' do
103
+ grid[3, 1] = O
104
+ grid.free?(3, 1).must_equal false
105
+ end
106
+
107
+ it 'raises IndexError when given a position the grid does not contain' do
108
+ proc { grid.free?(0, 4) }.must_raise IndexError
109
+ end
110
+ end
111
+
112
+ describe '#clear' do
113
+
114
+ it 'empties the grid' do
115
+ grid[1, 1] = X
116
+ grid[1, 3] = O
117
+ grid[3, 2] = X
118
+
119
+ grid.clear
120
+
121
+ grid.empty?.must_equal true
122
+ end
123
+ end
124
+
125
+ describe '#each' do
126
+
127
+ it "visits every position and yields a block that takes the position's row, column and value" do
128
+ grid[1, 1] = O
129
+ grid[2, 2] = X
130
+ grid[3, 3] = O
131
+
132
+ visited = {}
133
+
134
+ grid.each do |r, c, val|
135
+ # ensure that the value for the position is correct
136
+ grid[r, c].must_equal val
137
+
138
+ # keep track of every position we visit
139
+ visited[[r, c]] = val
140
+ end
141
+
142
+ visited.keys.size.must_equal(Grid::ROWS * Grid::COLS)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'xo/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'xo'
8
+ spec.version = XO::VERSION
9
+ spec.author = 'Dwayne R. Crooks'
10
+ spec.email = ['me@dwaynecrooks.com']
11
+ spec.summary = %q{A Ruby library for Tic-tac-toe.}
12
+ spec.description = %q{A Ruby library that can be used to develop Tic-tac-toe game clients.}
13
+ spec.homepage = 'https://github.com/dwayne/xo'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'rake', '~> 10.1', '>= 10.1.0'
22
+ spec.add_development_dependency 'minitest', '~> 5.3', '>= 5.3.2'
23
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Dwayne R. Crooks
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '10.1'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 10.1.0
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '10.1'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 10.1.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: minitest
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '5.3'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 5.3.2
43
+ type: :development
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '5.3'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 5.3.2
53
+ description: A Ruby library that can be used to develop Tic-tac-toe game clients.
54
+ email:
55
+ - me@dwaynecrooks.com
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - ".gitignore"
61
+ - LICENSE.txt
62
+ - README.md
63
+ - Rakefile
64
+ - lib/xo.rb
65
+ - lib/xo/ai.rb
66
+ - lib/xo/ai/advanced_beginner.rb
67
+ - lib/xo/ai/expert.rb
68
+ - lib/xo/ai/minimax.rb
69
+ - lib/xo/ai/novice.rb
70
+ - lib/xo/engine.rb
71
+ - lib/xo/evaluator.rb
72
+ - lib/xo/grid.rb
73
+ - lib/xo/version.rb
74
+ - spec/spec_helper.rb
75
+ - spec/xo/ai/minimax_spec.rb
76
+ - spec/xo/engine_spec.rb
77
+ - spec/xo/evaluator_spec.rb
78
+ - spec/xo/grid_spec.rb
79
+ - xo.gemspec
80
+ homepage: https://github.com/dwayne/xo
81
+ licenses:
82
+ - MIT
83
+ metadata: {}
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubyforge_project:
100
+ rubygems_version: 2.2.2
101
+ signing_key:
102
+ specification_version: 4
103
+ summary: A Ruby library for Tic-tac-toe.
104
+ test_files:
105
+ - spec/spec_helper.rb
106
+ - spec/xo/ai/minimax_spec.rb
107
+ - spec/xo/engine_spec.rb
108
+ - spec/xo/evaluator_spec.rb
109
+ - spec/xo/grid_spec.rb