line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
# $Id: AGATourn.pm,v 1.35 2005/01/24 04:32:17 reid Exp $ |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
# AGATourn |
4
|
|
|
|
|
|
|
# |
5
|
|
|
|
|
|
|
# Copyright (C) 1999, 2004, 2005 Reid Augustin reid@netchip.com |
6
|
|
|
|
|
|
|
# 1000 San Mateo Dr. |
7
|
|
|
|
|
|
|
# Menlo Park, CA 94025 USA |
8
|
|
|
|
|
|
|
# |
9
|
|
|
|
|
|
|
# This library is free software; you can redistribute it and/or modify it |
10
|
|
|
|
|
|
|
# under the same terms as Perl itself, either Perl version 5.8.5 or, at your |
11
|
|
|
|
|
|
|
# option, any later version of Perl 5 you may have available. |
12
|
|
|
|
|
|
|
# |
13
|
|
|
|
|
|
|
# This program is distributed in the hope that it will be useful, but |
14
|
|
|
|
|
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
15
|
|
|
|
|
|
|
# or FITNESS FOR A PARTICULAR PURPOSE. |
16
|
|
|
|
|
|
|
# |
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
=head1 NAME |
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
AGATourn - Perl extensions to ease the pain of using AGA tournament data files. |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
=head1 SYNOPSIS |
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
use Games::Go::AGATourn; |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
my $agaTourn = Bnew> (options); |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
=head1 DESCRIPTION |
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
An AGATourn object represents a round or several rounds of an American Go |
31
|
|
|
|
|
|
|
Association tournament. There are methods for parsing several type of AGA |
32
|
|
|
|
|
|
|
file format: |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
=over 4 |
35
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
=item tdlist |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
The entire list of AGA members including playing strength, club affiliation, |
39
|
|
|
|
|
|
|
and some other stuff. |
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
=item register.tde |
42
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
The starting point for a tournament. All players in a tournament must be |
44
|
|
|
|
|
|
|
entered in the register.tde file. |
45
|
|
|
|
|
|
|
|
46
|
|
|
|
|
|
|
=item round results: 1.tde, 2.tde, etc. |
47
|
|
|
|
|
|
|
|
48
|
|
|
|
|
|
|
Game results for each round of the tournament. |
49
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
=back |
51
|
|
|
|
|
|
|
|
52
|
|
|
|
|
|
|
A note on IDs: in general, hashes in an AGATourn object are keyed by the AGA |
53
|
|
|
|
|
|
|
ID. An AGA ID consists of a three letter country specifier (like USA or TMP |
54
|
|
|
|
|
|
|
for temporary IDs) concatenated to an integer. Here we specify the three |
55
|
|
|
|
|
|
|
letter country specifier as the 'country' and the integer part as the |
56
|
|
|
|
|
|
|
'agaNum'. The country concatenated with the agaNum is the ID. My ID for |
57
|
|
|
|
|
|
|
example is USA2122. IDs should be normalized (capitalize the country part and |
58
|
|
|
|
|
|
|
remove preceding 0s from the agaNum part) with the B method |
59
|
|
|
|
|
|
|
(below). |
60
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
Note also that some programs may accept limited integers in the agaNum part of |
62
|
|
|
|
|
|
|
the ID. Accelerat, for example, seems to accept only up to 32K (someone used |
63
|
|
|
|
|
|
|
a signed short somewhere?) |
64
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
=cut |
66
|
|
|
|
|
|
|
|
67
|
1
|
|
|
1
|
|
56906
|
use strict; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
63
|
|
68
|
|
|
|
|
|
|
require 5.001; |
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
package Games::Go::AGATourn; |
71
|
1
|
|
|
1
|
|
7
|
use Carp; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
76
|
|
72
|
1
|
|
|
1
|
|
6
|
use IO::File; |
|
1
|
|
|
|
|
7
|
|
|
1
|
|
|
|
|
292
|
|
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
our @ISA = qw(Exporter); |
75
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
# Items to export into callers namespace by default. Note: do not export |
77
|
|
|
|
|
|
|
# names by default without a very good reason. Use EXPORT_OK instead. |
78
|
|
|
|
|
|
|
# Do not simply export all your public functions/methods/constants. |
79
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
# This allows declaration use PackageName ':all'; |
81
|
|
|
|
|
|
|
# If you do not need this, moving things directly into @EXPORT or @EXPORT_OK |
82
|
|
|
|
|
|
|
# will save memory. |
83
|
|
|
|
|
|
|
our %EXPORT_TAGS = ( 'all' => [ qw( |
84
|
|
|
|
|
|
|
) ] ); |
85
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } ); |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
our @EXPORT = qw( |
89
|
|
|
|
|
|
|
); |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
BEGIN { |
92
|
1
|
|
|
1
|
|
43
|
our $VERSION = sprintf "%d.%03d", '$Revision: 1.35 $' =~ /(\d+)/g; |
93
|
|
|
|
|
|
|
} |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
###################################################### |
96
|
|
|
|
|
|
|
# |
97
|
|
|
|
|
|
|
# Class Variables |
98
|
|
|
|
|
|
|
# |
99
|
|
|
|
|
|
|
##################################################### |
100
|
|
|
|
|
|
|
|
101
|
1
|
|
|
1
|
|
6
|
use constant NOTARANK => -99.9; # illegal rank or rating |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
13688
|
|
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
###################################################### |
104
|
|
|
|
|
|
|
# |
105
|
|
|
|
|
|
|
# Public methods |
106
|
|
|
|
|
|
|
# |
107
|
|
|
|
|
|
|
##################################################### |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
=head1 METHODS |
110
|
|
|
|
|
|
|
|
111
|
|
|
|
|
|
|
=over 4 |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
=item my $agaTourn = Bnew> (options) |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
A B AGATourn by default reads the B file to get the name, |
116
|
|
|
|
|
|
|
rank, and AGA numbers for all the players in the tournament. It then reads |
117
|
|
|
|
|
|
|
all available game results (B files: 1.tde, 2.tde, etc.) and the game |
118
|
|
|
|
|
|
|
data is incorporated into the AGATourn object. |
119
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
=head2 Options: |
121
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
=over 4 |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
=item B |
125
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
Round file number to read. If B is 0, no round files are read. If |
127
|
|
|
|
|
|
|
B is 1 or greater, only the one round file will be read. If B |
128
|
|
|
|
|
|
|
is undef (or not specified), all existing round files are read. Round files |
129
|
|
|
|
|
|
|
should be named I<1.tde>, I<2.tde>, etc. |
130
|
|
|
|
|
|
|
|
131
|
|
|
|
|
|
|
Default: undef |
132
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
=item B |
134
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
Name of register.tde file. Use undef to prevent reading the register.tde |
136
|
|
|
|
|
|
|
file. Changing the name of this file is probably a bad idea. |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
Default 'register.tde' (in the current directory) |
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
=item B |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
Starting length of name field. While reading the register file (see |
143
|
|
|
|
|
|
|
B below), B grows to reflect the longest name |
144
|
|
|
|
|
|
|
seen so far (see B method below). |
145
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
Default: 0 |
147
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
=item B |
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
Default three-letter country name. |
151
|
|
|
|
|
|
|
|
152
|
|
|
|
|
|
|
The tdlist file does not include country information in the ID, so the |
153
|
|
|
|
|
|
|
B method returns country => B. |
154
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
Default: 'USA' |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
=back |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
=cut |
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
sub new { |
162
|
2
|
|
|
2
|
1
|
29
|
my ($proto, %args) = @_; |
163
|
|
|
|
|
|
|
|
164
|
2
|
|
|
|
|
5
|
my $self = {}; |
165
|
2
|
|
33
|
|
|
18
|
bless($self, ref($proto) || $proto); |
166
|
2
|
|
|
|
|
13
|
$self->{defaultCountry} = 'USA'; |
167
|
2
|
|
|
|
|
10
|
$self->Clear; |
168
|
|
|
|
|
|
|
# transfer user args |
169
|
2
|
|
|
|
|
9
|
foreach (keys(%args)) { |
170
|
4
|
|
|
|
|
10
|
$self->{$_} = $args{$_}; |
171
|
|
|
|
|
|
|
} |
172
|
2
|
100
|
|
|
|
9
|
if (defined($self->{register_tde})) { |
173
|
1
|
50
|
|
|
|
6
|
return(undef) unless($self->ReadRegisterFile($self->{register_tde})); |
174
|
|
|
|
|
|
|
} |
175
|
2
|
100
|
|
|
|
7
|
if (defined($self->{register_tde})) { |
176
|
1
|
50
|
|
|
|
4
|
if (defined($self->{Round})) { |
177
|
1
|
50
|
|
|
|
3
|
if ($self->{Round} > 0) { |
178
|
0
|
|
|
|
|
0
|
$self->ReadRoundFile("$self->{Round}.tde"); |
179
|
|
|
|
|
|
|
} |
180
|
|
|
|
|
|
|
} else { |
181
|
0
|
|
|
|
|
0
|
my $round = 1; |
182
|
0
|
|
|
|
|
0
|
while (-f "$round.tde") { |
183
|
0
|
|
|
|
|
0
|
$self->{Round} = $round; |
184
|
0
|
|
|
|
|
0
|
$self->ReadRoundFile("$self->{Round}.tde"); |
185
|
0
|
|
|
|
|
0
|
$round++; |
186
|
|
|
|
|
|
|
} |
187
|
|
|
|
|
|
|
} |
188
|
|
|
|
|
|
|
} |
189
|
2
|
|
|
|
|
11
|
return($self); |
190
|
|
|
|
|
|
|
} |
191
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
=item $agaTourn-EB |
193
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
Clears AGATourn database. |
195
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
=cut |
197
|
|
|
|
|
|
|
|
198
|
|
|
|
|
|
|
sub Clear { |
199
|
2
|
|
|
2
|
1
|
4
|
my ($self) = @_; |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
# set defaults |
202
|
2
|
|
|
|
|
6
|
$self->{Round} = undef; |
203
|
2
|
|
|
|
|
3
|
$self->{register_tde} = "register.tde"; # default |
204
|
2
|
|
|
|
|
9
|
$self->{Directive}{ROUNDS}[0] = 1; # I hope there's at least one! |
205
|
2
|
|
|
|
|
6
|
$self->{Directive}{TOURNEY}[0] = "Unknown tournament"; |
206
|
2
|
|
|
|
|
7
|
$self->{nameLength} = 0; |
207
|
2
|
|
|
|
|
4
|
$self->{Name} = {}; # empty hash |
208
|
2
|
|
|
|
|
4
|
$self->{Rating} = {}; |
209
|
2
|
|
|
|
|
5
|
$self->{Rank} = {}; |
210
|
2
|
|
|
|
|
5
|
$self->{Comment} = {}; |
211
|
2
|
|
|
|
|
6
|
$self->{Wins} = {}; |
212
|
2
|
|
|
|
|
4
|
$self->{Losses} = {}; |
213
|
2
|
|
|
|
|
4
|
$self->{NoResults} = {}; |
214
|
2
|
|
|
|
|
4
|
$self->{Played} = {}; |
215
|
2
|
|
|
|
|
3
|
$self->{gameAllList} = []; # empty array |
216
|
2
|
|
|
|
|
5
|
$self->{error} = 0; |
217
|
|
|
|
|
|
|
} |
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
=item my $hash = $agaTourn-EB ($line) |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
Parses a single line from the TDLIST file (the latest TDLIST file |
222
|
|
|
|
|
|
|
should be downloaded from the AGA at http://usgo.org shortly before |
223
|
|
|
|
|
|
|
the tournament, and either the tab-delimited tdlista or the |
224
|
|
|
|
|
|
|
space-delimited versions are accepted). The return value is a |
225
|
|
|
|
|
|
|
reference to a hash of the following values: |
226
|
|
|
|
|
|
|
agaNum => the number part if the ID |
227
|
|
|
|
|
|
|
country => the country part of the ID (always the default |
228
|
|
|
|
|
|
|
country) |
229
|
|
|
|
|
|
|
name => complains if there is no a comma |
230
|
|
|
|
|
|
|
memType => membership type or '' if none |
231
|
|
|
|
|
|
|
agaRating => rating in decimal form, or '' if none |
232
|
|
|
|
|
|
|
agaRank => undef unless rating is a D/K style rank |
233
|
|
|
|
|
|
|
expire => date membership expires or '' if none |
234
|
|
|
|
|
|
|
club => club affiliation or '' if none |
235
|
|
|
|
|
|
|
state => state or '' if none |
236
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
If the line is not parsable, prints a warning and returns undef. |
238
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
=cut |
240
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
# sadly, we need to deal with two formats |
242
|
|
|
|
|
|
|
# old tdlist input looks like this: |
243
|
|
|
|
|
|
|
# name AGA# MmbrTyp Rank expires Club State |
244
|
|
|
|
|
|
|
#Abe, Shozo 2443 L 8603 NJ |
245
|
|
|
|
|
|
|
#Abe, Y. 2043 8312 GA |
246
|
|
|
|
|
|
|
#Abell, John 3605 -1.4 9105 MHGA CO |
247
|
|
|
|
|
|
|
#Abrahms, Judy 1253 L 8012 MGA MA |
248
|
|
|
|
|
|
|
#Abrams, Michael 6779 L -27.4 9411 MIAM FL |
249
|
|
|
|
|
|
|
#Abramson, Allan 101 3.5 9504 NOVA VA |
250
|
|
|
|
|
|
|
# the new format is like this: |
251
|
|
|
|
|
|
|
#Abe, Shozo 2443 Limit 03/28/1986 NJ |
252
|
|
|
|
|
|
|
#Abe, Y. 2043 Full 12/28/1983 GA |
253
|
|
|
|
|
|
|
#Abell, John 3605 Full -1.4 05/28/1991 MHGA CO |
254
|
|
|
|
|
|
|
#Abrahms, Judy 1253 Limit 12/28/1980 MGA MA |
255
|
|
|
|
|
|
|
# |
256
|
|
|
|
|
|
|
# There's also a tab-delimited version |
257
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
sub ParseTdListLine { |
259
|
1
|
|
|
1
|
1
|
3
|
my ($self, $string) = @_; |
260
|
|
|
|
|
|
|
|
261
|
1
|
|
|
|
|
18
|
$string =~ s/[\n\r]*$/\t/s; # remove crlf, and tack on an extra tab |
262
|
1
|
|
|
|
|
6
|
my @fields = $string =~ m/(.*?)\t/g; # is it the tab-delimited version? |
263
|
1
|
50
|
|
|
|
5
|
if (@fields == 9) { |
264
|
|
|
|
|
|
|
return { |
265
|
0
|
|
|
|
|
0
|
name => $fields[0], # return ref to hash |
266
|
|
|
|
|
|
|
agaNum => $fields[1], |
267
|
|
|
|
|
|
|
memType => $fields[2], |
268
|
|
|
|
|
|
|
agaRating => $fields[3], |
269
|
|
|
|
|
|
|
expire => $fields[4], |
270
|
|
|
|
|
|
|
club => $fields[5], |
271
|
|
|
|
|
|
|
state => $fields[6], |
272
|
|
|
|
|
|
|
sigma => $fields[7], |
273
|
|
|
|
|
|
|
ratingDate => $fields[8], |
274
|
|
|
|
|
|
|
country => $self->{defaultCountry}, |
275
|
|
|
|
|
|
|
}; |
276
|
|
|
|
|
|
|
} |
277
|
|
|
|
|
|
|
# else parse a space-delimited version: |
278
|
1
|
|
|
|
|
2
|
my ($name, $agaNum, $agaRank, $misc); |
279
|
1
|
|
|
|
|
4
|
my ($agaRating, $memType, $club, $state, $expire) = (-99, '', '', '', ''); |
280
|
|
|
|
|
|
|
|
281
|
1
|
50
|
|
|
|
11
|
unless($string =~ m/^\s*(.*?)\s*(\d+) (.*)/) { # break into manageble groups |
282
|
0
|
|
|
|
|
0
|
carp("Error: can't extract AGA number from \"$string\"\n"); |
283
|
0
|
|
|
|
|
0
|
return(undef); |
284
|
|
|
|
|
|
|
} |
285
|
1
|
|
|
|
|
3
|
$name = $1; # part before is name |
286
|
1
|
|
|
|
|
3
|
$agaNum = $2; # middle part is the AGA number |
287
|
1
|
|
|
|
|
3
|
$misc = $3; # part after match |
288
|
1
|
50
|
|
|
|
11
|
if ($misc =~ m/([\w ]{6}?) ([-\d\. ]{5}) ([\d\/ ]{10}) ([\w ]{4}) (.*?)\s*$/) { |
289
|
|
|
|
|
|
|
# parse by character positions (blanks lined up in the right places) |
290
|
0
|
|
|
|
|
0
|
$memType = _ws_clean($1); |
291
|
0
|
|
|
|
|
0
|
$agaRating = _ws_clean($2); |
292
|
0
|
|
|
|
|
0
|
$expire = _ws_clean($3); |
293
|
0
|
|
|
|
|
0
|
$club = _ws_clean($4); |
294
|
0
|
|
|
|
|
0
|
$state = _ws_clean($5); |
295
|
0
|
0
|
|
|
|
0
|
if ($agaRating =~ m/(\d+)([dk])/i) { |
296
|
0
|
|
|
|
|
0
|
$agaRank = uc($agaRating); |
297
|
0
|
|
|
|
|
0
|
$agaRating = $1 + 0.5; |
298
|
0
|
0
|
|
|
|
0
|
$agaRating = -$agaRating if (uc($2) eq 'K'); |
299
|
|
|
|
|
|
|
} |
300
|
|
|
|
|
|
|
} else { # try to parse free-form style |
301
|
1
|
50
|
|
|
|
9
|
if ($misc =~ s/^\s*([^\s\d-]+) //) { # membership type, if any |
|
|
0
|
|
|
|
|
|
302
|
1
|
|
|
|
|
3
|
$memType = $1; |
303
|
|
|
|
|
|
|
} elsif (not $misc =~ s/^ //) { |
304
|
0
|
|
|
|
|
0
|
carp("Uh oh, no membership type space in: '$misc'"); |
305
|
|
|
|
|
|
|
} |
306
|
1
|
50
|
|
|
|
8
|
if ($misc =~ s/^\s*(-?\d+\.\d) //) { # find rank, if any |
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
307
|
1
|
|
|
|
|
3
|
$agaRating = $1; |
308
|
|
|
|
|
|
|
} elsif ($misc =~ s/^\s*(\d+)([dkDK]) //) { # 4D or 15k type rank |
309
|
0
|
|
|
|
|
0
|
$agaRank = uc("$1$2"); |
310
|
0
|
|
|
|
|
0
|
$agaRating = $1 + 0.5; |
311
|
0
|
0
|
|
|
|
0
|
$agaRating = -$agaRating if (uc($2) eq 'K'); |
312
|
|
|
|
|
|
|
} elsif ($misc =~ s/^\s*(-?\d\d?) //) { # one or two digit number, no decimal point? |
313
|
0
|
|
|
|
|
0
|
$agaRating = $1; # it's another way of indicating rank |
314
|
|
|
|
|
|
|
} elsif (not $misc =~ s/^ //) { |
315
|
0
|
|
|
|
|
0
|
carp("Uh oh, no rating space in: '$misc'"); |
316
|
|
|
|
|
|
|
} |
317
|
1
|
50
|
|
|
|
6
|
if ($misc =~ s/^\s*([\d\/]+) //) { # expiration date, if any |
|
|
0
|
|
|
|
|
|
318
|
1
|
|
|
|
|
3
|
$expire = $1; |
319
|
|
|
|
|
|
|
} elsif (not $misc =~ s/ //) { |
320
|
0
|
|
|
|
|
0
|
carp("Uh oh, no expire space in: '$misc'"); |
321
|
|
|
|
|
|
|
} |
322
|
1
|
50
|
33
|
|
|
5
|
unless(defined($expire) or defined($memType)) { |
323
|
0
|
|
|
|
|
0
|
carp "Uh oh"; |
324
|
|
|
|
|
|
|
} |
325
|
1
|
50
|
|
|
|
6
|
if ($misc =~ s/^(\w+)\s*//) { # club |
|
|
0
|
|
|
|
|
|
326
|
1
|
|
|
|
|
2
|
$club = $1; |
327
|
1
|
|
|
|
|
4
|
$club =~ s/\W//g; # remove all non-word chars |
328
|
|
|
|
|
|
|
} elsif (not $misc =~ s/ //) { |
329
|
0
|
|
|
|
|
0
|
carp("Uh oh, no expire space in: '$misc'"); |
330
|
|
|
|
|
|
|
} |
331
|
1
|
50
|
|
|
|
8
|
if ($misc =~ s/^\s*(.*?)\s*$//) { # state |
332
|
1
|
|
|
|
|
4
|
$state = $1; |
333
|
|
|
|
|
|
|
} |
334
|
1
|
50
|
|
|
|
4
|
if ($misc ne '') { |
335
|
0
|
|
|
|
|
0
|
carp("Error: \"$misc\" was left over after parsing \"$string\"\n", |
336
|
|
|
|
|
|
|
"name=$name, id=$agaNum, mem=$memType, rating=$agaRating, ", |
337
|
|
|
|
|
|
|
"expire=$expire, club=$club, state=$state\n"); |
338
|
|
|
|
|
|
|
} |
339
|
|
|
|
|
|
|
} |
340
|
|
|
|
|
|
|
return { |
341
|
1
|
|
|
|
|
22
|
agaNum => $agaNum, # return ref to hash |
342
|
|
|
|
|
|
|
country => $self->{defaultCountry}, |
343
|
|
|
|
|
|
|
name => $name, |
344
|
|
|
|
|
|
|
memType => $memType, |
345
|
|
|
|
|
|
|
agaRating => $agaRating, |
346
|
|
|
|
|
|
|
agaRank => $agaRank, |
347
|
|
|
|
|
|
|
expire => $expire, |
348
|
|
|
|
|
|
|
club => $club, |
349
|
|
|
|
|
|
|
state => $state, |
350
|
|
|
|
|
|
|
}; |
351
|
|
|
|
|
|
|
} |
352
|
|
|
|
|
|
|
|
353
|
|
|
|
|
|
|
sub _ws_clean { |
354
|
0
|
|
|
0
|
|
0
|
my $str = shift @_; |
355
|
0
|
|
|
|
|
0
|
$str =~ m/^\s*(.*?)\s*$/; |
356
|
0
|
|
|
|
|
0
|
return $1; |
357
|
|
|
|
|
|
|
} |
358
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
=item my $result = $agaTourn-EB ($fileName) |
360
|
|
|
|
|
|
|
|
361
|
|
|
|
|
|
|
Reads a register.tde file and calls B on each line of the file. |
362
|
|
|
|
|
|
|
|
363
|
|
|
|
|
|
|
Returns 0 if $fileName couldn't be opened for reading, 1 otherwise. |
364
|
|
|
|
|
|
|
|
365
|
|
|
|
|
|
|
=cut |
366
|
|
|
|
|
|
|
|
367
|
|
|
|
|
|
|
sub ReadRegisterFile { |
368
|
1
|
|
|
1
|
1
|
3
|
my ($self, $fName) = @_; |
369
|
|
|
|
|
|
|
|
370
|
1
|
|
|
|
|
5
|
$self->{fileName} = $fName; # set global name |
371
|
1
|
|
|
|
|
12
|
my $inFP = new IO::File("<$fName"); |
372
|
1
|
50
|
|
|
|
151
|
unless ($inFP) { |
373
|
0
|
|
|
|
|
0
|
carp("Error: can't open $fName for reading\n"), |
374
|
|
|
|
|
|
|
$self->{error} = 1, |
375
|
|
|
|
|
|
|
return(0); |
376
|
|
|
|
|
|
|
} |
377
|
1
|
|
|
|
|
132
|
while(my $line = <$inFP>) { |
378
|
17
|
|
|
|
|
36
|
$self->AddRegisterLine($line); |
379
|
|
|
|
|
|
|
} |
380
|
1
|
|
|
|
|
13
|
$inFP->close(); |
381
|
1
|
|
|
|
|
30
|
return(1); |
382
|
|
|
|
|
|
|
} |
383
|
|
|
|
|
|
|
|
384
|
|
|
|
|
|
|
=item $agaTourn-EB ($line) |
385
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
Calls B on $line. Information extracted about players and |
387
|
|
|
|
|
|
|
directives is added to the $agaTourn object. Comments and blank lines are |
388
|
|
|
|
|
|
|
ignored. |
389
|
|
|
|
|
|
|
|
390
|
|
|
|
|
|
|
=cut |
391
|
|
|
|
|
|
|
|
392
|
|
|
|
|
|
|
sub AddRegisterLine { |
393
|
17
|
|
|
17
|
1
|
27
|
my ($self, $line) = @_; |
394
|
|
|
|
|
|
|
|
395
|
17
|
50
|
33
|
|
|
104
|
my $fileMsg = (ref ($self) and exists ($self->{fileName})) ? |
396
|
|
|
|
|
|
|
" at line $. in $self->{fileName} " : |
397
|
|
|
|
|
|
|
''; |
398
|
17
|
|
|
|
|
35
|
my $h = $self->ParseRegisterLine($line); |
399
|
17
|
100
|
|
|
|
46
|
return unless(defined($h)); |
400
|
16
|
100
|
|
|
|
36
|
if (exists($h->{directive})) { |
401
|
9
|
|
|
|
|
16
|
foreach (qw(HANDICAPS ROUNDS RULES TOURNEY)) { # non-array directives |
402
|
30
|
100
|
|
|
|
78
|
if ($h->{directive} eq $_) { |
403
|
4
|
|
|
|
|
14
|
$self->{Directive}{$h->{directive}} = [$h->{value}]; # single value |
404
|
4
|
|
|
|
|
29
|
return; |
405
|
|
|
|
|
|
|
} |
406
|
|
|
|
|
|
|
} |
407
|
5
|
|
|
|
|
7
|
push(@{$self->{Directive}{$h->{directive}}}, $h->{value}); |
|
5
|
|
|
|
|
15
|
|
408
|
5
|
|
|
|
|
22
|
return; |
409
|
|
|
|
|
|
|
} |
410
|
7
|
100
|
|
|
|
25
|
return unless(exists($h->{agaNum})); # probably a comment |
411
|
4
|
|
|
|
|
9
|
my $id = "$h->{country}$h->{agaNum}"; |
412
|
4
|
50
|
|
|
|
11
|
if (defined($self->{Name}{$id})) { |
413
|
0
|
|
|
|
|
0
|
carp("Error: Player ID $id is duplicated$fileMsg\n"); |
414
|
0
|
|
|
|
|
0
|
$self->{error} = 1; |
415
|
|
|
|
|
|
|
} |
416
|
4
|
|
|
|
|
16
|
$self->{Name}{$id} = $h->{name}; |
417
|
4
|
|
|
|
|
8
|
$self->{Rating}{$id} = $h->{agaRating}; |
418
|
4
|
|
|
|
|
8
|
$self->{Rank}{$id} = $h->{agaRank}; |
419
|
4
|
|
|
|
|
138
|
$self->{Comment}{$id} = $h->{comment}; |
420
|
4
|
|
|
|
|
10
|
$self->{Club}{$id} = $h->{club}; |
421
|
4
|
|
|
|
|
10
|
$self->{Flags}{$id} = $h->{flags}; |
422
|
4
|
50
|
|
|
|
16
|
$self->{Played}{$id} = [] unless exists($self->{Played}{$id}); |
423
|
4
|
|
|
|
|
10
|
foreach (qw(Wins Losses NoResults)) { |
424
|
12
|
50
|
|
|
|
41
|
$self->{$_}{$id} = 0 unless exists($self->{$_}{$id}); |
425
|
|
|
|
|
|
|
} |
426
|
4
|
|
|
|
|
9
|
my $len = length($h->{name}); |
427
|
4
|
100
|
|
|
|
31
|
$self->{nameLength} = $len if ($len > $self->{nameLength}); |
428
|
|
|
|
|
|
|
} |
429
|
|
|
|
|
|
|
|
430
|
|
|
|
|
|
|
=item my $hash = $agaTourn-EB ($line) |
431
|
|
|
|
|
|
|
|
432
|
|
|
|
|
|
|
Parses a single line from the register.tde file (name lines). Here are some |
433
|
|
|
|
|
|
|
examples lines from register.tde file: |
434
|
|
|
|
|
|
|
|
435
|
|
|
|
|
|
|
# this line is a comment. the following line is a directive: |
436
|
|
|
|
|
|
|
## HANDICAPS MAX |
437
|
|
|
|
|
|
|
# the following line is a name line: |
438
|
|
|
|
|
|
|
USA02122 Augustin, Reid 5.0 CLUB=PALO # 12/31/2004 CA |
439
|
|
|
|
|
|
|
|
440
|
|
|
|
|
|
|
The return value is a reference to a hash of the following values: |
441
|
|
|
|
|
|
|
agaNum => just the number part of the ID |
442
|
|
|
|
|
|
|
country => just the country part of the ID (default ='USA') |
443
|
|
|
|
|
|
|
name => complains if name doesn't contain a comma |
444
|
|
|
|
|
|
|
agaRating => rating for the player |
445
|
|
|
|
|
|
|
agaRank => undef if line contains a rating and not a rank |
446
|
|
|
|
|
|
|
club => if there is a club association, '' if not |
447
|
|
|
|
|
|
|
flags => anything left over (excluding comment) |
448
|
|
|
|
|
|
|
comment => everything after the #, '' if none |
449
|
|
|
|
|
|
|
|
450
|
|
|
|
|
|
|
If the line is a directive, the return hash reference contains only: |
451
|
|
|
|
|
|
|
directive => the directive name |
452
|
|
|
|
|
|
|
value => the directive value ('' if none) |
453
|
|
|
|
|
|
|
|
454
|
|
|
|
|
|
|
If the line is a comment, leading and trailing whitespace is removed and the |
455
|
|
|
|
|
|
|
hash contains only: |
456
|
|
|
|
|
|
|
comment => comment contents (may be '') |
457
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
If the line is empty, returns undef. |
459
|
|
|
|
|
|
|
|
460
|
|
|
|
|
|
|
If the line is not parsable, prints a warning and returns undef. |
461
|
|
|
|
|
|
|
|
462
|
|
|
|
|
|
|
=cut |
463
|
|
|
|
|
|
|
|
464
|
|
|
|
|
|
|
sub ParseRegisterLine { |
465
|
18
|
|
|
18
|
1
|
30
|
my ($self, $line) = @_; |
466
|
|
|
|
|
|
|
|
467
|
18
|
|
|
|
|
134
|
$line =~ s/\s*$//s; # delete trailing spaces |
468
|
18
|
100
|
|
|
|
45
|
return undef if ($line eq ''); # nothing left? return undef |
469
|
|
|
|
|
|
|
|
470
|
17
|
100
|
|
|
|
57
|
if ($line =~ s/^\s*##\s*//) { |
471
|
9
|
|
|
|
|
40
|
$line =~ m/(\S+)\s*(.*?)\s*$/; |
472
|
|
|
|
|
|
|
return { |
473
|
9
|
|
|
|
|
39
|
directive => $1, |
474
|
|
|
|
|
|
|
value => $2 |
475
|
|
|
|
|
|
|
}; |
476
|
|
|
|
|
|
|
} |
477
|
8
|
|
|
|
|
11
|
my $comment = ''; |
478
|
8
|
50
|
|
|
|
73
|
if ($line =~ s/\s*#\s*(.*?)\s*$//) { |
479
|
8
|
|
|
|
|
18
|
$comment = $1; |
480
|
|
|
|
|
|
|
} |
481
|
8
|
100
|
|
|
|
19
|
if ($line eq '') { |
482
|
|
|
|
|
|
|
return { |
483
|
3
|
|
|
|
|
8
|
comment => $comment, |
484
|
|
|
|
|
|
|
}; |
485
|
|
|
|
|
|
|
} |
486
|
|
|
|
|
|
|
|
487
|
5
|
50
|
33
|
|
|
39
|
my $fileMsg = (ref ($self) and exists ($self->{fileName})) ? |
488
|
|
|
|
|
|
|
" at line $. in $self->{fileName} " : |
489
|
|
|
|
|
|
|
''; |
490
|
5
|
|
|
|
|
7
|
my $club = ''; |
491
|
5
|
50
|
|
|
|
53
|
if ($line =~ s/\s*CLUB=(\S*)\s*//) { |
492
|
5
|
|
|
|
|
8
|
$club = $1; |
493
|
5
|
|
|
|
|
10
|
$club =~ s/\W//g; # remove all non-word chars |
494
|
|
|
|
|
|
|
} |
495
|
5
|
|
|
|
|
18
|
my ($agaRating, $agaRank); |
496
|
5
|
100
|
|
|
|
89
|
if($line =~ s/^\s*(\S*)\s+(.*?)\s+(\d+[dDkK])\s*//) { # look for dan or kyu rank |
|
|
50
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
497
|
2
|
|
|
|
|
4
|
$agaRank = $3; |
498
|
2
|
|
|
|
|
7
|
$agaRating = $self->RankToRating($3); |
499
|
|
|
|
|
|
|
} elsif($line =~ s/^\s*(\S*)\s+(.*?)\s+(-*\d+\.\d+)\s*//) { # look for 5.4 or -13.6 type of rank |
500
|
3
|
|
|
|
|
9
|
$agaRating = $3; # ok as is |
501
|
|
|
|
|
|
|
} elsif($line =~ s/^\s*(\S*)\s+(.*?)\s+(-*\d+)\s*//) { # look for 5 or -13 type of rank |
502
|
0
|
|
|
|
|
0
|
carp("Warning: rank is non-decimalized:\n$line\n"); |
503
|
0
|
|
|
|
|
0
|
$agaRating = "$3.0"; |
504
|
|
|
|
|
|
|
} else { |
505
|
0
|
|
|
|
|
0
|
carp("Error: Can't parse name$fileMsg:\n$line\n"); |
506
|
0
|
|
|
|
|
0
|
$self->{error} = 1; |
507
|
0
|
|
|
|
|
0
|
return; |
508
|
|
|
|
|
|
|
} |
509
|
|
|
|
|
|
|
|
510
|
5
|
|
|
|
|
11
|
my $name = $2; |
511
|
5
|
|
|
|
|
23
|
my $agaNum = $self->NormalizeID($1); |
512
|
5
|
|
|
|
|
11
|
my $country = $self->{defaultCountry}; |
513
|
5
|
50
|
|
|
|
25
|
if ($agaNum =~ s/^(\D+)//) { |
514
|
5
|
|
|
|
|
10
|
$country = uc($1); |
515
|
|
|
|
|
|
|
} |
516
|
5
|
50
|
|
|
|
18
|
unless ($name =~ m/,/) { |
517
|
0
|
|
|
|
|
0
|
carp("Warning: no comma in name \"$name\"$fileMsg\n"); |
518
|
|
|
|
|
|
|
} |
519
|
|
|
|
|
|
|
return { # return ref to hash of: |
520
|
5
|
|
|
|
|
51
|
agaNum => $agaNum, |
521
|
|
|
|
|
|
|
name => $name, |
522
|
|
|
|
|
|
|
agaRating => $agaRating, |
523
|
|
|
|
|
|
|
agaRank => $agaRank, |
524
|
|
|
|
|
|
|
club => $club, |
525
|
|
|
|
|
|
|
country => $country, |
526
|
|
|
|
|
|
|
flags => $line, # whatever's left over |
527
|
|
|
|
|
|
|
comment => $comment, |
528
|
|
|
|
|
|
|
}; |
529
|
|
|
|
|
|
|
} |
530
|
|
|
|
|
|
|
|
531
|
|
|
|
|
|
|
=item my $result = $agaTourn-EB ($fileName) |
532
|
|
|
|
|
|
|
|
533
|
|
|
|
|
|
|
Reads a round file and calls B on each line of the file. |
534
|
|
|
|
|
|
|
Complains if filename is not in the form I<1.tde>, I<2.tde>, etc. |
535
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
Sets the current B number to the digit part of fileName. |
537
|
|
|
|
|
|
|
|
538
|
|
|
|
|
|
|
Returns 0 if fileName couldn't be opened for reading, 1 otherwise. |
539
|
|
|
|
|
|
|
|
540
|
|
|
|
|
|
|
=cut |
541
|
|
|
|
|
|
|
|
542
|
|
|
|
|
|
|
sub ReadRoundFile { |
543
|
1
|
|
|
1
|
1
|
3
|
my ($self, $fName) = @_; |
544
|
|
|
|
|
|
|
|
545
|
1
|
50
|
|
|
|
9
|
if ($fName =~ m/^\d+$/) { # no TDE extension? |
546
|
0
|
|
|
|
|
0
|
$fName .= '.tde'; |
547
|
|
|
|
|
|
|
} |
548
|
1
|
|
|
|
|
4
|
$self->{fileName} = $fName; # set global name |
549
|
1
|
50
|
|
|
|
8
|
if ($fName =~ m/(\d+).tde/) { |
550
|
1
|
|
|
|
|
5
|
$self->{Round} = $1; |
551
|
|
|
|
|
|
|
} else { |
552
|
0
|
|
|
|
|
0
|
carp "Round filename not in normal ('1.tde', '2.tde', etc) format\n"; |
553
|
|
|
|
|
|
|
} |
554
|
1
|
|
|
|
|
11
|
my $inFP = new IO::File("<$fName"); |
555
|
1
|
50
|
|
|
|
144
|
unless ($inFP) { |
556
|
0
|
|
|
|
|
0
|
carp("Error: can't open $fName for reading\n"); |
557
|
0
|
|
|
|
|
0
|
$self->{error} = 1; |
558
|
0
|
|
|
|
|
0
|
return(0); |
559
|
|
|
|
|
|
|
} |
560
|
1
|
|
|
|
|
31
|
while (my $line = <$inFP>) { |
561
|
4
|
|
|
|
|
12
|
$self->AddRoundLine($line); |
562
|
|
|
|
|
|
|
} |
563
|
1
|
|
|
|
|
8
|
$inFP->close(); |
564
|
1
|
|
|
|
|
28
|
return(1); |
565
|
|
|
|
|
|
|
} |
566
|
|
|
|
|
|
|
|
567
|
|
|
|
|
|
|
=item $agaTourn-EB ($line) |
568
|
|
|
|
|
|
|
|
569
|
|
|
|
|
|
|
Parses $line (by calling B) and adds the information to the |
570
|
|
|
|
|
|
|
B. Games without a result ('?') increment both players' NoResults |
571
|
|
|
|
|
|
|
list scores, and games with a result ('b' or 'w') increment the two players' |
572
|
|
|
|
|
|
|
Wins and Losses scores. If the game result is 'b' or 'w', the black player is |
573
|
|
|
|
|
|
|
added to the white player's B list and vica-versa. Note that |
574
|
|
|
|
|
|
|
B is not affected by games that are not complete. |
575
|
|
|
|
|
|
|
|
576
|
|
|
|
|
|
|
Complains if either player, or both, are not registered via |
577
|
|
|
|
|
|
|
B. |
578
|
|
|
|
|
|
|
|
579
|
|
|
|
|
|
|
=cut |
580
|
|
|
|
|
|
|
|
581
|
|
|
|
|
|
|
sub AddRoundLine { |
582
|
4
|
|
|
4
|
1
|
9
|
my ($self, $line) = @_; |
583
|
|
|
|
|
|
|
|
584
|
4
|
|
|
|
|
10
|
my $g = $self->ParseRoundLine($line); # get game result |
585
|
4
|
100
|
66
|
|
|
29
|
return unless(defined($g) and exists($g->{result})); |
586
|
2
|
|
|
|
|
11
|
my $wId = $self->NormalizeID("$g->{wcountry}$g->{wagaNum}"); |
587
|
2
|
|
|
|
|
8
|
my $bId = $self->NormalizeID("$g->{bcountry}$g->{bagaNum}"); |
588
|
2
|
50
|
|
|
|
8
|
carp("Game $wId.vs.$bId, $wId is not registered\n") unless (exists($self->{Rating}{$wId})); |
589
|
2
|
50
|
|
|
|
8
|
carp("Game $wId.vs.$bId, $bId is not registered\n") unless (exists($self->{Rating}{$bId})); |
590
|
2
|
|
|
|
|
5
|
foreach (qw(Wins Losses NoResults)) { |
591
|
6
|
50
|
|
|
|
14
|
$self->{$_}{$wId} = 0 unless exists($self->{$_}{$wId}); |
592
|
6
|
50
|
|
|
|
26
|
$self->{$_}{$bId} = 0 unless exists($self->{$_}{$bId}); |
593
|
|
|
|
|
|
|
} |
594
|
2
|
50
|
|
|
|
24
|
if ($g->{result} eq 'w') { |
|
|
100
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
595
|
0
|
|
|
|
|
0
|
$self->{Wins}{$wId}++; |
596
|
0
|
|
|
|
|
0
|
$self->{Losses}{$bId}++; |
597
|
0
|
|
|
|
|
0
|
push(@{$self->{Played}{$bId}}, $wId); |
|
0
|
|
|
|
|
0
|
|
598
|
0
|
|
|
|
|
0
|
push(@{$self->{Played}{$wId}}, $bId); |
|
0
|
|
|
|
|
0
|
|
599
|
|
|
|
|
|
|
} elsif ($g->{result} eq 'b') { |
600
|
1
|
|
|
|
|
3
|
$self->{Wins}{$bId}++; |
601
|
1
|
|
|
|
|
3
|
$self->{Losses}{$wId}++; |
602
|
1
|
|
|
|
|
3
|
push(@{$self->{Played}{$bId}}, $wId); |
|
1
|
|
|
|
|
4
|
|
603
|
1
|
|
|
|
|
2
|
push(@{$self->{Played}{$wId}}, $bId); |
|
1
|
|
|
|
|
3
|
|
604
|
|
|
|
|
|
|
} elsif ($g->{result} eq '?') { |
605
|
1
|
|
|
|
|
2
|
$self->{NoResults}{$bId}++; |
606
|
1
|
|
|
|
|
2
|
$self->{NoResults}{$wId}++; |
607
|
|
|
|
|
|
|
} else { |
608
|
0
|
|
|
|
|
0
|
carp("Unknown game result:$g->{result}"); # probably can't happen |
609
|
|
|
|
|
|
|
} |
610
|
2
|
|
|
|
|
10
|
my $game = "$wId,$bId,$g->{result},$g->{handi},$g->{komi},$self->{Round}"; |
611
|
2
|
|
|
|
|
3
|
push(@{$self->{gameAllList}}, $game); |
|
2
|
|
|
|
|
6
|
|
612
|
2
|
|
|
|
|
2
|
push(@{$self->{gameIDList}{$wId}}, $game); |
|
2
|
|
|
|
|
8
|
|
613
|
2
|
|
|
|
|
3
|
push(@{$self->{gameIDList}{$bId}}, $game); |
|
2
|
|
|
|
|
24
|
|
614
|
|
|
|
|
|
|
} |
615
|
|
|
|
|
|
|
|
616
|
|
|
|
|
|
|
=item my $hash = $agaTourn-EB ($line) |
617
|
|
|
|
|
|
|
|
618
|
|
|
|
|
|
|
Parses a single line from a results file (I<1.tde>, I<2.tde>, etc). Here's an |
619
|
|
|
|
|
|
|
example line from a results file: |
620
|
|
|
|
|
|
|
|
621
|
|
|
|
|
|
|
TMP18 TMP10 b 0 7 # Lee, Ken -28.5 : Yang, John -28.5 |
622
|
|
|
|
|
|
|
# wID bID result handi komi comment |
623
|
|
|
|
|
|
|
|
624
|
|
|
|
|
|
|
The return value is a reference to a hash of the following values: |
625
|
|
|
|
|
|
|
wcountry => combine with wagaNum to get complete ID |
626
|
|
|
|
|
|
|
wagaNum => the number part of white's AGA number |
627
|
|
|
|
|
|
|
bcountry => combine with bagaNum to get complete ID |
628
|
|
|
|
|
|
|
bagaNum => the number part of black's AGA number |
629
|
|
|
|
|
|
|
result => winner: 'b', 'w' or '?' |
630
|
|
|
|
|
|
|
handi => handicap game was played with |
631
|
|
|
|
|
|
|
komi => komi game was played with |
632
|
|
|
|
|
|
|
comment => everything after the # |
633
|
|
|
|
|
|
|
|
634
|
|
|
|
|
|
|
If $line is empty, returns undef. |
635
|
|
|
|
|
|
|
|
636
|
|
|
|
|
|
|
If $line is a comment, returns only: |
637
|
|
|
|
|
|
|
comment => everything after the # |
638
|
|
|
|
|
|
|
|
639
|
|
|
|
|
|
|
If the line is not parsable, prints a warning and returns undef. |
640
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
=cut |
642
|
|
|
|
|
|
|
|
643
|
|
|
|
|
|
|
sub ParseRoundLine { |
644
|
4
|
|
|
4
|
1
|
7
|
my ($self, $line) = @_; |
645
|
|
|
|
|
|
|
|
646
|
4
|
|
|
|
|
39
|
$line =~ s/\s*$//s; # delete trailing spaces |
647
|
4
|
50
|
|
|
|
15
|
return undef if ($line eq ''); # nothing left? return undef |
648
|
|
|
|
|
|
|
|
649
|
4
|
100
|
|
|
|
19
|
if ($line =~ s/^\s*##\s*//) { |
650
|
2
|
|
|
|
|
6
|
$line =~ m/(\S+)\s*(.*?)\s*/; |
651
|
|
|
|
|
|
|
return { |
652
|
2
|
|
|
|
|
11
|
directive => $1, |
653
|
|
|
|
|
|
|
value => $2 |
654
|
|
|
|
|
|
|
}; |
655
|
|
|
|
|
|
|
} |
656
|
2
|
|
|
|
|
4
|
my $comment = ''; |
657
|
2
|
50
|
|
|
|
24
|
if ($line =~ s/\s*#\s*(.*?)\s*$//) { |
658
|
2
|
|
|
|
|
7
|
$comment = $1; |
659
|
|
|
|
|
|
|
} |
660
|
2
|
50
|
|
|
|
5
|
if ($line eq '') { |
661
|
|
|
|
|
|
|
return { |
662
|
0
|
|
|
|
|
0
|
comment => $comment, |
663
|
|
|
|
|
|
|
}; |
664
|
|
|
|
|
|
|
} |
665
|
|
|
|
|
|
|
|
666
|
2
|
50
|
|
|
|
15
|
if ($line =~ m/^\s*(\w+)(\d+)\s+(\w+)(\d+)\s+([bwBW\?])\s+(\d+)\s+(-?\d+)$/) { |
667
|
|
|
|
|
|
|
return { |
668
|
2
|
|
|
|
|
47
|
wcountry => uc($1), |
669
|
|
|
|
|
|
|
wagaNum => $2, |
670
|
|
|
|
|
|
|
bcountry => uc($3), |
671
|
|
|
|
|
|
|
bagaNum => $4, |
672
|
|
|
|
|
|
|
result => lc($5), |
673
|
|
|
|
|
|
|
handi => $6, |
674
|
|
|
|
|
|
|
komi => $7, |
675
|
|
|
|
|
|
|
comment => $comment, |
676
|
|
|
|
|
|
|
}; |
677
|
|
|
|
|
|
|
} |
678
|
0
|
0
|
0
|
|
|
0
|
my $fileMsg = (ref ($self) and exists ($self->{fileName})) ? |
679
|
|
|
|
|
|
|
" at line $. in $self->{fileName} " : |
680
|
|
|
|
|
|
|
''; |
681
|
0
|
|
|
|
|
0
|
carp("Can't parse round line $.$fileMsg:\n$line\n"); |
682
|
0
|
|
|
|
|
0
|
$self->{error} = 1; |
683
|
0
|
|
|
|
|
0
|
return undef; |
684
|
|
|
|
|
|
|
} |
685
|
|
|
|
|
|
|
|
686
|
|
|
|
|
|
|
=item my $tourney = $agaTourn-EB |
687
|
|
|
|
|
|
|
|
688
|
|
|
|
|
|
|
Returns the name of the tournament from a ##TOURNEY directive added via |
689
|
|
|
|
|
|
|
B, or 'Unknown Tournament' if no TOURNEY directive has been |
690
|
|
|
|
|
|
|
added. |
691
|
|
|
|
|
|
|
|
692
|
|
|
|
|
|
|
=cut |
693
|
|
|
|
|
|
|
|
694
|
|
|
|
|
|
|
sub Tourney { |
695
|
2
|
|
|
2
|
1
|
4103
|
my ($self) = @_; |
696
|
2
|
|
|
|
|
17
|
return ($self->{Directive}{TOURNEY}[0]); # last TOURNEY directive |
697
|
|
|
|
|
|
|
} |
698
|
|
|
|
|
|
|
|
699
|
|
|
|
|
|
|
=item my $directive = $agaTourn-EB ($directive) |
700
|
|
|
|
|
|
|
|
701
|
|
|
|
|
|
|
Returns a list (or a reference to the list in scalar context) of directives |
702
|
|
|
|
|
|
|
added via calls to B. Directive names are always turned into |
703
|
|
|
|
|
|
|
upper case (but the case of the directive value, if any, is preserved). |
704
|
|
|
|
|
|
|
|
705
|
|
|
|
|
|
|
Since some directives (like BAND) may occur several times, all directives are |
706
|
|
|
|
|
|
|
stored as a list in the order added (either from B or |
707
|
|
|
|
|
|
|
B). Certain directives (HANDICAPS ROUNDS RULES TOURNEY) keep |
708
|
|
|
|
|
|
|
only the last directive added. |
709
|
|
|
|
|
|
|
|
710
|
|
|
|
|
|
|
Some directives have no associated value. |
711
|
|
|
|
|
|
|
|
712
|
|
|
|
|
|
|
B returns undef if $directive has not been added, or a list |
713
|
|
|
|
|
|
|
(possibly empty) if $directive has been added. |
714
|
|
|
|
|
|
|
|
715
|
|
|
|
|
|
|
If called with no arguments (or $directive is undef), returns a reference to a |
716
|
|
|
|
|
|
|
hash of all the current directives. |
717
|
|
|
|
|
|
|
|
718
|
|
|
|
|
|
|
=cut |
719
|
|
|
|
|
|
|
|
720
|
|
|
|
|
|
|
sub Directive { |
721
|
2
|
|
|
2
|
1
|
7
|
my ($self, $directive) = @_; |
722
|
|
|
|
|
|
|
|
723
|
2
|
50
|
|
|
|
10
|
if (defined($directive)) { |
724
|
0
|
|
|
|
|
0
|
$directive = uc($directive); # force to upper case |
725
|
0
|
0
|
|
|
|
0
|
if (exists($self->{Directive}{$directive})) { |
726
|
0
|
0
|
|
|
|
0
|
return wantarray ? @{$self->{Directive}{$directive}} : $self->{Directive}{$directive}; |
|
0
|
|
|
|
|
0
|
|
727
|
|
|
|
|
|
|
} |
728
|
0
|
|
|
|
|
0
|
return(undef); |
729
|
|
|
|
|
|
|
} |
730
|
2
|
|
|
|
|
33
|
return($self->{Directive}); # the whole shebang... |
731
|
|
|
|
|
|
|
} |
732
|
|
|
|
|
|
|
|
733
|
|
|
|
|
|
|
=item my $rounds = $agaTourn-EB |
734
|
|
|
|
|
|
|
|
735
|
|
|
|
|
|
|
Returns the total number of rounds the $agaTourn object knows about. If there |
736
|
|
|
|
|
|
|
has been a ##ROUNDS directive in a call to B file, this will |
737
|
|
|
|
|
|
|
return that number. If not, it will return the number part of the last |
738
|
|
|
|
|
|
|
I.tde file read or undef. |
739
|
|
|
|
|
|
|
|
740
|
|
|
|
|
|
|
=cut |
741
|
|
|
|
|
|
|
|
742
|
|
|
|
|
|
|
sub Rounds { |
743
|
2
|
|
|
2
|
1
|
6
|
my ($self) = @_; |
744
|
|
|
|
|
|
|
|
745
|
2
|
50
|
|
|
|
18
|
return $self->{Directive}{ROUNDS}[0] # fetch ROUNDS directive |
746
|
|
|
|
|
|
|
if(defined($self->{Directive}{ROUNDS}[0])); |
747
|
0
|
|
|
|
|
0
|
return($self->{Round}); |
748
|
|
|
|
|
|
|
} |
749
|
|
|
|
|
|
|
|
750
|
|
|
|
|
|
|
=item my $round = $agaTourn-EB |
751
|
|
|
|
|
|
|
|
752
|
|
|
|
|
|
|
Returns the number of the current round (based on the last I.tde |
753
|
|
|
|
|
|
|
file read). |
754
|
|
|
|
|
|
|
|
755
|
|
|
|
|
|
|
=cut |
756
|
|
|
|
|
|
|
|
757
|
|
|
|
|
|
|
sub Round { |
758
|
2
|
|
|
2
|
1
|
5
|
my ($self) = @_; |
759
|
2
|
|
|
|
|
10
|
return($self->{Round}); |
760
|
|
|
|
|
|
|
} |
761
|
|
|
|
|
|
|
|
762
|
|
|
|
|
|
|
=item my $name = $agaTourn-EB ($id) |
763
|
|
|
|
|
|
|
|
764
|
|
|
|
|
|
|
Returns the the name for $id. |
765
|
|
|
|
|
|
|
|
766
|
|
|
|
|
|
|
If $id is undef, returns a reference to the entire B hash (keyed by ID). |
767
|
|
|
|
|
|
|
|
768
|
|
|
|
|
|
|
=cut |
769
|
|
|
|
|
|
|
|
770
|
|
|
|
|
|
|
sub Name { |
771
|
2
|
|
|
2
|
1
|
5
|
my ($self, $id) = @_; |
772
|
|
|
|
|
|
|
|
773
|
2
|
50
|
|
|
|
9
|
return ($self->{Name}{$id}) if (defined($id)); |
774
|
2
|
|
|
|
|
24
|
return ($self->{Name}); |
775
|
|
|
|
|
|
|
} |
776
|
|
|
|
|
|
|
|
777
|
|
|
|
|
|
|
=item my $name_length = $agaTourn-EB |
778
|
|
|
|
|
|
|
|
779
|
|
|
|
|
|
|
Returns the length of the longest name. |
780
|
|
|
|
|
|
|
|
781
|
|
|
|
|
|
|
=cut |
782
|
|
|
|
|
|
|
|
783
|
|
|
|
|
|
|
sub NameLength { |
784
|
2
|
|
|
2
|
1
|
6
|
my ($self) = @_; |
785
|
2
|
|
|
|
|
13
|
return ($self->{nameLength}); |
786
|
|
|
|
|
|
|
} |
787
|
|
|
|
|
|
|
|
788
|
|
|
|
|
|
|
=item my $rating = $agaTourn-EB ($id, $newRating) |
789
|
|
|
|
|
|
|
|
790
|
|
|
|
|
|
|
Sets (if $newRating is defined) or returns the rating for $id. If $id is not |
791
|
|
|
|
|
|
|
defined, returns a reference to the entire B hash (keyed by IDs). |
792
|
|
|
|
|
|
|
|
793
|
|
|
|
|
|
|
$id can also be a rank ('4d', or '5k'), or a rating (4.2 or -5.3, but not |
794
|
|
|
|
|
|
|
between 1.0 and -1.0). This form is simply a converter - $newRating is not |
795
|
|
|
|
|
|
|
accepted. |
796
|
|
|
|
|
|
|
|
797
|
|
|
|
|
|
|
If $id is defined but not registered (via B), complains and |
798
|
|
|
|
|
|
|
returns undef. |
799
|
|
|
|
|
|
|
|
800
|
|
|
|
|
|
|
=cut |
801
|
|
|
|
|
|
|
|
802
|
|
|
|
|
|
|
sub Rating { |
803
|
2
|
|
|
2
|
1
|
6
|
my ($self, $id, $newRating) = @_; |
804
|
|
|
|
|
|
|
|
805
|
2
|
50
|
|
|
|
10
|
$self->{Rating}{$id} = $newRating if (defined($newRating)); |
806
|
2
|
50
|
|
|
|
43
|
if (defined($id)) { |
807
|
0
|
0
|
|
|
|
0
|
return ($self->{Rating}{$id}) if (exists($self->{Rating}{$id})); |
808
|
0
|
0
|
|
|
|
0
|
if ($id =~ m/^(-?\d+\.\d)\s*/) { # find rank |
809
|
0
|
|
|
|
|
0
|
return $1; # rating format |
810
|
|
|
|
|
|
|
} |
811
|
0
|
0
|
|
|
|
0
|
if ($id =~ m/^\s*(\d+)([dkDK])\b/) { # 4D or 15k type rank |
812
|
0
|
|
|
|
|
0
|
my $rating = $1; |
813
|
0
|
0
|
|
|
|
0
|
$rating = -$rating if (lc($2) eq 'k'); |
814
|
0
|
|
|
|
|
0
|
return $rating; |
815
|
|
|
|
|
|
|
} |
816
|
0
|
0
|
|
|
|
0
|
if ($id =~ m/^\s*(-?\d\d?)\b/) { # one or two digit number, no decimal point? |
817
|
0
|
|
|
|
|
0
|
return $1; # it's another way of indicating rank |
818
|
|
|
|
|
|
|
} |
819
|
0
|
|
|
|
|
0
|
carp ("Invalid Rating argument:$id\n"); |
820
|
0
|
|
|
|
|
0
|
return undef; # eh? |
821
|
|
|
|
|
|
|
} |
822
|
2
|
|
|
|
|
18
|
return ($self->{Rating}); |
823
|
|
|
|
|
|
|
} |
824
|
|
|
|
|
|
|
|
825
|
|
|
|
|
|
|
=item my $rank = $agaTourn-EB ($id) |
826
|
|
|
|
|
|
|
|
827
|
|
|
|
|
|
|
Returns the rank for $id. This field is undef unless the B |
828
|
|
|
|
|
|
|
contained a rank field of the form '4k' or '3d' as opposed to a rating of the |
829
|
|
|
|
|
|
|
form '-4.5' or '3.4'. |
830
|
|
|
|
|
|
|
|
831
|
|
|
|
|
|
|
If $id is not defined, returns a reference to the entire B hash (keyed |
832
|
|
|
|
|
|
|
by IDs). |
833
|
|
|
|
|
|
|
|
834
|
|
|
|
|
|
|
=cut |
835
|
|
|
|
|
|
|
|
836
|
|
|
|
|
|
|
sub Rank { |
837
|
0
|
|
|
0
|
1
|
0
|
my ($self, $id) = @_; |
838
|
|
|
|
|
|
|
|
839
|
0
|
0
|
|
|
|
0
|
return ($self->{Rank}{$id}) if(defined($id)); |
840
|
0
|
|
|
|
|
0
|
return ($self->{Rank}); |
841
|
|
|
|
|
|
|
} |
842
|
|
|
|
|
|
|
|
843
|
|
|
|
|
|
|
=item my $sigma = $agaTourn-EB ($id) |
844
|
|
|
|
|
|
|
|
845
|
|
|
|
|
|
|
Returns the sigma for $id. Sigma is determined by the rating/rank in the |
846
|
|
|
|
|
|
|
B. If the line contains a rank field of the form '4k' or '3d', |
847
|
|
|
|
|
|
|
sigma is 1.2 for 7k and stronger, and |
848
|
|
|
|
|
|
|
|
849
|
|
|
|
|
|
|
(k - 0.3) / 6 |
850
|
|
|
|
|
|
|
|
851
|
|
|
|
|
|
|
for 8k and weaker. If the line contains a rating of the form '-4.5' or '3.4', |
852
|
|
|
|
|
|
|
sigma is 0.6 for -8.0 and stronger, and |
853
|
|
|
|
|
|
|
|
854
|
|
|
|
|
|
|
(-rating - 4.4) / 6 |
855
|
|
|
|
|
|
|
|
856
|
|
|
|
|
|
|
for weaker than -8.0. |
857
|
|
|
|
|
|
|
|
858
|
|
|
|
|
|
|
Complains and returns undef if $id is undefined or unregistered. |
859
|
|
|
|
|
|
|
|
860
|
|
|
|
|
|
|
=cut |
861
|
|
|
|
|
|
|
|
862
|
|
|
|
|
|
|
sub Sigma { |
863
|
0
|
|
|
0
|
1
|
0
|
my ($self, $id) = @_; |
864
|
|
|
|
|
|
|
|
865
|
0
|
0
|
|
|
|
0
|
if (defined($id)) { |
866
|
0
|
0
|
|
|
|
0
|
if (defined($self->{Rank}{$id})) { |
|
|
0
|
|
|
|
|
|
867
|
0
|
|
|
|
|
0
|
$self->{Rank}{$id} =~ m/^([\d]+)([kdKD])$/; |
868
|
0
|
|
|
|
|
0
|
my $r = $1; |
869
|
0
|
0
|
|
|
|
0
|
$r = -$r if (lc($2) eq 'k'); |
870
|
0
|
|
|
|
|
0
|
my $sigma = (-$r - 0.3) / 6; |
871
|
0
|
0
|
|
|
|
0
|
return ($sigma > 1.2) ? $sigma : 1.2; |
872
|
|
|
|
|
|
|
} elsif (defined($self->{Rating}{$id})) { |
873
|
0
|
|
|
|
|
0
|
my $sigma = (-$self->{Rating}{$id} - 4.4) / 6; |
874
|
0
|
0
|
|
|
|
0
|
return ($sigma > 0.6) ? $sigma : 0.6; |
875
|
|
|
|
|
|
|
} else { |
876
|
0
|
|
|
|
|
0
|
carp("$id is not registered\n"); |
877
|
|
|
|
|
|
|
} |
878
|
|
|
|
|
|
|
} else { |
879
|
0
|
|
|
|
|
0
|
carp("called Sigma(\$id) without a valid ID\n"); |
880
|
|
|
|
|
|
|
} |
881
|
0
|
|
|
|
|
0
|
return(undef); |
882
|
|
|
|
|
|
|
} |
883
|
|
|
|
|
|
|
|
884
|
|
|
|
|
|
|
=item my $club = $agaTourn-EB ($id) |
885
|
|
|
|
|
|
|
|
886
|
|
|
|
|
|
|
Returns the club for $id or '' if no club is known. Returns undef if $id is |
887
|
|
|
|
|
|
|
not registered (via B). |
888
|
|
|
|
|
|
|
|
889
|
|
|
|
|
|
|
If no $id parameter is passed, returns a reference to the entire B hash |
890
|
|
|
|
|
|
|
(keyed by IDs). |
891
|
|
|
|
|
|
|
|
892
|
|
|
|
|
|
|
=cut |
893
|
|
|
|
|
|
|
|
894
|
|
|
|
|
|
|
sub Club { |
895
|
0
|
|
|
0
|
1
|
0
|
my ($self, $id) = @_; |
896
|
|
|
|
|
|
|
|
897
|
0
|
0
|
|
|
|
0
|
return ($self->{Club}{$id}) if (defined($id)); |
898
|
0
|
|
|
|
|
0
|
return($self->{Club}); |
899
|
|
|
|
|
|
|
} |
900
|
|
|
|
|
|
|
|
901
|
|
|
|
|
|
|
=item my $flags = $agaTourn-EB ($id) |
902
|
|
|
|
|
|
|
|
903
|
|
|
|
|
|
|
Returns the flags for $id or '' if no flags are known. Flags are anything |
904
|
|
|
|
|
|
|
left over (excluding the comment) after the ID, name, rating, and club have |
905
|
|
|
|
|
|
|
been parsed by B. It might include (for example) BYE or |
906
|
|
|
|
|
|
|
Drop. The case is preserved from the original line parsed. |
907
|
|
|
|
|
|
|
|
908
|
|
|
|
|
|
|
Returns undef if $id is not registered (via B). If no $id |
909
|
|
|
|
|
|
|
parameter is passed, returns a reference to the entire B hash (keyed by |
910
|
|
|
|
|
|
|
IDs). |
911
|
|
|
|
|
|
|
|
912
|
|
|
|
|
|
|
=cut |
913
|
|
|
|
|
|
|
|
914
|
|
|
|
|
|
|
sub Flags { |
915
|
0
|
|
|
0
|
1
|
0
|
my ($self, $id) = @_; |
916
|
|
|
|
|
|
|
|
917
|
0
|
0
|
|
|
|
0
|
if (defined($id)) { |
918
|
0
|
0
|
|
|
|
0
|
return ($self->{Flags}{$id}) if (exists($self->{Flags}{$id})); |
919
|
0
|
0
|
|
|
|
0
|
return ('') if exists($self->{Rating}{$id}); |
920
|
|
|
|
|
|
|
return (undef) |
921
|
0
|
|
|
|
|
0
|
} |
922
|
0
|
|
|
|
|
0
|
return($self->{Flags}); |
923
|
|
|
|
|
|
|
} |
924
|
|
|
|
|
|
|
|
925
|
|
|
|
|
|
|
=item $comment = $agaTourn-EB ($id) |
926
|
|
|
|
|
|
|
|
927
|
|
|
|
|
|
|
Returns the comment associated with $id line as added via B. |
928
|
|
|
|
|
|
|
|
929
|
|
|
|
|
|
|
If no $id argument is passed, returns a reference to the entire B |
930
|
|
|
|
|
|
|
hash (keyed by IDs). |
931
|
|
|
|
|
|
|
|
932
|
|
|
|
|
|
|
=cut |
933
|
|
|
|
|
|
|
|
934
|
|
|
|
|
|
|
sub Comment { |
935
|
2
|
|
|
2
|
1
|
8
|
my ($self, $id) = @_; |
936
|
|
|
|
|
|
|
|
937
|
2
|
50
|
|
|
|
12
|
if (defined($id)) { |
938
|
0
|
0
|
|
|
|
0
|
return ($self->{Comment}{$id}) if (exists($self->{Comment}{$id})); |
939
|
0
|
0
|
|
|
|
0
|
return ('') if exists($self->{Rating}{$id}); |
940
|
|
|
|
|
|
|
return (undef) |
941
|
0
|
|
|
|
|
0
|
} |
942
|
2
|
|
|
|
|
18
|
return ($self->{Comment}); |
943
|
|
|
|
|
|
|
} |
944
|
|
|
|
|
|
|
|
945
|
|
|
|
|
|
|
=item my $error = $agaTourn-EB |
946
|
|
|
|
|
|
|
|
947
|
|
|
|
|
|
|
If called with an argument, sets the error flag to the new value. |
948
|
|
|
|
|
|
|
Returns the current (or new) value of the error flag. |
949
|
|
|
|
|
|
|
|
950
|
|
|
|
|
|
|
=cut |
951
|
|
|
|
|
|
|
|
952
|
|
|
|
|
|
|
sub Error { |
953
|
2
|
|
|
2
|
1
|
7
|
my ($self, $error) = @_; |
954
|
|
|
|
|
|
|
|
955
|
2
|
50
|
|
|
|
13
|
$self->{error} = $error if (defined($error)); |
956
|
2
|
|
|
|
|
12
|
return ($self->{error}); |
957
|
|
|
|
|
|
|
} |
958
|
|
|
|
|
|
|
|
959
|
|
|
|
|
|
|
=item my $gamesList = $agaTourn-EB ($id, ...) |
960
|
|
|
|
|
|
|
|
961
|
|
|
|
|
|
|
Returns a list (or a reference to the list in scalar context) of games played |
962
|
|
|
|
|
|
|
by B(s). If no B argument is passed, returns the list of all |
963
|
|
|
|
|
|
|
games. |
964
|
|
|
|
|
|
|
|
965
|
|
|
|
|
|
|
Games are added via the B or the B methods. |
966
|
|
|
|
|
|
|
|
967
|
|
|
|
|
|
|
Entries in the returned list are comma separated strings. They can be parsed |
968
|
|
|
|
|
|
|
with: |
969
|
|
|
|
|
|
|
|
970
|
|
|
|
|
|
|
my ($whiteID, $blackID, $result, |
971
|
|
|
|
|
|
|
$handicap, $komi, $round) = split(',', $agaTourn->GamesList[$index]); |
972
|
|
|
|
|
|
|
|
973
|
|
|
|
|
|
|
=cut |
974
|
|
|
|
|
|
|
|
975
|
|
|
|
|
|
|
sub GamesList { |
976
|
3
|
|
|
3
|
1
|
10
|
my ($self, @arg) = @_; |
977
|
|
|
|
|
|
|
|
978
|
3
|
50
|
|
|
|
31
|
return($self->{gameAllList}) unless (@arg); |
979
|
0
|
|
|
|
|
0
|
my @games; |
980
|
0
|
|
|
|
|
0
|
foreach (@arg) { |
981
|
0
|
|
|
|
|
0
|
push(@games, @{$self->{gameIDList}{$_}}); |
|
0
|
|
|
|
|
0
|
|
982
|
|
|
|
|
|
|
} |
983
|
0
|
0
|
|
|
|
0
|
return(wantarray ? @games : \@games); |
984
|
|
|
|
|
|
|
} |
985
|
|
|
|
|
|
|
|
986
|
|
|
|
|
|
|
=item my $wins = $agaTourn-EB ($id) |
987
|
|
|
|
|
|
|
|
988
|
|
|
|
|
|
|
Returns the number of winning games recorded for $id. Wins are recorded |
989
|
|
|
|
|
|
|
via the B method. |
990
|
|
|
|
|
|
|
|
991
|
|
|
|
|
|
|
If no $id argument is passed, returns a reference to the entire B hash |
992
|
|
|
|
|
|
|
(keyed by IDs). |
993
|
|
|
|
|
|
|
|
994
|
|
|
|
|
|
|
=cut |
995
|
|
|
|
|
|
|
|
996
|
|
|
|
|
|
|
sub Wins { |
997
|
0
|
|
|
0
|
1
|
0
|
my ($self, $id) = @_; |
998
|
|
|
|
|
|
|
|
999
|
0
|
0
|
|
|
|
0
|
return($self->{Wins}{$id}) if (defined($id)); |
1000
|
0
|
|
|
|
|
0
|
return($self->{Wins}); |
1001
|
|
|
|
|
|
|
} |
1002
|
|
|
|
|
|
|
|
1003
|
|
|
|
|
|
|
=item my $losses = $agaTourn-EB ($id) |
1004
|
|
|
|
|
|
|
|
1005
|
|
|
|
|
|
|
Returns the number of losing games recorded for $id. Losses are |
1006
|
|
|
|
|
|
|
recorded via the B method. |
1007
|
|
|
|
|
|
|
|
1008
|
|
|
|
|
|
|
If no $id argument is passed, returns a reference to the entire B hash |
1009
|
|
|
|
|
|
|
(keyed by IDs). |
1010
|
|
|
|
|
|
|
|
1011
|
|
|
|
|
|
|
=cut |
1012
|
|
|
|
|
|
|
|
1013
|
|
|
|
|
|
|
sub Losses { |
1014
|
0
|
|
|
0
|
1
|
0
|
my ($self, $id) = @_; |
1015
|
|
|
|
|
|
|
|
1016
|
0
|
0
|
|
|
|
0
|
return($self->{Losses}{$id}) if (defined($id)); |
1017
|
0
|
|
|
|
|
0
|
return($self->{Losses}); |
1018
|
|
|
|
|
|
|
} |
1019
|
|
|
|
|
|
|
|
1020
|
|
|
|
|
|
|
=item my $no_results = $agaTourn-EB ($id) |
1021
|
|
|
|
|
|
|
|
1022
|
|
|
|
|
|
|
Returns the number of no-result games recorded for $id. No-results are |
1023
|
|
|
|
|
|
|
recorded via the B method. |
1024
|
|
|
|
|
|
|
|
1025
|
|
|
|
|
|
|
If no $id argument is passed, returns a reference to the entire B |
1026
|
|
|
|
|
|
|
hash (keyed by IDs). |
1027
|
|
|
|
|
|
|
|
1028
|
|
|
|
|
|
|
=cut |
1029
|
|
|
|
|
|
|
|
1030
|
|
|
|
|
|
|
sub NoResults { |
1031
|
0
|
|
|
0
|
1
|
0
|
my ($self, $id) = @_; |
1032
|
|
|
|
|
|
|
|
1033
|
0
|
0
|
|
|
|
0
|
return($self->{NoResults}{$id}) if (defined($id)); |
1034
|
0
|
|
|
|
|
0
|
return($self->{NoResults}); |
1035
|
|
|
|
|
|
|
} |
1036
|
|
|
|
|
|
|
|
1037
|
|
|
|
|
|
|
=item my @played = $agaTourn-EB ($id) |
1038
|
|
|
|
|
|
|
|
1039
|
|
|
|
|
|
|
Returns a list (or a reference to the list in scalar context) of $id's |
1040
|
|
|
|
|
|
|
opponents. The list is ordered as they were added by B method. |
1041
|
|
|
|
|
|
|
|
1042
|
|
|
|
|
|
|
If no $id argument is passed, returns a reference to the entire B hash |
1043
|
|
|
|
|
|
|
(keyed by IDs). |
1044
|
|
|
|
|
|
|
|
1045
|
|
|
|
|
|
|
=cut |
1046
|
|
|
|
|
|
|
|
1047
|
|
|
|
|
|
|
sub Played { |
1048
|
0
|
|
|
0
|
1
|
0
|
my ($self, $id) = @_; |
1049
|
|
|
|
|
|
|
|
1050
|
0
|
0
|
|
|
|
0
|
if (defined($id)) { |
1051
|
0
|
0
|
|
|
|
0
|
return wantarray ? @{$self->{Played}{$id}} : $self->{Played}{$id}; |
|
0
|
|
|
|
|
0
|
|
1052
|
|
|
|
|
|
|
} |
1053
|
0
|
|
|
|
|
0
|
return $self->{Played}; |
1054
|
|
|
|
|
|
|
} |
1055
|
|
|
|
|
|
|
|
1056
|
|
|
|
|
|
|
=item my $rating = $agaTourn-EB ($rank | $rating) |
1057
|
|
|
|
|
|
|
|
1058
|
|
|
|
|
|
|
Returns a value guaranteed to be in a correct AGA Rating format. The format |
1059
|
|
|
|
|
|
|
is a number with a tenths decimal, where the number represents the dan rank |
1060
|
|
|
|
|
|
|
(if positive) or the kyu rank (if negative). A rating of 3.5 represents |
1061
|
|
|
|
|
|
|
squarely in the middle of the 3 dan rank, and -1.9 represents a weak 1 kyu |
1062
|
|
|
|
|
|
|
rank. The range from 1.0 to -1.0 is not used (see |
1063
|
|
|
|
|
|
|
B/B below). |
1064
|
|
|
|
|
|
|
|
1065
|
|
|
|
|
|
|
=cut |
1066
|
|
|
|
|
|
|
|
1067
|
|
|
|
|
|
|
sub RankToRating { |
1068
|
32
|
|
|
32
|
1
|
53
|
my ($self, $rating) = @_; |
1069
|
|
|
|
|
|
|
|
1070
|
32
|
50
|
33
|
|
|
187
|
return (NOTARANK) if (not defined($rating) or ($rating eq '')); |
1071
|
32
|
100
|
|
|
|
132
|
return "$rating.0" if ($rating =~ m/^-?\d+$/); # not in decimalized format? |
1072
|
29
|
100
|
|
|
|
79
|
unless ($rating =~ m/^-?\d+\.\d+$/) { # not in rating format? |
1073
|
24
|
50
|
|
|
|
91
|
return(NOTARANK) unless($rating =~ m/^(\d+)([dDkK])$/); # not in rank format either? |
1074
|
24
|
|
|
|
|
52
|
$rating = "$1.5"; # it's in rank format (like 5D or 2k), convert to rating |
1075
|
24
|
100
|
|
|
|
85
|
$rating = -$rating if (uc($2) eq "K"); # kyus are negative |
1076
|
|
|
|
|
|
|
} |
1077
|
29
|
|
|
|
|
87
|
return($rating); |
1078
|
|
|
|
|
|
|
} |
1079
|
|
|
|
|
|
|
|
1080
|
|
|
|
|
|
|
=item my $band_idx = $agaTourn-EB ($rank | $rating) |
1081
|
|
|
|
|
|
|
|
1082
|
|
|
|
|
|
|
Returns the band index for a B or B. Returns NOTARANK if |
1083
|
|
|
|
|
|
|
rank/rating is not in any bands. |
1084
|
|
|
|
|
|
|
|
1085
|
|
|
|
|
|
|
See also B below. |
1086
|
|
|
|
|
|
|
|
1087
|
|
|
|
|
|
|
=cut |
1088
|
|
|
|
|
|
|
|
1089
|
|
|
|
|
|
|
sub WhichBandIs { |
1090
|
8
|
|
|
8
|
1
|
15
|
my ($self, $r) = @_; |
1091
|
|
|
|
|
|
|
|
1092
|
8
|
100
|
|
|
|
24
|
unless (exists($self->{bandTop})) { |
1093
|
2
|
|
|
|
|
10
|
$self->_setBands(); |
1094
|
|
|
|
|
|
|
} |
1095
|
8
|
|
|
|
|
22
|
$r = $self->RankToRating($r); |
1096
|
8
|
|
|
|
|
14
|
my $ii; |
1097
|
8
|
|
|
|
|
11
|
for ($ii = 0; $ii < @{$self->{bandTop}}; $ii++) { |
|
21
|
|
|
|
|
52
|
|
1098
|
15
|
50
|
|
|
|
44
|
next if ($r > $self->{bandTop}[$ii]); |
1099
|
15
|
100
|
|
|
|
39
|
if ($r >= $self->{bandBot}[$ii]) { |
1100
|
2
|
|
|
|
|
14
|
return($ii); # this is it |
1101
|
|
|
|
|
|
|
} |
1102
|
|
|
|
|
|
|
} |
1103
|
6
|
|
|
|
|
12
|
return(NOTARANK); |
1104
|
|
|
|
|
|
|
} |
1105
|
|
|
|
|
|
|
|
1106
|
|
|
|
|
|
|
=item my $band_name = $agaTourn-EB ($bandIndex) |
1107
|
|
|
|
|
|
|
|
1108
|
|
|
|
|
|
|
Returns the name of a band specified by the B or undef of not known. |
1109
|
|
|
|
|
|
|
|
1110
|
|
|
|
|
|
|
Scoring bands are specified via B with ##BAND directives. |
1111
|
|
|
|
|
|
|
|
1112
|
|
|
|
|
|
|
AGATourn complains if bands are specified with holes between them. |
1113
|
|
|
|
|
|
|
|
1114
|
|
|
|
|
|
|
The bands are sorted (by strength) and indexed. B returns the |
1115
|
|
|
|
|
|
|
original name (as specified in the ##BAND directive) from a band index. |
1116
|
|
|
|
|
|
|
|
1117
|
|
|
|
|
|
|
=cut |
1118
|
|
|
|
|
|
|
|
1119
|
|
|
|
|
|
|
sub BandName { |
1120
|
4
|
|
|
4
|
1
|
10
|
my ($self, $idx) = @_; |
1121
|
|
|
|
|
|
|
|
1122
|
4
|
|
|
|
|
7
|
my ($band, $top, $bot); |
1123
|
4
|
|
|
|
|
7
|
foreach $band (@{$self->{Directive}{'BAND'}}) { |
|
4
|
|
|
|
|
12
|
|
1124
|
6
|
|
|
|
|
16
|
($top, $bot) = split(/\s+/, $band); |
1125
|
6
|
|
|
|
|
17
|
$top = int($self->RankToRating($top)); |
1126
|
6
|
100
|
|
|
|
24
|
return undef unless defined($self->{bandTop}[$idx]); |
1127
|
5
|
100
|
|
|
|
17
|
if ($top == int($self->{bandTop}[$idx])) { |
1128
|
3
|
|
|
|
|
16
|
return($band); |
1129
|
|
|
|
|
|
|
} |
1130
|
|
|
|
|
|
|
} |
1131
|
0
|
|
|
|
|
0
|
return(undef); |
1132
|
|
|
|
|
|
|
} |
1133
|
|
|
|
|
|
|
|
1134
|
|
|
|
|
|
|
=item my ($handicap, $komi) = $agaTourn-EB ($player1, $player2) |
1135
|
|
|
|
|
|
|
|
1136
|
|
|
|
|
|
|
Returns the appropriate handicap and komi for two players. Players can be in |
1137
|
|
|
|
|
|
|
any form acceptable to B. |
1138
|
|
|
|
|
|
|
|
1139
|
|
|
|
|
|
|
If player1 is stronger than player two, the handicap is a |
1140
|
|
|
|
|
|
|
positive number. If player1 is weaker than player2, (players need to be |
1141
|
|
|
|
|
|
|
swapped), the returned handicap is a negative number. If the handicap would |
1142
|
|
|
|
|
|
|
normally be 0 and the players need to be swapped, the returned handicap is -1. |
1143
|
|
|
|
|
|
|
|
1144
|
|
|
|
|
|
|
A handicap of 1 is never returned. The returned handicap and komi are always |
1145
|
|
|
|
|
|
|
integers (you may assume that komi needs a additional half-point if you like). |
1146
|
|
|
|
|
|
|
|
1147
|
|
|
|
|
|
|
If either player1 or player2 is invalid, B complains (during the |
1148
|
|
|
|
|
|
|
call to B for the player) and returns (-1, -1). |
1149
|
|
|
|
|
|
|
|
1150
|
|
|
|
|
|
|
B uses the following table (same as the AGA handicap practice): |
1151
|
|
|
|
|
|
|
|
1152
|
|
|
|
|
|
|
rating handi Ing AGA |
1153
|
|
|
|
|
|
|
diff Komi Komi |
1154
|
|
|
|
|
|
|
0.000-0.650 0 7 6 even, normal komi |
1155
|
|
|
|
|
|
|
0.651-1.250 0 -1* 0 no komi (* black wins ties under Ing) |
1156
|
|
|
|
|
|
|
1.251-2.200 0 -7 -6 reverse komi |
1157
|
|
|
|
|
|
|
2.201-3.300 2 -2 0 two stones |
1158
|
|
|
|
|
|
|
3.301-4.400 3 -3 0 three stones ... |
1159
|
|
|
|
|
|
|
|
1160
|
|
|
|
|
|
|
=cut |
1161
|
|
|
|
|
|
|
|
1162
|
|
|
|
|
|
|
sub Handicap { |
1163
|
0
|
|
|
0
|
1
|
0
|
my ($self, $p1, $p2) = @_; |
1164
|
|
|
|
|
|
|
|
1165
|
0
|
|
|
|
|
0
|
$p1 = $self->CollapseRating($self->Rating($p1)); |
1166
|
0
|
|
|
|
|
0
|
$p2 = $self->CollapseRating($self->Rating($p2)); |
1167
|
0
|
0
|
0
|
|
|
0
|
return (-1, -1) unless(defined($p1) and defined($p2)); |
1168
|
0
|
|
|
|
|
0
|
my $diff = $p1 - $p2; |
1169
|
0
|
|
|
|
|
0
|
my $ing = $self->{Directive}{RULES}[0] eq 'ING'; |
1170
|
0
|
|
|
|
|
0
|
my $swap = 1; |
1171
|
0
|
|
|
|
|
0
|
my ($handi, $komi) = (0, 0); |
1172
|
0
|
0
|
|
|
|
0
|
if ($diff < 0) { |
1173
|
0
|
|
|
|
|
0
|
$swap = $handi = -1; |
1174
|
0
|
|
|
|
|
0
|
$diff = -$diff; |
1175
|
|
|
|
|
|
|
} |
1176
|
0
|
0
|
|
|
|
0
|
if ($diff <= .650) { |
|
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
1177
|
0
|
0
|
|
|
|
0
|
$komi = $ing ? 7 : 6; # normal komi game |
1178
|
|
|
|
|
|
|
} elsif ($diff <= 1.25) { |
1179
|
0
|
0
|
|
|
|
0
|
$komi = $ing ? -1 : 0; # no komi game |
1180
|
|
|
|
|
|
|
} elsif ($diff <= 2.2) { |
1181
|
0
|
0
|
|
|
|
0
|
$komi = $ing ? -7 : -6; # reverse komi game |
1182
|
|
|
|
|
|
|
} else { |
1183
|
0
|
|
|
|
|
0
|
$handi = $swap * int($diff / 1.1); |
1184
|
0
|
|
|
|
|
0
|
$komi = 0; |
1185
|
|
|
|
|
|
|
} |
1186
|
0
|
|
|
|
|
0
|
return (int($handi), int($komi)); |
1187
|
|
|
|
|
|
|
} |
1188
|
|
|
|
|
|
|
|
1189
|
|
|
|
|
|
|
=item my $collapsed_rating = $agaTourn-EB ($aga_rating) |
1190
|
|
|
|
|
|
|
|
1191
|
|
|
|
|
|
|
AGA ratings have a hole between 1.0 and -1.0. This method fills the hole by |
1192
|
|
|
|
|
|
|
adding 1 to kyu ratings and subtracting 1 from dan ratings. If $aga_rating is |
1193
|
|
|
|
|
|
|
between 1.0 and -1.0, complains and returns the original $rating. |
1194
|
|
|
|
|
|
|
|
1195
|
|
|
|
|
|
|
=cut |
1196
|
|
|
|
|
|
|
|
1197
|
|
|
|
|
|
|
sub CollapseRating { |
1198
|
0
|
|
|
0
|
1
|
0
|
my ($self, $rating) = @_; |
1199
|
|
|
|
|
|
|
|
1200
|
0
|
0
|
|
|
|
0
|
if ($rating >= 1) { |
|
|
0
|
|
|
|
|
|
1201
|
0
|
|
|
|
|
0
|
$rating -= 1; # pull dan ratings down to 0 |
1202
|
|
|
|
|
|
|
} elsif ($rating <= -1) { |
1203
|
0
|
|
|
|
|
0
|
$rating += 1; # pull kyu ratings up to 0 |
1204
|
|
|
|
|
|
|
} else { |
1205
|
0
|
|
|
|
|
0
|
carp "CollapseRating called on a rating between -1 and +1: $rating\n"; |
1206
|
|
|
|
|
|
|
} |
1207
|
0
|
|
|
|
|
0
|
return $rating; |
1208
|
|
|
|
|
|
|
} |
1209
|
|
|
|
|
|
|
|
1210
|
|
|
|
|
|
|
=item my $AGA_rating = $agaTourn-EB ($collapsed_rating) |
1211
|
|
|
|
|
|
|
|
1212
|
|
|
|
|
|
|
AGA ratings have a hole between 1.0 and -1.0. This method converts a |
1213
|
|
|
|
|
|
|
continuous rating with no hole into a valid AGA rating by adding 1 to ratings |
1214
|
|
|
|
|
|
|
greater than 0 and subtracting 1 from ratings less than 0. |
1215
|
|
|
|
|
|
|
|
1216
|
|
|
|
|
|
|
=cut |
1217
|
|
|
|
|
|
|
|
1218
|
|
|
|
|
|
|
sub ExpandRating { |
1219
|
0
|
|
|
0
|
1
|
0
|
my ($self, $rating) = @_; |
1220
|
|
|
|
|
|
|
|
1221
|
0
|
0
|
|
|
|
0
|
if ($rating >= 0) { |
1222
|
0
|
|
|
|
|
0
|
$rating += 1; # dan ratings are upwards from 1 |
1223
|
|
|
|
|
|
|
} else { |
1224
|
0
|
|
|
|
|
0
|
$rating -= 1; # kyu ratings are downwards from -1 |
1225
|
|
|
|
|
|
|
} |
1226
|
0
|
|
|
|
|
0
|
return $rating; |
1227
|
|
|
|
|
|
|
} |
1228
|
|
|
|
|
|
|
|
1229
|
|
|
|
|
|
|
=item my $normalized_id = $agaTourn-EB ($id) |
1230
|
|
|
|
|
|
|
|
1231
|
|
|
|
|
|
|
Performs normalization of $id so the we can compare variations of $id without |
1232
|
|
|
|
|
|
|
considering them as different. Normalization consists of turning the country |
1233
|
|
|
|
|
|
|
part of $id to all upper-case and removing leading zeros from the number part. |
1234
|
|
|
|
|
|
|
|
1235
|
|
|
|
|
|
|
All $ids used as hash keys should be normalized. |
1236
|
|
|
|
|
|
|
|
1237
|
|
|
|
|
|
|
=cut |
1238
|
|
|
|
|
|
|
|
1239
|
|
|
|
|
|
|
sub NormalizeID { |
1240
|
9
|
|
|
9
|
1
|
23
|
my ($self, $id) = @_; |
1241
|
|
|
|
|
|
|
|
1242
|
9
|
|
|
|
|
14
|
$id = uc ($id); # make all letters upper case |
1243
|
9
|
|
|
|
|
56
|
$id =~ s/^([A-Z]*)0*([1-9].*)/$1$2/; # remove leading zeros from number part |
1244
|
9
|
|
|
|
|
25
|
return($id); |
1245
|
|
|
|
|
|
|
} |
1246
|
|
|
|
|
|
|
|
1247
|
|
|
|
|
|
|
###################################################### |
1248
|
|
|
|
|
|
|
# |
1249
|
|
|
|
|
|
|
# Private methods |
1250
|
|
|
|
|
|
|
# |
1251
|
|
|
|
|
|
|
##################################################### |
1252
|
|
|
|
|
|
|
|
1253
|
|
|
|
|
|
|
sub _setBands { |
1254
|
2
|
|
|
2
|
|
4
|
my ($self) = @_; |
1255
|
|
|
|
|
|
|
|
1256
|
2
|
100
|
|
|
|
12
|
unless(exists($self->{Directive}{'BAND'})) { |
1257
|
|
|
|
|
|
|
# carp("Note: no bands selected, assuming one band.\n"); |
1258
|
1
|
|
|
|
|
2
|
unshift(@{$self->{Directive}{'BAND'}}, '99D 99K'); |
|
1
|
|
|
|
|
7
|
|
1259
|
|
|
|
|
|
|
} |
1260
|
2
|
|
|
|
|
9
|
$self->{bandTop} = []; # ref to empty array (to prevent infinite recursion) |
1261
|
2
|
|
|
|
|
4
|
my ($band, $ovBand, $top, $bot); |
1262
|
2
|
|
|
|
|
6
|
foreach $band (@{$self->{Directive}{'BAND'}}) { |
|
2
|
|
|
|
|
7
|
|
1263
|
6
|
|
|
|
|
20
|
($top, $bot) = split(/\s+/, $band); |
1264
|
6
|
|
|
|
|
18
|
$top = int($self->RankToRating($top)); |
1265
|
6
|
100
|
|
|
|
20
|
$top += 0.99999 if ($top > 0); |
1266
|
6
|
|
|
|
|
17
|
$bot = int($self->RankToRating($bot)); |
1267
|
6
|
100
|
|
|
|
31
|
$bot -= 0.99999 if ($bot < 0); |
1268
|
6
|
50
|
33
|
|
|
51
|
if (($top > 9999) || ($bot < -9999) || ($bot >= $top)) { |
|
|
|
33
|
|
|
|
|
1269
|
0
|
|
|
|
|
0
|
carp("Error: can't parse BAND directive at line $. in $self->{fileName}: $band\n"); |
1270
|
0
|
|
|
|
|
0
|
$self->{error} = 1; |
1271
|
|
|
|
|
|
|
return |
1272
|
0
|
|
|
|
|
0
|
} |
1273
|
6
|
|
|
|
|
20
|
$ovBand = $self->WhichBandIs($top); # check for overlapped bands |
1274
|
6
|
50
|
|
|
|
83
|
$ovBand = $self->WhichBandIs($bot) unless ($ovBand eq NOTARANK); |
1275
|
6
|
50
|
|
|
|
32
|
unless ($ovBand eq NOTARANK) { |
1276
|
0
|
|
|
|
|
0
|
carp("Warning: band conflict: $band\n (overlaps $self->{Directive}{'BAND'}[$ovBand])\n"); |
1277
|
|
|
|
|
|
|
} |
1278
|
6
|
|
|
|
|
6
|
push(@{$self->{bandTop}}, $top); |
|
6
|
|
|
|
|
16
|
|
1279
|
6
|
|
|
|
|
7
|
push(@{$self->{bandBot}}, $bot); |
|
6
|
|
|
|
|
18
|
|
1280
|
|
|
|
|
|
|
} |
1281
|
2
|
|
|
|
|
5
|
my (@tops) = sort({ $b <=> $a; } @{$self->{bandTop}}); # now check for holes |
|
8
|
|
|
|
|
15
|
|
|
2
|
|
|
|
|
12
|
|
1282
|
2
|
|
|
|
|
4
|
my (@bots) = sort({ $b <=> $a; } @{$self->{bandBot}}); |
|
8
|
|
|
|
|
12
|
|
|
2
|
|
|
|
|
5
|
|
1283
|
2
|
|
|
|
|
4
|
my $ii; |
1284
|
2
|
|
|
|
|
16
|
for ($ii = 0; $ii < @tops - 1; $ii++) { |
1285
|
4
|
100
|
66
|
|
|
21
|
next if (($bots[$ii] == 1) && ($tops[$ii + 1] == -1)); # 1d to 1k is a legitimate hole |
1286
|
3
|
50
|
|
|
|
22
|
if ($bots[$ii] - $tops[$ii + 1] > 0.001) { |
1287
|
0
|
|
|
|
|
0
|
carp( "Warning: hole between bands\n"); |
1288
|
|
|
|
|
|
|
} |
1289
|
|
|
|
|
|
|
} |
1290
|
2
|
|
|
|
|
7
|
$self->{bandTop} = \@tops; # use sorted bands |
1291
|
2
|
|
|
|
|
7
|
$self->{bandBot} = \@bots; |
1292
|
|
|
|
|
|
|
} |
1293
|
|
|
|
|
|
|
|
1294
|
|
|
|
|
|
|
1; |
1295
|
|
|
|
|
|
|
|
1296
|
|
|
|
|
|
|
__END__ |