Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit, not importing from darcs repo.

  • Loading branch information...
commit f5dadda41233c684706fab2aab973aa387f655ac 0 parents
Henzell authored
Showing with 5,014 additions and 0 deletions.
  1. +3 −0  .gitignore
  2. +12 −0 TODO
  3. +103 −0 commands.txt
  4. +232 −0 commands/apt-new.pl
  5. +203 −0 commands/apt.pl
  6. +31 −0 commands/cdoplayers.pl
  7. +26 −0 commands/chars.py
  8. +14 −0 commands/cmdinfo.py
  9. +36 −0 commands/commands.txt
  10. +98 −0 commands/currLeader.pl
  11. +50 −0 commands/deathsby.py
  12. +69 −0 commands/deathsin.py
  13. +16 −0 commands/dump.pl
  14. +59 −0 commands/ftw.pl
  15. +55 −0 commands/gamesby.pl
  16. +32 −0 commands/ghostkills.py
  17. +29 −0 commands/help.pl
  18. +256 −0 commands/helper.pl
  19. +435 −0 commands/helper.py
  20. +308 −0 commands/helper.rb
  21. +20 −0 commands/hsn.rb
  22. +26 −0 commands/idle.pl
  23. +39 −0 commands/killsby.py
  24. +16 −0 commands/lastgame.py
  25. +39 −0 commands/lastlog.pl
  26. +21 −0 commands/lastlog.rb
  27. +52 −0 commands/learn.pl
  28. +27 −0 commands/learn/add.pl
  29. +35 −0 commands/learn/delete.pl
  30. +81 −0 commands/learn/edit.pl
  31. +156 −0 commands/learn/helper.pl
  32. +63 −0 commands/learn/query.pl
  33. +20 −0 commands/listgame.rb
  34. +274 −0 commands/listgameold.py
  35. +26 −0 commands/lsa.py
  36. +23 −0 commands/message/all_input.pl
  37. +57 −0 commands/message/helper.pl
  38. +35 −0 commands/message/messages.pl
  39. +44 −0 commands/message/tell.pl
  40. +115 −0 commands/players.pl
  41. +16 −0 commands/rc.pl
  42. +151 −0 commands/rng.pl
  43. +29 −0 commands/screensize.pl
  44. +32 −0 commands/seen.pl
  45. +275 −0 commands/sqlhelper.rb
  46. +71 −0 commands/stats.rb
  47. +87 −0 commands/streak.py
  48. +6 −0 commands/test.py
  49. +65 −0 commands/whereis.py
  50. +102 −0 commands/won.rb
  51. +90 −0 commands/wtf.py
  52. +8 −0 commands/xlogtest.py
  53. +13 −0 commands_ideas.txt
  54. +65 −0 game_parser.pl
  55. +439 −0 henzell.pl
  56. +106 −0 launch_command.pl
  57. +13 −0 milestone_ideas.txt
  58. +210 −0 sqllog.pl
