xo 0.0.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/.gitignore +1 -0
- data/LICENSE.txt +22 -0
- data/README.md +55 -0
- data/Rakefile +7 -0
- data/lib/xo.rb +25 -0
- data/lib/xo/ai.rb +4 -0
- data/lib/xo/ai/advanced_beginner.rb +17 -0
- data/lib/xo/ai/expert.rb +64 -0
- data/lib/xo/ai/minimax.rb +125 -0
- data/lib/xo/ai/novice.rb +11 -0
- data/lib/xo/engine.rb +137 -0
- data/lib/xo/evaluator.rb +108 -0
- data/lib/xo/grid.rb +95 -0
- data/lib/xo/version.rb +3 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/xo/ai/minimax_spec.rb +63 -0
- data/spec/xo/engine_spec.rb +55 -0
- data/spec/xo/evaluator_spec.rb +90 -0
- data/spec/xo/grid_spec.rb +146 -0
- data/xo.gemspec +23 -0
- metadata +109 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
*.gem
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
data/lib/xo.rb
ADDED
@@ -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'
|
data/lib/xo/ai.rb
ADDED
@@ -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
|
data/lib/xo/ai/expert.rb
ADDED
@@ -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
|
data/lib/xo/ai/novice.rb
ADDED
data/lib/xo/engine.rb
ADDED
@@ -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
|
data/lib/xo/evaluator.rb
ADDED
@@ -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
|
data/lib/xo/grid.rb
ADDED
@@ -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
|
data/lib/xo/version.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -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
|
data/xo.gemspec
ADDED
@@ -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
|