xo 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|