#!/usr/local/bin/perl -w # $Id: aradjust,v 1.32 2005/01/24 03:11:09 reid Exp $ # aradjust: adjust Accelrat 2.5.1 pairings # Copyright (C) 1999, 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: # add support for adding dummy and single players to register.tde? # use strict; require 5.001; BEGIN { our $VERSION = sprintf "%d.%03d", '$Revision: 1.32 $' =~ /(\d+)/g; } package TextBox; # make ROText dialog box for Help and About windows use Tk; use Tk::widgets qw(DialogBox ROText); use base qw(Tk::Toplevel Tk::DialogBox); # TextBox is composite widget Construct Tk::Widget 'TextBox'; ###################################################### # # methods # ##################################################### sub InitObject { my ($self, $args) = @_; $self->SUPER::InitObject($args); $self->{text} = $self->Scrolled('ROText', '-scrollbars' => 'osow', '-wrap' => 'word'); $self->{text}->pack('-side' => 'top', '-fill' => 'both'); $self->ConfigSpecs( '-text' => ['METHOD', 'text', 'Text', '' ], DEFAULT => [$self->{text}],); return($self); } sub text { my ($self, $text) = @_; $self->{text}->configure('-state' => 'normal'); $self->{text}->insert('insert', $text); $self->{text}->configure('-state' => 'disabled'); } package Tk::ArAdj; use IO::File; use Tk; use Tk::widgets qw(FileSelect Dialog ROText ErrorDialog); use Tk qw(Ev); use Games::Go::AGATourn; # stuff to parse AGA tournament data files use base qw(Tk::Frame); # composite widget Construct Tk::Widget 'ArAdj'; ###################################################### # # class variables # ##################################################### my $selectBg = '#ffffe8'; # 255 255 224 = light yellow my $hkGood = '#90ee90'; # 144 238 144 = light green my $hkGuess = '#add8e6'; # 173 216 230 = light blue my $hkBad = '#ffb6c1'; # 255 182 193 = light pink my $alreadyBg = '#f0f0c0'; # 240 240 230 = less light yellow ###################################################### # # methods # ##################################################### sub Populate { my ($self, $args) = @_; $self->SUPER::Populate($args); $self->ConfigSpecs( '-filename' => ['PASSIVE', 'filename', 'FileName', 'file name not set' ], '-format_only' => ['PASSIVE', 'format_only', 'Format_only', 0 ], DEFAULT => [$self->{text} ],); my $frame = $self->Frame('-bg' => 'blue'); $frame->pack('-expand' => 'true', '-fill' => 'both'); my $t = $self->{text} = $frame->Scrolled('ROText', '-scrollbars' => 'osow', # scrollbars left and bottom if needed '-wrap' => 'none'); # don't wrap text $t->pack( '-side' => 'bottom', '-expand' => 'true', '-fill' => 'both'); my $bbar = $frame->Frame(); # put a button-bar along the top $self->PopulateBBar($bbar); # set up the buttons $bbar->pack('-side' => 'top', '-fill' => 'x'); $t->tagConfigure("header", '-background' => 'lightgrey', '-relief' => 'raised', '-underline' => 'true'); $t->tagConfigure("win", '-foreground' => 'blue', '-underline' => 'true'); $t->tagConfigure("lose", '-foreground' => 'darkblue'); my $sw = $t->Subwidget('scrolled'); # get the scrolled widget $sw->bindtags([$sw, ref $sw, $sw->toplevel, 'all']); # re-order bindings so our subs are called first foreach(qw(w W b B x X)) { $sw->bind("<$_>", [$self => 'ChangeResult', $_]); } $sw->bind('', [$self => 'ChangeResult', 'x']); $sw->bind('', [$self => 'ChangeResult', 'X']); $sw->bind('', [$self => 'AdjustValue', 1]); $sw->bind('', [$self => 'AdjustValue', 1]); $sw->bind('', [$self => 'AdjustValue', -1]); $sw->bind('', [$self => 'AdjustValue', -1]); $sw->bind('', [$self => 'Button1', undef, 'up']); $sw->bind('', [$self => 'Button1', undef, 'down']); $sw->bind('', [$self => 'Button1', undef, 'left']); $sw->bind('', [$self => 'Button1', undef, 'right']); $self->{lines} = 0; $self->Clear(); unless (exists($args->{'-format_only'}) and $args->{'-format_only'}) { $self->afterIdle(sub { $self->Open($self->cget('-filename')); }); } $self->afterIdle( sub { $self->{normalBackground} = $t->cget('-background'); } ); return($self); } sub ClassInit { my ($class, $self) = @_; return($class->SUPER::ClassInit($self)); } sub PopulateBBar { my ($self, $bbar) = @_; my ($b, $m); # temporary button and menu variables my %menubuttonStyle = ('-borderwidth' => 3, '-relief' => 'groove'); my %buttonStyle = ('-borderwidth' => 3, '-relief' => 'sunken'); # a "File" button $b = $bbar->Menubutton('-text' => 'File', %menubuttonStyle); my $menu = $b->Menu(); $b->configure('-menu' => $menu); $menu->configure('-postcommand' => [$self => 'MakeFileMenu', $menu]); $b->pack('-side' => "left"); # the Undo menubutton $b = $bbar->Button('-text' => 'Undo', '-command' => [$self => 'Backward'], %buttonStyle); $b->pack('-side' => 'left'); $b->configure('-state' => 'disabled'); $self->{undoButton} = $b; # the Redo menubutton $b = $bbar->Button('-text' => 'Redo', '-command' => [$self => 'Forward'], %buttonStyle); $b->pack('-side' => 'left'); $b->configure('-state' => 'disabled'); $self->{redoButton} = $b; # the Help button $b = $bbar->Menubutton('-text' => 'Help', %menubuttonStyle, '-menuitems' => [ ['Button' => 'Help...', '-command' => [$self => 'Help', 'Help']], ['Button' => 'Bindings...', '-command' => [$self => 'Help', 'Bindings']], ['Button' => 'Tourney...', '-command' => [$self => 'Help', 'Tourney']], ['Button' => 'About...', '-command' => [$self => 'Help', 'About']], ]); $b->pack('-side' => 'right'); # the Save/Quit button $b = $bbar->Button('-text' => 'Save/Quit', '-command' => [$self => 'SaveQuit'], %buttonStyle); $b->pack('-side' => 'right'); if(defined($DB::single)) { # debug stuff # a Test button # $b = $bbar->Button('-text' => 'Test', '-command' => [$self => 'Test'], %menubuttonStyle); # $b->pack('-side' => "right"); # a Debug button $b = $bbar->Button('-text' => 'Debug', '-command' => ['Debug' => $self], '-underline' => 3, %buttonStyle); $self->bind('' => [$self => 'Debug']); $b->pack('-side' => "right"); } } sub MakeFileMenu { my ($self, $menu) = @_; $menu->delete(0, 'end'); # delete all menu entries $menu->add('command', '-label' => 'Open...', '-command' => [$self => 'Open']); $menu->add('command', '-label' => 'Save', '-command' => [$self => 'Save']); $menu->add('command', '-label' => 'Save/Quit', '-command' => [$self => 'SaveQuit']); $menu->add('command', '-label' => 'SaveAs...', '-command' => [$self => 'SaveAs']); $menu->add('separator'); my ($round); for ($round = 1; -f "$round.tde"; $round++) { $menu->add('command', '-label' => "Round $round", '-command' => [$self => 'Open', "$round.tde"]); } if ($round > 1) { $menu->add('separator'); # if there were any rounds, add another seperator } $menu->add('command', '-label' => 'Re-Pair this Round', '-command' => [$self => 'RunPairings', 0]); $menu->add('command', '-label' => 'Pair Next Round', '-command' => [$self => 'RunPairings', 1]); $menu->add('separator'); $menu->add('command', '-label' => 'Quit', '-command' => [$self => 'Quit']); } sub Debug { my ($self) = @_; $DB::single = 1; $DB::single = 0; } sub Quit { my ($self) = @_; return if ($self->SaveQuery() eq 'Abort'); exit; } sub Open { my ($self, $filename) = @_; unless(exists($self->{fileSelector})) { $self->{fileSelector} = $self->toplevel()->FileSelect('-filter' => "*.tde"); } my ($response, $error); for ( ; ; ) { $filename = $self->{fileSelector}->Show() unless(defined($filename)); return if (!defined($filename) or ($filename eq '')); if (-f $filename) { return unless($self->Read($filename)); $error = "error in Read"; } else { $error = "$filename doesn't exist.", } $response = $self->toplevel->Dialog('-title' => "File Error!", '-text' => $error, '-buttons' => ['Cancel', 'Try again']) ->Show(); return unless ($response eq 'Try again'); $filename = undef; } $self->Button1_noBreak(undef, 'none'); } sub SaveQuery { my ($self) = @_; return('') unless ($self->{histIdx} > $self->{saveIdx}); my $rsp = $self->toplevel->Dialog('-title' => "Changes Pending!", '-text' => "You have made changes. Save them now?", '-buttons' => ['Save now', 'SaveAs...', 'Throw away changes', 'Abort']) ->Show(); if ($rsp eq 'Save now') { $self->Save($self->cget('-filename')); } elsif ($rsp eq 'SaveAs...') { $self->SaveAs(); } return($rsp); # caller should check for 'Abort' } sub SaveAs { my ($self) = @_; unless(exists($self->{fileSelector})) { $self->{fileSelector} = $self->toplevel()->FileSelect('-filter' => "*.tde"); } my ($response, $filename); for ( ; ; ) { $filename = $self->{fileSelector}->Show(); return if (!defined($filename) or ($filename eq '')); return unless($self->Save($filename)); $response = $self->toplevel->Dialog('-title' => "File Error!", '-text' => "error in Save", '-buttons' => ['Cancel', 'Try again']) ->Show(); return unless ($response eq 'Try again'); } } sub SaveQuit { my ($self, $filename) = @_; $self->Quit unless ($self->Save); } sub Save { my ($self, $filename) = @_; $filename = $self->cget('-filename') unless(defined($filename)); unless(defined($filename)) { # still not defined? get user input $self->SaveAs(); return(0); } my $fp = IO::File->new(">$filename"); # open the file unless ($fp) { $self->Error("Can't open $filename for writing"); return(1); } my $oldCursor = $self->{text}->cget('cursor'); $self->{text}->configure('-cursor' => 'watch'); $self->update(); my $tourney = $self->{agaTourn}->Tourney(); $fp->print(" # $tourney Round $self->{round}\n\n"); my $maxNameLength = $self->{agaTourn}->NameLength(); my ($idx, $bId, $wId, $resu, $handi, $komi); for ($idx = 0; $idx < @{$self->{wId}}; $idx++) { $wId = $self->{wId}[$idx]; $bId = $self->{bId}[$idx]; $resu = $self->{resu}{"$wId,$bId"}; $handi = $self->{handi}{"$wId,$bId"}; $komi = $self->{komi}{"$wId,$bId"}; $resu = '?' unless (defined($resu)); $handi = $self->{handiGuess}{"$wId,$bId"} unless(defined($handi)); $komi = $self->{komiGuess}{"$wId,$bId"} unless(defined($komi)); $fp->printf("%-8.8s", $wId); $fp->printf(" %-8.8s", $bId); $fp->printf(" %1.1s", $resu); $fp->printf(" %1.1s", $handi > 0 ? $handi : -$handi); $fp->printf(" %3.3s", $komi); if ($handi < 0) { $self->Error(sprintf "Converting negative handicap at line %d", $idx + 1); } $fp->printf(" # "); $fp->printf("%*.*s %5.1f", $maxNameLength, $maxNameLength, $self->{names}{$wId}, $self->{rating}{$wId}); $fp->printf(" "); $fp->printf("%-5.1f %-*.*s", $self->{rating}{$bId}, $maxNameLength, $maxNameLength, $self->{names}{$bId}); $fp->printf("\n"); } $fp->printf("\n"); my ($type, $comments, $byeId); for( ; $idx < @{$self->{bye}}; $idx++) { $byeId = $self->{bye}[$idx]; $type = 'BYE: '; $comments = lc($self->{comments}{$byeId}); $type = 'DROP:' if ($comments =~ m/drop/); $fp->printf("# $type %-8.8s %s\n", $byeId, $self->{names}{$byeId}); } $fp->close; $self->{saveIdx} = $self->{histIdx}; my $fname = $self->cget('-filename'); my @msg = `tpairs $fname 2>&1`; # reformat into a nicer printable form foreach (@msg) { # next if (/AGATourn:ReadTdeFile: Reading .*tde\n$/); next if (/^Writing pairs\d+\n$/); $self->Error(join('', "tpairs program printed:\n\n", @msg)); # hmm, something unexpected. show the whole message last; } $self->{text}->configure('-cursor' => $oldCursor); return(0) } sub RunPairings { # run pairings program my ($self, $increment) = @_; return if ($self->SaveQuery() eq 'Abort'); # save current info, if necessary my $round = $self->{round} + $increment; my $filename = "$round.tde"; my $response; if (-f $filename) { $response = $self->toplevel->Dialog('-title' => "File Exists!", '-text' => "$filename already exists. Overwrite it?", '-buttons' => ['OverWrite', 'Cancel']) ->Show(); return unless ($response eq 'OverWrite'); } my $allDone = 1; # assume all results are in my $anyDone = 0; # assume no results are in foreach(values(%{$self->{resu}})) { if ($_ eq '?') { $allDone = 0; # at least one not done } else { $anyDone = 1; # at least one is done } } if ($round == $self->{round}) { # re-pair this round if ($anyDone) { # have we already got some results recorded? $response = $self->toplevel->Dialog('-title' => "Results Recorded!", '-text' => "Results have already been recorded. Throw them away?", '-buttons' => ['Throw Away Results', 'Cancel "Re-Pair"']) ->Show(); return unless($response eq 'Throw Away Results'); } } else { # pair next round unless ($allDone) { # have we got all results from this round yet? my $nextRound = $self->{round} + 1; $response = $self->toplevel->Dialog('-title' => "Not All Results Recorded!", '-text' => "Not all results have been recorded" . " for round $self->{round}. Pair round " . "$nextRound anyway?", '-buttons' => ['Continue pairing', 'Cancel pairing']) ->Show(); return unless($response eq 'Continue pairing'); } } my $oldCursor = $self->{text}->cget('cursor'); $self->{text}->configure('-cursor' => 'watch'); $self->update(); system("around -ow -x $round"); # run around to do pairings $self->{text}->configure('-cursor' => $oldCursor); $self->Open($filename); } sub Help { my ($self, $type) = @_; my $text; if ($type eq 'Help') { $text = "$main::myName is used to adjust American Go Association (AGA) " . "tournament pairings files. These " . "files are called 1.tde, 2.tde...\n" . "\n" . "You can start the program with \"$main::myName 3\" to adjust the third round file 3.tde.\n" . "\n" . "Adjustments to the round are done using various event bindings " . "(see \"Help -> Bindings\" for more details)\n" . "\n" . "$main::myName keeps a history of your changes, so you can undo and re-do " . "them using the Undo and Redo buttons (only the latest 'branch' is kept - " . "if you undo 3 changes, then make a change, the three undone changes cannot be redone).\n" . "\n" . "$main::myName tries to guess handicap and komi for new matches. " . "If the match needs a negative or a very large handicap (more than 9 stones), " . "$main::myName marks the background red. " . "If the handicap is reasonable, $main::myName colors the background " . "pale blue. Green background indicates the handicap/komi are the 'official' " . "values (from the tournament pairing file).\n" . "\n"; } elsif ($type eq 'Bindings') { $text = "Results:\n" . "Winner/loser can be set to w, b, or '?' (white, black, or unknown). Pressing " . "'w', 'b', 'x', or '?' sets the results on the currently selected line, and moves the " . "selection to the next line. Using capitals ('W', 'B', 'X', or '/') causes the selection " . "to move up one line instead of down. Arrow keys move the selection up, down, left, and right.\n" . "\n" . "Handi, Komi:\n" . "Adjust handicaps and komi by selecting the appropriate item (left mouse " . "click or arrow keys) and typing '+' or '-'. \n" . "\n" . "Changing players:\n" . " The right mouse button pops up a context menu with several selections:\n" . " File -> Exit: another way to quit $main::myName\n" . " Edit -> Copy: does nothing\n" . " Edit -> Select All: selects all the text for export\n" . " -> Unselect All: removes selection\n" . " Search -> Find: pops up a search dialog box\n" . " -> Find Next: finds next occurance of search term\n" . " -> Find Previous: finds previous occurance of search term\n" . " View -> Select All: selects all the text for export\n" . " Swap: swap with selected player\n" . " Match: match with selected player\n" . " Unmatch: unmatch game under cursor\n" . "\n"; } elsif ($type eq 'Tourney') { } elsif ($type eq 'About') { $text = " $main::myName version $main::VERSION\n\n" . " copyright (C) 1999, 2004, 2005 Reid Augustin.\n\n" . " " . $self->{agaTourn}->Tourney() . "\n" . " Round $self->{round}\n"; } else { $self->Error("Unknown help type: $type\n"); return; } $self->toplevel->TextBox( '-title' => "$main::myName help: $type", '-text' => $text); } sub Read { my ($self, $newName) = @_; return if ($self->SaveQuery() eq 'Abort'); $self->configure('-filename', $newName); my $oldCursor = $self->{text}->cget('cursor'); $self->{text}->configure('-cursor' => 'watch'); $self->update() if($self->{notFirstTime}); $self->{notFirstTime} = 1;; # create agaTourn object, read register.tde, and all round files $self->{agaTourn} = Games::Go::AGATourn->new(); die("Aborting...\n") if (not defined($self->{agaTourn}) or $self->{agaTourn}->Error); my $filename = my $round = $self->cget('-filename'); $round =~ s/\.tde//i; # remove suffix die ("Round filename format problem: I need 1.tde, 2.tde, etc.\n") if ($round =~ m/\D/); $self->{round} = $round; foreach (qw(wId bId bye)) { $self->{$_} = []; # empty the arrays } foreach (qw(resu handi komi)) { $self->{$_} = {}; # empty the hashes } my $games = $self->{agaTourn}->GamesList(); # "$wId,$bId,$resu,$handi,$komi,$self->{Round}" unless(@{$games}) { $self->Error("No Games in $filename!"); die "No Games in $filename!\n" if($self->cget('-format_only')); } $self->{names} = $self->{agaTourn}->Name(); $self->{rating} = $self->{agaTourn}->Rating(); $self->{comments} = $self->{agaTourn}->Comment(); # so we know BYEs from DROPs $self->{rules} = uc($self->{agaTourn}->Directive('RULES')->[0]); $self->{rules} = 'AGA' unless defined($self->{rules}); if ($self->{rules} eq 'ING') { $self->{normalKomi} = 7; $self->{noKomi} = -1; # because black wins ties $self->{reverseKomi} = -7; } else { $self->{normalKomi} = 5; $self->{noKomi} = 0; $self->{reverseKomi} = -5; } my %byes = %{$self->{names}}; # copy to find BYEs (by removing all active players) my ($wId, $bId, $resu, $handi, $komi, $gRound); foreach (@$games) { ($wId, $bId, $resu, $handi, $komi, $gRound) = split(/,/, $_); if (($resu eq 'w') or ($resu eq 'b')) { push(@{$self->{already}{$wId}}, $bId); # record two 'already played' push(@{$self->{already}{$bId}}, $wId); } next if ($round != $gRound); if (exists($self->{names}{$wId}) and exists($self->{names}{$bId})) { push(@{$self->{bye}}, undef); # put undefs in lower part of bye array push(@{$self->{wId}}, $wId); push(@{$self->{bId}}, $bId); $self->{resu}{"$wId,$bId"} = $resu; $self->{handi}{"$wId,$bId"} = $handi; $self->{komi}{"$wId,$bId"} = $komi; $self->{komi}{"$wId,$bId"} = $self->CheckHK($handi, $komi); $self->{hkBg}{"$wId,$bId"} = $hkGood; # set background to known good for this match unless(exists($byes{$wId})) { $self->Error("$wId $self->{names}{$wId} is playing more than one game this round?"); } unless(exists($byes{$bId})) { $self->Error("$bId $self->{names}{$bId} is playing more than one game this round?"); } delete($byes{$wId}); # not a BYE - remove him delete($byes{$bId}); # not a BYE - remove him } else { unless(exists($self->{names}{$wId})) { $self->Error("Player ID $wId not in register.tde?"); delete($byes{$wId}); } unless(exists($self->{names}{$bId})) { $self->Error("Player ID $bId not in register.tde?"); delete($byes{$bId}); } } } push(@{$self->{bye}}, sort {$self->{rating}{$b} <=> $self->{rating}{$a}} (keys(%byes))); # add byes to end of list $self->Clear(); my $idx; for ($idx = -1; $idx < @{$self->{bye}}; $idx++) { $self->RefreshIdx($idx); } $self->{text}->focus(); $self->{round} = $round; $self->toplevel()->title("$main::myName: $newName"); # set window title $self->{text}->configure('-cursor' => $oldCursor); return(0); } sub RefreshIdx { my ($self, $idx) = @_; my ($line, $t, $tagStart); $line = $idx; $line += 2; # lines in text are 1 based, line 1 is the header $t = $self->{text}; $t->configure('-state' => 'normal'); $t->delete("$line.0", "$line.end"); # clear the current stuff out $t->markSet('insert', "$line.0"); my $maxNameLength = $self->{agaTourn}->NameLength(); if ($line == 1) { # print a header $t->insert('insert', "tbl win hndi komi", "header"); $t->insert('insert', sprintf("%*.*s rtg ", $maxNameLength-2, $maxNameLength-2, "White Player"), "header"); $t->insert('insert', sprintf("rtg %-*.*s", $maxNameLength, $maxNameLength, "Black Player"), "header"); my $width = length($t->get("$line.0", 'insert')); $t->configure('-width' => $width + 1); } elsif (($idx < @{$self->{wId}})) { # an active game line my $wId = $self->{wId}[$idx]; my $bId = $self->{bId}[$idx]; my $resu = $self->{resu}{"$wId,$bId"}; my $handi = $self->{handi}{"$wId,$bId"}; my $komi = $self->{komi}{"$wId,$bId"}; $resu = '?' if (!defined($resu) or ($resu eq 'x')); # change 'x' to '?' in result field #$handi = $self->{handiGuess}{"$wId,$bId"} if (defined($self->{handiGuess}{"$wId,$bId"})); #$komi = $self->{komiGuess}{"$wId,$bId"} if (defined($self->{komiGuess}{"$wId,$bId"})); $handi = $self->{handiGuess}{"$wId,$bId"} unless(defined($handi)); $komi = $self->{komiGuess}{"$wId,$bId"} unless(defined($komi)); $t->insert('insert', sprintf("%3.3s ", $line - 1)); $t->insert('insert', sprintf(" %1.1s ", $resu), "resu$idx"); $t->tagBind("resu$idx", '<1>', [$self => 'Button1', 'resu', $idx]); $t->insert('insert', sprintf(" %2.2s ", $handi), "handi$idx"); $t->tagBind("handi$idx", '<1>', [$self => 'Button1', 'handi', $idx]); $t->insert('insert', sprintf("%3.3s ", $komi), "komi$idx"); $t->tagBind("komi$idx", '<1>', [$self => 'Button1', 'komi', $idx]); $t->tagConfigure("handi$idx", '-background' => $self->{hkBg}{"$wId,$bId"}); $t->tagConfigure("komi$idx", '-background' => $self->{hkBg}{"$wId,$bId"}); my $wName = $self->{names}{$wId}; $t->insert('insert', ' ' x ($maxNameLength - length($wName))); # space before white name $tagStart = $t->index('insert'); $t->insert('insert', sprintf("$wName %5.1f", $self->{rating}{$wId}), ["wId$idx", $self->alreadyTags($wId)]); if ($resu eq 'w') { $t->tagAdd('win', $tagStart, $t->index('insert')); } elsif ($resu eq 'b') { $t->tagAdd('lose', $tagStart, $t->index('insert')); } $t->tagBind("wId$idx", '<1>', [$self => 'Button1', 'wId', $idx]); $t->tagBind("wId$idx", '<3>', [$self => 'Button3', 'wId', $idx]); $t->insert('insert', " "); # blank between players my $bName = $self->{names}{$bId}; $tagStart = $t->index('insert'); $t->insert('insert', sprintf("%-5.1f $bName", $self->{rating}{$bId}), ["bId$idx", $self->alreadyTags($bId)]); if ($resu eq 'w') { $t->tagAdd('lose', $tagStart, $t->index('insert')); } elsif ($resu eq 'b') { $t->tagAdd('win', $tagStart, $t->index('insert')); } $t->tagBind("bId$idx", '<1>', [$self => 'Button1', 'bId', $idx]); $t->tagBind("bId$idx", '<3>', [$self => 'Button3', 'bId', $idx]); } elsif (exists($self->{bye}) and ($idx < @{$self->{bye}})) { # a BYE my $byeId = $self->{bye}[$idx]; my $type = 'BYE: '; my $comments = lc($self->{comments}{$byeId}); $type = 'DROP:' if ($comments =~ m/drop/); $t->insert('insert', sprintf("$type %*.*s %5.1f", $maxNameLength, $maxNameLength, $self->{names}{$byeId}, $self->{rating}{$byeId},), ["bye$idx", $self->alreadyTags($byeId)]); $t->tagBind("bye$idx", '<1>', [$self => 'Button1', 'bye', $idx]); $t->tagBind("bye$idx", '<3>', [$self => 'Button3', 'bye', $idx]); } else { $self->Error("ArAdj::RefreshIdx($idx): index too large"); } if ($line > $self->{lines}) { $t->insert('insert', "\n"); $self->{lines} = $line; } $self->{text}->configure('-state' => 'disabled'); if (defined($self->{selectedIdx}) && ($idx == $self->{selectedIdx})) { $self->Button1_noBreak($self->{selectedField}, $self->{selectedIdx}); # make sure this line shows as selected $t->yview('-pickplace', "$line.0"); } } # return an array of tags used to highlight opponents we've already played sub alreadyTags { my ($self, $id) = @_; my @tags; foreach (@{$self->{already}{$id}}) { push (@tags, "already$_"); } return @tags; } sub Button1_noBreak { my ($self, $field, $idx) = @_; $self->{selectedIdx} = 0 unless(defined($self->{selectedIdx})); $self->{selectedField} = 'resu' unless(defined($self->{selectedField})); $field = $self->{selectedField} unless(defined($field)); if ($idx eq 'up') { $idx = $self->{selectedIdx}; $idx-- if ($idx > 0); } elsif ($idx eq 'down') { $idx = $self->{selectedIdx}; $idx++ if ($idx < (@{$self->{bye}} - 1)); } elsif ($idx eq 'left') { if ($field eq 'bId') { $field = 'wId'; } elsif ($field eq 'wId') { $field = 'komi'; } elsif ($field eq 'komi') { $field = 'handi'; } elsif ($field eq 'handi') { $field = 'resu'; } $idx = $self->{selectedIdx}; } elsif ($idx eq 'right') { if ($field eq 'resu') { $field = 'handi'; } elsif ($field eq 'handi') { $field = 'komi'; } elsif ($field eq 'komi') { $field = 'wId'; } elsif ($field eq 'wId') { $field = 'bId'; } $idx = $self->{selectedIdx}; } if ($idx >= @{$self->{wId}}) { # in the 'bye' area? $field = 'bye'; # there's only one field in this area } else { # in the valid games area? if ($field eq 'bye') { $field = $self->{prevSelField}; # restore from when we were in the valid area before } else { $self->{prevSelField} = $field; } } $self->{selectedField} = $field; $self->{selectedIdx} = $idx; my $t = $self->{text}; my @ranges = $t->tagRanges("$field$idx"); $t->tagDelete('selected'); if(@ranges) { $t->tagAdd('selected', $ranges[0], $ranges[1]); $t->markSet('insert', $ranges[0]); # move insertion mark to start of selected area } $t->tagConfigure("selected", '-background' => $selectBg); $t->yview('-pickplace', $ranges[0]); # now if we're selecting a name, pick out the people this # person has already played and highlight them if (exists($self->{alreadyTagged})) { $t->tagConfigure($self->{alreadyTagged}, # unhighlight old 'already' tags -background => $self->{normalBackground}); $t->tagRaise(delete($self->{alreadyTagged})); # need to raise it to show } $self->update; if (($field eq 'wId') or ($field eq 'bId') or ($field eq 'bye')) { my $id; if ($field eq 'wId') { $id = $self->{wId}[$idx]; } elsif ($field eq 'bId') { $id = $self->{bId}[$idx]; } elsif ($field eq 'bye') { $id = $self->{bye}[$idx]; } $self->{alreadyTagged} = "already$id"; $t->tagConfigure("already$id", -background => $alreadyBg); $t->tagRaise("already$id"); } } sub Button1 { my ($self, $field, $idx) = @_; $self->Button1_noBreak($field, $idx); $self->{text}->break; # prevent default button1 bindings } sub AddSwap { my ($self, $srcField, $srcIdx, $destField, $destIdx) = @_; $self->Add('forward', [$self => 'Swap', $srcField, $srcIdx, $destField, $destIdx]); $self->Add('backward', [$self => 'Swap', $destField, $destIdx, $srcField, $srcIdx]); $self->Forward(scalar(@{$self->{forward}})); # forward to end of history buffer } sub AddMatch { my ($self, $bIdx, $wIdx, $matchIdx) = @_; $self->Add('forward', [$self => 'Match', $bIdx, $wIdx, $matchIdx]); $self->Add('backward', [$self => 'Unmatch', $matchIdx, $bIdx + 1, $wIdx + 1]); $self->Forward(scalar(@{$self->{forward}})); # forward to end of history buffer } sub AddUnMatch { my ($self, $idx, $byeIdx) = @_; $self->Add('forward', [$self => 'Unmatch', $idx, $byeIdx, $byeIdx + 1]); $self->Add('backward', [$self => 'Match', $byeIdx - 1, $byeIdx, $idx]); $self->Forward(scalar(@{$self->{forward}})); # forward to end of history buffer } sub Button3 { my ($self, $field, $idx) = @_; my $t = $self->{text}; my $selField = $self->{selectedField}; my $selIdx = $self->{selectedIdx}; my $m = $t->menu; unless(exists($self->{context})) { # add to default context menu: $self->{context} = $m; $m->add('command', -label => 'Swap', -state => 'disabled', ); $m->add('command', -label => 'UnMatch', -state => 'disabled', ); $m->add('command', -label => 'Match', -state => 'disabled', ); } my ($swapState, $unmatchState, $matchState) = qw(disabled disabled disabled); # Swap menu entry: if(defined($selField) and # swapee defined? ((($selField eq 'wId') or ($selField eq 'bye') or # can only swap IDs ($selField eq 'bId')) and # or byes (($field eq 'wId') or ($field eq 'bId') or ($field eq 'bId')) and (($selIdx != $idx) or ($selField ne $field)))) { # only if they're not the same person $swapState = 'normal'; # enable Swap menu entry } $m->entryconfigure('Swap', -command => [$self => 'AddSwap', $selField, $selIdx, $field, $idx], -state => $swapState); # UnMatch menu entry: if($idx <= @{$self->{wId}}) { # can't unmatch byes $unmatchState = 'normal'; # enable Match menu entry } $m->entryconfigure('UnMatch', -command => [$self => 'AddUnMatch', $idx, scalar(@{$self->{wId}})], -state => $unmatchState); # Match menu entry: my $matchIdx = @{$self->{wId}}; # add new match to end of match list my ($bIdx, $wIdx); if(defined($selField) and # first player defined? (defined($selIdx) and ($selIdx != $idx)) and # can't match player against himself (($field eq 'bye') and ($selField eq 'bye'))) { # can only match bye against bye if ($self->{rating}{$self->{$selField}[$selIdx]} < $self->{rating}{$self->{$field}[$idx]}) { $bIdx = $idx; $wIdx = $selIdx; } else { $bIdx = $selIdx; $wIdx = $idx; } $matchState = 'normal'; # enable Match menu entry } $m->entryconfigure('Match', -command => [$self => 'AddMatch', $bIdx, $wIdx, scalar(@{$self->{wId}})], -state => $matchState); $t->break; # prevent default text button3 action } sub Add { my ($self, $direction, @args) = @_; splice(@{$self->{$direction}}, $self->{histIdx}); # remove everything after our current seek position push(@{$self->{$direction}[$self->{histIdx}]}, \@args); } sub Forward { my ($self, $idx) = @_; $idx = $self->{histIdx} + 1 unless(defined($idx)); my ($cmdList, $obj, $method, @cmd); while ($self->{histIdx} < $idx) { foreach $cmdList (@{$self->{forward}[$self->{histIdx}]}) { foreach (@$cmdList) { @cmd = @$_; $obj = shift(@cmd); $method = shift(@cmd); $obj->$method(@cmd); } } $self->{histIdx}++; } if ($self->{histIdx} >= @{$self->{forward}}) { $self->{redoButton}->configure('-state' => 'disabled'); } if ($self->{histIdx} >= 0) { $self->{undoButton}->configure('-state' => 'normal'); } } sub Backward { my ($self, $idx) = @_; $idx = $self->{histIdx} unless(defined($idx)); my ($cmdList, $obj, $method, @cmd); do { $self->{histIdx}--; foreach $cmdList (@{$self->{backward}[$self->{histIdx}]}) { foreach (@$cmdList) { @cmd = @$_; $obj = shift(@cmd); $method = shift(@cmd); $obj->$method(@cmd); } } } while ($self->{histIdx} >= $idx); if ($self->{histIdx} <= 0) { $self->{undoButton}->configure('-state' => 'disabled'); } if ($self->{histIdx} <= @{$self->{forward}}) { $self->{redoButton}->configure('-state' => 'normal'); } } sub Match { # match two byes to create a new game my ($self, $byeWidx, $byeBidx, $matchIdx) = @_; my $wId = $self->{bye}[$byeWidx]; my $bId = $self->{bye}[$byeBidx]; splice(@{$self->{wId}}, $matchIdx, 0, $wId); # insert into game list splice(@{$self->{bId}}, $matchIdx, 0, $bId); $self->{resu}{"$wId,$bId"} = '?' unless(defined($self->{resu}{"$wId,$bId"})); $self->GuessHK($matchIdx); # make a guess at handi and komi for new match if ($byeWidx > $byeBidx) { splice(@{$self->{bye}}, $byeWidx, 1); # remove from BYE list splice(@{$self->{bye}}, $byeBidx, 1); } else { splice(@{$self->{bye}}, $byeBidx, 1); # reverse removal order (remove larger index first) splice(@{$self->{bye}}, $byeWidx, 1); } splice(@{$self->{bye}}, 0, 0, undef); # insert one undef at start of BYE list $self->{selectedField} = 'wId'; $self->{selectedIdx} = $matchIdx; my $idx; for ($idx = $matchIdx; $idx < @{$self->{bye}}; $idx++) { $self->RefreshIdx($idx); } my $last = @{$self->{bye}} + 2; $self->{text}->configure('-state' => 'normal'); $self->{text}->delete("$last.0", "$last.end + 1 char"); # delete extra line at the end $self->{text}->configure('-state' => 'disabled'); $self->{lines}--; } sub Unmatch { my ($self, $matchIdx, $byeWidx, $byeBidx) = @_; my $wId = $self->{wId}[$matchIdx]; my $bId = $self->{bId}[$matchIdx]; splice(@{$self->{wId}}, $matchIdx, 1); # remove entries from game lists splice(@{$self->{bId}}, $matchIdx, 1); if ($byeWidx > $byeBidx) { splice(@{$self->{bye}}, $byeWidx - 1, 0, $wId); # insert into BYE list splice(@{$self->{bye}}, $byeBidx, 0, $bId); } else { splice(@{$self->{bye}}, $byeBidx - 1, 0, $bId); # reverse insertion order (insert larger index first) splice(@{$self->{bye}}, $byeWidx, 0, $wId); } splice(@{$self->{bye}}, 0, 1); # remove one undef at start of BYE list my $idx; for ($idx = $matchIdx; $idx < @{$self->{bye}}; $idx++) { $self->RefreshIdx($idx); } } sub Swap { my ($self, $selField, $selIdx, $field, $idx) = @_; my $tmp = $self->{$selField}[$selIdx]; $self->{$selField}[$selIdx] = $self->{$field}[$idx]; $self->{$field}[$idx] = $tmp; $self->GuessHK($selIdx); $self->RefreshIdx($selIdx); if ($selIdx != $idx) { $self->GuessHK($idx); $self->RefreshIdx($idx); } } sub CheckHK { # check handicap and komi against the RULES directive - handicap has priority my ($self, $handi, $komi) = @_; my $newKomi = $komi; if ($handi == 0) { if ($komi > $self->{noKomi}) { $newKomi = $self->{normalKomi}; } elsif ($komi > $self->{reverseKomi}) { $newKomi = $self->{noKomi}; } else { $newKomi = $self->{reverseKomi}; } } else { $newKomi = ($self->{rules} eq 'ING') ? -$handi : 0; } if ($newKomi != $komi) { STDERR->print("Warning: $self->{rules} rules, handicap: $handi, komi: $komi changed to $newKomi\n"); } return($newKomi); } sub GuessHK { # guess at handicap and komi for a new match my ($self, $matchIdx) = @_; return unless($matchIdx < @{$self->{wId}}); # if this is a bye, just return my $wId = $self->{wId}[$matchIdx]; my $bId = $self->{bId}[$matchIdx]; if (defined($self->{handi}{"$wId,$bId"}) && defined($self->{komi}{"$wId,$bId"})) { $self->{hkBg}{"$wId,$bId"} = $hkGood; # just used "official" value return; } my ($handi, $komi) = $self->{agaTourn}->Handicap($wId, $bId); if ($handi < 0) { $self->{hkBg}{"$wId,$bId"} = $hkBad; } else { $self->{hkBg}{"$wId,$bId"} = $hkGuess; } $self->{handiGuess}{"$wId,$bId"} = $handi; $self->{komiGuess}{"$wId,$bId"} = $komi; $self->{resu}{"wId,bId"} = '?' unless(defined($self->{resu}{"wId,bId"})); } sub AdjustValue { my ($self, $val) = @_; return unless(defined($self->{selectedField})); my $idx = $self->{selectedIdx}; return if ($idx > @{$self->{wId}}); # can't adjust bye values my $field = $self->{selectedField}; return unless(($field eq 'handi') or ($field eq 'komi')); $self->{selectedIdx} = $idx; $self->{selectedField} = $field; my $id = "$self->{wId}[$idx],$self->{bId}[$idx]"; my $handi = exists ($self->{handi}{$id}) ? $self->{handi}{$id} : $self->{handiGuess}{$id} ; my $komi = exists ($self->{komi}{$id}) ? $self->{komi}{$id} : $self->{komiGuess}{$id} ; if ($field eq 'handi') { if ($val == -1) { if ($handi == 0) { if ($komi < $self->{noKomi}) { $komi = $self->{noKomi}; } elsif ($komi < $self->{normalKomi}) { $komi = $self->{normalKomi}; } # else - don't go any farther - need to swap black/white } elsif ($handi == 2) { $handi = 0; $komi = $self->{reverseKomi}; } else { $handi--; $komi = ($self->{rules} eq 'ING') ? -$handi : 0; } } else { if ($handi == 0) { if ($komi > $self->{noKomi}) { $komi = $self->{noKomi}; } elsif ($komi > $self->{reverseKomi}) { $komi = $self->{reverseKomi}; } else { $handi = 2; $komi = ($self->{rules} eq 'ING') ? -$handi : 0; } } elsif ($handi < 9) { $handi++; $handi++ if ($handi == 1); $komi = ($self->{rules} eq 'ING') ? -$handi : 0; } } } else { $komi += $val; } return if (exists($self->{handi}{$id}) and ($self->{handi}{$id} == $handi) and exists($self->{komi}{$id}) and ($self->{komi}{$id} == $komi)); $self->Add('forward', [$self => 'SetValue', $handi, $komi, $id, $idx]); $self->Add('backward', [$self => 'SetValue', $self->{handi}{$id}, $self->{komi}{$id}, $id, $idx]); $self->Forward(scalar(@{$self->{forward}})); # forward to end of history buffer } sub SetValue { my ($self, $handi, $komi, $id, $idx) = @_; $self->{handi}{$id} = $handi; $self->{komi}{$id} = $komi; $self->RefreshIdx($idx); } sub ChangeResult { my ($self, $char) = @_; $self->{selectedIdx} = 0 unless(defined($self->{selectedIdx})); my $newIdx = $self->{selectedIdx}; return unless($newIdx < @{$self->{wId}}); $self->{selectedField} = 'resu' unless(defined($self->{selectedField})); my $id = "$self->{wId}[$newIdx],$self->{bId}[$newIdx]"; my $prevResu = $self->{resu}{$id}; my $newResu = lc($char); if ($newResu eq $char) { $newIdx++; $newIdx = $#{$self->{bye}} if ($newIdx > $#{$self->{bye}}); } else { $newIdx--; $newIdx = 0 if ($newIdx < 0); } $newResu = '?' if ($newResu eq 'x'); $self->Add('forward', [$self => 'SetResult', $self->{selectedIdx}, $newResu, $newIdx, 'resu']); $self->Add('backward', [$self => 'SetResult', $self->{selectedIdx}, $prevResu, $self->{selectedIdx}, $self->{selectedField}]); $self->Forward(scalar(@{$self->{forward}})); # forward to end of history buffer } sub SetResult { my ($self, $idx, $newResu, $newIdx, $newField) = @_; my $id = "$self->{wId}[$idx],$self->{bId}[$idx]"; $self->{resu}{$id} = $newResu; $self->RefreshIdx($idx); $self->{selectedIdx} = $newIdx; $self->{selectedField} = $newField; $self->RefreshIdx($newIdx); } sub ps { my ($self) = @_; my ($ii, $wId, $wName, $bId, $bName, $handi, $komi, $resu); for ($ii = 0; $ii < @{$self->{wId}}; $ii++) { $wId = $self->{wId}[$ii]; $wName = $self->{name}{$wId}; $bId = $self->{bId}[$ii]; $bName = $self->{name}{$bId}; $handi = $self->{handi}{"$wId,$bId"}; $komi = $self->{komi}{"$wId,$bId"}; $resu = $self->{resu}{"$wId,$bId"}; foreach(qw(wId wName bId bName handi komi resu)) { no strict 'refs'; $$_ = 'undef' unless defined($$_); } STDERR->print("$ii: $wId $wName vs $bId $bName handi=$handi, komi=$komi, result=$resu\n"); } for ($ii = 0; $ii < @{$self->{bye}}; $ii++) { next unless(defined($self->{bye}[$ii])); STDERR->print("$ii: BYE $self->{bye}[$ii]"); if ($ii < @{$self->{wId}}) { STDERR->print("Oops! game/BYE conflict!"); } STDERR->print("\n"); } } sub Clear { my ($self) = @_; $self->{text}->configure('-state' => 'normal'); $self->{text}->delete('1.0', 'end'); $self->{text}->configure('-state' => 'disabled'); $self->{lines} = 0; $self->{selectedField} = undef; $self->{selectedIdx} = undef; $self->{history} = (); # start a new history buffer $self->{histIdx} = 0; $self->{saveIdx} = 0; $self->{selectedIdx} = 0; $self->{selectedField} = 'resu'; $self->toplevel()->title("$main::myName: no file"); # set window title } package main; use Tk; our ($myName, $myDir); $myName = $0; # full pathname of this file $myName =~ s"(.*)/""; # delete any preceding path $myDir = $1; main(); sub Usage { print("Usage: aradjust [ options ]\n", " [ filename or round number] adjust pairings/results for single file or round\n", " [ -f ] format only. re-writes round file making minor\n", " corrections (like converting ING no-komi\n", " from -1 to 0)\n"); } sub main { my ($round, $ii, $filename); my $format_only = 0; for ($ii = 0; $ii < @ARGV; $ii++) { if ($ARGV[$ii] eq '-f') { $format_only = 1; } elsif ($ARGV[$ii] =~ m/\D/) { print("unknown option: $ARGV[$ii]\n"); Usage(); exit(1); } else { if (defined($round)) { print("I can handle all rounds (no rounds option), or just one round at a time.\n"); Usage(); exit(1); } $round = $ARGV[$ii]; } } die ("No register.tde file\n") unless (-f 'register.tde'); unless(defined($round)) { for ($round = 1; $round < 99; $round++) { last unless (-f $round + 1 . ".tde"); # find last existing round file } } for ($ii = 1; $ii < $round; $ii++) { $filename = $ii . ".tde"; die "$filename not found. Sequential numbering, please.\n" unless (-f $filename); } my $mw = MainWindow->new(); my $aradj; if ($format_only) { $aradj = $mw->ArAdj(-filename => "$round.tde", -format_only => $format_only); $aradj->Read("$round.tde"); $aradj->Save; } else { $aradj = $mw->ArAdj(-filename => "$round.tde", -format_only => $format_only); $SIG{'INT'} = sub {$aradj->Quit;}; $mw->protocol('WM_DELETE_WINDOW', sub {$aradj->Quit;}); $aradj->pack('-expand' => 'true', '-fill' => 'both'); $mw->MainLoop(); # start up the event loop } } __END__ =head1 NAME aradjust - adjust pairings and enter results for a round =head1 SYNOPSIS aradjust [ round_number ] [ -f ] =head1 DESCRIPTION GUI to adjust AGA (American Go Association) tournament round files (1.tde, 2.tde, etc). Pairings can be made, broken, and changed, and final results entered (winners and losers). =head1 OPTIONS =over 4 =item B The default is to adjust the current round (based on the last n.tde file). Previous rounds can be adjusted by adding providing round number as a command line argument. The round can also be selected within the GUI. =item B<-f> If -f (format only) is specified, aradjust reads the 1.tde file, reformats it, and writes it back out. Only minor corrections (such as changing ING no-komi games from -1 komi to 0) are made. =back =head1 SEE ALSO =over 0 =item o tdfind(1) - prepare register.tde for an AGA Go tournament =item o around(1) - pair a tournament round =item o aradjust(1) - adjust pairings and enter results for a round =item o tpairs(1) - convert pairings from AGA format to printable =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 Games::Go::TDEntry(3) - perl/Tk widget support for TDFinder =item o Games::Go::TDFinder(3) - perl/Tk tdfind support widgets =back =head1 AUTHOR Reid Augustin, Ereid@netchip.comE =head1 COPYRIGHT AND LICENSE Copyright (C) 1999, 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