File Coverage

blib/lib/Games/Go/AGA/DataObjects/Register.pm
Criterion Covered Total %
statement 138 260 53.0
branch 29 82 35.3
condition 7 28 25.0
subroutine 27 39 69.2
pod 11 24 45.8
total 212 433 48.9


line stmt bran cond sub pod time code
1             #===============================================================================
2             #
3             # FILE: Games::Go::AGA::DataObjects::Register.pm
4             #
5             # USAGE: use Games::Go::AGA::DataObjects::Register;
6             #
7             # PODNAME: Games::Go::AGA::DataObjects::Register
8             # ABSTRACT: models AGA register.tde file information
9             #
10             # AUTHOR: Reid Augustin (REID),
11             # CREATED: 11/19/2010 03:13:05 PM PST
12             #===============================================================================
13              
14              
15 2     2   1473 use strict;
  2         2  
  2         58  
16 2     2   10 use warnings;
  2         2  
  2         73  
17              
18             package Games::Go::AGA::DataObjects::Register;
19 2     2   8 use Moo;
  2         2  
  2         11  
20 2     2   560 use namespace::clean;
  2         4  
  2         14  
21 2     2   321 use parent 'Games::Go::AGA::DataObjects::Directives';
  2         3  
  2         24  
22              
23 2     2   113 use Carp;
  2         4  
  2         126  
24 2     2   634 use Readonly;
  2         3948  
  2         121  
25 2     2   10 use Scalar::Util qw( refaddr looks_like_number );
  2         3  
  2         99  
26 2     2   1349 use IO::File;
  2         17344  
  2         279  
27 2     2   15 use Games::Go::AGA::Parse::Util qw( normalize_ID );
  2         4  
  2         92  
28 2     2   8 use Games::Go::AGA::Parse::Util qw( Rank_to_Rating );
  2         4  
  2         70  
29 2     2   8 use Games::Go::AGA::DataObjects::Player;
  2         3  
  2         50  
30 2     2   8 use Games::Go::AGA::DataObjects::Directives;
  2         3  
  2         48  
31 2     2   7 use Games::Go::AGA::DataObjects::Types qw( isa_CodeRef isa_ArrayRef isa_HashRef );
  2         8  
  2         4772  
