File Coverage

blib/lib/Games/Tournament/Swiss.pm
Criterion Covered Total %
statement 262 332 78.9
branch 46 82 56.1
condition 41 87 47.1
subroutine 30 39 76.9
pod 17 17 100.0
total 396 557 71.1


line stmt bran cond sub pod time code
1             package Games::Tournament::Swiss;
2              
3             # Last Edit: 2016 Jan 01, 12:09:39
4             # $Id: Swiss.pm 1358 2016-01-01 04:13:24Z drbean $
5              
6 26     26   11904 use warnings;
  26         44  
  26         753  
7 26     26   123 use strict;
  26         40  
  26         568  
8 26     26   118 use Carp;
  26         40  
  26         1421  
9              
10 26     26   522 use Games::Tournament::Swiss::Config;
  26         43  
  26         1154  
11              
12 26 100       1659 use constant ROLES => @Games::Tournament::Swiss::Config::roles?
13             @Games::Tournament::Swiss::Config::roles:
14 26     26   120 Games::Tournament::Swiss::Config->roles;
  26         40  
15 26     26   131 use constant FIRSTROUND => $Games::Tournament::Swiss::Config::firstround;
  26         52  
  26         1387  
16              
17 26     26   114 use base qw/Games::Tournament/;
  26         42  
  26         1456  
18              
19             # use Games::Tournament::Swiss::Bracket;
20             #use Games::Tournament::Contestant::Swiss -mixin =>
21             # qw/score scores rating title name pairingNumber oldId roles/;
22 26     26   1043 use Games::Tournament::Contestant::Swiss;
  26         45  
  26         623  
23 26     26   11141 use Games::Tournament::Swiss::Procedure;
  26         70  
  26         904  
24 26     26   204 use Games::Tournament::Contestant::Swiss::Preference;
  26         48  
  26         842  
25              
26 26     26   129 use List::Util qw/max min reduce sum first/;
  26         49  
  26         2515  
27 26     26   135 use List::MoreUtils qw/any all/;
  26         52  
  26         163  
