neuro_gammon 0.7.0
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.
- data/.gitignore +1 -0
- data/.hgignore +7 -0
- data/.loadpath +6 -0
- data/.project +24 -0
- data/README +3 -0
- data/Rakefile +37 -0
- data/VERSION +1 -0
- data/fred.net +34 -0
- data/lib/main.rb +49 -0
- data/lib/main2.rb +73 -0
- data/lib/neuro_gammon.rb +1 -0
- data/lib/neuro_gammon/base_neural_player.rb +62 -0
- data/lib/neuro_gammon/base_player.rb +18 -0
- data/lib/neuro_gammon/best_of_tournament_engine.rb +60 -0
- data/lib/neuro_gammon/board.rb +236 -0
- data/lib/neuro_gammon/board_tools.rb +46 -0
- data/lib/neuro_gammon/dice.rb +60 -0
- data/lib/neuro_gammon/fann_player.rb +116 -0
- data/lib/neuro_gammon/game.rb +44 -0
- data/lib/neuro_gammon/game_engine.rb +75 -0
- data/lib/neuro_gammon/lazy_player.rb +25 -0
- data/lib/neuro_gammon/random_player.rb +38 -0
- data/nbproject/private/rake-t.txt +1 -0
- data/nbproject/project.properties +6 -0
- data/nbproject/project.xml +15 -0
- data/neuro_gammon.gemspec +82 -0
- data/test/test_base_player.rb +32 -0
- data/test/test_board.rb +384 -0
- data/test/test_board_tools.rb +91 -0
- data/test/test_dice.rb +109 -0
- data/test/test_fann_player.rb +50 -0
- data/test/test_game.rb +111 -0
- data/test/test_game_engine.rb +35 -0
- metadata +136 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
#
|
2
|
+
# To change this template, choose Tools | Templates
|
3
|
+
# and open the template in the editor.
|
4
|
+
|
5
|
+
require 'neuro_gammon/board'
|
6
|
+
|
7
|
+
module NeuroGammon
|
8
|
+
module BoardTools
|
9
|
+
|
10
|
+
def reverse_state state
|
11
|
+
#first reverse all the colours (except the bar)
|
12
|
+
new_state=[]
|
13
|
+
new_state[0]=[]
|
14
|
+
new_state[1]=[]
|
15
|
+
state[0].each_index do |i|
|
16
|
+
new_state[0] << state[0][i]*-1
|
17
|
+
end
|
18
|
+
|
19
|
+
state[1].each_index do |i|
|
20
|
+
new_state[1] << state[1][i]
|
21
|
+
end
|
22
|
+
|
23
|
+
new_state[0].reverse!
|
24
|
+
new_state[1].reverse!
|
25
|
+
return new_state
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_for_gammon state,colour
|
29
|
+
on_bar= (colour==Board::WHITE ? state[1][0] != 0 : state[1][1] != 0)
|
30
|
+
on_board= state[0].find() {|x| x*colour>0}
|
31
|
+
return false if (on_board or on_bar)
|
32
|
+
blank=state.flatten.find {|x| x!=0}.nil?
|
33
|
+
return false if blank
|
34
|
+
if state[1].find() {|x| x!=0}
|
35
|
+
return false if colour==Board::WHITE ? state[1][0]!=0 : state[1][1]!=0
|
36
|
+
end
|
37
|
+
|
38
|
+
range=colour==Board::WHITE ? (18..23) : (0..5)
|
39
|
+
return range.find(){|x| state[0][x]*(-colour) > 0} == nil
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2008 Stuart Owen
|
3
|
+
#
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
+
|
17
|
+
module NeuroGammon
|
18
|
+
class Dice
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@values=[]
|
22
|
+
roll
|
23
|
+
end
|
24
|
+
|
25
|
+
def roll
|
26
|
+
@values=[]
|
27
|
+
@values << rand(6)+1
|
28
|
+
@values << rand(6)+1
|
29
|
+
check_state
|
30
|
+
end
|
31
|
+
|
32
|
+
def double?
|
33
|
+
@double
|
34
|
+
end
|
35
|
+
|
36
|
+
def [] x
|
37
|
+
return @values[x]
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_a
|
41
|
+
@values
|
42
|
+
end
|
43
|
+
|
44
|
+
def each
|
45
|
+
to_a.each {|obj| yield(obj)}
|
46
|
+
end
|
47
|
+
|
48
|
+
def use! value
|
49
|
+
#TODO is there a method on Array to delete one value only?
|
50
|
+
i=@values.index(value)
|
51
|
+
raise Exception.new("No value " << value << " in dice " << self.to_a.to_s) if i==nil
|
52
|
+
@values.delete_at(i)
|
53
|
+
end
|
54
|
+
|
55
|
+
def check_state
|
56
|
+
@double=(@values[0]==@values[1])
|
57
|
+
@values = double? ? Array.new(4,self[0]) : @values
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
#
|
2
|
+
# To change this template, choose Tools | Templates
|
3
|
+
# and open the template in the editor.
|
4
|
+
|
5
|
+
require 'neuro_gammon/base_neural_player'
|
6
|
+
require 'rubygems'
|
7
|
+
require 'ruby_fann/neural_network'
|
8
|
+
require 'neuro_gammon/board_tools'
|
9
|
+
require 'tempfile'
|
10
|
+
|
11
|
+
module NeuroGammon
|
12
|
+
|
13
|
+
class FannPlayer < BaseNeuralPlayer
|
14
|
+
include BoardTools
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def cascade_learn! games
|
21
|
+
corpus=generate_corpus games
|
22
|
+
training_data=RubyFann::TrainData.new(:inputs=>corpus[0], :desired_outputs=>corpus[1])
|
23
|
+
@net.cascadetrain_on_data(training_data,5,5,0.0004)
|
24
|
+
store_net_data
|
25
|
+
end
|
26
|
+
|
27
|
+
def learn! games
|
28
|
+
corpus=generate_corpus games
|
29
|
+
training_data=RubyFann::TrainData.new(:inputs=>corpus[0], :desired_outputs=>corpus[1])
|
30
|
+
@net.train_on_data(training_data,1500,30,0.0004)
|
31
|
+
store_net_data
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_yaml_properties
|
35
|
+
["@n_inputs","@n_outputs","@hidden_layers","@learning_rate","@net_data"]
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.from_yaml yaml
|
39
|
+
obj=YAML::load(yaml)
|
40
|
+
class << obj
|
41
|
+
def pub_read_net_data
|
42
|
+
read_net_data
|
43
|
+
end
|
44
|
+
end
|
45
|
+
obj.pub_read_net_data
|
46
|
+
obj
|
47
|
+
end
|
48
|
+
|
49
|
+
#see store_net_data
|
50
|
+
def read_net_data
|
51
|
+
f=Tempfile.new("fann-net-in")
|
52
|
+
f.write(@net_data)
|
53
|
+
f.flush
|
54
|
+
@net=RubyFann::Standard.new(:filename=>f.path)
|
55
|
+
f.close
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
def run_net input
|
61
|
+
return @net.run(input)
|
62
|
+
end
|
63
|
+
|
64
|
+
def evaluate_output output,board,dice,colour
|
65
|
+
score=0
|
66
|
+
if (colour==Board::WHITE)
|
67
|
+
score = output[0]-output[2]
|
68
|
+
score += 2*(output[1]-output[3])
|
69
|
+
else
|
70
|
+
score = output[2]-output[0]
|
71
|
+
score += 2*(output[3]-output[1])
|
72
|
+
end
|
73
|
+
return score
|
74
|
+
end
|
75
|
+
|
76
|
+
def generate_input_pattern board_state
|
77
|
+
pattern=[]
|
78
|
+
board_state.flatten.each do |v|
|
79
|
+
pattern << v
|
80
|
+
end
|
81
|
+
return pattern
|
82
|
+
end
|
83
|
+
|
84
|
+
def generate_output_pattern board_state,game
|
85
|
+
result=[-1,-1,-1,-1]
|
86
|
+
if test_for_gammon(board_state,game.winner_colour)
|
87
|
+
game.winner_colour==Board::WHITE ? result[1]=1 : result[3]=1
|
88
|
+
end
|
89
|
+
game.winner_colour==Board::WHITE ? result[0]=1 : result[2]=1
|
90
|
+
return result
|
91
|
+
end
|
92
|
+
|
93
|
+
def init_net
|
94
|
+
@n_inputs=26
|
95
|
+
@n_outputs=4
|
96
|
+
@hidden_layers=[300]
|
97
|
+
@learning_rate=0.4
|
98
|
+
@net=RubyFann::Standard.new(:num_inputs=>n_inputs,:hidden_neurons=>hidden_layers,:num_outputs=>n_outputs)
|
99
|
+
# @net.set_training_algorithm :incremental
|
100
|
+
@net.set_learning_rate(learning_rate)
|
101
|
+
store_net_data
|
102
|
+
end
|
103
|
+
|
104
|
+
#this is required to store the binary of the @net, for YAML conversion.
|
105
|
+
#since ruby-fann only seems to like saving to and from a file, this is created by saving the net to a tmp file, then reading back in
|
106
|
+
def store_net_data
|
107
|
+
f=Tempfile.new("fann-net-out")
|
108
|
+
@net.save(f.path)
|
109
|
+
f2=open(f.path)
|
110
|
+
@net_data=f2.read
|
111
|
+
f.close
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
#
|
2
|
+
# To change this template, choose Tools | Templates
|
3
|
+
# and open the template in the editor.
|
4
|
+
|
5
|
+
module NeuroGammon
|
6
|
+
class Game
|
7
|
+
attr_accessor :winner_colour
|
8
|
+
attr_reader :board_states
|
9
|
+
attr_reader :dice_states
|
10
|
+
attr_reader :moves
|
11
|
+
attr_reader :black_player
|
12
|
+
attr_reader :white_player
|
13
|
+
attr_reader :colour_moved
|
14
|
+
|
15
|
+
def initialize white_player,black_player
|
16
|
+
@black_player=black_player
|
17
|
+
@white_player=white_player
|
18
|
+
@board_states=[]
|
19
|
+
@moves=[]
|
20
|
+
@dice_states=[]
|
21
|
+
@colour_moved=[]
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_board_state state
|
25
|
+
self.board_states << Marshal.load(Marshal.dump(state))
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_dice_state dice
|
29
|
+
dice_states << Marshal.load(Marshal.dump(dice))
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_move move
|
33
|
+
moves << Marshal.load(Marshal.dump(move))
|
34
|
+
#TODO test for a valid range for coming on and bearing off, throw Exceptions otherwise, and harden unit tests for this
|
35
|
+
if move[0]==-1 #come on
|
36
|
+
colour_moved << (move[1]>17 ? Board::WHITE : Board::BLACK)
|
37
|
+
elsif move[1]==-1 #bear off
|
38
|
+
colour_moved << (move[0]<=5 ? Board::WHITE : Board::BLACK)
|
39
|
+
else
|
40
|
+
colour_moved << (move[0]-move[1]>0 ? Board::WHITE : Board::BLACK)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2008 Stuart Owen
|
3
|
+
#
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
+
|
17
|
+
require 'neuro_gammon/board'
|
18
|
+
require 'neuro_gammon/dice'
|
19
|
+
require 'neuro_gammon/game'
|
20
|
+
|
21
|
+
module NeuroGammon
|
22
|
+
class GameEngine
|
23
|
+
attr_accessor :board
|
24
|
+
attr_accessor :dice
|
25
|
+
attr_accessor :colour
|
26
|
+
attr_reader :black_player,:white_player
|
27
|
+
attr_reader :game_data
|
28
|
+
attr_reader :winner
|
29
|
+
|
30
|
+
def initialize white_player,black_player
|
31
|
+
@board=Board.new
|
32
|
+
@dice=Dice.new
|
33
|
+
@black_player=black_player
|
34
|
+
@white_player=white_player
|
35
|
+
@colour=Board::WHITE
|
36
|
+
end
|
37
|
+
|
38
|
+
def play_game
|
39
|
+
game=Game.new white_player,black_player
|
40
|
+
while !(board.piece_count(Board::BLACK)==0 || board.piece_count(Board::WHITE)==0)
|
41
|
+
dice.roll
|
42
|
+
while dice.to_a.size>0
|
43
|
+
game.add_dice_state(dice.to_a)
|
44
|
+
game.add_board_state(board.state) #TODO final board state isn't recorded (i.e. always ends with 1 piece remainining)
|
45
|
+
player=player_to_move
|
46
|
+
move=player.suggest_move(board,dice,@colour)
|
47
|
+
if (move!=nil)
|
48
|
+
board.move!(move,colour,dice)
|
49
|
+
game.add_move(move)
|
50
|
+
else #can't move
|
51
|
+
#TODO this needs some more careful thought, and unit tests. Do we want to record when a move couldn't be made??
|
52
|
+
game.dice_states.delete(game.dice_states.last)
|
53
|
+
game.board_states.delete(game.board_states.last)
|
54
|
+
break
|
55
|
+
end
|
56
|
+
end
|
57
|
+
toggle_player
|
58
|
+
end
|
59
|
+
game.winner_colour=board.winner
|
60
|
+
@winner=(board.winner==Board::WHITE ? white_player : black_player)
|
61
|
+
|
62
|
+
return game
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def player_to_move
|
68
|
+
@colour==Board::BLACK ? black_player : white_player
|
69
|
+
end
|
70
|
+
|
71
|
+
def toggle_player
|
72
|
+
@colour=(@colour==Board::BLACK ? Board::WHITE : Board::BLACK)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#
|
2
|
+
# To change this template, choose Tools | Templates
|
3
|
+
# and open the template in the editor.
|
4
|
+
|
5
|
+
#always picks the first move avaliable - just for testing really
|
6
|
+
|
7
|
+
require 'neuro_gammon/base_player'
|
8
|
+
|
9
|
+
module NeuroGammon
|
10
|
+
class LazyPlayer < BasePlayer
|
11
|
+
def initialize
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def suggest_move board,dice,colour
|
16
|
+
valid=board.valid_moves(colour, dice)
|
17
|
+
if (valid.size>0)
|
18
|
+
move=valid.first
|
19
|
+
return move
|
20
|
+
else
|
21
|
+
return nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2008 Stuart Owen
|
3
|
+
#
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
7
|
+
# (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
|
+
|
17
|
+
require 'neuro_gammon/board'
|
18
|
+
require 'neuro_gammon/dice'
|
19
|
+
require 'neuro_gammon/base_player'
|
20
|
+
|
21
|
+
module NeuroGammon
|
22
|
+
class RandomPlayer < BasePlayer
|
23
|
+
def initialize
|
24
|
+
super
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
def suggest_move board,dice,colour
|
29
|
+
valid=board.valid_moves(colour, dice)
|
30
|
+
if (valid.size>0)
|
31
|
+
move=valid[rand(valid.size)]
|
32
|
+
return move
|
33
|
+
else
|
34
|
+
return nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
(in C:/Users/sowen/Documents/NetBeansProjects/NeuralGammon)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<project xmlns="http://www.netbeans.org/ns/project/1">
|
3
|
+
<type>org.netbeans.modules.ruby.rubyproject</type>
|
4
|
+
<configuration>
|
5
|
+
<data xmlns="http://www.netbeans.org/ns/ruby-project/1">
|
6
|
+
<name>NeuralGammon</name>
|
7
|
+
<source-roots>
|
8
|
+
<root id="src.dir"/>
|
9
|
+
</source-roots>
|
10
|
+
<test-roots>
|
11
|
+
<root id="test.src.dir"/>
|
12
|
+
</test-roots>
|
13
|
+
</data>
|
14
|
+
</configuration>
|
15
|
+
</project>
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{neuro_gammon}
|
8
|
+
s.version = "0.7.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Stu"]
|
12
|
+
s.date = %q{2010-08-04}
|
13
|
+
s.email = %q{sowen@stuzart.org}
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"README"
|
16
|
+
]
|
17
|
+
s.files = [
|
18
|
+
".gitignore",
|
19
|
+
".hgignore",
|
20
|
+
".loadpath",
|
21
|
+
".project",
|
22
|
+
"README",
|
23
|
+
"Rakefile",
|
24
|
+
"VERSION",
|
25
|
+
"fred.net",
|
26
|
+
"lib/main.rb",
|
27
|
+
"lib/main2.rb",
|
28
|
+
"lib/neuro_gammon.rb",
|
29
|
+
"lib/neuro_gammon/base_neural_player.rb",
|
30
|
+
"lib/neuro_gammon/base_player.rb",
|
31
|
+
"lib/neuro_gammon/best_of_tournament_engine.rb",
|
32
|
+
"lib/neuro_gammon/board.rb",
|
33
|
+
"lib/neuro_gammon/board_tools.rb",
|
34
|
+
"lib/neuro_gammon/dice.rb",
|
35
|
+
"lib/neuro_gammon/fann_player.rb",
|
36
|
+
"lib/neuro_gammon/game.rb",
|
37
|
+
"lib/neuro_gammon/game_engine.rb",
|
38
|
+
"lib/neuro_gammon/lazy_player.rb",
|
39
|
+
"lib/neuro_gammon/random_player.rb",
|
40
|
+
"nbproject/private/rake-t.txt",
|
41
|
+
"nbproject/project.properties",
|
42
|
+
"nbproject/project.xml",
|
43
|
+
"neuro_gammon.gemspec",
|
44
|
+
"test/test_base_player.rb",
|
45
|
+
"test/test_board.rb",
|
46
|
+
"test/test_board_tools.rb",
|
47
|
+
"test/test_dice.rb",
|
48
|
+
"test/test_fann_player.rb",
|
49
|
+
"test/test_game.rb",
|
50
|
+
"test/test_game_engine.rb"
|
51
|
+
]
|
52
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
53
|
+
s.require_paths = ["lib"]
|
54
|
+
s.rubygems_version = %q{1.3.7}
|
55
|
+
s.summary = %q{Neural Net Backgammon utility library - just a bit of messing about - nothing serious.}
|
56
|
+
s.test_files = [
|
57
|
+
"test/test_game_engine.rb",
|
58
|
+
"test/test_dice.rb",
|
59
|
+
"test/test_base_player.rb",
|
60
|
+
"test/test_board_tools.rb",
|
61
|
+
"test/test_fann_player.rb",
|
62
|
+
"test/test_game.rb",
|
63
|
+
"test/test_board.rb"
|
64
|
+
]
|
65
|
+
|
66
|
+
if s.respond_to? :specification_version then
|
67
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
68
|
+
s.specification_version = 3
|
69
|
+
|
70
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
71
|
+
s.add_runtime_dependency(%q<uuidtools>, [">= 2.1.1"])
|
72
|
+
s.add_runtime_dependency(%q<ruby-fann>, [">= 1.1.3"])
|
73
|
+
else
|
74
|
+
s.add_dependency(%q<uuidtools>, [">= 2.1.1"])
|
75
|
+
s.add_dependency(%q<ruby-fann>, [">= 1.1.3"])
|
76
|
+
end
|
77
|
+
else
|
78
|
+
s.add_dependency(%q<uuidtools>, [">= 2.1.1"])
|
79
|
+
s.add_dependency(%q<ruby-fann>, [">= 1.1.3"])
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|