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