32              
33             our $VERSION = '0.152'; # VERSION
34              
35             # has 'directives' => (
36             # isa => 'Games::Go::AGA::DataObjects::Directives',
37             # is => 'rw',
38             # default => sub {
39             # Games::Go::AGA::DataObjects::Directives->new();
40             # },
41             # );
42             # has 'comments' => (
43             # isa => 'ArrayRef',
44             # is => 'ro',
45             # default => sub { [] }
46             # );
47             # has 'players' => (
48             # isa => 'ArrayRef',
49             # is => 'ro',
50             # default => sub { [] }
51             # );
52             has change_callback => (
53             isa => \&isa_CodeRef,
54             is => 'rw',
55             lazy => 1,
56             default => sub { sub { } }
57             );
58             has _bye_candidates => (
59             is => 'rw',
60             );
61             has _drops => (
62             is => 'rw',
63             );
64             sub BUILD {
65 2     2 0 11 my ($self) = @_;
66 2         7 $self->{comments} = [];
67 2         45 $self->{players} = [];
68             }
69              
70 0     0 0 0 sub directive_is_boolean { return $_[0]->is_boolean($_[1]) } # proxy to Directives
71              
72             # hashref of player IDs, each value true or false
73             sub bye_candidates {
74 0     0 0 0 my ($self) = @_;
75              
76 0 0       0 if (not $self->_bye_candidates) {
77 0   0     0 my %bye_candidates = map { $_ => $self->get_player($_) }
  0         0  
78             split /\s+/, $self->get_directive_value('BYE_CANDIDATES') || '';
79 0         0 $self->_bye_candidates(\%bye_candidates);
80             }
81 0         0 return $self->_bye_candidates;
82             }
83              
84             sub add_bye_candidates {
85 0     0 0 0 my ($self, @candidates) = @_;
86              
87 0         0 my $bye_candidates_hash = $self->bye_candidates;
88              
89 0         0 for my $candidate (@candidates) {
90 0 0       0 if (not $bye_candidates_hash->{$candidate}) {
91 0         0 $self->_find_player_idx($candidate); # croaks if not found
92 0         0 $self->_bye_candidates(undef); # force refresh on next ->bye_candidates
93 0         0 $bye_candidates_hash->{$candidate} = 1;
94             }
95             }
96 0 0       0 if (not $self->_bye_candidates) {
97 0         0 $self->set_directive_value('BYE_CANDIDATES', join(' ', keys %{$bye_candidates_hash}));
  0         0  
98             }
99             }
100              
101             sub delete_bye_candidates {
102 0     0 0 0 my ($self, @candidates) = @_;
103              
104 0         0 my $bye_candidates_hash = $self->bye_candidates;
105              
106 0         0 my @deleted;
107 0         0 for my $candidate (@candidates) {
108 0 0       0 if ($bye_candidates_hash->{$candidate}) {
109 0         0 $self->_bye_candidates(undef); # force refresh on next ->bye_candidates
110 0 0       0 push @deleted, $candidate if (delete $bye_candidates_hash->{$candidate});
111             }
112             }
113 0 0       0 if (not $self->_bye_candidates) {
114 0         0 $self->set_directive_value('BYE_CANDIDATES', join(' ', keys %{$bye_candidates_hash}));
  0         0  
115             }
116 0         0 return @deleted;
117             }
118              
119             # arrayref of rounds, each entry a hashref of player IDs, true or false
120             sub drops {
121 0     0 0 0 my ($self, $round_num) = @_;
122              
123 0 0       0 if (not $self->_drops) {
124 0         0 my @drops;
125 0   0     0 for my $ii (0 .. ($self->get_directive_value('ROUNDS') || 7)) {
126 0 0       0 my $key = $ii ? "DROP_$ii" : 'DROP_ALL';
127 0   0     0 my %round_drops = map { $_ => $self->get_player($_) }
  0         0  
128             split /\s+/, $self->get_directive_value($key) || '';
129 0         0 push @drops, \%round_drops;
130             }
131 0         0 $self->_drops(\@drops);
132             }
133 0 0       0 return $self->_drops->[$round_num] if (@_ > 1);
134 0         0 return $self->_drops;
135             }
136              
137             sub add_drops {
138 0     0 0 0 my ($self, $round_num, @ids) = @_;
139              
140 0         0 my $drops_hash = $self->drops($round_num);
141              
142 0         0 for my $id (@ids) {
143 0 0       0 if (not $drops_hash->{$id}) {
144 0         0 $self->_find_player_idx($id); # croaks if not found
145 0         0 $self->_drops(undef); # force refresh on next ->drops
146 0         0 $drops_hash->{$id} = 1;
147             }
148             }
149 0 0       0 if (not $self->_drops) {
150 0   0     0 $round_num ||= 'All'; # round 0 means All
151 0         0 $self->set_directive_value("DROP_$round_num", join(' ', keys %{$drops_hash}));
  0         0  
152             }
153             }
154              
155             sub delete_drops {
156 0     0 0 0 my ($self, $round_num, @ids) = @_;
157              
158 0 0       0 if (not defined $round_num) { # all rounds
159 0         0 my @dropped_ary;
160 0         0 my $ii = 0;
161 0         0 for my $drop_round (@{$self->drops}) {
  0         0  
162 0   0     0 $dropped_ary[$ii] = { map { $_ => 1 } $self->delete_drops($ii, @ids) } || {};
163 0         0 $ii++;
164             }
165 0         0 return @dropped_ary;
166             }
167              
168 0         0 my $drops_hash = $self->drops($round_num);
169              
170 0         0 my @deleted;
171 0         0 for my $id (@ids) {
172 0 0       0 if ($drops_hash->{$id}) {
173 0         0 $self->_drops(undef); # force refresh on next ->drops
174 0 0       0 push @deleted, $id if (delete $drops_hash->{$id});
175             }
176             }
177 0 0       0 if (not $self->_drops) {
178 0   0     0 $round_num ||= 'All'; # round 0 means All
179 0         0 $self->set_directive_value("DROP_$round_num", join(' ', keys %{$drops_hash}));
  0         0  
180             }
181             return @deleted
182 0         0 }
183              
184             sub changed {
185 23     23 0 27 my ($self) = @_;
186              
187 23         23 &{$self->change_callback}(@_);
  23         427  
188             }
189              
190             sub comments {
191 0     0 1 0 my ($self) = @_;
192              
193 0 0       0 return wantarray ? @{$self->{comments}} : $self->{comments};
  0         0  
194             }
195              
196             sub add_comment {
197 0     0 0 0 my ($self, $comment) = @_;
198              
199 0         0 push @{$self->{comments}}, $comment;
  0         0  
200             }
201              
202             sub players {
203 2     2 1 10 my ($self) = @_;
204              
205 2 100       11 return wantarray ? @{$self->{players}} : $self->{players};
  1         8  
206             }
207              
208             sub id_is_duplicate {
209 25     25 1 1431 my ($self, $id, $player) = @_;
210              
211             # normalize the ID first. IDs in Players are already normalised.
212 25         59 $id = normalize_ID($id);
213 25         171 my @matched = grep { $id eq $_->id } @{$self->{players}};
  213         3624  
  25         64  
214 25   50     226 my $my_refaddr = refaddr $player || 0;
215 25         53 foreach my $p (@matched) {
216 2 50       8 if (refaddr $p != $my_refaddr) {
217 2         10 return 1
218             }
219             }
220 23         61 return 0;
221             }
222              
223 4     4 0 49 sub add_player { shift->insert_player_at_idx(-1, @_) }
224              
225             sub insert_player_at_idx {
226 23     23 1 1235 my ($self, $idx, $player) = @_;
227              
228 23         337 my $id = $player->id;
229 23 50       1415 if (not $id) {
230 0         0 $id = 1;
231 0         0 $id++ while ($self->id_is_duplicate("TMP$id", undef));
232 0         0 $id = "TMP$id";
233 0         0 $player->id($id);
234             }
235 23 100       55 if ($self->id_is_duplicate($id, undef)) {
236 1         198 croak "duplicate ID: $id\n";
237             }
238             #print "insert_player ", $player->id, " at $idx\n";
239 22 50 33     66 if ($idx < 0 or
240 0         0 $idx > $#{$self->{players}}) {
241 22         23 push (@{$self->{players}}, $player);
  22         46  
242             }
243             else {
244 0         0 splice (@{$self->{players}}, $idx, 0, $player);
  0         0  
245             }
246 22         53 $self->changed;
247             }
248              
249             sub get_player_idx {
250 0     0 1 0 my ($self, $idx) = @_;
251              
252 0         0 return $self->_find_player_idx($idx);
253             }
254              
255             sub get_player {
256 4     4 1 10 my ($self, $idx) = @_;
257              
258 4         19 $idx = $self->_find_player_idx($idx);
259 4         61 return $self->{players}[$idx];
260             }
261              
262             sub delete_player {
263 0     0 1 0 my ($self, $idx) = @_;
264              
265 0         0 $idx = $self->_find_player_idx($idx);
266              
267 0         0 my $players = $self->{players};
268 0 0       0 if (@{$players->[$idx]->games}) {
  0         0  
269 0         0 my $id = $players->[$idx]->id;
270 0         0 croak "Games recorded for $id, can't delete\n";
271             }
272 0         0 my $player = splice(@{$players}, $idx, 1); # delete and return it
  0         0  
273 0         0 $self->delete_bye_candidates($player->id);
274 0         0 $self->delete_drops(undef, $player->id);
275             #print "delete ", $player->id, "\n";
276 0         0 $self->changed;
277 0         0 return $player;
278             }
279              
280             sub _find_player_idx {
281 4     4   5 my ($self, $idx) = @_;
282              
283 4         9 my $players = $self->{players};
284 4 50       30 if (looks_like_number($idx)) {
    50          
285             # already what we need
286             }
287             elsif (ref $idx) { # must be a Player dataobject
288             # find Player object with matching refaddr
289             FIND_REFADDR : {
290 0         0 my $player = $idx;
  0         0  
291 0         0 my $my_refaddr = refaddr($player);
292 0         0 for my $ii (0 .. $#{$players}) {
  0         0  
293 0 0       0 if (refaddr($players->[$ii]) == $my_refaddr) {
294 0         0 $idx = $ii;
295 0         0 last FIND_REFADDR;
296             }
297             }
298 0         0 my $id = $player->id;
299 0         0 croak "can't find player at refaddr = $my_refaddr (ID=$id)\n";
300             }
301             }
302             else {
303             # find Player with matching ID
304             FIND_ID : {
305 4         5 my $id = normalize_ID($idx);
  4         19  
306 4         79 for my $ii (0 .. $#{$players}) {
  4         20  
307 10 100       327 if ($players->[$ii]->id eq $id) {
308 4         37 $idx = $ii;
309 4         17 last FIND_ID;
310             }
311             }
312 0         0 croak "can't find player matching ID $id\n";
313             }
314             }
315 4 50 33     21 if ($idx < 0 or
316 4         22 $idx > $#{$players}) {
317 0         0 croak "index=$idx is out of bounds\n";
318             }
319 4         11 return $idx;
320             }
321              
322             # override some Directives methods so we can intercept
323             # setting/getting of certain directives (which requires player info)
324             sub set_directive_value {
325 5     5 1 2002 my $self = shift;
326 5         9 my ($key, $val) = @_;
327              
328 5 50       31 if (uc $key eq 'BYE_CANDIDATES') {
    50          
329 0         0 $self->_bye_candidates(undef); # force refresh on next ->bye_candidates
330 0 0       0 if (not $val) {
331 0         0 return $self->SUPER::delete_directive(@_);
332             }
333             }
334             elsif ($key =~ m/^DROP_(\d+|ALL)$/i) {
335 0         0 $self->_drops(undef); # force refresh on next ->drops
336 0 0       0 if (not $val) {
337 0         0 return $self->SUPER::delete_directive(@_);
338             }
339             }
340 5         29 return $self->SUPER::set_directive_value(@_);
341             }
342              
343             sub get_directive_value {
344 20     20 1 24 my $self = shift;
345 20         27 my ($key) = $_[0];
346              
347 20 100       59 if (uc $key eq 'BAND_BREAKS') {
348 19         37 return $self->break_bands;
349             }
350 1         4 return $self->SUPER::get_directive_value(@_);
351             }
352              
353             # break players into number of bands in BANDS directives
354             sub break_bands {
355 19     19 0 22 my ($self) = @_;
356              
357 19         71 my $band_breaks = $self->SUPER::get_directive_value('BAND_BREAKS');
358 19 100       143 return $band_breaks if ($band_breaks);
359              
360 1         5 my $num_bands = $self->get_directive_value('BANDS');
361 1 50 33     12 return if (not $num_bands # no BANDS directive and not BAND_BREAKS
362             or $num_bands == 1); # everyone is in same band
363              
364 1         7 my @players = $self->players;
365 1         3 my %entrants_per_rating;
366 1         4 for my $player (@players) {
367 18         417 $entrants_per_rating{int $player->rating}++; # count entrants of each rating
368             }
369 1         119 my @sorted_ranks = sort {$b <=> $a} keys %entrants_per_rating;
  9         20  
370 1         4 my $running_total = 0;
371 1         3 my $ii = 0;
372 1         1 my @band_breaks;
373 1         6 for my $band (1 .. $num_bands - 1) {
374 2 50       8 last if ($ii >= @sorted_ranks);
375 2         8 my $next_break = $band * (@players / $num_bands);
376 2         11 while ($running_total + $entrants_per_rating{$sorted_ranks[$ii]} < $next_break) {
377 3         11 $running_total += $entrants_per_rating{$sorted_ranks[$ii++]};
378             }
379 2 50       10 if (($running_total + ($entrants_per_rating{$sorted_ranks[$ii]} / 2) < $next_break)) {
380 2         4 $running_total += $entrants_per_rating{$sorted_ranks[$ii++]};
381             }
382             #print "$running_total to band $band=$sorted_ranks[$ii - 1]\n";
383 2         7 push(@band_breaks, $sorted_ranks[$ii - 1]);
384             }
385 1         7 $band_breaks = join ' ', @band_breaks;
386 1         8 $self->set_directive_value('BAND_BREAKS', $band_breaks);
387 1         14 return $band_breaks;
388             }
389              
390             sub bands {
391 18     18 1 19 my ($self) = @_;
392              
393 18   50     34 my @breaks = split /[^\d\.\-]+/, ($self->get_directive_value('BAND_BREAKS') || '');
394             return wantarray
395             ? @breaks
396 18 50       55 : \@breaks;
397             }
398              
399             sub which_band_is {
400 18     18 1 930 my ($self, $rating) = @_;
401              
402 18         34 my $bands = $self->bands;
403 18 50       22 return 0 if (@{$bands} < 1); # one or no bands defined, all players are in band 0
  18         39  
404              
405 18         50 $rating = Rank_to_Rating($rating); # make sure it's numeric
406 18         337 my $idx;
407 18         24 for ($idx = 0; $idx < @{$bands}; $idx++) { # go past the end
  35         71  
408 30         35 my $limit = $bands->[$idx];
409 30 100 66     115 if ($limit < 0 and
410             $limit == int $limit) { # -2.0 means entire 2K range down to -2.9999,
411 12         14 $limit = $limit - 1; # but -2.5 really means -2.5.
412 12 100       30 last if ($rating > $limit); # dan: -3.0 is a 3 kyu
413             }
414             else {
415 18 100       52 last if ($rating >= $limit); # dan: 3.0 is a 3 dan
416             }
417             }
418 18         121 return $idx;
419             }
420              
421             sub fprint {
422 0     0 0   my ($self, $fh) = @_;
423              
424 0           $self->SUPER::fprint($fh); # print the directives
425 0           foreach my $comment ($self->comments) {
426 0           $fh->print("#$comment\n");
427             }
428 0           foreach my $player ($self->players) {
429 0           $player->fprint_register($fh);
430             }
431             }
432              
433             1;
434              
435             __END__