File Coverage

blib/lib/Games/Tournament/RoundRobin.pm
Criterion Covered Total %
statement 154 175 88.0
branch 50 56 89.2
condition 7 19 36.8
subroutine 17 20 85.0
pod 17 17 100.0
total 245 287 85.3


line stmt bran cond sub pod time code
1             package Games::Tournament::RoundRobin;
2              
3             # Last Edit: 2009 3月 07, 14時35分49秒
4              
5 14     14   478581 use warnings;
  14         37  
  14         652  
6 14     14   80 use strict;
  14         32  
  14         35801  
7              
8             =head1 NAME
9              
10             Games::Tournament::RoundRobin - Round-Robin Tournament Schedule Pairings
11              
12             =head1 VERSION
13              
14             Version 0.02
15              
16             =cut
17              
18             our $VERSION = '0.02';
19              
20             =head1 SYNOPSIS
21              
22             $schedule = Games::Tournament::RoundRobin->new;
23              
24             $pairings = $schedule->indexesInRound($roundm);
25             $round = $schedule->meeting($member1, [$member2, $member3]);
26             ...
27              
28             =head1 DESCRIPTION
29              
30             Every member of a league of 2n players can be paired with every other member in 2n-1 rounds.
31              
32             If the league members are (Inf, 1 .. 2n-1), then in round i, i can be paired with Inf, and a can meet b, where a+b = 2i (mod 2n-1).
33              
34             =head1 REQUIREMENTS
35              
36             Installing this module requires Module::Build.
37              
38             =head1 METHODS
39              
40             =head2 new
41              
42             Games::Tournament::RoundRobin->new( v => 5, league => [qw/Ha Be He/])
43             Games::Tournament::RoundRobin->new( league => {A => $a, B => $b})
44              
45             Where v (optional) is the number of league members, and league (optional) is a list (or a hash) reference to the individual unique league members. One of v, or league (which takes precedence) is necessary, and if league is not given, the members are identified by the numbers 0 .. n-1.
46              
47             If the league is a list (or hash) of n objects, they should be instances of a class that overloads both string quoting with a 'name' method and arithmetical operations with an 'index' method. The index method, called on the n objects in order, should return the n numbers, 0 .. n-1, and in that order if they are presented as an array. If they are presented as a hash, the hash is stored internally as an array and the keys are discarded.
48              
49             If the league is a list of strings or numbers, indexes are constructed for the values on the basis of their positions in the list, and if a hash of strings or numbers, on the basis of the lexicographic order of their keys. Each string is expected to be unique.
50              
51             If n is odd, an additional n-1, 'Bye' or object (a Games::League::Member object, by default) member, depending on the type of the first member in the league, is added at the end and n is increased by 1.
52              
53             TODO This was not such a good idea. v should not change if it is odd. That is the size of the league is the number of real members. The Bye member should not be included. This will require some refactoring.
54              
55             =cut
56              
57             sub new
58             {
59 78     78 1 23622 my $class = shift;
60 78         516 my %args = @_;
61 78         127 my $n;
62             my $members;
63 78         155 $members = $args{league};
64 78 100       268 if ( ref $members )
65             {
66 75 100       316 $members = _hash2array( $members ) if ref $members eq 'HASH';
67 75         163 $n = $#$members + 1;
68 75         110 my $memberClass;
69 75 100       450 if ( $memberClass = ref $members->[0] )
    100          
70             {
71             $members->[$_]->index == $_ or warn
72             "Index of ${_}th member is $members->[$_]->{index}, not $_,"
73 18   33     127 foreach ( 0 .. $n-1 );
74 18   50     74 $memberClass ||= 'Games::League::Member';
75 18 100       114 push @$members, $memberClass->new(
76             index => $n++, name => 'Bye' ) if $n%2;
77             }
78             elsif ($members->[0] =~ m/^\d+$/)
79             {
80 26 100       990 push @$members, $n++ if $n%2;
81             }
82             else {
83 31 100       114 if ($n%2)
84 18         39 { push @$members, 'Bye' ;
85 18         37 $n++;
86             }
87             }
88             }
89             else {
90 3   33     21 $n ||= $args{v};
91 3 50       13 $n++ if $n%2;
92             }
93 78   100     248 $members ||= [ 0 .. $n-1 ];
94 78         142 $args{v} = $n;
95 78         148 $args{league} = $members;
96 78         436 bless \%args, $class;
97             }
98              
99             # Converts an hash into a array discarding the keys. Used internally to
100             # store the league argument if a hash is passed.
101              
102             sub _hash2array
103             {
104 18     18   46 my $hash = shift;
105 18         561 my $array;
106             my $index;
107 18         48 my $n = 0;
108             # $array->[$n++] = $hash->{$_} foreach ( keys %$hash );
109 18         113 foreach my $key ( sort keys %$hash )
110             {
111 59 100       119 if ( ref $hash->{$key} )
112             {
113 19 50       131 $hash->{$key}->{index} = $n
114             unless exists $hash->{$key}->{index};
115 19         33 my $index = $hash->{$key}->{index};
116 19         35 $array->[$index] = $hash->{$key};
117 19         37 $n++;
118             }
119             else
120             {
121 40         648 $array->[$n++] = $hash->{$key};
122             }
123             }
124 18         47 return $array;
125             }
126              
127             =head2 indexesInRound
128              
129             $schedule->indexesInRound($m)
130              
131             Returns an array reference of the pairings in round $m. This method is useful if you are using numbers to represent your league members. It is not so useful if you are using strings or objects and you don't know their index numbers. Positions in the array represent members. The values represent their partners. Each member is thus represented twice.
132              
133             =cut
134              
135             sub indexesInRound
136             {
137 120     120 1 4534 my $self = shift;
138 120         220 my $n = $self->size;
139 120         176 my $round = shift;
140 120         210 my @pairings = ($round);
141 120         424 for my $i (1 .. $n-1)
142             {
143 966 100       1610 if ($i == $round)
144             {
145 120         219 push @pairings, 0;
146             }
147             else
148             {
149 846         1316 my $modPartner = ((2*$round-$i) % ($n-1));
150 846 100       1258 my $partner = $modPartner? $modPartner: $n-1;
151 846         1591 push @pairings, $partner;
152             }
153             }
154 120         522 return \@pairings;
155             }
156              
157             =head2 roundsInTournament
158              
159             $t = $schedule-> roundsInTournament;
160             $round1 = $t[0];
161             $inRound1FourthWith = $t->[0]->[3];
162             $inLastRoundLastWith = $$t[-1][-1];
163              
164             Returns, as a reference to an array of arrays, the pairings in all rounds of the tournament. This method is useful if you are using the algorithm indexes.
165              
166             =cut
167              
168             sub roundsInTournament
169             {
170 3     3 1 22 my $self = shift;
171 3         3 my $matrix;
172             push @$matrix, $self->indexesInRound($_)
173 3         10 foreach 1 .. $self->rounds;
174 3         20 return $matrix;
175             }
176              
177             =head2 partner
178              
179             $schedule->partner($member, $m)
180              
181             Returns the partner of $member in round $m.
182              
183             =cut
184              
185             sub partner
186             {
187 8     8 1 49 my $self = shift;
188 8         12 my $member = shift;
189 8         10 my $round = shift;
190 8         10 my @partners = @{$self->indexesInRound($round)};
  8         17  
191 8         26 my $index = $self->index($member);
192 8         21 my $partner = $self->member($partners[$index]);
193 8         42 return $partner;
194             }
195              
196             =head2 membersInRound
197              
198             $schedule->membersInRound($m)
199              
200             Returns an hash reference of the pairings in round $m. This method is useful if you are using strings or objects. Keys in the hash represent league members. If the league members are objects, their names are used as keys. If 2 names are the same, the names are changed to ${name}1, ${name}2 etc. The values are their partners. Each player is thus represented twice.
201              
202             =cut
203              
204             sub membersInRound
205             {
206 4     4 1 32 my $self = shift;
207 4         11 my $n = $self->size;
208 4         6 my $round = shift;
209 4         6 my %pairings;
210 4         6 my @indexes = @{$self->indexesInRound($round)};
  4         12  
211 4         11 for my $i (0 .. $n-1)
212             {
213 24         76 my $member = $self->member($i);
214             # my $index = $self->index($member);
215 24 100       62 if ( defined $pairings{$member} ) {
216 2         5 my $clobbered = $member . 1;
217 2         4 $pairings{$clobbered} = $pairings{$member};
218 2         6 delete $pairings{$member};
219 2         4 $member = $member . 2;
220             }
221 24         100 my $partner = $indexes[$i];
222 24         40 $partner = $self->member($partner);
223 24         592 $pairings{$member} = $partner;
224             }
225 4         38 return \%pairings;
226             }
227              
228             =head2 wholeSchedule
229              
230             $schedule->wholeSchedule();
231              
232             Returns a reference to an array of arrays, with each of the latter 2-element arrays representing the players in each match in each round. Thus, you can iterate through the top-level array and inner-loop through the lower-level array and print the schedule as it has to be.
233              
234             This does not work with numbers as the names of the members and also not with "v >= n".
235              
236             =cut
237              
238             sub wholeSchedule
239             {
240 19     19 1 54 my $self = shift;
241 19         25 my @schedule;
242 19         109 for my $i ( 1 .. $self->rounds ) {
243 71         88 my @pairings = @{ $self->indexesInRound($i) };
  71         164  
244 71         108 my @round;
245 71         147 for my $member ( 0 .. $#pairings ) {
246 466 100       1319 next unless exists $pairings[$member];
247 233         362 my $partner = $pairings[$member];
248 233         333 delete $pairings[$partner];
249 233         560 push @round, [ $self->member($member),
250             $self->member($partner) ];
251             }
252 71         204 push @schedule, \@round;
253             }
254 19         93 return \@schedule;
255             }
256              
257             =head2 byelessSchedule
258              
259             $schedule->byelessSchedule();
260              
261             Returns a reference to an array of arrays. The arrays are the same as returned by C without the "Byes," so you can iterate through them and print the schedule as it has to be.
262              
263             This does not work with numbers as the names of the members and also not with "v >= n".
264              
265             =cut
266              
267             sub byelessSchedule
268             {
269 5     5 1 34 my $self = shift;
270 5         14 my $schedule = $self->wholeSchedule;
271 5         10 my @byeless;
272 5         12 for my $round ( @$schedule ) {
273 21         23 my @matches;
274 21         56 for my $match ( @$round ) {
275 73         121 my @contestants = @$match;
276 146         455 push @matches, $match unless
277 73 100       84 grep { $_ eq 'Bye' } @contestants;
278             }
279 21         50 push @byeless, \@matches;
280             }
281 5         43 return \@byeless;
282             }
283              
284             =head2 memberSchedule
285              
286             $schedule->memberSchedule($member)
287              
288             Returns, as an array reference, the partners who $member is matched with in the order in which they meet, ie round by round.
289              
290             =cut
291              
292             sub memberSchedule
293             {
294 0     0 1 0 my $self = shift;
295 0         0 my $member = shift;
296 0         0 my $schedule;
297 0         0 foreach my $round ( 0 .. $self->rounds-1 )
298             {
299 0         0 my $allMembers = $self->indexesInRound($round);
300 0         0 push @$schedule, $$allMembers[$member];
301             }
302 0         0 return $schedule;
303             }
304              
305             =head2 meeting
306              
307             $schedule->meeting($member,$partner)
308              
309             Returns the rounds (TODO and the venue) at which $member meets $partner.
310              
311             =cut
312              
313             sub meeting
314             {
315 65     65 1 5929 my $self = shift;
316 65         123 my $n = $self->size;
317 65         92 my ($member, $partner) = @_;
318 65         125 my $a = $self->index($member);
319 65         130 my $b = $self->index($partner);
320 65         91 my $round = $a+$b;
321 65 100       197 if ($a == 0)
    100          
    100          
322             {
323 13         44 return 0+$b;
324             }
325             elsif ($b == 0)
326             {
327 10         32 return 0+$a;
328             }
329             elsif ( $round % 2) {
330 25         76 $round = ($round + $n-1)/2 % ($n-1);
331 25   66     74 $round ||= $n-1;
332 25         82 return 0+$round;
333             }
334             else {
335 17         72 return 0+($round/2)%($n-1);
336             }
337             }
338              
339             =head2 meetings
340              
341             $schedule->meetings($member1,[$member2,$member3,...])
342              
343             Returns, as an array reference, the rounds (TODO and the venue) at which $member1 meets $member2, $member3, ...
344              
345             =cut
346              
347             sub meetings
348             {
349 18     18 1 14513 my $self = shift;
350 18         42 my $n = $self->size;
351 18         30 my ($member, $partners) = @_;
352 46         101 my @meetings = map {
353 18         27 $self->meeting($member,$_);
354             } @$partners;
355 18         66 return \@meetings;
356             }
357              
358             =head2 index
359              
360             $schedule->index($member)
361              
362             Returns $member's index, the number which is used to pair it with other members. The index is the position, 0..n-1, of the $member in the league argument to the constructor (if an array) or the constructed array (if a hash.)
363              
364             If $member is not a member of the array, or is itself an index, undef is returned.
365              
366             =cut
367              
368             sub index
369             {
370 336     336 1 4461 my $self = shift;
371 336         443 my $member = shift;
372 336         514 my $members = $self->{league};
373 336         385 my $i = 0;
374 336         502 foreach my $candidate ( @$members )
375             {
376 2374 100       6208 if ( $candidate =~ m/^\d+$/)
377             {
378 2109 100       4546 return $i if $candidate == $member;
379             }
380             else {
381 265 100       653 return $i if $candidate eq $member;
382             }
383 2042         2385 $i++;
384             }
385 4         674 return undef;
386             }
387              
388             =head2 member
389              
390             $schedule->member($index)
391             $schedule->member($name)
392             $bye = $schedule->member( $schedule->size-1 )
393              
394             Returns the member represented by $index, a number which ranges from 0..n-1, or by $name, a string. If there is no such member, undef is returned.
395              
396             =cut
397              
398             sub member
399             {
400 572     572 1 3465 my $self = shift;
401 572         1250 my $handle = shift;
402 572         767 my $members = $self->{league};
403 572 100       2028 if ( $handle =~ /\d+/ ) {
404 562         2271 return $members->[$handle];
405             }
406             else
407             {
408 10         22 foreach my $member ( @$members )
409             {
410 38 100       162 return $member if $member eq $handle;
411             }
412 2         7 return undef;
413             }
414             }
415              
416             =head2 hasBye
417              
418             $schedule->hasBye($index)
419              
420             Returns an array reference of all the partners of the $indexed member, excluding the 'Bye' member. Don't use this if you have no 'Bye' member, as it just leaves off the last member.
421              
422             =cut
423              
424             sub hasBye
425             {
426 0     0 1 0 my $self = shift;
427 0         0 my $index = shift;
428 0         0 my $members = $self->{league};
429 0         0 my @partners;
430 0         0 foreach my $member ( @$members )
431             {
432 0 0 0     0 push @partners, $member unless
433             ($member == $index or $member == $self->size-1);
434             }
435 0         0 return \@partners;
436             }
437              
438             =head2 partners
439              
440             $schedule->partners($index)
441             $schedule->partners($name)
442              
443             Returns an array reference of all the partners of the $indexed or $named member, in index order, or the order in the league argument.
444              
445             =cut
446              
447             sub partners
448             {
449 21     21 1 88 my $self = shift;
450 21         32 my $handle = shift;
451 21         47 my $members = $self->{league};
452 21         51 my $partneredOne = $self->member($handle);
453 21         28 my @partners;
454 21         43 foreach my $member ( @$members )
455             {
456 200 100       611 if ( $handle =~ /\d+/ )
457             {
458 178 100       351 push @partners, $member unless
459             $self->index($member) == $handle;
460             }
461             else
462             {
463 22 100       58 push @partners, $member unless $member eq $handle;
464             }
465             }
466 21         162 return \@partners;
467             }
468              
469             =head2 realPartners
470              
471             $schedule->realPartners($index)
472              
473             Returns an array reference of all the partners of the $indexed member, excluding the 'Bye' member. Don't use this if you have no 'Bye' member, as it just leaves off the last member.
474              
475             =cut
476              
477             sub realPartners
478             {
479 0     0 1 0 my $self = shift;
480 0         0 my $index = shift;
481 0         0 my $members = $self->{league};
482 0         0 my @partners;
483 0         0 foreach my $member ( @$members )
484             {
485 0 0 0     0 push @partners, $member unless
486             ($member == $index or $member == $self->size-1);
487             }
488 0         0 return \@partners;
489             }
490              
491             =head2 size
492              
493             $schedule->size
494              
495             Returns the number of members in the round robin. Sometimes this may not be the same as the number of league members specified, because the array of league members takes precedence if supplied, and a bye is added if the number is odd.
496              
497             =cut
498              
499             sub size
500             {
501 256     256 1 378 my $self = shift;
502 256         711 $self->{v};
503             }
504              
505             =head2 rounds
506              
507             $schedule->rounds
508              
509             Returns the number of rounds in the round robin. This equals the number of league members, minus 1.
510              
511             =cut
512              
513             sub rounds
514             {
515 38     38 1 86 my $self = shift;
516 38         98 $self->size - 1;
517             }
518              
519             =head1 AUTHOR
520              
521             Dr Bean, C<< >>
522              
523             =head1 BUGS
524              
525             Please report any bugs or feature requests to C, or through the web interface at L. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
526              
527             =head1 SUPPORT
528              
529             You can find documentation for this module with the perldoc command.
530              
531             perldoc Games::Tournament::RoundRobin
532              
533             You can also look for information at:
534              
535             =over 4
536              
537             =item * AnnoCPAN: Annotated CPAN documentation
538              
539             L
540              
541             =item * CPAN Ratings
542              
543             L
544              
545             =item * RT: CPAN's request tracker
546              
547             L
548              
549             =item * Search CPAN
550              
551             L
552              
553             =back
554              
555             =head1 ACKNOWLEDGEMENTS
556              
557             The algorithm saw perl attention on Mark Jason Dominus's Quiz of the Week in January 2005, last seen on the Internet at L
558              
559             The wholeSchedule method is due to Richard Möhn.
560              
561             =head1 COPYRIGHT & LICENSE
562              
563             Copyright 2008 Dr Bean, All Rights Reserved.
564              
565             This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
566              
567             =cut
568              
569             1; # End of Games::Tournament::RoundRobin