3  .gitignore
@@ -0,0 +1,3 @@
+nohup.*
+_darcs/
+t/
12 TODO
@@ -0,0 +1,12 @@
+* Have greensnark add realtime, possibly also number of monsters in sight, to each where file.
+
+* PM support.
+
+* doy (probably also other people) milestone idea: when HP drops below a certain percent (say, 10%).. probably also a minimum level cap so the milestone file doesn't get too large.
+
+* Message-leaving system needs some improvements before it can be called done
+ - really needs PM support
+ - limit of say, 5 messages
+ - when we get a seendb, limit message-target to someone who is in the seendb
+ - probably need to make sure the target is identified to nickserv as well
+
103 commands.txt
@@ -0,0 +1,103 @@
+ WRITING NEW HENZELL COMMANDS
+--------------------------------------------------------------------------------
+Henzell effectively accepts new commands written in any language. This is because commands are simply UNIX programs. Of course, the languages are limited to what the server is configured for.
+
+--------
+SYNOPSIS
+--------
+
+Someone types !command. Henzell forks and execs the script associated with that command. Henzell passes some relevant arguments and receives the command's output on STDOUT. That output is sent to the channel.
+
+-----
+INPUT
+-----
+
+Henzell passes five arguments to the command. Note that any single quotes are removed from the fields for security reasons. Consider if a player typed:
+
+!arbitrary_command '; rm -rf *; echo '
+
+If the single quotes were not removed, this would cause some (ahem) headaches.
+
+ARGUMENT THE FIRST
+
+What Henzell thinks the command is really targeting. Most commands should look only at this argument. This argument is for commands that request information about a player. If the speaker passed no arguments to your command, the speaker's nick is used in place. This argument is guaranteed to match the following regex:
+
+/^[a-zA-Z0-9]*?[a-zA-Z]+$/
+
+So in plain English: any amount of alphanumeric characters followed by at least one alphabetical character. (This is because Crawl does not support names ending with numbers, so they're stripped off -- consider aristotle73 typing "!gamesby".. the command should target his account "aristotle")
+
+ARGUMENT THE SECOND
+
+The person who is issuing the command. I'm not sure if it's in the IRC RFC that nicks cannot contain single quotes, but in either case any single quotes will be stripped off the nick.
+
+ARGUMENT THE THIRD
+
+The verbatim command exactly as the user typed it, minus single quotes. If you need to look at just the arguments that the user provided, strip off the first word-thing (and probably a space, because it's required for commands) like so (again, Perl):
+
+$ARGV[2] =~ s/^\S+ //;
+
+ARGUMENT THE FOURTH
+
+This is used to tell the command whether or not to print help information. Ordinarily this is a simple empty string. However, if !help !yourcommand is used, this becomes 1. In this case you should print help text and exit. Note that you have to be alert for multiple commands in the same file. Generally you figure out which command the person is using, and then see if help mode is on. You do not need to put the command name in the output; !help does that for you. The !help command simply passes its own arguments along, with three exceptions: the fourth argument (which was the empty string) becomes 1, the third argument (verbatim text the user typed) has the following substitution applied to it (to make it easy for command authors):
+
+$ARGV[2] =~ s/^!help\s+//i;
+
+and the third change is $ARGV[2] has an exclamation point prepended to it if there is not one already. (this makes it so "!help stats" does the right thing even if stats is in a multi-command file)
+
+ARGUMENT THE FIFTH
+
+This is used to tell the command whether the command came from the channel, a private message, or a notice. For all intents and purposes, PMs and notices should be treated equally. But who knows, you might have some use. This argument is empty, '', if the command came from the channel. This argument is '1' if it was a PM, or '2' if it was a notice. The following test could be used to exit if the command only works if it was issued in the channel (probably done the same in no_pm() in your language's helper library):
+
+if ($ARGV[4])
+{
+ printf "This command cannot be used in a %s.\n",
+ $ARGV[4] eq '1' ? 'private message' : 'notice';
+ exit;
+}
+
+------
+OUTPUT
+------
+
+Henzell currently accepts two forms of output. The exit code of the script and STDERR are (mostly) ignored. The STDERR of any script is logged.
+
+Henzell truncates all output to 400 characters (after any processing). Future versions might support longer lines which are broken up before outputting (a la Rodney3).
+
+NORMAL OUTPUT
+
+This is to be used for most commands. Simply print the output to STDOUT. Henzell currently only looks at the first line of output, but in the future he may pay attention to subsequent lines. Henzell simply echoes this output to whatever medium he received the command over (whether it be a channel or, in the future, a private message or notice)
+
+LOGFILE OUTPUT
+
+This is used for when you want Henzell to "pretty print" a logfile line. This should be used for any command that displays a logfile line, so that consistency is maintained. To use the logfile output mode you print to STDOUT just like in normal output mode, except you begin with an "\n". Henzell will then look at the next line and try to parse it as a logfile entry. Logfile entries begin and end with a colon. Henzell will strip off any text before the first colon in the line and prepend it to the pretty-printed logfile entry. Similarly for any text after the last colon. This means you cannot include a colon in the pre-text or post-text. Note that in the new 0.2.x logfile format, you have to specifically wrap the xlogline in colons.
+
+------------
+COMMON USAGE
+------------
+
+Unfortunately since Henzell commands are external scripts, they cannot make use of standard utility functions (like finding the games for a player or building a more suitable data structure from a logfile line). However, for some languages (currently Perl, Python, and Ruby) we have helper scripts that contain useful utility functions and constant definitions. Use these whenever possible (again for consistency, but also because that's just a good coding practice). A fringe benefit of writing commands this way (as external programs) is that they're very easy to test. The real reason commands are external programs is because they're easier to update (no need to reboot Henzell) and being able to be written in multiple languages is a boon for getting new functionality for Henzell.
+
+Anyway, the helper scripts should make writing new commands very painless. Here's a somewhat simplified version of !hsn in Perl.
+
+#!/usr/bin/perl
+do 'commands/helper.pl';
+print "List a player's high scoring game." and exit if $ARGV[3];
+my $nick = shift;
+my $games_ref = games_by($nick);
+my $hsn_ref = (sort { $b->{score} <=> $a->{score} } @$games_ref)[0];
+print "\n" . munge_game($hsn_ref);
+
+If you're going to write new helper scripts (or simply extend an existing helper script) please use the same external interface as used in other helper scripts (except where it makes sense to diverge.. for example the Perl helper script uses references where possible to save time and space in passing things around).
+
+-----------------------
+CAVEATS FOR CONSISTENCY
+-----------------------
+
+1. When writing a command, where possible use the same messages as other commands (for example, "No games for NICK." instead of "NICK has played no games.").
+2. When printing out the resulting nickname ("Eidolos has played X games..."), where possible use a nickname directly from the logfile. So (in Perl), use $games[0]{name} not $nick (which would be from user input). This is so "!gamesby EIDOLOS" ends up with the correct case. (Rodney in #nethack does not do this: most logfile commands have the player name in lowercase regardless of the actual capitalization). Yes this means if you're doing an victory-based command you may have to look at all games (not just victories.. but you were going to anyway, to distinguish between "No games for NICK." versus "No victories for NICK.", right?).
+3. If you're printing a floating point number, please use a printf (or similar) so that the appropriate amount of trailing zeros are included (so printf '%.2f%%', 100*$vics/$games instead of print int(10000*$vics/$games)/100 -- the former will always print two digits after the decimal point, the latter will print zero, one, or two). The exact precision used isn't much of an issue, though (but prefer two places after the decimal.. any more and it gets a bit clunky).
+4. Output should (almost) always begin with a capital letter. One exception is when output begins with a player name and that player name is not capitalized.
+5. If you're printing a frequency table (such as the output of !won), sort based on frequency (whether ascending or descending) and then by the item name (ascending). In Perl, that's:
+ sort { $won{$b} <=> $won{$a} || $a cmp $b } @races;
+6. If your command takes a player name and additional arguments, then those additional arguments should be formed such that an argument cannot be confused for a nickname. For example, let's pretend the !won command is extended to accept a race name. So a player can type !won NICK RACE. Now suppose there's someone with the nickname "Human" on akrasiac. What should "!won Human" produce -- the human victories of the person typing the command, or all the victories of this Human account? The answer is the former. The RACE argument should (for example) be prepended with a - so it's completely unambiguous. As a guideline, !command <args> should produce the exact same results as !command SPEAKERNICK <args>. Note that since Crawl nicks cannot contain only numbers so something like !game_number 10 is unambiguous (whereas in NetHack it isn't, so Rodney3 requires the number argument be prepended with a #)
+
232 commands/apt-new.pl
@@ -0,0 +1,232 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+do 'commands/helper.pl';
+help("Looks up aptitudes for specified race/skill combination.");
+
+our (%fullDB, %transRaceDB, %skillList, %bestApt, %dracColours, @gameOrder);
+sub buildTransDB # {{{
+{
+ %dracColours = (red=>"SP_RED_DRACONIAN", white=>"SP_WHITE_DRACONIAN",
+ green=>"SP_GREEN_DRACONIAN", yellow=>"SP_YELLOW_DRACONIAN",
+ grey=>"SP_GREY_DRACONIAN", black=>"SP_BLACK_DRACONIAN",
+ purple=>"SP_PURPLE_DRACONIAN", mottled=>"SP_MOTTLED_DRACONIAN",
+ pale=>"SP_PALE_DRACONIAN");
+ @gameOrder = ("SK_FIGHTING", "SK_SHORT_BLADES", "SK_LONG_BLADES", "SK_AXES",
+ "SK_MACES_FLAILS", "SK_POLEARMS", "SK_STAVES", "SK_UNARMED_COMBAT",
+ "SK_THROWING", "SK_SLINGS", "SK_BOWS", "SK_CROSSBOWS", "SK_DARTS",
+ "SK_ARMOUR", "SK_DODGING", "SK_STEALTH", "SK_STABBING", "SK_SHIELDS", "SK_TRAPS_DOORS",
+ "SK_SPELLCASTING", "SK_CONJURATIONS", "SK_ENCHANTMENTS", "SK_SUMMONINGS",
+ "SK_NECROMANCY", "SK_TRANSLOCATIONS", "SK_TRANSMIGRATION", "SK_DIVINATIONS",
+ "SK_FIRE_MAGIC", "SK_ICE_MAGIC", "SK_AIR_MAGIC", "SK_EARTH_MAGIC", "SK_POISON_MAGIC",
+ "SK_INVOCATIONS", "SK_EVOCATIONS");
+ %skillList = ("SK_FIGHTING"=>"Fighting", "SK_SHORT_BLADES"=>"Short",
+ "SK_LONG_BLADES"=>"Long", "SK_AXES"=>"Axes", "SK_MACES_FLAILS"=>"Maces",
+ "SK_POLEARMS"=>"Polearms", "SK_STAVES"=>"Staves", "SK_SLINGS"=>"Slings",
+ "SK_BOWS"=>"Bows", "SK_CROSSBOWS"=>"Crossbows", "SK_DARTS"=>"Darts",
+ "SK_THROWING"=>"Throw", "SK_ARMOUR"=>"Armour",
+ "SK_DODGING"=>"Dodge", "SK_STEALTH"=>"Stealth", "SK_STABBING"=>"Stab",
+ "SK_SHIELDS"=>"Shields", "SK_TRAPS_DOORS"=>"Traps",
+ "SK_UNARMED_COMBAT"=>"Unarmed", "SK_SPELLCASTING"=>"Spellcasting",
+ "SK_CONJURATIONS"=>"Conj", "SK_ENCHANTMENTS"=>"Ench",
+ "SK_SUMMONINGS"=>"Summ", "SK_NECROMANCY"=>"Nec",
+ "SK_TRANSLOCATIONS"=>"Tloc", "SK_TRANSMIGRATION"=>"Tmig",
+ "SK_DIVINATIONS"=>"Div", "SK_FIRE_MAGIC"=>"Fire", "SK_ICE_MAGIC"=>"Ice",
+ "SK_AIR_MAGIC"=>"Air", "SK_EARTH_MAGIC"=>"Earth",
+ "SK_POISON_MAGIC"=>"Poison", "SK_INVOCATIONS"=>"Inv",
+ "SK_EVOCATIONS"=>"Evo");
+ %transRaceDB = (Hu=>"SP_HUMAN",
+ HE=>"SP_HIGH_ELF",
+ GE=>"SP_GREY_ELF",
+ DE=>"SP_DEEP_ELF",
+ SE=>"SP_SLUDGE_ELF",
+ MD=>"SP_MOUNTAIN_DWARF",
+ Ha=>"SP_HALFLING",
+ HO=>"SP_HILL_ORC",
+ Ko=>"SP_KOBOLD",
+ Mu=>"SP_MUMMY",
+ Na=>"SP_NAGA",
+ Gn=>"SP_GNOME",
+ Og=>"SP_OGRE",
+ Tr=>"SP_TROLL",
+ OM=>"SP_OGRE_MAGE",
+ Dr=>"SP_BASE_DRACONIAN",
+ Ce=>"SP_CENTAUR",
+ DG=>"SP_DEMIGOD",
+ Sp=>"SP_SPRIGGAN",
+ Mi=>"SP_MINOTAUR",
+ DS=>"SP_DEMONSPAWN",
+ Gh=>"SP_GHOUL",
+ Ke=>"SP_KENKU",
+ Mf=>"SP_MERFOLK",
+ Vp=>"SP_VAMPIRE");
+} # }}}
+sub parseSkillsFile # {{{
+{
+ open my $infile, "<", "db.cc";
+ my $currRace;
+ while(<$infile>)
+ {
+ # Determine race
+ if(m#\{\s*// ([A-Z\(\)0-9_]+)#)
+ {
+ $currRace=$1 if(m#\{\s*// ([A-Z\(\)0-9_]+)#);
+ }
+
+ # Determine attribute and aptitude
+ if(m#^\s*([ 0-9-\+\(\)/\*]+),\s+// ([A-Z0-9_]+)#)
+ {
+ my $evaled = (eval $1);
+ if( defined($currRace) ) # This figures out what the best
+ { # aptitudes are.
+ if( defined($bestApt{$2}) ) # Check if the old value is better
+ {
+ $bestApt{$2}=$evaled if($evaled < $bestApt{$2});
+ }
+ else # If there is no old value, just store
+ {
+ $bestApt{$2}=$evaled;
+ }
+ $fullDB{$currRace}{$2}=$evaled if( defined($currRace) );
+ }
+ }
+ }
+} # }}}
+
+# Prepare DB's
+buildTransDB();
+parseSkillsFile();
+$_ = $ARGV[2];
+
+chomp;
+s/^!apt\s+(.*)$/$1/;
+my ($race, $skill, $sort, $colour);
+my @args = split / /;
+
+# parse arguments
+foreach (@args)
+{
+ if(/r=([A-Za-z]{2})/) {$race=$1;} # -race=Sp
+ elsif(/s=([A-Z_]*)/) {$skill=$1;} # -skill=SK_DODGING
+ elsif(/so=([a-z]*)/) {$sort=$1;} # -sort=alph
+ elsif(/c=([a-z]*)/) {$colour=$1;} # -colour=red
+}
+
+if( defined($race) ) # atleast $race is defined
+{
+ if( defined($skill) ) # $race && $skill are defined {{{
+ {
+ if( defined($colour) ) # $race && $skill && $colour are defined
+ {
+ # checking for invalid arguments
+ unless( defined($transRaceDB{$race}) ) { print "Not a valid race.\n"; next; }
+ unless( defined($skillList{$skill}) ) { print "Not a valid skill.\n"; next; }
+ unless( defined($dracColours{$colour})) { print "Non valid colour.\n"; next; }
+
+ # Check for dracs
+ if($race eq "Dr")
+ {
+ my $fullRace = $dracColours{$colour};
+ print "Dr[$colour]($skill)=", $fullDB{$fullRace}{$skill};
+ print "!" if($fullDB{$fullRace}{$skill} == $bestApt{$skill});
+ print "\n";
+ } else { print "Only draconians get colours.\n"; next; }
+ }
+ else
+ {
+ # checking for invalid arguments
+ unless( defined($transRaceDB{$race}) ) { print "Not a valid race.\n"; next; }
+ unless( defined($skillList{$skill}) ) { print "Not a valid skill.\n"; next; }
+
+ my $fullRace = $transRaceDB{$race};
+ print "$race($skill)=", $fullDB{$fullRace}{$skill};
+ print "!" if($fullDB{$fullRace}{$skill} == $bestApt{$skill});
+ print "\n";
+ }
+ } # }}}
+ else # $race is defined {{{
+ {
+ # valid race
+ unless( defined($transRaceDB{$race}) ) { print "Not a valid race.\n"; next; }
+
+ if( defined($colour) ) # -colour=red{{{
+ {
+ unless($race eq "Dr") { print "Only draconians get colours.\n"; next;}
+ unless( defined($dracColours{$colour})) { print "Not a valid colour.\n"; next; }
+
+ my $fullRace = $dracColours{$colour};
+ my %tempDB = %{ $fullDB{$fullRace} };
+ my ($ref, @tempVar);
+
+ if( defined($sort) && $sort eq "alpha" ) {
+ @tempVar = (sort keys %tempDB);
+ $ref = \@tempVar;
+ }
+ else {
+ $ref = \@gameOrder;
+ }
+
+ my $output;
+ foreach my $k (@$ref)
+ {
+ my $v = $tempDB{$k};
+ unless($k eq "SK_UNUSED_1" || $k eq "undefined")
+ {
+ $output .= $skillList{$k} . ":" . $v;
+ $output .= "!" if($v == $bestApt{$k});
+ $output .= ", ";
+ }
+ }
+ chop $output; chop $output;
+ print "$output\n";
+ } # }}}
+ else
+ {
+ # Do reference magic here
+ my $fullRace = $transRaceDB{$race};
+ my %tempDB = %{ $fullDB{$fullRace} };
+ my ($output, $ref, @tempVar);
+
+ if( defined($sort) && $sort eq "alpha" ) {
+ @tempVar = (sort keys %tempDB);
+ $ref = \@tempVar;
+ }
+ else {
+ $ref = \@gameOrder;
+ }
+ foreach my $k (@$ref)
+ {
+ my $v = $tempDB{$k};
+ unless($k eq "SK_UNUSED_1" || $k eq "undefined")
+ {
+ $output .= $skillList{$k} . ":" . $v;
+ $output .= "!" if($v == $bestApt{$k});
+ $output .= ", ";
+ }
+ }
+ chop $output; chop $output;
+ print "$output\n";
+ }
+ } # }}}
+}
+elsif( defined($skill) ) # only $skill is defined {{{
+{
+ unless( defined($skillList{$skill}) ) { print "Not a valid skill.\n"; next; }
+
+ my $output;
+ foreach my $k (sort keys %transRaceDB)
+ {
+ my $v = $transRaceDB{$k};
+ $output .= "$k=";
+ $output .= $fullDB{$v}{$skill};
+ $output .= "!" if($bestApt{$skill}==$fullDB{$v}{$skill});
+ $output .= ", ";
+ }
+ chop $output; chop $output;
+ print "$output\n";
+} # }}}
+else # !($skill && $race) {{{
+{
+ print "Neither the s nor the r parameters have been set.\n";
+} # }}}
203 commands/apt.pl
@@ -0,0 +1,203 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+do 'commands/helper.pl';
+
+help("Looks up aptitudes for specified race/skill combination.");
+our (%fullDB, %transRaceDB, %skillList, %bestApt, %dracColours);
+my %raceMap;
+
+sub buildTransDB # {{{
+{
+ %dracColours = (red=>"SP_RED_DRACONIAN", white=>"SP_WHITE_DRACONIAN",
+ green=>"SP_GREEN_DRACONIAN", yellow=>"SP_YELLOW_DRACONIAN",
+ grey=>"SP_GREY_DRACONIAN", black=>"SP_BLACK_DRACONIAN",
+ purple=>"SP_PURPLE_DRACONIAN", mottled=>"SP_MOTTLED_DRACONIAN",
+ pale=>"SP_PALE_DRACONIAN");
+ %skillList = ("SK_FIGHTING"=>"Fighting", "SK_SHORT_BLADES"=>"Short",
+ "SK_LONG_BLADES"=>"Long", "SK_AXES"=>"Axes", "SK_MACES_FLAILS"=>"Maces",
+ "SK_POLEARMS"=>"Polearms", "SK_STAVES"=>"Staves", "SK_SLINGS"=>"Slings",
+ "SK_BOWS"=>"Bows", "SK_CROSSBOWS"=>"Crossbows", "SK_DARTS"=>"Darts",
+ "SK_THROWING"=>"Throw", "SK_ARMOUR"=>"Armour",
+ "SK_DODGING"=>"Dodge", "SK_STEALTH"=>"Stealth", "SK_STABBING"=>"Stab",
+ "SK_SHIELDS"=>"Shields", "SK_TRAPS_DOORS"=>"Traps",
+ "SK_UNARMED_COMBAT"=>"Unarmed", "SK_SPELLCASTING"=>"Spellcasting",
+ "SK_CONJURATIONS"=>"Conj", "SK_ENCHANTMENTS"=>"Ench",
+ "SK_SUMMONINGS"=>"Summ", "SK_NECROMANCY"=>"Nec",
+ "SK_TRANSLOCATIONS"=>"Tloc", "SK_TRANSMIGRATION"=>"Tmig",
+ "SK_DIVINATIONS"=>"Div", "SK_FIRE_MAGIC"=>"Fire", "SK_ICE_MAGIC"=>"Ice",
+ "SK_AIR_MAGIC"=>"Air", "SK_EARTH_MAGIC"=>"Earth",
+ "SK_POISON_MAGIC"=>"Poison", "SK_INVOCATIONS"=>"Inv",
+ "SK_EVOCATIONS"=>"Evo");
+ %transRaceDB = (Hu=>"SP_HUMAN",
+ HE=>"SP_HIGH_ELF",
+ GE=>"SP_GREY_ELF",
+ DE=>"SP_DEEP_ELF",
+ SE=>"SP_SLUDGE_ELF",
+ MD=>"SP_MOUNTAIN_DWARF",
+ Ha=>"SP_HALFLING",
+ HO=>"SP_HILL_ORC",
+ Ko=>"SP_KOBOLD",
+ Mu=>"SP_MUMMY",
+ Na=>"SP_NAGA",
+ Gn=>"SP_GNOME",
+ Og=>"SP_OGRE",
+ Tr=>"SP_TROLL",
+ OM=>"SP_OGRE_MAGE",
+ Dr=>"SP_BASE_DRACONIAN",
+ Ce=>"SP_CENTAUR",
+ DG=>"SP_DEMIGOD",
+ Sp=>"SP_SPRIGGAN",
+ Mi=>"SP_MINOTAUR",
+ DS=>"SP_DEMONSPAWN",
+ Gh=>"SP_GHOUL",
+ Ke=>"SP_KENKU",
+ Mf=>"SP_MERFOLK",
+ Vp=>"SP_VAMPIRE");
+
+ %raceMap = map { (lc, $_) } keys(%transRaceDB);
+ $transRaceDB{+lc} = $transRaceDB{$_} for keys %transRaceDB;
+ $skillList{+lc} = $skillList{$_} for keys %skillList;
+} # }}}
+sub parseSkillsFile # {{{
+{
+ open my $infile, "<", "db.cc";
+ my $currRace;
+ while(<$infile>)
+ {
+ # Determine race
+ if(m#\{\s*// ([A-Z\(\)0-9_]+)#)
+ {
+ $currRace=$1 if(m#\{\s*// ([A-Z\(\)0-9_]+)#);
+ }
+
+ # Determine attribute and aptitude
+ if(m#^\s*([ 0-9-\+\(\)/\*]+),\s+// ([A-Z0-9_]+)#)
+ {
+ my $evaled = (eval $1);
+ if( defined($currRace) ) # This figures out what the best
+ { # aptitudes are.
+ if( defined($bestApt{$2}) ) # Check if the old value is better
+ {
+ $bestApt{$2}=$evaled if($evaled < $bestApt{$2});
+ }
+ else # If there is no old value, just store
+ {
+ $bestApt{$2}=$evaled;
+ }
+ $fullDB{$currRace}{$2}=$evaled if( defined($currRace) );
+ }
+ }
+ }
+
+ $bestApt{+lc} = $bestApt{$_} for keys %bestApt;
+ $fullDB{+lc} = $fullDB{$_} for keys %fullDB;
+ for my $hash (values %fullDB) {
+ $$hash{+lc} = $$hash{$_} for keys %$hash;
+ }
+} # }}}
+
+# Prepare DB's
+buildTransDB();
+parseSkillsFile();
+
+# 3rd argument is the entire command line
+$_ = lc($ARGV[2]);
+s/^!apt\s+(.*)$/$1/; # Strip away the !apt part of the string
+
+chomp;
+if(/^\s* Dr \[([a-z]*)\] \s+ (SK_[A-Z_0-9]+)/xi) # !apt Dr[red] SK_SOMETHING {{{
+{
+ unless($dracColours{$1}) { print "Invalid colour.\n"; next; }
+ my $race = $dracColours{$1};
+ if( defined($skillList{$2}) )
+ {
+ print "Dr[$1](@{ [uc $2] })=", $fullDB{$race}{$2};
+ print "!" if($fullDB{$race}{$2} == $bestApt{$2});
+ print "\n";
+ }
+ else
+ {
+ print "Invalid skill.\n";
+ }
+} # }}}
+elsif(/^\s* ([A-Za-z]{2}) \s+ (SK_[A-Z_0-9]+)/xi) # !apt Sp SK_SOMETHING {{{
+{
+ unless($transRaceDB{$1}) { print "Invalid race.\n"; next; }
+ my $race = $transRaceDB{$1};
+ my $abbr = $raceMap{$1};
+ if( defined($skillList{$2}) )
+ {
+ print "$abbr (@{ [ uc $2 ]})=", $fullDB{$race}{$2};
+ print "!" if($fullDB{$race}{$2} == $bestApt{$2});
+ print "\n";
+ }
+ else
+ {
+ print "Invalid skill.\n";
+ }
+} # }}}
+elsif(/^\s* (SK_[A-Z_0-9]+)/xi) # !apt SK_SOMETHING {{{
+{
+ unless( defined($skillList{uc $1}) ) # Bad skill
+ {
+ print "Skill does not exist.\n";
+ next;
+ }
+ my $output;
+ my $sk = $1;
+ foreach my $k (sort keys %transRaceDB)
+ {
+ next unless $k =~ /^[A-Z]/;
+ my $v = $transRaceDB{$k};
+ $output .= "$k=";
+ $output .= $fullDB{$v}{$sk};
+ $output .= "!" if($bestApt{$sk}==$fullDB{$v}{$sk});
+ $output .= ", ";
+ }
+ chop $output; chop $output;
+ print "$output\n";
+} # }}}
+elsif(/^\s* Dr \[([a-z]*)\]/xi) # !apt Dr[red] {{{
+{
+ unless($dracColours{$1}) { print "Invalid colour.\n"; next; }
+ my $race = $dracColours{$1};
+ my %tempDB = %{ $fullDB{$race} };
+ my $output;
+ foreach my $k (sort keys %tempDB)
+ {
+ next unless $k =~ /^[A-Z]/;
+ my $v = $tempDB{$k};
+ unless($k eq "SK_UNUSED_1" || $k eq "undefined")
+ {
+ $output .= $skillList{$k} . ":" . $v;
+ $output .= "!" if($v == $bestApt{$k});
+ $output .= ", ";
+ }
+ }
+ chop $output; chop $output;
+ print "$output\n";
+} #}}}
+elsif(/^\s* ([A-Za-z]{2})/x) # !apt Sp {{{
+{
+ unless($transRaceDB{$1}) { print "Invalid race.\n"; next; }
+ my $race = $transRaceDB{$1};
+ my %tempDB = %{ $fullDB{$race} };
+ my $output;
+
+ foreach my $k (sort keys %tempDB)
+ {
+ next unless $k =~ /^[A-Z]/;
+ my $v = $tempDB{$k};
+ unless($k eq "SK_UNUSED_1" || $k eq "undefined")
+ {
+ $output .= $skillList{$k} . ":" . $v;
+ $output .= "!" if($v == $bestApt{$k});
+ $output .= ", ";
+ }
+ }
+ chop $output; chop $output;
+ print "$output\n";
+} #}}}
31 commands/cdoplayers.pl
@@ -0,0 +1,31 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+my $baseURL='http://crawl.develz.org/cgi-bin/crawl_active_players.sh';
+my $columns=200;
+my $output="";
+my $numPlayers=0;
+
+# do 'commands/helper.pl';
+# help("Shows what players are online and playing on CDO.");
+
+# Get dump from the cgi script
+$_ = `lynx -width $columns -dump $baseURL`;
+my @arr = split /\n/;
+
+foreach (@arr)
+{
+ # (L5 DSCK), a worshipper of Xom, is currently on D:4 after 5027 turns.
+ if(/^\s* ([0-9a-zA-Z_]+) \s* \((L[0-9]+) \s* ([A-Za-z]+) \) .* currently \s [io]n \s ([A-Za-z:0-9]+) \s after \s ([0-9]+) .*/x)
+ {
+ $numPlayers++;
+ $output .= "$1 ($2 $3 \@ $4, T:$5), ";
+ }
+}
+
+# Fix the final output and print it
+$output = "$numPlayers players: $output";
+chop $output; chop $output;
+print $output, "\n";
26 commands/chars.py
@@ -0,0 +1,26 @@
+#!/usr/bin/python
+
+import sys
+from helper import *
+
+help("Lists the frequency of all character types a player started.")
+
+games = games_for(sys.argv[1])
+if len(games) == 0:
+ print("No games for %s." % sys.argv[1])
+ sys.exit()
+
+types = []
+uniq_types = []
+for game in games:
+ type = game['char']
+ types.append(type)
+ if type not in uniq_types:
+ uniq_types.append(type)
+
+uniq_types.sort(lambda x, y: cmp(types.count(y), types.count(x)) or cmp(x,y))
+charstring = ''
+for type in uniq_types:
+ charstring += "%dx%s " % (types.count(type), type)
+
+print("%s has played %d games: %s" % (games[0]['name'], len(games), charstring))
14 commands/cmdinfo.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+
+import os, sys
+from helper import *
+
+help("Lists all available Henzell commands.")
+
+commands = os.popen('cat commands/commands.txt').readlines()
+
+output = ''
+for line in commands:
+ output += line.split(' ')[0] + ' '
+
+print output
36 commands/commands.txt
@@ -0,0 +1,36 @@
+!cdo cdoplayers.pl
+!aplayers players.pl
+!apt apt.pl
+!aptnew apt-new.pl
+!chars chars.py
+!cmdinfo cmdinfo.py
+!currleader currLeader.pl
+!deathsby deathsby.py
+!dump dump.pl
+!eplayers players.pl
+!ftw ftw.pl
+!gamesby gamesby.pl
+!ghostkills ghostkills.py
+!gkills ghostkills.py
+!help help.pl
+!hs hsn.rb
+!idle idle.pl
+!killsby killsby.py
+!log lastlog.rb
+!lastlog lastlog.rb
+!learn learn.pl
+!lastgame listgame.rb
+!listgame listgame.rb
+!lsa lsa.py
+!messages message/messages.pl
+!players players.pl
+!rc rc.pl
+!rng rng.pl
+!screensize screensize.pl
+!seen seen.pl
+!size screensize.pl
+!streak streak.py
+!tell message/tell.pl
+!whereis whereis.py
+!won won.rb
+!wtf wtf.py
98 commands/currLeader.pl
@@ -0,0 +1,98 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+do 'commands/helper.pl';
+help("Script to look up the winner of the current competition.");
+
+our $scoreFile = "/home/crawl/chroot/var/games/crawl03/saves/scores";
+
+our @startDate = (2006, 11, 5, 23);
+our @endDate = (2009, 1, 1, 1);
+
+our $cls = 'Wanderer';
+our $char = 'MuWn';
+
+# our $cls = 'Paladin';
+# our $char = '[A-Za-z]+';
+
+our $v = '[0-9\.]+';
+our $ktyp = 'winning';
+
+open my $infile, '<', $scoreFile or die 'Unable to open score file.\n';
+
+while(<$infile>)
+{
+ # Filter out stuff
+ my $logEntry = $_;
+ if(/v=($v).*?
+ name=([a-zA-Z]+).*?
+ cls=($cls).*?
+ char=($char).*?
+ sklev=([0-9]+).*
+ title=([a-zA-Z ]+).*?
+ start=([0-9]+).*?
+ turn=([0-9]+).*?
+ sc=([0-9]+).*?
+ ktyp=($ktyp).*?
+ end=([0-9]+)/x)
+ {
+ my $v = $1;
+ my $name = $2;
+ my $cls = $3;
+ my $char = $4;
+ my $sklev = $5;
+ my $title = $6;
+ my $gameStart = $7;
+ my $turn = $8;
+ my $sc = $9;
+ my $ktyp = $10;
+ my $gameEnd = $11;
+
+ # Check date range
+ if( &dateOk($gameStart) && &dateOk($gameEnd))
+ {
+ if($logEntry =~ /god=([A-Za-z ]+).*/) {
+ print "$sc $name ($char) the $title, worshipper of $1 escaped with the Orb after $turn turns.\n";
+ exit;
+ }
+ else {
+ print "$sc $name ($char) the $title, escaped with the Orb after $turn turns.\n";
+ exit;
+ }
+ }
+ }
+}
+print "No game matching the criteria.\n";
+
+sub dateOk() # dateToCheck {{{
+{
+ my $dateToCheck = \$_[0];
+ my $year = substr($_[0], 0, 4);
+ my $month = substr($_[0], 4, 2);
+ my $day = substr($_[0], 6, 2);
+ my $hour = substr($_[0], 8, 2);
+
+ if($year>$startDate[0] ||
+ ($year>=$startDate[0] && $month>$startDate[1]) ||
+ ($year>=$startDate[0] && $month>=$startDate[1] && $day>$startDate[2]) ||
+ ($year>=$startDate[0] && $month>=$startDate[1] && $day>=$startDate[2] && $hour>=$startDate[3]) )
+ {
+ if($year<$endDate[0] ||
+ ($year<=$endDate[0] && $month<$endDate[1]) ||
+ ($year<=$endDate[0] && $month<=$endDate[1] && $day<$endDate[2]) ||
+ ($year<=$endDate[0] && $month<=$endDate[1] && $day<=$endDate[2] && $hour<=$endDate[3]) )
+ {
+ return 1;
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ else
+ {
+ return 0;
+ }
+} # }}}
50 commands/deathsby.py
@@ -0,0 +1,50 @@
+#!/usr/bin/python
+
+import sys, re
+from helper import *
+
+help("Lists a player's 10 most frequent causes of death.")
+
+player = sys.argv[1]
+
+games = games_for(player)
+if not games:
+ print "No games for %s." % player
+ sys.exit()
+
+deaths = []
+uniq_deaths = []
+deathcount = 0
+for game in games:
+ ktyp = game['ktyp']
+ if ktyp in ['quitting', 'leaving', 'winning']: # Quits, escapes, and wins don't count
+ continue
+ deathcount += 1
+ if ktyp in ['beam', 'mon']:
+ source = game['killer']
+ for char, index in zip(source[1:3], [1,2]):
+ if ' ' == char:
+ source = source[index + 1:]
+ else:
+ source = ktyp
+
+ deaths.append(source)
+ if source not in uniq_deaths:
+ uniq_deaths.append(source)
+
+if deathcount == 0:
+ print("No deaths for %s! Wow!" % player)
+ sys.exit()
+if len(deaths) == 1: deathstring = "1 death"
+else: deathstring = "%s deaths" % str(len(deaths))
+
+uniq_deaths.sort(lambda x,y: cmp(deaths.count(y),deaths.count(x)) or cmp(x,y))
+freqstring = ''
+numstats = 0
+for death in uniq_deaths:
+ if numstats > 9:
+ break
+ numstats +=1
+ freqstring += "%dx %s(%.2f%%) " % (deaths.count(death), death,
+ 100*deaths.count(death)/float(len(deaths)))
+print("%s for %s. Most frequent: %s" % (deathstring, player, freqstring))
69 commands/deathsin.py
@@ -0,0 +1,69 @@
+#!/usr/bin/python
+
+import sys, re
+from helper import *
+
+lcbranches_abbrev = [string.lower(abbrev) for abbrev in branches_abbrev]
+lclevels_abbrev = [string.lower(level) for level in levels_abbrev]
+
+help("Lists the top ten players to die in a certain place.")
+
+placestring = sys.argv[3].split(' ')[1]
+if ':' in placestring:
+ branch, level = placestring.split(':')
+ level = int(level)
+else:
+ if placestring.isdigit():
+ branch, level = 'D', int(placestring)
+ else:
+ branch, level = placestring, ''
+
+branch = branch.lower()
+branch_aliases = dict([('mines','orc'),('v','vault'),('vaults','vault'),('hall', 'blade'), ('blades', 'blade'), ('tartarus', 'tar'),('gehenna','geh'),('cocytus','coc')])
+if branch in branch_aliases:
+ branch = branch_aliases[branch]
+if branch not in lcbranches_abbrev and branch not in lclevels_abbrev:
+ print "I don't know where that is!"
+ sys.exit()
+games = deaths_in(branch, level)
+if not games:
+ if level:
+ level = ':' + str(level)
+ print("No one has died in %s%s." % (branch, level))
+ sys.exit()
+
+deaths = []
+uniq_deaths = []
+deathcount = 0
+for game in games:
+ if game['ktyp'] in ('quitting', 'leaving', 'winning'): # Quits, escapes, and wins don't count
+ continue
+ deathcount += 1
+
+ death = game['name']
+ deaths.append(death)
+ if death not in uniq_deaths:
+ uniq_deaths.append(death)
+
+if deathcount == 0:
+ print("No deaths here.")
+ sys.exit()
+if len(deaths) == 1: deathstring = "1 death"
+else: deathstring = "%s deaths" % str(len(deaths))
+if branch in lclevels_abbrev:
+ branchstring = games[0]['ltyp']
+else: #branch in lcbranches_abbrev
+ branchstring = games[0]['br']
+
+uniq_deaths.sort(lambda x,y: cmp(deaths.count(y),deaths.count(x)) or cmp(x,y))
+freqstring = ''
+numstats = 0
+if level:
+ level = ':' + str(level)
+for death in uniq_deaths:
+ if numstats > 9:
+ break
+ numstats +=1
+ freqstring += "%dx %s(%.2f%%) " % (deaths.count(death), death,
+ 100*deaths.count(death)/float(len(deaths)))
+print("%s in %s%s. Most frequent: %s" % (deathstring, branchstring, level, freqstring))
16 commands/dump.pl
@@ -0,0 +1,16 @@
+#!/usr/bin/perl
+
+# use strict;
+# use warnings;
+
+my $nick = shift;
+my $baseURL = "http://crawl.akrasiac.org/rawdata/";
+my $localPath = "/var/www/crawl/rawdata/";
+
+my $localDump = $localPath . $nick . "/" . $nick . ".txt";
+
+do 'commands/helper.pl';
+help("Gives an URL to the specified users crawl configuration file.");
+
+if (-e $localDump) { print $baseURL . $nick . "/" . $nick . ".txt"; }
+else { print "User does not exist.\n"; }
59 commands/ftw.pl
@@ -0,0 +1,59 @@
+#!/usr/bin/perl
+
+# use strict;
+# use warnings;
+
+do 'commands/helper.pl';
+
+help("Abbreviates race/role abbreviations. Example usage: !ftw Troll Berserker");
+
+sub race_lookup {
+ my $key = shift;
+ my $i;
+ for($i=0; $i<@races; $i++) {
+ return $races_abbrev[$i] if ($races[$i] eq $key);
+ }
+ return '';
+}
+sub role_lookup {
+ my $key = shift;
+ my $i;
+ for($i=0; $i<@roles; $i++) {
+ return $roles_abbrev[$i] if ($roles[$i] eq $key);
+ }
+ return '';
+}
+
+sub ftw {
+ my ($word1, $word2, $word3, $word4, $remainder) = split(' ');
+ my @keys = ();
+
+ push @keys, $word1 unless $word1 eq '';
+ push @keys, $word2 unless $word2 eq '';
+ push @keys, $word3 unless $word3 eq '';
+ push @keys, $word4 unless $word4 eq '';
+ push @keys, "$word1 $word2" unless $word2 eq '';
+ push @keys, "$word2 $word3" unless $word3 eq '';
+ push @keys, "$word3 $word4" unless $word4 eq '';
+
+ my $race = '??';
+ my $role = '??';
+ my $temp_race = '';
+ my $temp_role = '';
+ foreach(@keys) {
+ $temp_race = race_lookup($_);
+ $temp_role = role_lookup($_);
+ $race = $temp_race if $temp_race ne '';
+ $role = $temp_role if $temp_role ne '';
+ last if (($race ne '??') and ($role ne '??'));
+ }
+ return "$race$role";
+}
+
+# 3rd argument is the entire command line
+$_ = lc($ARGV[2]);
+s/^!ftw\s+(.*)$/$1/;
+
+chomp;
+
+print ftw($_);
55 commands/gamesby.pl
@@ -0,0 +1,55 @@
+#!/usr/bin/perl
+do 'commands/helper.pl';
+
+help("Summarizes a player's crawl.akrasiac.org career.");
+
+my $nick = shift;
+my $games_ref = games_for($nick);
+
+if (@$games_ref == 0)
+{
+ printf "No games for %s.\n", $nick;
+ exit;
+}
+
+my $games = 0;
+my $first;
+my $last;
+my $high;
+my $turns = 0;
+my $time = 0;
+my $total = 0;
+my $won = 0;
+
+foreach my $game_ref (@$games_ref)
+{
+ ++$games;
+ if ($games == 1)
+ {
+ $first = $game_ref->{start};
+ $high = $game_ref->{sc};
+ $nick = $game_ref->{name};
+ }
+ $last = $game_ref->{end};
+ $high = $game_ref->{sc} if $game_ref->{sc} > $high;
+ $total += $game_ref->{sc};
+ $turns += $game_ref->{turn};
+ $time += $game_ref->{dur};
+ ++$won if $game_ref->{ktyp} eq 'winning';
+}
+
+$first =~ s/^(\d\d\d\d)(\d\d)(\d\d).*/sprintf "%04d%02d%02d", $1, $2+1, $3/e;
+$last =~ s/^(\d\d\d\d)(\d\d)(\d\d).*/sprintf "%04d%02d%02d", $1, $2+1, $3/e;
+
+printf "%s has played %d game%s, between %s and %s, won %s, high score %d, total score %d, total turns %d, total time %s.\n",
+ $nick,
+ $games,
+ $games == 1 ? "" : "s",
+ $first,
+ $last,
+ once($won),
+ $high,
+ $total,
+ $turns,
+ serialize_time($time);
+
32 commands/ghostkills.py
@@ -0,0 +1,32 @@
+#!/usr/bin/python
+
+import sys, re
+from helper import *
+
+help("Lists the top ten kills for a player's ghost.")
+
+ghost = sys.argv[1]
+kills = kills_by("%ss ghost" % ghost)
+if len(kills) == 0:
+ print "That ghost has never killed anyone!"
+ sys.exit()
+
+killed = []
+uniq_killed = []
+for kill in kills:
+ killed.append(kill['name'])
+ if kill['name'] not in uniq_killed:
+ uniq_killed.append(kill['name'])
+if len(kills) == 1: killstring = "1 kill"
+else: killstring = str(len(kills)) + " kills"
+
+uniq_killed.sort(lambda x,y: cmp(killed.count(y),killed.count(x)) or cmp(x,y))
+freqstring = ''
+numstats = 0
+for kill in uniq_killed:
+ if numstats > 9:
+ break
+ numstats +=1
+ freqstring += "%dx %s(%.2f%%) " % (killed.count(kill), kill,
+ 100*killed.count(kill)/float(len(kills)))
+print("%s by %s's ghost. Most frequent: %s" % (killstring, ghost, freqstring))
29 commands/help.pl
@@ -0,0 +1,29 @@
+#!/usr/bin/perl
+do 'commands/helper.pl';
+
+help("Displays help on a command. For a list of commands, see !cmdinfo.");
+
+# prep the new @ARGV for the help command
+$ARGV[3] = 1;
+$ARGV[2] =~ s/^!help\s+//i;
+$ARGV[2] = "!$ARGV[2]" unless substr($ARGV[2], 0, 1) eq '!';
+
+# which command are we going for?
+$ARGV[2] =~ /(\S+)/;
+my $requested = defined($1) ? $1 : "!help";
+
+# find the command in commands.txt
+open my $handle, '<', 'commands/commands.txt' or print "Unable to open commands/commands.txt: $!" and exit;
+while (<$handle>)
+{
+ my ($command, $file) = /^(\S+)\s+(.+)$/;
+ $file = "commands/$file";
+ if ($requested eq lc($command))
+ {
+ print "$command: ";
+ exec $file, @ARGV;
+ }
+}
+
+print "Unable to find help on $requested.";
+
256 commands/helper.pl
@@ -0,0 +1,256 @@
+#!/usr/bin/perl
+
+our $logfile = '/var/www/crawl/allgames.txt';
+
+our @field_names = qw/v lv name uid race cls xl sk sklev title place br lvl
+ ltyp hp mhp mmhp str int dex start dur turn sc ktyp
+ killer kaux end tmsg vmsg god piety pen char nrune urune/;
+
+our @roles_abbrev = qw/Fi Wz Pr Th Gl Ne Pa As Be Hu Cj En FE IE Su AE EE Cr DK
+ VM CK Tm He XX Re St Mo Wr Wn/;
+our @races_abbrev = qw/XX Hu El HE GE DE SE HD MD Ha HO Ko Mu Na Gn Og Tr OM Dr
+ Dr Dr Dr Dr Dr Dr Dr Dr Dr Dr Dr Ce DG Sp Mi DS Gh Ke Mf/;
+
+our @roles =
+(
+ 'fighter',
+ 'wizard',
+ 'priest',
+ 'thief',
+ 'gladiator',
+ 'necromancer',
+ 'paladin',
+ 'assassin',
+ 'berserker',
+ 'hunter',
+ 'conjurer',
+ 'enchanter',
+ 'fire elementalist',
+ 'ice elementalist',
+ 'summoner',
+ 'air elementalist',
+ 'earth elementalist',
+ 'crusader',
+ 'death knight',
+ 'venom mage',
+ 'chaos knight',
+ 'transmuter',
+ 'healer',
+ 'quitter',
+ 'reaver',
+ 'stalker',
+ 'monk',
+ 'warper',
+ 'wanderer',
+);
+
+our @races =
+(
+ 'null',
+ 'human',
+ 'elf',
+ 'high elf',
+ 'grey elf',
+ 'deep elf',
+ 'sludge elf',
+ 'hill dwarf',
+ 'mountain dwarf',
+ 'halfling',
+ 'hill orc',
+ 'kobold',
+ 'mummy',
+ 'naga',
+ 'gnome',
+ 'ogre',
+ 'troll',
+ 'ogre mage',
+ 'red draconian',
+ 'white draconian',
+ 'green draconian',
+ 'golden draconian',
+ 'grey draconian',
+ 'black draconian',
+ 'purple draconian',
+ 'mottled draconian',
+ 'pale draconian',
+ 'unk0 draconian',
+ 'unk1 draconian',
+ 'base draconian',
+ 'centaur',
+ 'demigod',
+ 'spriggan',
+ 'minotaur',
+ 'demonspawn',
+ 'ghoul',
+ 'kenku',
+ 'merfolk',
+);
+
+our @death_method =
+(
+ 'killed by a monster',
+ 'succumbed to poison',
+ 'engulfed by something',
+ 'killed by a beam',
+ 'stepped on Death\'s door', #deprecated apparently
+ 'took a swim in molten lava',
+ 'drowned',
+ 'forgot to breathe',
+ 'collapsed under their own weight',
+ 'slipped on a banana peel',
+ 'killed by a trap',
+ 'got out of the dungeon',
+ 'escaped with the Orb',
+ 'quit the game',
+ 'was drained of all life',
+ 'starved to death',
+ 'froze to death',
+ 'burnt to a crisp',
+ 'killed by wild magic',
+ 'killed for Xom\'s enjoyment',
+ 'killed by a statue',
+ 'rotted away',
+ 'killed themself with bad targetting',
+ 'killed by an exploding spore',
+ 'smote by The Shining One',
+ 'turned to stone',
+ '<deprecated>',
+ 'died somehow',
+ 'fell down a flight of stairs',
+ 'splashed by acid',
+ 'asphyxiated',
+ 'melted into a puddle',
+ 'bled to death',
+);
+
+sub ntimes
+{
+ my $times = shift;
+
+ $times == 1 ? "once" :
+ $times == 2 ? "twice" :
+ $times == 3 ? "thrice" :
+ "$times times"
+}
+
+sub once
+{
+ my $times = shift;
+
+ $times == 1 ? "once" :
+ $times == 2 ? "twice" :
+ $times == 3 ? "thrice" :
+ $times
+}
+
+sub demunge_logline
+{
+ my $line = shift;
+ my %game;
+
+ $line = substr($line, 1, -1); #remove leading and trailing :
+
+ @game{@field_names} = split /:/, $line;
+
+ return \%game;
+}
+
+sub demunge_xlogline
+{
+ my $line = shift;
+ return {} if $line eq '';
+ my %game;
+
+ chomp $line;
+ die "Unable to handle internal newlines." if $line =~ y/\n//;
+ $line =~ s/::/\n\n/g;
+
+ while ($line =~ /\G(\w+)=([^:]*)(?::(?=[^:])|$)/cg)
+ {
+ my ($key, $value) = ($1, $2);
+ $value =~ s/\n\n/:/g;
+ $game{$key} = $value;
+ }
+
+ if (!defined(pos($line)) || pos($line) != length($line))
+ {
+ my $pos = defined(pos($line)) ? "Problem started at position " . pos($line) . "." : "Regex doesn't match.";
+ die "Unable to demunge_xlogline($line).\n$pos";
+ }
+
+ return \%game;
+}
+
+sub munge_game # DOES THIS WORK? XXX I haven't tested it, and its not in a script yet, so who cares? :)
+{
+ my $game_ref = shift;
+ return join ':', map {my ($k, $v) = ($_, $game{$_}); for ($k, $v) {s/:/::/g } "$k=$v"} keys %game;
+ #return ':' . join(':', map {$game_ref->{$_}} @field_names) . ':';
+}
+
+sub games_for
+{
+ my $nick = lc(shift);
+ my $regex = qr/:name=$nick:/i;
+ my @games;
+
+ open my $handle, '<', $logfile or warn "Unable to open $logfile: $!";
+ while (<$handle>)
+ {
+ chomp;
+ if ($_ =~ $regex)
+ {
+ my $game_ref = demunge_xlogline($_);
+ push @games, $game_ref if lc($game_ref->{name}) eq $nick;
+ }
+ }
+
+ return \@games;
+}
+
+sub serialize_time
+{
+ my $seconds = int shift;
+ my $long = shift;
+
+ if (not $long)
+ {
+ my $hours = int($seconds/3600);
+ $seconds %= 3600;
+ my $minutes = int($seconds/60);
+ $seconds %= 60;
+
+ return sprintf "%d:%02d:%02d", $hours, $minutes, $seconds;
+ }
+
+ my $minutes = int($seconds / 60);
+ $seconds %= 60;
+ my $hours = int($minutes / 60);
+ $minutes %= 60;
+ my $days = int($hours / 24);
+ $hours %= 24;
+ my $weeks = int($days / 7);
+ $days %= 7;
+ my $years = int($weeks / 52);
+ $weeks %= 52;
+
+ my @fields;
+ push @fields, "about ${years}y" if $years;
+ push @fields, "${weeks}w" if $weeks;
+ push @fields, "${days}d" if $days;
+ push @fields, "${hours}h" if $hours;
+ push @fields, "${minutes}m" if $minutes;
+ push @fields, "${seconds}s" if $seconds;
+
+ return join ' ', @fields if @fields;
+ return '0s';
+}
+
+sub help
+{
+ if ($ARGV[3])
+ {
+ print join('', @_) . "\n";
+ exit;
+ }
+}
435 commands/helper.py
@@ -0,0 +1,435 @@
+#!/usr/bin/python
+import string, re, os, sys
+import os.path
+from glob import glob
+
+logfile = '/var/www/crawl/allgames.txt'
+
+www_rawdatapath = '/var/www/crawl/rawdata/'
+
+xkeychars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
+
+details_names = ['v', 'lv', 'name', 'uid', 'race', 'cls', 'xl', 'sk', 'sklev',
+ 'title', 'place', 'br', 'lvl', 'ltyp', 'hp', 'mhp', 'mmhp',
+ 'str', 'int', 'dex', 'start', 'dur', 'turn', 'sc', 'ktyp',
+ 'killer', 'kaux', 'end', 'tmsg', 'vmsg', 'god', 'piety',
+ 'pen', 'char', 'nrune', 'urune']
+
+races = ['Null', 'Human', 'Elf', 'High Elf', 'Grey Elf', 'Deep Elf',
+ 'Sludge Elf', 'Hill Dwarf', 'Mountain Dwarf', 'Halfling',
+ 'Hill Orc', 'Kobold', 'Mummy', 'Naga', 'Gnome', 'Ogre', 'Troll',
+ 'Ogre Mage', 'Red Draconian', 'White Draconian',
+ 'Green Draconian', 'Golden Draconian', 'Grey Draconian',
+ 'Black Draconian', 'Purple Draconian', 'Mottled Draconian',
+ 'Pale Draconian', 'Unk0 Draconian', 'Unk1 Draconian',
+ 'Base Draconian', 'Centaur', 'Demigod', 'Spriggan', 'Minotaur',
+ 'Demonspawn', 'Ghoul', 'Kenku', 'Merfolk']
+
+races_abbrev = ["XX", "Hu", "El", "HE", "GE", "DE", "SE", "HD", "MD", "Ha",
+ "HO", "Ko", "Mu", "Na", "Gn", "Og", "Tr", "OM", "Dr", "Dr",
+ "Dr", "Dr", "Dr", "Dr", "Dr", "Dr", "Dr", "Dr", "Dr", "Dr",
+ "Ce", "DG", "Sp", "Mi", "DS", "Gh", "Ke", "Mf"]
+
+god_names = ['None','Zin','The Shining One','Kikubaaqudgha','Yredelemnul','Xom',
+ 'Vehumet','Okawaru','Makhleb','Sif Muna','Trog','Nemelex Xobeh',
+ 'Elyvilon']
+god_names_abbrev = ['None','Zin','TSO','Kiku','Yred','Xom','Vehumet','Okie',
+ 'Makhleb','Sif','Trog','Nemelex','Ely']
+
+roles = ["Fighter","Wizard", "Priest", "Thief", "Gladiator", "Necromancer",
+ "Paladin", "Assassin", "Berserker", "Hunter", "Conjurer", "Enchanter",
+ "Fire Elementalist", "Ice Elementalist", "Summoner",
+ "Air Elementalist", "Earth Elementalist", "Crusader", "Death Knight",
+ "Venom Mage", "Chaos Knight", "Transmuter", "Healer", "Quitter",
+ "Reaver", "Stalker", "Monk", "Warper", "Wanderer"]
+roles_abbrev = ["Fi", "Wz", "Pr", "Th", "Gl", "Ne", "Pa", "As", "Be", "Hu",
+ "Cj", "En", "FE", "IE", "Su", "AE", "EE", "Cr", "DK", "VM",
+ "CK", "Tm", "He", "XX", "Re", "St", "Mo", "Wr", "Wn"]
+
+death_methods = [
+ 'killed by a monster',
+ 'succumbed to poison',
+ 'engulfed by something',
+ 'killed by a beam',
+ 'stepped on Death\'s door', #deprecated apparently
+ 'took a swim in molten lava',
+ 'drowned',
+ 'forgot to breathe',
+ 'collapsed under their own weight',
+ 'slipped on a banana peel',
+ 'killed by a trap',
+ 'got out of the dungeon',
+ 'escaped with the Orb',
+ 'quit the game',
+ 'was drained of all life',
+ 'starved to death',
+ 'froze to death',
+ 'burnt to a crisp',
+ 'killed by wild magic',
+ 'killed for Xom\'s enjoyment',
+ 'killed by a statue',
+ 'rotted away',
+ 'killed themself with bad targetting',
+ 'killed by an exploding spore',
+ 'smote by The Shining One',
+ 'turned to stone',
+ '<deprecated>',
+ 'died somehow',
+ 'fell down a flight of stairs',
+ 'splashed by acid',
+ 'asphyxiated',
+ 'melted into a puddle',
+ 'bled to death',
+]
+
+
+skills = [("Skirmisher", "Grunt", "Veteran", "Warrior", "Slayer"),
+ ("Stabber", "Cutter", "Knifefighter", "Eviscerator", "Blademaster"),
+ ("Slasher", "Slicer", "Fencer", "Swordfighter", "Swordmaster"),
+ ("Great sworder - you deprecated bastard!"),
+ ("Chopper", "Cleaver", "Hacker", "Severer", "Axe Maniac"),
+ ("Basher", "Cudgeler", "Shatterer", "Bludgeoner", "Skullcrusher"),
+ ("Spear-Bearer", "Pike-Bearer", "Phalangite", "Lancer", "Halberdier"),
+ ("Twirler", "Cruncher", "Smasher", "Stickfighter", "Skullbreaker"),
+ ("Vandal", "Slinger", "Whirler", "Crazy", "Very Crazy"),
+ ("Shooter", "Yeoman", "Archer", "Merry", "Merry"),
+ ("Shooter", "Sharpshooter", "Archer", "Ballista", "Ballista"),
+ ("Dart Thrower", "Hurler", "Hurler, First Class", "Darts Champion", "Universal Darts Champion"),
+ ("Chucker", "Thrower", "Deadly Accurate", "Hawkeye", "Sniper"),
+ ("Covered", "Protected", "Tortoise", "Impregnable", "Invulnerable"),
+ ("Ducker", "Dodger", "Nimble", "Spry", "Acrobat"),
+ ("Footpad", "Sneak", "Covert", "Unseen", "Imperceptible"),
+ ("Miscreant", "Blackguard", "Backstabber", "Cutthroat", "Politician"),
+ ("Shield-Bearer", "Blocker", "Barricade", "Peltast", "Hoplite"),
+ ("Disarmer", "Trapper", "Architect", "Engineer", "Dungeon Master"),
+ ("Ruffian", "Grappler", "Brawler", "Wrestler", "Boxer"),
+ ("none"),
+ ("none"),
+ ("none"),
+ ("none"),
+ ("none"),
+ ("Magician", "Thaumaturge", "Eclecticist", "Sorcerer", "Archmage"),
+ ("Ruinous", "Conjurer", "Destroyer", "Devastator", "Annihilator"),
+ ("Charm-Maker", "Infuser", "Bewitcher", "Enchanter", "Spellbinder"),
+ ("Caller", "Summoner", "Convoker", "Demonologist", "Hellbinder"),
+ ("Grave Robber", "Reanimator", "Necromancer", "Thanatomancer", "Character of Death"),
+ ("Jumper", "Blinker", "Shifter", "Portalist", "Plane Walker"),
+ ("Changer", "Transmogrifier", "Transformer", "Alchemist", "Transmuter"),
+ ("Seer", "Soothsayer", "Diviner", "Augur", "Oracle"),
+ ("Firebug", "Arsonist", "Scorcher", "Pyromancer", "Infernalist"),
+ ("Chiller", "Frost Mage", "Ice Mage", "Cryomancer", "Englaciator"),
+ ("Wind Mage", "Cloud Mage", "Air Mage", "Sky Mage", "Storm Mage"),
+ ("Digger", "Geomancer", "Earth Mage", "Metallomancer", "Petrodigitator"),
+ ("Stinger", "Tainter", "Polluter", "Poisoner", "Envenomancer"),
+ ("Believer", "Servant", "Worldly Agent", "Theurge", "Avatar"),
+ ("Charlatan", "Prestidigitator", "Fetichist", "Evocator", "Talismancer"),
+ ("none"),
+ ("none"),
+ ("none"),
+ ("none"),
+ ("none"),
+ ("none"),
+ ("none"),
+ ("none"),
+ ("none"),
+ ("none"),
+ ("none")
+]
+
+level_types = [("in the main dungeon"),
+ ("in a labyrinth"),
+ ("in the Abyss"),
+ ("in Pandemonium")
+]
+
+levels_abbrev = ["D", "Lab","Abyss","Pan"]
+
+branches = [("of the main dungeon"),
+ ("of Dis"),
+ ("of Gehenna"),
+ ("in the Vestibule of Hell"),
+ ("of Cocytus"),
+ ("of Tartarus"),
+ ("of the Inferno"),
+ ("of the Pit"),
+ ("null"),
+ ("null"),
+ ("of the Mines"),
+ ("of the Hive"),
+ ("of the Lair"),
+ ("of the Slime Pits"),
+ ("of the Vaults"),
+ ("of the Crypt"),
+ ("in the Hall"),
+ ("of Zot's Hall"),
+ ("in the Temple"),
+ ("of the Snake Pit"),
+ ("of the Elf Hall"),
+ ("of the Tomb"),
+ ("of the Swamp")
+]
+branches_abbrev = ["D","Dis","Geh","Hell","Coc","Tar",
+ "Inferno","Pit","null","null","Orc","Hive","Lair","Slime",
+ "Vault","Crypt","Blade","Zot","Temple","Snake","Elf","Tomb",
+ "Swamp"]
+lcbranches_abbrev = [string.lower(abbrev) for abbrev in branches_abbrev]
+lclevels_abbrev = [string.lower(level) for level in levels_abbrev]
+
+def plural(int):
+ if int > 1:
+ return 's'
+ else:
+ return ''
+def ntimes(times):
+ if times == 1: return 'once'
+ elif times == 2: return 'twice'
+ elif times == 3: return 'thrice'
+ else: return str(times) + ' times'
+
+def canonicalize_nick(nick):
+ where_dirs = [ f for f in glob(www_rawdatapath + '/*')
+ if os.path.isdir(f) ]
+ lnickw = nick.lower()
+ match = [ x for x in where_dirs if x.lower().endswith('/' + lnickw) ]
+ return len(match) > 0 and extract_nick_from_wherepath(match[0]) or None
+
+def extract_nick_from_wherepath(wherepath):
+ windex = wherepath.rfind('/')
+ if windex == -1:
+ return None
+ where = wherepath[windex + 1 : ]
+ if os.path.isfile(wherepath + '/' + where + '.where'):
+ return where
+
+def strip_extension(name):
+ dotind = name.rfind('.')
+ return dotind == -1 and name or name[0 : dotind]
+
+def once(times):
+ if times == 1: return 'once'
+ elif times == 2: return 'twice'
+ elif times == 3: return 'thrice'
+ else: return str(times)
+
+def replace(string, toreplace, replacewith):
+ while string.count(toreplace) > 0:
+ string = string[:string.index(toreplace)] + replacewith + string[string.index(toreplace) + len(toreplace):]
+ return string
+
+def parse_argstring(argstring): # Takes a string of everything after !cmd.
+ arglist = []
+ if not argstring:
+ return [], {}
+ if argstring.count('"') % 2:
+ print("Missing a quote mark?")
+ sys.exit()
+ if argstring.count('"') == 0:
+ arglist = argstring.split(' ')
+ else:
+ i = 0
+ quotelist = argstring.split('"')
+ for string in quotelist:
+ if not i % 2 and string: # if this is outside of quotes and exists
+ list = string.split(' ')
+ if not list[0]:
+ list = list[1:]
+ # join the quoted material with the string before it
+ if len(quotelist) > i + 1:
+ list[-1] = ''.join([list[-1], quotelist[i + 1]])
+ arglist.extend(list)
+ i += 1
+ # Now we have the One True Arglist. parse using parse_argslist.
+ return parse_argslist(arglist)
+
+def parse_argslist(argslist):
+ # Takes a list of everything after the !command. (sys.argv[3].split(' ')[1:]
+ # Returns args, opts
+ args = []
+ opts = []
+ optlist = []
+ if len(argslist) == 1 and len(argslist[0]) == 0:
+ return args, dict(opts)
+ for arg in argslist:
+ if arg[0] == '-':
+ optlist.append(arg[1:])
+ else:
+ args.append(string.lower(arg))
+ for opt in optlist:
+ opt = string.lower(opt)
+ if '=' in opt and opt.split('=')[0] and opt.split('=')[1]:
+ opts.append(tuple(opt.split('=')[:2]))
+ else:
+ opts.append((opt, ''))
+ return args, dict(opts)
+
+def munge_game(game):
+ logline = ''
+ for detail in details_names:
+ try:
+ game[detail] # try to catch a keyerror early
+ logline += detail + '=' + str(game[detail]).replace(':', '::') + ':'
+ except KeyError:
+ continue
+
+ logline = logline[:-1]
+ return logline
+
+def demunge_logline(logline):
+ details = dict(zip(details_names, logline.split(':')[1:-1]))
+ for key, detail in details.iteritems():
+ try:
+ details[key] = int(detail)
+ except:
+ pass
+ return details
+
+def demunge_xlogline(logline):
+ if not logline:
+ return {}
+ if logline[0] == ':' or (logline[-1] == ':' and not logline[-2] == ':'):
+ sys.exit(1)
+ if '\n' in logline:
+ sys.exit(1)
+ logline = replace(logline, "::", "\n")
+ details = dict([(item[:item.index('=')], item[item.index('=') + 1:]) for item in logline.split(':')])
+ for key in details:
+ for char in key:
+ if char not in xkeychars:
+ sys.exit(1)
+ details[key] = replace(details[key], "\n", ":")
+ return details
+
+def games_for(nick):
+ nick = string.lower(nick)
+ filter = re.compile('[^a-z0-9]')
+ nick = filter.sub('',nick)
+ potentialgames = os.popen("grep -i 'name=" + nick + ":' " + logfile).readlines()
+ games = []
+ i = 1
+ for potentialgame in potentialgames:
+ game = demunge_xlogline(potentialgame[:-1])
+ if string.lower(game['name']) == nick:
+ game['_num'] = i
+ games.append(game)
+ i += 1
+ return games
+
+def games_such_that(list): # list = [(field, value),(field2, value2)]
+ filter = re.compile('[^a-z0-9]')
+ namefilter = re.compile('[0-9]+$')
+ grepstring = ''
+ for item in list:
+ field, value = item
+ if field == 'place':
+ value = value.replace(':','::')
+ else:
+ value = string.lower(str(value))
+ value = filter.sub('',value)
+ if field == 'name':
+ value = namefilter.sub('',value)
+ if field == 'killer':
+ grepstring += " | grep -Ei ':killer=(an? )?" + value + ":'"
+ else:
+ grepstring += " | grep -i ':" + field + "=" + value + ":'"
+ potentialgames = os.popen("cat " + logfile + grepstring).readlines()
+
+ games = []
+ i = 1
+ for potentialgame in potentialgames:
+
+ game = demunge_xlogline(potentialgame[:-1])
+ game['_num'] = i
+ games.append(game)
+ i += 1
+ return games
+
+def allgames():
+ games = []
+ i = 1
+ for game in os.popen("cat " + logfile).readlines():
+ game = demunge_xlogline(game)
+ game['_num'] = i
+ games.append(game)
+ i += 1
+ return games
+
+
+
+def deathstring(game):
+ deathstring = ''
+ if game['death_type']:
+ deathstring = death_methods[game['death_type']]
+ if game['death_source_name']:
+ deathstring += " - %s" % game['death_source_name']
+ else:
+ deathstring = "killed by %s" % game['death_source_name']
+ if game['auxkilldata']:
+ deathstring += " (%s)" % game['auxkilldata']
+ return deathstring
+
+def deaths_in(branch, level):
+ branch = string.lower(branch)
+ filter = re.compile('[^a-z0-9 -]')
+ branch = filter.sub('',branch)
+
+ if branch in lclevels_abbrev and branch != 'd':
+ level_type = branch
+ postbranchstr = 'ltyp=' + level_type + ':'
+ prebranchstr = ''
+ else:
+ level_type = ''
+ if branch not in lcbranches_abbrev:
+ print("Unknown branch. Borkage! Borkage, will robinson!")
+ sys.exit()
+ prebranchstr = ':place=' + str(branch) + ':'
+ postbranchstr = ''
+
+
+ if not level:
+ levelstr = ''
+ else:
+ levelstr = 'lvl=' + str(level) + ':'
+ potentialgames = os.popen("grep -i '" + prebranchstr + levelstr + postbranchstr + "' " + logfile).readlines()
+ games = []
+ i = 0
+ for potentialgame in potentialgames:
+ game = demunge_xlogline(potentialgame[:-1])
+ game['_num'] = i
+ games.append(game)
+ i += 1
+ return games
+
+def kills_by(monster):
+ if monster in ('pois', 'starvation', 'stupidity', 'water', 'burning',
+ 'draining', 'weakness', 'cloud', 'lava', 'clumsiness',
+ 'trap', 'freezing', 'wild_magic', 'statue', 'rotting',
+ 'targeting', 'spore', 'falling_down_stairs', 'petrification',
+ 'acid', 'curare', 'melting', 'bleeding', 'statue', 'xom',
+ 'tso_smiting', 'deaths_door'):
+ potentialgames = os.popen("grep -i ':ktyp=" + monster + ":' " + logfile).readlines()
+ else:
+ monster = string.lower(monster)
+ filter = re.compile('[^a-z0-9 -]')
+ monster = filter.sub('',monster)
+ if "s ghost" in monster:
+ monster = monster[:-7] + "'" + monster[-7:]
+ potentialgames = os.popen('grep -i ":killer=' + monster + ':" ' + logfile).readlines()
+ else:
+ potentialgames = os.popen("grep -Ei ':killer=(an? )?" + monster + ":' " + logfile).readlines()
+
+ games = []
+ i = 0
+ for potentialgame in potentialgames:
+ game = demunge_xlogline(potentialgame[:-1])
+ game['_num'] = i
+ games.append(game)
+ i += 1
+ return games
+
+def serialize_time(s):
+ return '%d:%02d:%02d' % (s/60/60%60, s/60%60, s%60)
+
+def help(helpstring):
+ if sys.argv[4]:
+ print helpstring
+ sys.exit()
308 commands/helper.rb
@@ -0,0 +1,308 @@
+#!/usr/bin/ruby
+
+# fields end in S if they're strings, I if integral
+$field_names = %w<vS lvS nameS uidI raceS clsS xlI skS sklevI titleS placeS brS lvlI ltypS hpI mhpI mmhpI strI intI dexI startS durI turnI scI ktypS killerS kauxS endS tmsgS vmsgS godS pietyI penI charS nruneI uruneI>
+XKEYCHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
+
+$field_types = { }
+
+FIELD_SUBST = { 'species' => 'race', 'role' => 'cls', 'class' => 'cls' }
+MAX_GREP_LINES = 3000
+
+# Directory containing player directories that contain morgues.
+DGL_MORGUE_DIR = '/var/www/crawl/rawdata'
+
+# HTTP URL corresponding to DGL_MORGUE_DIR, with no trailing slash.
+DGL_MORGUE_URL = 'http://crawl.akrasiac.org/rawdata'
+
+$field_names.each do |field|
+ if field =~ /(\w+)(\w)/
+ $field_types[$1] = $2
+ end
+end
+
+def munge_game(game)
+ game.to_a.map { |x,y| "#{x}=#{y.to_s.gsub(':', '::')}" }.join(':')
+end
+
+def demunge_logline(logline)
+ fields = logline.split(':')
+ fields.shift
+ fields.pop
+
+ h = Hash.new()
+ $field_names.each do |key|
+ type = key[-1,1]
+ if type == "I"
+ h[key.chop] = fields.shift.to_i
+ else
+ h[key.chop] = fields.shift
+ end
+ end
+ h
+end
+
+def demunge_xlogline(logline) # TODO XXX XPLODE OMG
+ h = Hash.new()
+ if logline == ''
+ return hash
+ elsif logline[0] == ?: or (logline[-1] == ?: and logline[-2] != ?:)
+ raise "Evil logline: #{logline}"
+ end
+ logline.gsub!(/::/, "\n")
+ pairs = logline.split(':')
+ pairs.each do |pair|
+ key, value = pair.split('=', 2)
+ value.gsub!(/\n/, ':')
+ if $field_types[key] == 'I'
+ value = value.to_i
+ end
+ h[key] = value
+ end
+ h
+end
+
+def split_selector_predicates(sel)
+ pre, post = Array.new(), Array.new()
+ sel.each do |key, op, val|
+ raise "Unknown selector: #{key}" unless $field_types.has_key?(key)
+ raise "Bad selector #{key}#{op}#{val}" if key.empty? or val.empty?
+ val = val.downcase.sub(':', '::')
+ val = val.to_i if $field_types[key] == 'I'
+ case op
+ when '='
+ pre << Proc.new { |line| line.index("#{key}=#{val}") }
+ when '=='
+ pre << Proc.new { |line| line.index("#{key}=#{val}:") }
+ when '!='
+ pre << Proc.new { |line| not line.index("#{key}=#{val}") }
+ when '!=='
+ pre << Proc.new { |line| not line.index("#{key}=#{val}:") }
+ when '<'
+ post << Proc.new { |g| (g[key] || 0) < val }
+ when '>'
+ post << Proc.new { |g| (g[key] || 0) > val }
+ when '<='
+ post << Proc.new { |g| (g[key] || 0) <= val }
+ when '>='
+ post << Proc.new { |g| (g[key] || 0) >= val }
+ when '=~'
+ post << Proc.new { |g| (g[key] || '').downcase =~ /#{val.downcase}/ }
+ when '!~'
+ post << Proc.new { |g| (g[key] || '').downcase !~ /#{val.downcase}/ }
+ else
+ raise "Bad operator in #{keyval}"
+ end
+ end
+ [ pre, post ]
+end
+
+def key_scrub(s)
+ s.gsub('[^\w]', '').downcase
+end
+
+def val_scrub(s)
+ s.gsub(':', '::').downcase
+end
+
+def get_predicates(nick, sel)
+ arr = []
+ if not nick.empty? and nick != '.' and nick != '*'
+ arr << Proc.new { |line| line.index("name=#{nick}:") }
+ end
+ arr + sel
+end
+
+def do_grep(logfile, nick, hsel)
+ matchlist = get_predicates(nick, hsel)
+ raise "No selectors, will not slurp whole logfile." if matchlist.empty?
+ lines = []
+ File.open(logfile) do |logfile|
+ count = 0
+ logfile.each_line do |line|
+ testline = line.downcase
+ if not matchlist.find { |p| not p[testline] }
+ lines << line.chomp
+ count += 1
+ raise "Too many matching games." if count > MAX_GREP_LINES
+ end
+ end
+ end
+ lines
+end
+
+def select_games(nick, hsel)
+ # No more direct grepping.
+ ####games = %x{grep -i ':name=#{nick}:' /var/www/crawl/allgames.txt} .
+ do_grep('/var/www/crawl/allgames.txt', nick, hsel).
+ map {|line| demunge_xlogline(line) }
+end
+
+def games_for(nick, selectors)
+ # this is partly to cleanse for safe inclusion in a shell command
+ # but also partly to find nicks easier
+ # the removal of numbers at the end is because Crawl nicks can't end with #s
+ nick = nick.downcase.gsub(/[^a-z0-9]/, '') #.sub(/\d+$/, '')
+
+ hsel, nsel = split_selector_predicates(selectors)
+ games = select_games(nick, hsel)
+ nsel.empty? ? games : games.delete_if { |g| nsel.find { |p| not p[g] } }
+end
+
+def split_selectors(selectors, regex=/^-(\w+)(<=?|>=?|[!=]~|!==?|==?)(.*)/)
+ sels = [ ]
+ selectors.split(' ').each do |keyval|
+ if keyval =~ regex
+ key, op, val = $1, $2, $3.tr('_', ' ')
+ key = FIELD_SUBST[key] if FIELD_SUBST.has_key?(key)
+ sels << [key, op, val]
+ else
+ raise "Bad selector #{keyval} - must be of the form -key=val"
+ end
+ end
+ sels
+end
+
+def parse_game_select_args(args)
+ words = args[1].split(' ')[ 1..-1 ]
+ return [ args[0], -1, '' ] if !words || words.empty?
+
+ if words[0] =~ /^[a-zA-Z]\w+$/ or words[0] == '*' or words[0] == '.'
+ nick = words.slice!(0)
+ nick = '*' if nick == '.'
+ end
+
+ if not words.empty? and words[0] =~ /^[+\-]?\d+$/
+ num = words.slice!(0).to_i
+ end
+
+ rest = words.join(' ') unless words.empty?
+
+ nick ||= args[0]
+ num ||= -1
+ rest ||= ''
+ [ nick, num, rest ]
+end
+
+def get_game_select_sorts(preds)
+ sorts = []
+ preds.each do |key, op, val|
+ if key == 'max' then
+ raise "No field named #{val}" unless $field_types[val]
+ # Sort high values to end
+ sorts << Proc.new { |a,b| a[val] <=> b[val] }
+ elsif key == 'min' then
+ raise "No field named #{val}" unless $field_types[val]
+ sorts << Proc.new { |a,b| b[val] <=> a[val] }
+ end
+ end
+ preds.delete_if { |k,o,v| k == 'max' or k == 'min' }
+ sorts
+end
+
+def sort_games(a, b, sorts)
+ sorts.each do |p|
+ val = p[a, b]
+ return val unless val == 0
+ end
+ 0
+end
+
+def get_named_game(args)
+ nick, num, selectors = parse_game_select_args(args)
+ preds = split_selectors(selectors)
+ sorts = get_game_select_sorts(preds)
+ games = games_for(nick, preds)
+ games.sort! { |a,b| sort_games(a, b, sorts) }
+
+ if games.empty?
+ raise(
+ sprintf("No games for #{nick}%s.",
+ selectors.empty? ? "" : " (#{selectors})"))
+ end
+
+ num = -1 if num == 0
+ index = num < 0 ? games.size + num : num - 1
+ raise "Index out of range: #{num}" if index < 0 or index >= games.size
+
+ [ index + 1, games[index] ]
+end
+
+def morgue_assemble_filename(dir, e, time, ext)
+ dir + '/' + e["name"] + '/morgue-' + e["name"] + '-' + time + ext
+end
+
+def binary_search(arr, what)
+ size = arr.size
+
+ if size == 1
+ return what < arr[0] ? arr[0] : nil
+ end
+
+ s = 0
+ e = size
+
+ while e - s > 1
+ pivot = (s + e) / 2
+ if arr[pivot] == what
+ return arr[pivot]
+ elsif arr[pivot] < what
+ s = pivot
+ else
+ e = pivot
+ end
+ end
+
+ e < size ? arr[e] : nil
+end
+
+def game_morgues(name)
+ Dir[ DGL_MORGUE_DIR + '/' + name + '/' + 'morgue-*.txt' ].sort
+end
+
+def find_game_morgue(e)
+ timestamp = e["end"].dup
+ timestamp.sub!(/(\d{4})(\d{2})(\d{2})/) do |m|
+ "#$1#{sprintf('%02d', $2.to_i + 1)}#$3-"
+ end
+ timestamp.sub!(/[DS]$/, "")
+
+ fulltime = timestamp
+
+ # Look for full timestamp
+ morgue = morgue_assemble_filename(DGL_MORGUE_DIR, e, fulltime, '.txt')
+ if File.exist?(morgue)
+ return morgue_assemble_filename(DGL_MORGUE_URL, e, fulltime, '.txt')
+ end
+
+ parttime = fulltime.sub(/\d{2}$/, '')
+ morgue = morgue_assemble_filename(DGL_MORGUE_DIR, e, parttime, '.txt')
+ if File.exist?(morgue)
+ return morgue_assemble_filename(DGL_MORGUE_URL, e, parttime, '.txt')
+ end
+
+ # We're in El Suck territory. Scan the directory listing.
+ morgue_list = game_morgues(e["name"])
+
+ # morgues are sorted. The morgue date should be greater than the
+ # full timestamp.
+
+ found = binary_search(morgue_list, morgue)
+ if found then
+ found.sub!(/.*morgue-\w+-(.*)/, '\1')
+ return morgue_assemble_filename(DGL_MORGUE_URL, e, found, '')
+ end