28              
29             =head1 NAME
30              
31             Games::Tournament::Swiss - FIDE Swiss Same-Rank Contestant Pairing
32              
33             =head1 VERSION
34              
35             Version 0.19
36              
37             =cut
38              
39             our $VERSION = '0.19';
40              
41             =head1 SYNOPSIS
42              
43             @Games::Tournament::Swiss::roles = qw/Black White/;
44             $tourney = Games::Tournament::Swiss->new($rounds, \@entrants);
45             @rankedPlayers = $tourney->assignPairingNumbers;
46             $tourney->initializePreferences;
47              
48              
49             ...
50              
51             $tourney->collectCards(@games);
52             @groups = $tourney->formBrackets($round);
53             $round5 = $tourney->pairing( \@groups );
54             $matches = $round5->matchPlayers;
55             $round5->allocateColors;
56              
57             =head1 DESCRIPTION
58              
59             In a Swiss tournament, there is a pre-declared number of rounds, each contestant meets every other contestant zero or one times, and in each round contestants are paired with other players with the same, or similar, scores.
60              
61             =head1 METHODS
62              
63             =head2 assignPairingNumbers
64              
65             @rankings = $tourney->assignPairingNumbers;
66              
67             Sets the participants pairing numbers, sorting on rating, title and name, and substitutes this for the id they had before (The id was, but is no longer, saved as oldId. But don't change id to pairingNumber. It will change with late entries.) This function uses Games::Tournament::rank. Before the first round, all scores are usually 0. A2
68              
69             =cut
70              
71             sub assignPairingNumbers {
72 78     78 1 4061 my $self = shift;
73 78         132 my @players = @{ $self->entrants };
  78         210  
74 78         474 $self->log( 'Pairing numbers' );
75             my $numbers = sub { join ', ',
76 78     78   237 map { $_->id . ": " . $_->pairingNumber } @players;
  1632         3724  
77 78         295 };
78 78 100   97   463 if ( all { $_->pairingNumber } @players ) {
  97         287  
79 1         4 $self->log( &$numbers );
80 1         5 return;
81             }
82 77         396 my @rankings = $self->rank(@players);
83 77         456 foreach my $n ( 0 .. $#rankings ) {
84 1627         3709 my $id = $rankings[$n]->id;
85 1627         3724 my $player = $self->ided($id);
86 1627         6269 $player->pairingNumber( $n+1 );
87             }
88 77         184 $self->log( &$numbers );
89 77         353 $self->entrants( \@players );
90             }
91              
92              
93             =head2 initializePreferences
94              
95             @rankings = $tourney->initializePreferences;
96              
97             Before the first round, the color (role) preference of the highest ranked player and the other odd-numbered players in the top half of the rankings is determined by lot. The preference of the even-numbered players in the top half is given to the other color. If there is only one player in the tournament, the preference is not initialized. The method assumes all entrants have a preference attribute. This accessor is given the player by the Games::Tournament::Contestant::Swiss constructor. We take care to put the players back in the same order that we got them from entrants method. Users may rely on the original order being maintained in web app cookies. E5
98              
99             =cut
100              
101             sub initializePreferences {
102 125     125 1 498 my $self = shift;
103 125         698 my @players = @{ $self->{entrants} };
  125         582  
104 125         395 my @rankings = $self->rank( @players );
105 125         668 my ( $evenRole, $oddRole ) = $self->randomRole;
106 125         311 my $p = int( @rankings / 2 );
107 125 100       323 if ( $p == 0 ) {
108 1         4 $rankings[ 0 ]->preference->sign('');
109 1         4 $rankings[ 0 ]->preference->difference(0);
110 1         4 return $self->entrants( \@rankings );
111             }
112 124         366 for ( my $n=0; $n <= $p-1; $n+=2 ) {
113 690         1681 $rankings[ $n ]->preference->sign($evenRole);
114 690         1792 $rankings[ $n ]->preference->difference(0);
115             }
116 124         355 for ( my $n=1; $n <= $p-1; $n+=2 ) {
117 630         1482 $rankings[ $n ]->preference->sign($oddRole);
118 630         1580 $rankings[ $n ]->preference->difference(0);
119             }
120 124         291 foreach my $n ( 0 .. $#rankings ) {
121 2700         6235 my $id = $rankings[$n]->id;
122 2700         6415 my $player = $self->ided($id);
123 2700         10104 my $preference = $rankings[$n]->preference;
124 2700         6158 $player->preference( $preference );
125             }
126 124         438 $self->entrants( \@players );
127             }
128              
129              
130             =head2 recreateCards
131              
132             $tourney->recreateCards( {
133             round => $round,
134             opponents => { 1 => 2, 2 => 1, 3 => 'Bye', 4 => '-' },
135             roles => { 1 => 'W', 2 => 'B', 3 => 'Bye', 4 => '-' },
136             floats => { 1 => 'U', 2=> 'D', 3 => 'Down', 4 => 'Not' }
137             } )
138              
139             From hashes of the opponents, roles and floats for each player in a round (as provided by a pairing table), draws up the original game cards for each of the matches of the round. Returned is a list of Games::Tournament::Card objects, with undefined result fields. Pairing numbers are not used. Ids are used. Pairing numbers change with late entries.
140              
141             =cut
142              
143             sub recreateCards {
144 6     6 1 344 my $self = shift;
145 6         10 my $args = shift;
146 6         23 my $round = $args->{round};
147 6         18 my $opponents = $args->{opponents};
148 6         13 my $roles = $args->{roles};
149 6         12 my $floats = $args->{floats};
150 6         26 my $players = $self->entrants;
151 6         20 my @ids = map { $_->id } @$players;
  120         346  
152 6         31 my $absentees = $self->absentees;
153 6         19 my @absenteeids = map { $_->id } @$absentees;
  0         0  
154             my $test = sub {
155 6     6   16 my %count = ();
156 6         243 $count{$_}++ for @ids, keys %$opponents, keys %$roles, keys %$floats;
157 6         42 return grep { $count{$_} != 4 } keys %count;
  120         236  
158 6         38 };
159 6         20 carp "Game card not constructable for player $_ in round $round" for &$test;
160 6         19 my (%games, @games);
161 6         17 for my $id ( @ids )
162             {
163 120 100       293 next if $games{$id};
164 60         175 my $player = $self->ided($id);
165 60 50       245 next if $round < $player->firstround;
166 60         122 my $opponentId = $opponents->{$id};
167 60 50       126 croak "Round $round: opponent info for Player $id?" unless $opponentId;
168 60         151 my $opponent = $self->ided($opponentId);
169 60         200 my $opponentsOpponent = $opponents->{$opponentId};
170 60 50 33     434 croak
      33        
171             "Player ${id}'s opponent is $opponentId, but ${opponentId}'s opponent is $opponentsOpponent, not $id in round $round"
172             unless $opponentId eq 'Bye' or $opponentId eq 'Unpaired'
173             or $opponentsOpponent eq $id;
174 60         107 my $role = $roles->{$id};
175 60         92 my $opponentRole = $roles->{$opponentId};
176 60 50       147 if ( $opponentId eq 'Unpaired' ) {
    50          
177 0 0 0     0 croak "Player $id has $role, in round $round?"
178             unless $player and $role eq 'Unpaired';
179 0         0 next;
180 0         0 next;
181             }
182             elsif ( $opponentId eq 'Bye' ) {
183 0 0 0     0 croak "Player $id has $role role, in round $round?"
184             unless $player and $role eq 'Bye';
185             }
186             else {
187 60 50 33     540 croak
      33        
      33        
188             "Player $id is $role, and opponent $opponentId is $opponentRole, in round $round?"
189             unless $player
190             and $opponent
191             and $role
192             and $opponentRole;
193              
194             }
195 60 50 33     376 croak
      33        
196             "Player $id has same $role role as opponent $opponentId in round $round?"
197             if $opponentId and defined $opponentRole and $role eq $opponentRole;
198 60         97 my $contestants;
199 60 50       106 if ( $opponentId eq 'Bye' ) { $contestants = { Bye => $player } }
  0         0  
200 60         179 else { $contestants = { $role => $player, $opponentRole => $opponent } }
201 60         226 my $game = Games::Tournament::Card->new(
202             round => $round,
203             contestants => $contestants,
204             result => undef
205             );
206 60         126 my $float = $floats->{$id};
207 60         157 $game->float( $player, $float );
208              
209 60 50       145 unless ( $opponentId eq 'Bye' ) {
210 60         98 my $opponentFloat = $floats->{$opponentId};
211 60         161 $game->float( $opponent, $opponentFloat );
212             }
213 60         110 $games{$id} = $game;
214 60         114 $games{$opponentId} = $game;
215 60         137 push @games, $game;
216             }
217 6         97 return @games;
218             }
219              
220              
221             =head2 collectCards
222              
223             $play = $tourney->collectCards( @games );
224             next if $htable->{$player1->id}->{$player2->id};
225              
226             Records @games after they have been played. Stored as $tourney's play field, keyed on round and ids of players. Returns the new play field. Updates player scores, preferences, unless the player forfeited the game or had a Bye. TODO Die (or warn) if game has no results TODO This has non-Swiss subclass elements I could factor out into a method in Games::Tournament. TODO What if player is matched more than one time in the round, filling in for someone? XXX It looks like all the games have to be the same round, or you have to collect all cards in one round before collecting cards in following rounds. XXX I'm having problems with recording roles. I want to be lazy about it, and trust the card I get back before the next round. The problem with this is, I may be getting the role from the wrong place. It should come from the card, and is a role which was assigned in the previous round, and is only now being recorded, at this point between the previous round and the next round. Or is the problem copying by value rather than reference of the entrants? Now I also need to record floats. It would be good to do this at the same time as I record roles. The card is the appropriate place to get this info according to A4.
227              
228             =cut
229              
230             sub collectCards {
231 71     71 1 995 my $self = shift;
232 71         176 my @games = @_;
233 71   100     291 my $play = $self->play || {};
234             # my @entrants = @{ $self->entrants };
235 71         123 my %games;
236 71         160 for my $game ( @games )
237             {
238 339         840 my $round = $game->round;
239 339 50       1445 carp "round $round is not a number." unless $round =~ m/^\d+$/;
240 339         397 push @{ $games{$round} }, $game;
  339         925  
241             }
242 71         253 for my $round ( sort { $a <=> $b } keys %games )
  2         5  
243             {
244 72         130 my $games = $games{$round};
245 72         142 for my $game ( @$games ) {
246 339         904 my @players = $game->myPlayers;
247 339         638 for my $player ( @players ) {
248 648         1758 my $id = $player->id;
249 648         1747 my $entrant = $self->ided($id);
250 648         2830 my $oldroles = $player->roles;
251 648         1673 my $scores = $player->scores;
252 648         902 my ( $role, $float, $score );
253 648         1625 $role = $game->myRole($player);
254 648         1663 $float = $game->myFloat($player);
255             $scores->{$round} = ref $game->result eq 'HASH'?
256 648 100       1642 $game->result->{$role}: undef;
257 648         1053 $score = $scores->{$round};
258             #carp
259             # "No result on card for player $id as $role in round $round,"
260             # unless $score;
261 648   50     1375 $game ||= "No game";
262 648         1357 $play->{$round}->{$id} = $game;
263 648         1670 $entrant->scores($scores);
264             carp "No record in round $round for player $id $player->{name},"
265 648 50       1541 unless $play->{$round}->{$id};
266 648         1663 $entrant->roles( $round, $role );
267 648         1628 $entrant->floats( $round, $float );
268 648         1563 $entrant->floating('');
269 648 100 66     3531 $entrant->preference->update( $entrant->rolesPlayedList ) unless
      66        
270             $score and ( $score eq 'Bye' or $score eq 'Forfeit' );
271             ;
272             }
273             }
274             }
275 71         244 $self->play($play);
276             }
277              
278              
279             =head2 orderPairings
280              
281             @schedule = $tourney->orderPairings( @games );
282              
283             Tables are ordered by scores of the player with the higher score at the table, then the total scores of the players (in other words, the scores of the other player), then the A2 ranking of the higher-ranked player, in that order. F1
284              
285             =cut
286              
287             sub orderPairings {
288 20     20 1 81 my $self = shift;
289 20         46 my @games = @_;
290 20         58 my $entrants = $self->entrants;
291 20         56 my @rankedentrants = $self->rank(@$entrants);
292 20         93 my %ranking = map { $rankedentrants[$_]->id => $_ } 0 .. $#rankedentrants;
  140         343  
293             my @orderings = map {
294 20         56 my @players = $_->myPlayers;
  70         181  
295 70 100       107 my @scores = map { $_->score || 0 } @players;
  140         329  
296 70         167 my $higherscore = max @scores;
297 70         143 my $totalscore = sum @scores;
298 70         164 my @rankedplayers = $self->rank( @players );
299             { higherscore => $higherscore,
300             totalscore => $totalscore,
301 70         292 higherranking => $ranking{$rankedplayers[0]->id} };
302             } @games;
303 70         111 my @neworder = map { $games[$_] } sort {
304 20         68 $orderings[$b]->{higherscore} <=> $orderings[$a]->{higherscore} ||
305             $orderings[$b]->{totalscore} <=> $orderings[$a]->{totalscore} ||
306             $orderings[$a]->{higherranking} <=> $orderings[$b]->{higherranking}
307 74 50 100     326 } 0 .. $#orderings;
308 20         137 return @neworder;
309             }
310              
311              
312             =head2 publishCards
313              
314             $schedule = $tourney->publishCards( @games );
315              
316             Stores @games, perhaps before they have been played, as $tourney's play field, keyed on round and ids of players. Returns the games in F1 ordering.
317              
318             =cut
319              
320             sub publishCards {
321 0     0 1 0 my $self = shift;
322 0   0     0 my $play = $self->play || {};
323 0         0 my @entrants = @{ $self->entrants };
  0         0  
324 0         0 my @games = @_;
325 0         0 for my $game (@games) {
326 0         0 my $round = $game->round;
327 0         0 my $contestants = $game->contestants;
328 0         0 my @players = map { $contestants->{$_} } keys %$contestants;
  0         0  
329 0         0 for my $player (@players) {
330 0         0 my $id = $player->id;
331 0         0 my $entrant = $self->ided($id);
332 0 0 0     0 die "Player $id $entrant in round $round?"
333             unless $entrant
334             and $entrant->isa("Games::Tournament::Contestant::Swiss");
335 0         0 $play->{$round}->{$id} = $game;
336             }
337             }
338 0         0 $self->orderPairings( @games );
339             }
340              
341              
342             =head2 myCard
343              
344             $game = $tourney->myCard(round => 4, player => 13301616);
345              
346             Finds match from $tourney's play accessor, which is keyed on round and IDS of players. 'player' is id of player.
347              
348             =cut
349              
350             sub myCard {
351 0     0 1 0 my $self = shift;
352 0         0 my %args = @_;
353 0         0 my $round = $args{round};
354 0         0 my $id = $args{player};
355 0         0 my $roundmatches = $self->{play}->{$round};
356 0         0 return $roundmatches->{$id};
357             }
358              
359              
360             =head2 formBrackets
361              
362             @groups = $tourney->formBrackets
363              
364             Returns for the next round a hash of Games::Tournament::Swiss::Bracket objects grouping contestants with the same score, keyed on score. Late entrants without a score cause the program to die. Some groups may have odd numbers of players, etc, and players will have to be floated to other score groups. A number, from 1 to the total number of brackets, reflecting the order of pairing, is given to each bracket.
365              
366             =cut
367              
368             sub formBrackets {
369 60     60 1 526 my $self = shift;
370 60         197 my $players = $self->entrants;
371 60         208 my $absentees = $self->absentees;
372 60         105 my %hashed;
373             my %brackets;
374 60         143 foreach my $player (@$players) {
375 456         1117 my $id = $player->id;
376 456 50   0   2138 next if any { $id eq $_->id } @$absentees;
  0         0  
377 456 100       1756 my $score = defined $player->score ? $player->score : 0;
378             # die "$player has no score. Give them a zero, perhaps?"
379             # if $score eq "None";
380 456         1921 $hashed{$score}{ $player->pairingNumber } = $player;
381             }
382 60         101 my $number = 1;
383 60         237 foreach my $score ( reverse sort keys %hashed ) {
384 166         212 my @members;
385 166         210 foreach
386 492         783 my $pairingNumber ( sort { $a <=> $b } keys %{ $hashed{$score} } )
  166         628  
387             {
388 456         978 push @members, $hashed{$score}{$pairingNumber};
389             }
390 26     26   88901 use Games::Tournament::Swiss::Bracket;
  26         67  
  26         43148  
391 166         733 my $group = Games::Tournament::Swiss::Bracket->new(
392             score => $score,
393             members => \@members,
394             number => $number
395             );
396 166         285 $brackets{$score} = $group;
397 166         403 $number++;
398             }
399 60         470 return %brackets;
400             }
401              
402             =head2 pairing
403              
404             $pairing = $tourney->pairing( \@groups );
405              
406             Returns a Games::Tournament::Swiss::Procedure object. Groups are Games::Tournament::Swiss::Brackets objects of contestants with the same score and they are ordered by score, the group with the highest score first, and the group with the lowest score last. This is the point where round i becomes round i+1. But the program is expected to update the Games::Tournament::Swiss object itself. (Why?)
407              
408             =cut
409              
410             sub pairing {
411 60     60 1 320 my $self = shift;
412 60         167 my $entrants = $self->entrants;
413 60         105 my $brackets = shift;
414 60         184 my $round = $self->round;
415 60         282 return Games::Tournament::Swiss::Procedure->new(
416             round => $round + 1,
417             brackets => $brackets,
418             whoPlayedWho => $self->whoPlayedWho,
419             colorClashes => $self->colorClashes,
420             byes => $self->byesGone,
421             );
422             }
423              
424              
425             =head2 compatible
426              
427             $games = $tourney->compatible
428             next if $games->{$alekhine->pairingNumber}->{$capablanca->pairingNumber}
429              
430             Returns an anonymous hash, keyed on the ids of @grandmasters, indicating whether or not the individual @grandmasters could play each other in the next round. But what is the next round? This method uses the whoPlayedWho and colorClashes methods to remove incompatible players.
431              
432             =cut
433              
434             sub compatible {
435 0     0 1 0 my $self = shift;
436 0         0 my $players = $self->entrants;
437 0         0 my @ids = map { $_->id } @$players;
  0         0  
438 0         0 my $play = $self->play;
439 0         0 my $dupes = $self->whoPlayedWho;
440 0         0 my $colorbar = $self->colorClashes;
441 0         0 my $compat;
442 0         0 for my $id1 (@ids) {
443              
444 0         0 for my $id2 ( grep { $_ != $id1 } @ids ) {
  0         0  
445             $compat->{$id1}->{$id2} = 1
446             unless exists $dupes->{$id1}->{$id2}
447 0 0 0     0 or exists $colorbar->{$id1}->{$id2};
448             }
449             }
450 0         0 return $compat;
451             }
452              
453              
454             =head2 whoPlayedWho
455              
456             $games = $tourney->whoPlayedWho
457             next if $games->{$alekhine->pairingNumber}->
458             {$capablanca->pairingNumber}
459              
460             Returns an anonymous hash, keyed on the ids of the tourney's entrants, of the round in which individual entrants met. Don't forget to collect scorecards in the appropriate games first! (No tracking of how many times players have met if they have met more than once!) Do you know what round it is? B1 XXX Unplayed pairings are not considered illegal in future rounds. F2 See also Games::Tournament::met.
461              
462             =cut
463              
464             sub whoPlayedWho {
465 68     68 1 111 my $self = shift;
466 68         173 my $players = $self->entrants;
467 68         151 my @ids = map { $_->id } @$players;
  520         1188  
468 68         210 my $absentees = $self->absentees;
469 68         172 my @absenteeids = map { $_->id } @$absentees;
  0         0  
470 68         213 my $play = $self->play;
471 68         105 my $dupes;
472 68         177 my $lastround = $self->round;
473 68         196 for my $round ( FIRSTROUND .. $lastround ) {
474 140         238 for my $id (@ids) {
475 1312         3336 my $player = $self->ided($id);
476 1312 50       4849 die "No player with $id id in round $round game of @ids"
477             unless $player;
478 1312         2663 my $game = $play->{$round}->{$id};
479 1312 100 66     7364 if ( $game and $game->can("myRole") ) {
    50 33        
480 1308 50 66     3121 next if $game->result and $game->result eq 'Bye';
481 1308         3271 my $role = $game->myRole($player);
482             die
483             "Player $id, $player->{name}'s role is $role, in round $round?"
484 1308 50   2019   5947 unless any { $_ eq $role } ROLES, 'Bye';
  2019         4385  
485             next if $game->result and exists $game->result->{$role} and
486 1308 50 66     4922 $game->result->{$role} eq 'Forfeit';
      33        
487 1308 100   1981   5519 if ( any { $role eq $_ } ROLES ) {
  1981         4243  
488 1270     1905   4960 my $otherRole = first { $role ne $_ } ROLES;
  1905         3050  
489 1270         4641 my $opponent = $game->contestants->{$otherRole};
490 1270         3931 $dupes->{$id}->{ $opponent->id } = $round;
491             }
492             }
493             elsif ( $player->firstround > $round or
494 0     0   0 any { $id eq $_ } @absenteeids ) { next }
  4         10  
495 0         0 else { warn "Player ${id} game in round $round?"; }
496             }
497             }
498 68         403 return $dupes;
499             }
500              
501              
502             =head2 colorClashes
503              
504             $nomatch = $tourney->colorClashes
505             next if $nomatch->{$alekhine->id}->{$capablanca->id}
506              
507             Returns an anonymous hash, keyed on the ids of the tourney's entrants, of a color (role) if 2 of the individual @grandmasters both have an absolute preference for it in the next round and so can't play each other (themselves). Don't forget to collect scorecards in the appropriate games first! B2
508              
509             =cut
510              
511             sub colorClashes {
512 60     60 1 95 my $self = shift;
513 60         172 my $players = $self->entrants;
514 60         143 my @id = map { $_->id } @$players;
  456         1024  
515 60         105 my $clashes;
516 60         173 for my $player ( 0 .. $#$players ) {
517 456         1152 for ( 0 .. $#$players ) {
518 5158 100 100     14177 $clashes->{ $id[$player] }->{ $id[$_] } =
      100        
      100        
      100        
519             $players->[$player]->preference->role
520             if $players->[$player]->preference->role
521             and $players->[$_]->preference->role
522             and $players->[$player]->preference->role eq
523             $players->[$_]->preference->role
524             and $players->[$player]->preference->strength eq 'Absolute'
525             and $players->[$player]->preference->strength eq
526             $players->[$_]->preference->strength;
527             }
528             }
529 60         327 return $clashes;
530             }
531              
532             =head2 byesGone
533              
534             next if $tourney->byesGone($grandmasters)
535              
536             Returns an anonymous hash of either the round in which the tourney's entrants had a 'Bye' or the empty string, keyed on @$grandmasters' ids. If a grandmaster had more than one bye, the last one is returned. Don't forget to collect scorecards in the appropriate games first! B1
537              
538             =cut
539              
540              
541             sub byesGone {
542 60     60 1 93 my $self = shift;
543 60         213 my $players = $self->entrants;
544 60         150 my @ids = map { $_->id } @$players;
  456         1085  
545 60         211 my $absentees = $self->absentees;
546 60         154 my @absenteeids = map { $_->id } @$absentees;
  0         0  
547 60         184 my $play = $self->play;
548 60         121 my $byes = {};
549 60         175 my $round = $self->round;
550 60         176 for my $round ( FIRSTROUND .. $round ) {
551 116         225 for my $id (@ids) {
552 1120         2693 my $player = $self->ided($id);
553 1120         4046 my $game = $play->{$round}->{$id};
554 1120 100 66     6158 if ( $game and $game->can("myRole") ) {
    50 33        
555 1116         1523 eval { $game->myRole($player) };
  1116         2712  
556 1116 50 33     3772 die "Role of player $id in round $round? $@"
557             if not $player or $@;
558 1116         2652 my $role = $game->myRole($player);
559 1116 100       3581 if ( $role eq 'Bye' ) {
560 38         134 $byes->{$id} = $round;
561             }
562             }
563             elsif ( $player->firstround > $round or
564 0     0   0 any { $id eq $_ } @absenteeids ) { next }
  4         27  
565 0         0 else { warn "Player ${id} had Bye in round $round?"; }
566             }
567             }
568 60         587 return $byes;
569             }
570              
571             =head2 incompatibles
572              
573             $nomatch = $tourney->incompatibles(@grandmasters)
574             next if $nomatch->{$alekhine->id}->{$capablanca->id}
575              
576             Collates information from the whoPlayedWho and colorClashes methods to show who cannot be matched or given a bye in the next round, returning an anonymous hash keyed on the ids of @grandmasters. B1,2 C1,6
577              
578             =cut
579              
580             sub incompatibles {
581 0     0 1 0 my $self = shift;
582 0         0 my $oldOpponents = $self->whoPlayedWho;
583 0         0 my $colorIncompatible = $self->colorClashes;
584 0         0 my $players = $self->entrants;
585 0         0 my @id = map { $_->id } @$players;
  0         0  
586 0         0 my $unavailables;
587 0         0 for my $player ( 0 .. $#$players ) {
588 0         0 for ( 0 .. $#$players ) {
589 0         0 my $color = $colorIncompatible->{ $id[$player] }->{ $id[$_] };
590 0         0 my $round = $oldOpponents->{ $id[$player] }->{ $id[$_] };
591 0 0       0 $unavailables->{ $id[$player] }->{ $id[$_] } = $color if $color;
592 0 0 0     0 $unavailables->{ $id[$player] }->{ $id[$_] } ||= $round if $round;
593             }
594             }
595 0         0 return $unavailables;
596             }
597              
598              
599             =head2 medianScore
600              
601             $group = $tourney->medianScore($round)
602              
603             Returns the score equal to half the number of rounds that have been played. Half the contestants will have scores above or equal to this score and half will have ones equal to or below it, assuming everyone has played every round. What IS the number of rounds played, again?
604              
605             =cut
606              
607             sub medianScore {
608 4     4 1 10 my $self = shift;
609 4         5 my $round = shift;
610 4         21 return $round / 2;
611             }
612              
613             =head2 rounds
614              
615             $tourney->rounds
616              
617             Gets/sets the total number of rounds to be played in the competition
618              
619             =cut
620              
621             sub rounds {
622 0     0 1   my $self = shift;
623 0           my $rounds = shift;
624 0 0         if ( defined $rounds ) { $self->{rounds} = $rounds; }
  0 0          
625 0           elsif ( $self->{rounds} ) { return $self->{rounds}; }
626             }
627              
628              
629             =head2 size
630              
631             $size = 'Maxi' if $tourney->size > 2**$tourney->rounds
632              
633             Gets the number of entrants
634              
635             =cut
636              
637             sub size {
638 0     0 1   my $self = shift;
639 0           return scalar @{ $self->entrants };
  0            
640             }
641              
642             =head1 AUTHOR
643              
644             Dr Bean, C<< >>
645              
646             =head1 BUGS
647              
648             Please report any bugs or feature requests to
649             C, or through the web interface at
650             L.
651             I will be notified, and then you'll automatically be notified of progress on
652             your bug as I make changes.
653              
654             =head1 SUPPORT
655              
656             You can find documentation for this module with the perldoc command.
657              
658             perldoc Games::Tournament::Swiss
659              
660             You can also look for information at:
661              
662             =over 4
663              
664             =item * AnnoCPAN: Annotated CPAN documentation
665              
666             L
667              
668             =item * CPAN Ratings
669              
670             L
671              
672             =item * RT: CPAN's request tracker
673              
674             L
675              
676             =item * Search CPAN
677              
678             L
679              
680             =back
681              
682             =head1 ACKNOWLEDGEMENTS
683              
684             See L for the FIDE's Swiss rules.
685              
686             =head1 COPYRIGHT & LICENSE
687              
688             Copyright 2006 Dr Bean, all rights reserved.
689              
690             This program is free software; you can redistribute it and/or modify it
691             under the same terms as Perl itself.
692              
693             =cut
694              
695             1; # End of Games::Tournament::Swiss
696              
697             # vim: set ts=8 sts=4 sw=4 noet: