#!/usr/bin/perl -w # $Id: gopair,v 1.12 2005/01/23 18:59:53 reid Exp $ # gopair # # Copyright (C) 2004, 2005 Reid Augustin reid@netchip.com # 1000 San Mateo Dr. # Menlo Park, CA 94025 USA # # This library is free software; you can redistribute it and/or modify it # under the same terms as Perl itself, either Perl version 5.8.5 or, at your # option, any later version of Perl 5 you may have available. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY # or FITNESS FOR A PARTICULAR PURPOSE. # # todo: =head1 NAME gopair - Perl script to generate go tournament pairings =head1 SYNOPSIS $ gopair [ options ] [ round_number ] =head1 DESCRIPTION gopair uses the Pair perl extension to find the locally best pairings according to criteria determined by some register.tde directives. A normal handicapped pairing (##HANDICAPS MAX) uses the following Pair scores: -1 * abs(rating difference) -1.5 * abs (wins difference) -2 * (belong to the same club) -16 * (already played each other) A non-handicapped pairing (## HANDICAPS MIN) uses these scores: 0 * abs(rating difference) -1.5 * abs (wins difference) 0 * (belong to the same club) -16 * (already played each other) Additionally, early round pairings are adjusted so that the 'best' opponent is artifically knocked down a few places so that the most interesting games are delayed until later rounds. =cut # first, some boilerplate: use strict; require 5.001; BEGIN { our $VERSION = sprintf "%d.%03d", '$Revision: 1.12 $' =~ /(\d+)/g; } use IO::File; use Games::Go::AGATourn; use Algorithm::Pair::Best; use File::stat; # stat fields by name our ($round, $nextRound, $agaTourn); our ($byeOpt, %drops, $byeId, %IDtoPair, @directives); our ($over_write, $no_rats, $no_unfinished, $no_handi, $no_early, $pairing); $over_write = $no_rats = $no_unfinished = $no_handi = $no_early = 0; # scoreSubs: subroutines for Pair to call when scoring a candidate our @scoreSubs = ( sub { # difference in rating. my ($my, $candidate, $explain) = @_; # the multiplier on this is 1, so that makes this the 'normal' factor my $score = -(abs($my->rating - $candidate->rating)); $score -= 5 if ($score >= 9); # even more penalty for more than 9 stones $score = -rand(0.1) if ($no_handi); # very small randomization return sprintf "%5.1fr", $score if ($explain); return $score; }, sub { # already played? my ($my, $candidate, $explain) = @_; my $already = -0; foreach (@{$my->{info}{played}}) { $already++ if ($_ eq $candidate->id); # we might have played him several times! } # large penalty for each time we've already played my $score = -16 * $already; return sprintf "%3.0fp", $score if ($explain); return $score; }, sub { # number of wins. encourage winners agains winners. my ($my, $candidate, $explain) = @_; # 1.5 stone penalty for each difference in win scores my $score = -1.5 * abs($my->wins - $candidate->wins); return sprintf "%4.1fw", $score if ($explain); return $score; }, sub { # belong to the same club? my ($my, $candidate, $explain) = @_; return 0 if ($no_handi); # small penalty for being in the same club my $score = (($my->{info}{club} ne '') and ($my->{info}{club} eq $candidate->{info}{club})) ? -2 : 0; return sprintf "%1.0fc", $score if ($explain); return $score; }, ); sub Usage { print("\nUsage: gopair [options] run pairings for round Number\n"); print(" options:\n"); print(" -rr use round-robin pairing\n"); print(" -best use \'best\' pairing (this is the default)\n"); print(" -nr don't create/read the ratings adjustment file\n"); print(" -nu don't warn if unfinished games are found\n"); print(" -ow overwrite .tde file if it already exists\n"); print(" -nh no handicaps (same as ##HANDICAPS MIN directive)\n"); print(" -ne suppress early-round adjustment (same as ##NOEARLY directive)\n"); print(" -bye ID force ID to be the BYE player for this round\n"); print(" -drop ID DROP ID for this round\n"); print(" N N is the round number to pair\n"); print("\n"); } =head1 OPTIONS With no options, gopair reads the register.tde file and all available round files (of the form 1.tde, 2.tde...). gopair then generates pairings for the next round following the last available round file. Output files are N.tde and pairs_N.txt (where N is the number following the last available round file). The following options modify gopair default behavior: =over 4 =item B<-rr>, B<-best> Normally, gopair uses the 'Best' pairing algorithm (as described in perldoc Algorithm::Pair::Best). Best pairing can lead to bad results when the number of players is only slightly greater than the number of rounds to be played. Specifically, it might pair early rounds such that in the final round, there is no choice but to pair people who have already played each other. In this case, round-robin pairing is better. Use -rr, or better, add the following directive to the register.tde file: ## PAIR ROUND_ROBIN gopair will abort if best (the default) pairing is selected and round-robin appears to be better. If you really need to use best pairing in this case, over-ride the abort by adding: ## PAIR BEST to the register.tde file. =item B<-nr> When pairing a handicap tournament (##HANDICAPS MAX), gopair attempts to read a ratings adjustment file of the form 'rats_N.txt'. If such a file is not found, or if it is found but it's older than its corresponding result file (N.tde where N is the previous round), gopair runs the 'rats' script to create an up-to-date ratings adjustment file. The adjusted ratings are used instead of the entry-time ratings in register.tde to pair the next round. Generation and reading of the ratings adjustment file can be suppressed with the B<-nr> (no ratings) option. For non-handicapped tournaments it is better to use the: ##HANDICAPS MIN directive in the register.tde file (which also prevents ratings adjustment) because there is no chance to accidentally forget to suppress ratings adjustments for a round. If you wish to pair a handicapped tournament but prevent ratings adjustments, it is better to use: ## NORATS in register.tde which is equivilent to the B<-nr> option. Ratings adjustment is never performed when pairing round 1. =item B<-nu> If games are found in the round files that do not yet have a result (i.e. the result is not 'w', 'W', 'b', or 'B'), gopair prints a warning message and asks if you wish to proceed. The query can be suppressed with the B<-nu> (no unfinished) option. =item B<-ow> If the round that gopair is attempting to create already exists, gopair normally prints a warning and asks if you wish to proceed. The warning and query can be suppressed with the B<-ow> (overwrite) option. =item B<-nh> gopair normally tries to pair a handicap tournament. This is not appropriate for an 'open' tournament, for example. Handicapping can be turned of by adding a directive to the register.tde file: ## HANDICAPS MIN or by using the B<-nh> (no handicaps) option. The directive in the register.tde file is preferred because it reduces the chance that rounds might be paired with handicapping left on by mistake. When handicapping is disabled, the pairing score ignores both the difference in rating and whether or not players belong to the same club. (see B above). In addition, handicap stones for all games is set to -0, and komi for all games is set to either 7.5 for ING rules or 6.5 for non-ING rules. =item B<-ne> gopair normally adjusts early round pairing to reduce the chance of pairing the most interesting matches in the first rounds. The B<-ne> option or the: ## NOEARLY directive suppresses this adjustment. Early round adjustment is done by changing the cached pairing scores after an initial scoring sort. The top N-1 candidates for the top 2 * N players get 2 * the score of candidate N added to their score. This drops the first N-1 candidates to somewhere below candidate N. N is the total number of rounds minus the current round number minus 2. The adjustment is fairly strong in round 1, relatively mild in the third to the last round, and there is no adjustment for the last two rounds. =item B<-bye> id Forces player B to be the BYE player for this round. If B is not a valid AGA ID, gopair aborts with an error. Using B<-bye> when there are an even number of players to pair has no effect (see B<-drop>). Only one B<-bye> option is accepted, and if provided, it over-rides BYE players in the register.tde file. =item B<-drop> id Force player B to be the DROPped for this round. If B is not a valid AGA ID, gopair aborts with an error. Any number of B<-drop>s may be used. =item B (where N is a round number) gopair attempts to pair round B. If round B already exists, a warning is issued and gopair asks if you wish to proceed (see the B<-ow> option above). If any round less than B does not exists, gopair aborts with an error. =cut for (my $ii = 0; $ii < @ARGV; $ii++) { if ($ARGV[$ii] eq '-ow') { $over_write = 1; } elsif ($ARGV[$ii] eq '-nr') { # no ratings adjustment file? $no_rats = 1; } elsif ($ARGV[$ii] eq '-nu') { # no warning for unfinished games? $no_unfinished = 1; } elsif ($ARGV[$ii] eq '-nh') { # no handicaps $no_handi = 1; } elsif ($ARGV[$ii] eq '-ne') { # no early round adjustment $no_early = 1; } elsif ($ARGV[$ii] eq '-rr') { # use round-robin pairing $pairing = 'rr'; } elsif ($ARGV[$ii] eq '-best') { # use best pairing $pairing = 'best'; } elsif ($ARGV[$ii] eq '-bye') { # bye player die ("Please provide an AGA ID for the -bye option\n") unless defined($ARGV[++$ii]); die ("I can only accept one -bye option\n") if defined($byeOpt); $byeOpt = Games::Go::AGATourn->NormalizeID($ARGV[$ii]); } elsif ($ARGV[$ii] eq '-drop') { # drop a player die ("Please provide an AGA ID for the -drop option\n") unless defined($ARGV[++$ii]); $drops{Games::Go::AGATourn->NormalizeID($ARGV[$ii])} = 1; } elsif ($ARGV[$ii] =~ m/\D/) { # not a number? print("unknown option: $ARGV[$ii]\n"); Usage(); exit(1); } else { if (defined($nextRound)) { print("\nI can only handle one round at a time\n"); Usage(); exit(1); } $nextRound = $ARGV[$ii]; } } # create agaTourn, read register.tde and all round files up to $nextRound, or # all round file if $nextRound isn't set. $round = $nextRound - 1 if (defined($nextRound)); # read register.tde. If nextRound not called out on command line, read all # round files too. Otherwise, read up to nextRound - 1: if (defined($nextRound)) { $agaTourn = Games::Go::AGATourn->new(Round => 0); die("Error in AGATourn\n") if (not defined($agaTourn) or $agaTourn->Error); for ($round = 1; $round < $nextRound; $round++) { last unless (-f "$round.tde"); $agaTourn->ReadRoundFile("$round.tde"); } $round--; if ($nextRound != $round + 1) { die("The last valid round file is $round.tde, I can't pair round $nextRound\n"); } } else { $agaTourn = Games::Go::AGATourn->new; die("Error in AGATourn\n") if (not defined($agaTourn) or $agaTourn->Error); $round = $agaTourn->Round || 0; $nextRound = $round + 1; } die("Error in AGATourn\n") if ($agaTourn->Error); $no_rats = 1 if (defined($agaTourn->Directive('NORATS'))); push(@directives, 'NoRats') if $no_rats; $no_rats = 1 if ($round < 1); $no_early = 1 if (defined($agaTourn->Directive('NOEARLY'))); unless(defined($pairing)) { if (defined($agaTourn->Directive('PAIR'))) { $pairing = $agaTourn->Directive('PAIR')->[0]; if ($pairing =~ m/^round_robin$/i) { $pairing = 'rr'; }elsif ($pairing =~ m/^best$/i) { $pairing = 'best'; } else { die ("Unknown PAIR directive in register.tde: $pairing\n" . " I only know about 'BEST' and ROUND_ROBIN'\n"); } } else { $pairing = 'best'; } } if (defined($agaTourn->Directive('HANDICAPS'))) { $no_handi = 1 if (uc($agaTourn->Directive('HANDICAPS')->[0]) eq 'MIN'); } push(@directives, 'Pairing:' . (($pairing eq 'rr') ? 'Round_Robin' : 'Best')); push(@directives, "Handicaps:" . ($no_handi ? 'Min' : 'Max')); my $rounds = $agaTourn->Rounds; my $numPlayers = scalar(keys(%{$agaTourn->Rating})); my $numByes = 0; if ($numPlayers % 2) { my $flags = $agaTourn->Flags; # get ref to flags hash foreach my $id (keys %{$flags}) { $numByes++ if($flags->{$id} =~ m/\bbye\b/i); # found BYE candidate> } if ($numByes == 1) { $numByes = -1; # bye will always be removed from pairings } else { $numByes = 1; # there will always be a BYE pair, so it's like an extra person } } if (($numPlayers + $numByes) == $agaTourn->Rounds + 1) { if (($pairing ne 'rr') and (not (defined($agaTourn->Directive('PAIR')) and (uc($agaTourn->Directive('PAIR')->[0]) eq 'BEST')))) { my $byeMsg = ($numByes == -1) ? " (1 Bye)" : ""; print("With $numPlayers players$byeMsg and $rounds rounds, I strongly suggest that\n", " you use Round Robin pairing. Please add '## Pair Round_Robin' to the\n", " register.tde file (or '## Pair Best' to force 'Best' pairing, but be\n", " warned that 'Best' pairing could have bad results in the last round -\n", " like forcing two players who have already played to play again).\n"); die "Aborting...\n"; } } else { if ($pairing eq 'rr') { print("Note: using round_robin pairing, but 'Best' might be a better alternative\n"); } } unless ($no_unfinished) { my @noResult; foreach my $g (@{$agaTourn->GamesList}) { my ($whiteID, $blackID, $result, $handicap, $komi, $round) = split(',', $g); next if ($result =~ m/^[bw]$/i); $noResult[$round]++; } my @msg; for (my $ii = 0; $ii < @noResult; $ii++) { if (defined($noResult[$ii]) and ($noResult[$ii] > 0)) { push (@msg, "$noResult[$ii] unfinished games in round $ii") } } if (@msg) { print(join (' and ', @msg), " - continue anyway (y/n)? "); my $response = lc(); chomp($response); exit(0) unless (($response eq 'y') || ($response eq 'yes')); } } my $out_filename = "$nextRound.tde"; if (-f $out_filename) { unless($over_write) { print("$out_filename already exists - overwrite it (y/n)? "); my $response = lc(); chomp($response); exit(0) unless (($response eq 'y') || ($response eq 'yes')); } print("Moving $out_filename to ${nextRound}_tde.old\n"); rename($out_filename, "${nextRound}_tde.old"); } my @pairs; if ($pairing eq 'rr') { @pairs = pairRR(); } else { @pairs = pairBest(); } my $pairFileName = "pairs$nextRound"; my $tdeFileName = "$nextRound.tde"; print "pairing complete, writing $pairFileName and $tdeFileName\n\n"; my $pairFP = IO::File->new(">$pairFileName") or die( "Error opening $pairFileName for writing\n"); my $tdeFP = IO::File->new(">$tdeFileName") or die( "Error opening $tdeFileName for writing\n"); my $totalScore = 0; my $line = 999; my $tourney = $agaTourn->Tourney; my $nameW = $agaTourn->NameLength; $nameW = ($nameW > 23) ? 23 : $nameW; # adjusted by hand to give max 80 chars wide output my $nameW2 = $nameW + 9; # name width plus some for win/loss and rating my $page = 1; my ($header, $pairTxt, $tdeTxt); for (my $ii = 0; $ii < @pairs; $ii+= 2) { $header = headerTxt() if ($line >= 55); my $id1 = $pairs[$ii]; my $id2 = $pairs[$ii+1]; my ($handi, $komi); if ($no_handi) { $handi = rand(2) - 1; # random number from -1 to 1 } else { ($handi, $komi) = $agaTourn->Handicap($id1, $id2); } if ($handi < 0) { ($pairTxt, $tdeTxt) = pairLines($ii, $id2, $id1); } else { ($pairTxt, $tdeTxt) = pairLines($ii, $id1, $id2); } $tdeFP->print("$tdeTxt\n"); $pairFP->print($header, "$pairTxt\n\n"); print($header) if ($page == 2); print("$pairTxt"); $header = ''; if($pairing eq 'best') { printf " S:% 5.1f = %s\n", score($id1, $id2), explainScores($id1, $id2); $totalScore += score($id1, $id2); } else { print "\n"; } $line += 2; } if (defined($byeId)) { $pairFP->printf("BYE: (%d)%-5.1f %s\n", $agaTourn->Wins($byeId), $agaTourn->Rating($byeId), $agaTourn->Name($byeId)); $line++; $tdeFP->printf ("# BYE: (%d)%-5.1f %s\n", $agaTourn->Wins($byeId), $agaTourn->Rating($byeId), $agaTourn->Name($byeId)); printf ("BYE: (%d)%-5.1f %s\n", $agaTourn->Wins($byeId), $agaTourn->Rating($byeId), $agaTourn->Name($byeId)); } foreach my $id (keys(%drops)) { $header = headerTxt() if ($line >= 55); $pairFP->printf($header); $pairFP->printf("DROP: (%d)%-5.1f %s\n", $agaTourn->Wins($id), $agaTourn->Rating($id), $agaTourn->Name($id)); $line++; $tdeFP->printf ("# DROP: (%d)%-5.1f %s\n", $agaTourn->Wins($id), $agaTourn->Rating($id), $agaTourn->Name($id)); printf ($header); printf ("DROP: (%d)%-5.1f %s\n", $agaTourn->Wins($id), $agaTourn->Rating($id), $agaTourn->Name($id)); $header = ''; } $totalScore = int($totalScore * 100) / 100; $pairFP->print("Pairing score: $totalScore\n"); $tdeFP->print ("# Pairing score: $totalScore\n"); print ("Pairing score: $totalScore\n"); close $pairFP; close $tdeFP; sub pairLines { my ($ii, $id1, $id2) = @_; my ($handi, $komi); if ($no_handi) { ($handi, $komi) = $agaTourn->Handicap($id1, $id1); # always even handicap } else { ($handi, $komi) = $agaTourn->Handicap($id1, $id2); } my $wId = sprintf("%*.*s (%d)%-5.1f", $nameW, $nameW, $agaTourn->Name($id1), $agaTourn->Wins($id1), $agaTourn->Rating($id1)); my $bId = sprintf("%5.1f(%d) %-*.*s", $agaTourn->Rating($id2), $agaTourn->Wins($id2), $nameW, $nameW, $agaTourn->Name($id2)); my $how; if ($handi) { $how = sprintf("% 2.0f %-4s", $handi, 'hndi'); } else { if ($komi >= 0) { $how = sprintf("% 4.1f %-4s", $komi + 0.5, 'komi'); } elsif ($komi < -1) { $how = sprintf("% 4.1f %-4s", $komi - 0.5, 'rvrs'); } else { $how = "B win tie"; } } return (sprintf("%3d %*.*s %*.*s %-9s", # the pairs line 1 + $ii / 2, $nameW2, $nameW2, $wId, -$nameW2, $nameW2, $bId, $how), sprintf("%-8s %-8s ? % 2.0f % 2.0f # %3d %5.1f %*.*s:%*.*s", # the .tde line $id1, $id2, $handi, $komi, 1 + $ii / 2, score($id1, $id2), $nameW2, $nameW2, $wId, -$nameW2, $nameW2, $bId)); } ################################# # # subroutines for 'round-robin' pairing (as opposed to 'best') # ################################# sub pairRR { # get a random sorting of AGA IDs that is reproducable across # multiple calls to this script: my @pairs = randomList($agaTourn->Tourney, keys %{$agaTourn->Rating}); for (my $ii = 0; $ii < $round; $ii++) { @pairs = roundRobin(@pairs); } return @pairs; } # from a phrase and a list of items, generate a somewhat random, yet reproducable list # of the items. sub randomList { my ($phrase, @items) = @_; use Digest::SHA1 'sha1'; my %hash; foreach my $item (@items) { my $key = sha1("$phrase,$item"); if (exists($hash{$key})) { return(randomList("$phrase x", @items)); # try again with different phrase } $hash{$item} = $key; } return(sort {$hash{$a} cmp $hash{$b}} keys(%hash)); } sub roundRobin { my @items = @_; my @rItems; # return items my $ii; for ($ii = 1; $ii < scalar(@items) - 1; $ii++) { if ($ii % 2) { $rItems[$ii + 2] = $items[$ii]; # odd items down 2 } else { $rItems[$ii - 2] = $items[$ii]; # even items up 2 } } $rItems[1] = $rItems[0]; # item 2 should go to 1 instead of 0 $rItems[0] = $items[0]; # first item stays put $rItems[$ii - 1] = $items[$ii]; # last item only moves 1 return @rItems; } ################################# # # subroutines for 'best' pairing (as opposed to 'round-robin') # ################################# # pairing score for two player IDs sub score { my ($id1, $id2) = @_; my $score = 0; foreach my $s (@scoreSubs) { $score += $IDtoPair{$id1}->$s($IDtoPair{$id2}); # verbose scoring } return $score; } # when called on to explain candidate scoring: sub explainScores { my ($id1, $id2) = @_; my @reasons; foreach my $s (@scoreSubs) { push(@reasons, $IDtoPair{$id1}->$s($IDtoPair{$id2}, 1)); # verbose scoring } return join('+', @reasons); } { package Algorithm::Pair::Best; # add some methods to the Pair object sub rating { # add method to access ratings (used in scoreSubs above) my $my = shift; return $my->{info}{rating}; } sub wins { # add method to access wins my $my = shift; return $my->{info}{wins}; } sub id { # add method to access id my $my = shift; return $my->{info}{id}; } # show progress made for each finalized pairing: sub progress { my ($my, $item0, $item1) = @_; print $item0->id, " paired with ", $item1->id, "\n"; } } package main; sub pairBest { unless($no_rats or $no_handi) { # see if we need to run rats to create latest ratings file my $rats_fname = "rats_$round.txt"; die ("Can't read $round.tde\n") unless (-r "$round.tde"); my $r_mtime = stat("$round.tde")->mtime or die("'stat'ing $round.tde: $!"); unless (-f $rats_fname and (stat($rats_fname)->mtime >= $r_mtime)) { system ("rats $round"); # create new ratings file } readRats($rats_fname); } print "loading information, "; my $pair = Algorithm::Pair::Best->new(window => 7, scoreSubs => \@scoreSubs, ); my @idByRating; # collect all players who are not DROPped for this round foreach my $id (keys(%{$agaTourn->Rating})) { if (($agaTourn->Flags($id) =~ m/\bdrop\b/i) or ($agaTourn->Comment($id) =~ m/\bdrop$nextRound\b/i)) { $drops{$id} = 1; } else { push (@idByRating, $id) unless exists($drops{$id}); } } unless($no_handi) { # sort non-dropped players by rating @idByRating = sort {$agaTourn->Rating($b) <=> $agaTourn->Rating($a) } @idByRating; } $byeId = getBye({wins => 1, # exclude people with all wins extremes => 0.1, # exclude top and bottom 10 percent missed => 1, # exclude people who have missed a round drop => 1, # exclude people who will drop a future round }, \@idByRating) if (@idByRating % 2); # even number? no BYE needed unless(defined($agaTourn->Rating($byeId))) { die ("I can't use '$byeId' for the BYE player. It is not a\n" . "valid AGA ID, or it isn't registered in this tournament\n") } foreach my $d (keys(%drops)) { unless (defined($agaTourn->Rating($d))) { die ("I can't DROP '$d'. It is not a valid AGA ID, or\n" . "it isn't registered in this tournament\n") } } foreach my $id (@idByRating) { next if(defined($byeId) and ($id eq $byeId)); $IDtoPair{$id} = $pair->add( # info structure attached to each item to be Pair'ed # Note: this hash is entirely user-defined, # Pair makes no assumptions about it: {id => $id, # so we can get back to $agaTourn information rating => $agaTourn->CollapseRating($agaTourn->Rating($id)), club => $agaTourn->Club($id), wins => $agaTourn->Wins($id), played => scalar($agaTourn->Played($id)), # people $id has played already }); } my $erAdj = $agaTourn->Rounds - $round; unless($no_early or ($erAdj < 3)) { my $ii = $erAdj * 2; # number of players to adjust $erAdj -= 2; print "early round adjustment (by $erAdj)\n\n"; $pair->sortCandidates; # do initial candidate sorting for (my $p = $pair->next; defined($p); $p = $p->next) { my $citems = $p->citems; # ref to sorted array of candidate items next if (@{$citems} < $erAdj + 1); my $cscores = $p->cscores; # ref to hash of cached scores my $adj = 2 * $cscores->{$citems->[$erAdj]}; #score of item a bit down the list for (my $jj = 0; $jj < $erAdj; $jj++) { $cscores->{$citems->[$jj]} += $adj; } last if($ii-- < 0); # stop after adjusting the top few players } push(@directives, 'Early_round_adjustment: Yes'); } else { push(@directives, 'Early_round_adjustment: No'); } print "picking pairs\n\n"; my @items = $pair->pick; my @pairs; foreach (@items) { push (@pairs, $_->id); # convert form pair items to AGA IDs. } return @pairs; } =head1 BYEs BYEs are assigned when there are an odd number of players to pair (after all DROPped players have been removed). If any players are assigned as BYE players in the register.tde file, those players are the entire pool of BYE candidates. Otherwise, the BYE candidates consist of the middle 80% of players (sorted by rating) who have already lost some games (except in round 1), and who have not already missed one or more rounds (due to DROPs or BYEs). The final BYE player for the round is chosen randomly from amongst the candidates. Re-running gopair will most likely choose a different random BYE player (unless the B<-bye> option is used). =cut sub getBye { my ($rules, $candidates) = @_; return($byeOpt) if (defined($byeOpt)); # defined on command line - overrides everything else my $count = @{$candidates}; return(undef) unless ($count); my (@cand, @explicit); my $round = $agaTourn->Round; foreach(my $ii = 0; $ii < $count; $ii++) { my $id = $candidates->[$ii]; next unless(defined($id)); push(@explicit, $id) if ($agaTourn->Flags($id) =~ m/\bbye\b/i); next if (exists($rules->{extremes}) and (($ii < $count * $rules->{extremes}) or ($ii > $count * (1 - $rules->{extremes})))); # exclude top and bottom extremes next if (exists($rules->{missed}) and (@{$agaTourn->Played($id)} < $round)); # skip if alrady missed a round next if (exists($rules->{wins}) and ($agaTourn->Wins($id) >= $round)); # skip if haven't lost any games next if (exists($rules->{drop}) and ($agaTourn->Comment($id) =~ m/\bdrop(\d+)\b/i) and ($1 > $round)); # skip if going to miss a round push(@cand, $id); } @cand = @explicit if (@explicit); # explicit BYE candidates have priority unless(@cand) { foreach(qw(drop missed extremes wins)) { if (exists($rules->{$_})) { delete($rules->{$_}); return(getBye($rules, $candidates)); } } die "I ran out of rules to delete while finding a BYE candidate - please selecton on explicitly\n"; } #finally, bye candidate should have played as many games as possible my $wins = 0; foreach my $id (@cand) { $wins = @{$agaTourn->Played($id)} if ($wins < @{$agaTourn->Played($id)}); } @explicit = (); foreach my $id (@cand) { push(@explicit, $id) if (@{$agaTourn->Played($id)} >= $wins); } return($explicit[int(rand(scalar(@explicit)))]); # the sacrificial lamb } sub headerTxt { my $h; $h .= ("\f") if ($page > 1); $h .= ("\n $tourney\n ("); $h .= join(', ', @directives); $h .= (")\n Round $nextRound of $rounds, page $page\n"); $h .= sprintf("\n %*.*s %-*.*s\n", $nameW2, $nameW2, 'White ', $nameW2, $nameW2, ' Black'); $line = 7; $page++; return($h); } # read ratings file, update AGATourn database with new reatings sub readRats { my $rats_filename = shift; if (-f ($rats_filename)) { my $inFp = IO::File->new("<$rats_filename") or die "can't open $rats_filename for reading: $!\n"; print "reading $rats_filename for ratings adjustments\n"; while (<$inFp>) { # USA2172 Watanabe, Makoto: -5.633 ( 4.500 --> -3.133) next unless(m/^(\S*).*\(.* \-\->\s+([\d\.\-]+)\)/); my ($id, $newRat) = ($1, $2); $agaTourn->Rating($id, $newRat); } close $inFp; } else { print "No rating adjustment file for round $round (should be $rats_filename)\n"; print("Continue anyway? (y/n)? "); my $response = lc(); chomp($response); exit(0) unless (($response eq 'y') || ($response eq 'yes')); } } __END__ =head1 *.tde FILES The *.tde files all follow the standard American Go Association (AGA) defined formats. In particular, in the register.tde file, gopair recognizes the: ## RULES ... directive with respect to ING and non-ING komi and handicapping, the: ## NOEARLY directive to suppress early-round adjustments, and: ## HANDICAPS MIN directive to turn off handicapping. Also, name lines in the register.tde file that include BYE or DROP are recognized as BYE candidates or players who have DROPped out, respectively. In addition, if a name line includes one or more DROPn tokens in the comment field, that player is DROPped for round 'n'. Example: USA54321 Foo, A. 10k CLUB=foobar bye # drop2 TMP65432 Bar, B. 2k CLUB=foobar # drop3, DROP5 Player A. Foo is a BYE candidate (first choice to receive BYEs if there are an odd number of players), and he will NOT be paired in round 2 (regardless of BYEs). Player B. Bar will not be paired in rounds 3 or 5. =head1 SEE ALSO =over 0 =item o tdfind(1) - prepare register.tde for an AGA Go tournament =item o rats(1) - calculate new ratings from game results =item o around(1) - old Accelerat pairing script (use gopair instead) =item o aradjust(1) - adjust pairings and enter results for a round =item o tscore(1) - score a tournament =item o send2AGA(1) - prepare tournament result for sending to AGA =item o Games::Go::AGATourn(3) - perl module provides AGA file support =item o Algorithm::Pair::Best(3) - generalized pairing algorithm =back =head1 AUTHOR Reid Augustin, Ereid@netchip.comE =head1 COPYRIGHT AND LICENSE Copyright (C) 1995, 2004, 2005 by Reid Augustin This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.5 or, at your option, any later version of Perl 5 you may have available. =cut