#!/usr/bin/perl
+#
+# Normally run from
+# update-master-info
+#
+# usage: ./yppedia-chart-parser <Oceanname>
+# updates OCEAN-Oceanname.db and _ocean-<oceanname>.txt
+# from YPPedia (chart and ocean page) and source-info.txt
+
+# This is part of ypp-sc-tools, a set of third-party tools for assisting
+# players of Yohoho Puzzle Pirates.
+#
+# Copyright (C) 2009 Ian Jackson <ijackson@chiark.greenend.org.uk>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# 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. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# Yohoho and Puzzle Pirates are probably trademarks of Three Rings and
+# are used without permission. This program is not endorsed or
+# sponsored by Three Rings.
use strict (qw(vars));
use warnings;
use Graph::Undirected;
use Commods;
use CommodsDatabase;
-
-my $ocean= 'Midnight';
-
+use CommodsScrape;
my $widists= Graph::Undirected->new();
my $wiarchs= Graph::Undirected->new();
my %winode2lines;
my %wiccix2arch;
my $wialldists;
+my %wtisland2arch;
-my $dbdists= Graph::Undirected->new();
+my $dbdists;
my %dbisland2arch;
+my $debugfh;
+
+my @msgkinds= qw(change warning error);
my %msgs;
-sub pmsg ($$) { push @{ $msgs{$_[0]} }, "$_[0]: $_[1]\n"; }
+my %msgprinted;
+my %msgkindprinted;
+sub pmsg ($$) {
+ my $m= "$_[0]: $_[1]\n";
+ print $debugfh "D $m";
+ push @{ $msgs{$_[0]} }, $m;
+}
sub warning ($) { pmsg("warning",$_[0]); }
sub error ($) { pmsg("error", $_[0]); }
sub change ($) { pmsg("change", $_[0]); }
sub print_messages () {
- foreach my $k (qw(change warning error)) {
- my $m= $msgs{$k};
- next unless $m;
- print sort @$m or die $!;
+ foreach my $k (@msgkinds) {
+ my $ms= $msgs{$k};
+ next unless $ms;
+ foreach my $m (sort @$ms) {
+ next if $msgprinted{$m};
+ print $m or die $!;
+ $msgprinted{$m}++;
+ $msgkindprinted{$k}++;
+ }
}
}
sub progress ($) { print "($_[0])\n"; }
-if (@ARGV && $ARGV[0] eq '--debug') {
- shift @ARGV;
- open DEBUG, ">&STDOUT" or die $!;
- select(DEBUG); $|=1;
-} else {
- open DEBUG, ">/dev/null" or die $!;
+my $stdin_chart=0;
+
+$debugfh= new IO::File ">/dev/null" or die $!;
+
+while (@ARGV) {
+ last unless $ARGV[0] =~ m/^-/;
+ $_= shift @ARGV;
+ last if m/^--$/;
+ if ($_ eq '--debug') {
+ $debugfh= new IO::File ">&STDOUT" or die $!;
+ select($debugfh); $|=1; select(STDOUT);
+ } elsif ($_ eq '--stdin-chart') {
+ $stdin_chart=1;
+ } else {
+ die;
+ }
}
-select(STDOUT); $|=1;
+$|=1;
+
+@ARGV==1 or die;
+my $ocean= shift @ARGV;
+
my $parity;
sub nn_xy ($$) {
return $n;
}
-sub yppedia_chart_parse () {
- # We don't even bother with tag soup; instead we do line-oriented parsing.
-
- while (<>) {
- s/\<--.*--\>//g;
- s/^\s*//; chomp; s/\s+$//; s/\s+/ /g;
- s/\<\/?(?:b|em)\>//g;
- s/\{\{Chart\ style\|[^{}]*\}\}//g;
- next unless m/\{\{/; # only interested in chart template stuff
-
- my ($x,$y, $arch,$island,$solid,$dirn);
- my $nn= sub { return nn_xy($x,$y) };
-
- if (($x,$y,$arch) =
- m/^\{\{ chart\ label \|(\d+)\|(\d+)\| .*
- \'\[\[ [^][\']* \| (\S+)\ archipelago \]\]\'*\}\}$/xi) {
- printf DEBUG "%2d,%-2d arch %s\n", $x,$y,$arch;
- push @wiarchlabels, [ $x,$y,$arch ];
- } elsif (($x,$y,$island) =
- m/^\{\{ chart\ island\ icon \|(\d+)\|(\d+)\|
- ([^| ][^|]*[^| ]) \| .*\}\}$/xi) {
- my $n= $nn->();
- $wiisland2node{$island}= $n;
- $winode2island{$n}= $island;
- $widists->add_vertex($n);
- $wiarchs->add_vertex($n);
- printf DEBUG "%2d,%-2d island %s\n", $x,$y,$island;
- } elsif (($solid,$x,$y,$dirn) =
- m/^\{\{ chart\ league((?:\ solid)?) \|(\d+)\|(\d+)\|
- ([-\/\\o]) \| .*\}\}$/xi) {
- next if $dirn eq 'o';
-
- my ($bx,$by) = ($x,$y);
- if ($dirn eq '-') { $bx+=2; }
- elsif ($dirn eq '\\') { $bx++; $by++; }
- elsif ($dirn eq '/') { $x++; $by++; }
- else { die; }
-
- my $nb= nn_xy($bx,$by);
- $widists->add_weighted_edge($nn->(), $nb, 1);
- $wiarchs->add_edge($nn->(), $nb) if $solid;
- $wiarchs->add_edge($nn->(), $nb) if $solid;
-
- printf DEBUG "%2d,%-2d league %-6s %s %s\n", $x,$y,
- $solid?'solid':'dotted', $dirn, $nb;
- } elsif (
- m/^\{\{ chart\ head \}\}$/xi
- ) {
- next;
- } else {
- warning("line $.: ignoring incomprehensible: $_");
- }
- }
-}
-
-sub database_fetch_ocean () {
- my ($row,$sth);
- $sth= $dbh->prepare('SELECT islandname, archipelago FROM islands');
- $sth->execute();
- while ($row= $sth->fetchrow_hashref) {
- print DEBUG "database-island $row->{'islandname'}".
- " $row->{'archipelago'}\n";
- $dbisland2arch{$row->{'islandname'}}= $row->{'archipelago'};
- }
- $sth= $dbh->prepare('SELECT dist, a.islandname a, b.islandname b
- FROM dists
- JOIN islands AS a ON dists.aiid==a.islandid
- JOIN islands AS b ON dists.biid==b.islandid');
- $sth->execute();
- while ($row= $sth->fetchrow_hashref) {
- $dbdists->add_weighted_edge($row->{'a'}, $row->{'b'}, $row->{'dist'});
- }
-}
-
-sub database_graph_spr () {
- $dbspr= shortest_path_reduction('db',$dbdists);
+sub run_yppedia_chart_parse ($) {
+ my ($oceanfh) = @_;
+ yppedia_chart_parse($oceanfh, $debugfh,
+ \&nn_xy,
+ sub {
+ my ($x,$y,$arch) = @_;
+ push @wiarchlabels, [ $x,$y,$arch ];
+ },
+ sub {
+ my ($n, $island) = @_;
+ $wiisland2node{$island}= $n;
+ $winode2island{$n}= $island;
+ $widists->add_vertex($n);
+ $wiarchs->add_vertex($n);
+ },
+ sub {
+ my ($na, $nb, $solid) = @_;
+ $widists->add_weighted_edge($na, $nb, 1);
+ $wiarchs->add_edge($na, $nb) if $solid;
+ $wiarchs->add_edge($na, $nb) if $solid;
+ },
+ sub {
+ my ($lno,$l) = @_;
+ warning("line $l: ignoring incomprehensible: $l");
+ });
}
sub yppedia_graphs_add_shortcuts () {
my $q= sprintf "%d,%d", $ax+$_[0], $ay+$_[1];
return unless $widists->has_vertex($q);
return if $widists->has_edge($p,$q);
- printf DEBUG "%-5s league-shortcut %-5s\n", $p, $q;
+ printf $debugfh "%-5s league-shortcut %-5s\n", $p, $q;
$widists->add_weighted_edge($p,$q,1);
};
$add_shortcut->( 2,0);
map { $weight += $widists->get_edge_weight($delete, $_) } @neigh;
$widists->add_weighted_edge(@neigh, $weight);
$widists->delete_vertex($delete);
- printf DEBUG "%-5s elide %5s %-5s %2d\n", $delete, @neigh, $weight;
+ printf $debugfh "%-5s elide %5s %-5s %2d\n", $delete, @neigh, $weight;
}
}
foreach my $arch (sort keys %{ $oceans{$ocean} }) {
foreach my $islename (sort keys %{ $oceans{$ocean}{$arch} }) {
my $islenode= $wiisland2node{$islename};
- defined $islenode or
+ if (!defined $islenode) {
error("island $islename in source-info but not in WP map");
+ next;
+ }
my $ccix= $wiarchs->connected_component_by_vertex($islenode);
my $oldarch= $wiccix2arch{$ccix};
- error("island $islename in $arch in source-info".
- " connected to $oldarch as well")
+ error("island in $arch in source-info".
+ " connected to $oldarch as well: $islename")
if defined $oldarch && $oldarch ne $arch;
- printf DEBUG "%-5s force-island-arch cc%-2d %-10s %s\n",
+ printf $debugfh "%-5s force-island-arch cc%-2d %-10s %s\n",
$islenode, $ccix, $arch, $islename;
$wiccix2arch{$ccix}= $arch;
}
#
foreach my $label (@wiarchlabels) {
my ($ax,$ay,$arch) = @$label;
- my $best_ccmulti= -1;
- my $best_d2= 0;
+ my $best_d2= 9999999;
my $best_n;
-# print DEBUG "$ax,$ay arch-island-search $arch\n";
+# print $debugfh "$ax,$ay arch-island-search $arch\n";
$ay += 1; $ax += 2; # coords are rather to the top left of label
foreach my $vertex ($wiarchs->vertices()) {
next unless exists $winode2island{$vertex};
my $ccix= $wiarchs->connected_component_by_vertex($vertex);
my @cc= $wiarchs->connected_component_by_index($ccix);
- my $ccmulti= @cc > 1;
- my ($vx,$vy) = split /,/, $vertex;
+ my ($vx,$vy) = split /,/, $vertex; # /
my $d2= ($vx-$ax)*($vx-$ax) + ($vy-$ay)*($vy-$ay);
- my $cmp= $ccmulti <=> $best_ccmulti
- || $best_d2 <=> $d2;
- printf DEBUG "%2d,%-2d arch-island-search %5s d2=%4d cc%-2d".
- " #cc=%2d ccmulti=%d cmp=%2d %s\n",
- $ax,$ay, $vertex, $d2, $ccix, scalar(@cc), $ccmulti, $cmp,
+ my $cmp= $best_d2 <=> $d2;
+ printf $debugfh "%2d,%-2d arch-island-search %5s d2=%4d cc%-2d".
+ " #cc=%2d cmp=%2d %s\n",
+ $ax,$ay, $vertex, $d2, $ccix, scalar(@cc), $cmp,
$winode2island{$vertex};
next unless $cmp > 0;
$best_n= $vertex;
$best_d2= $d2;
- $best_ccmulti= $ccmulti;
}
die 'no island vertices?!' unless defined $best_n;
my $ccix= $wiarchs->connected_component_by_vertex($best_n);
- printf DEBUG
+ printf $debugfh
"%2d,%-2d arch-island-select %-5s d2=%4d cc%-2d %-10s %s\n",
$ax,$ay, $best_n, $ccix, $best_d2, $arch, $winode2island{$best_n};
my $desc= join "\n", map {
next unless @islandnodes; # don't care, then
foreach my $islandnode (@islandnodes) {
- printf DEBUG "%-5s arch-join-need cc%-2d %s\n",
+ printf $debugfh "%-5s arch-join-need cc%-2d %s\n",
$islandnode, $sourceccix, $winode2island{$islandnode};
}
my $best_dist= 9999999;
my $arch= $wiccix2arch{$best_targetccix};
my $best_island= $winode2island{$best_target};
- printf DEBUG "%-5s arch-join-to %-5s dist=%2d cc%-2d %-10s %s\n",
+ printf $debugfh "%-5s arch-join-to %-5s dist=%2d cc%-2d %-10s %s\n",
$best_source, $best_target, $best_dist,
$best_targetccix, $arch,
defined($best_island) ? $best_island : "-";
# die "$p $q" unless defined $pl;
# my @pv= $wialldists->path_vertices($p,$q);
# if (@pv == $pl) { return $pl; }
-# printf DEBUG "%-5s PATHLENGTH %-5s pl=%s pv=%s\n", $p,$q,$pl,join('|',@pv);
+# printf $debugfh "%-5s PATHLENGTH %-5s pl=%s pv=%s\n", $p,$q,$pl,join('|',@pv);
return $pl;
}
}
}
foreach my $island (sort keys %wiisland2node) {
+ my $wtarch= $wtisland2arch{$island};
+ my $wiarch= wiisland2arch($island);
+ if (!$stdin_chart) {
+ if (!defined $wtarch) {
+ error("island from chart not found on ocean page: $island");
+ } elsif (defined $wiarch and $wtarch ne $wiarch) {
+ error("island in $wtarch on ocean page but".
+ " concluded $wiarch from chart: $island");
+ }
+ }
+
my $dbarch= $dbisland2arch{$island};
if (!defined $dbarch) {
my $wiarch= wiisland2arch($island);
change("island new in $wiarch: $island");
}
}
+ if (!$stdin_chart) {
+ foreach my $island (sort keys %wtisland2arch) {
+ my $node= $wiisland2node{$island};
+ next if defined $node;
+ error("island on ocean page but not in chart: $island");
+ }
+ }
}
sub shortest_path_reduction ($$) {
1. F is an undirected weighted graph with positive edge weights.
- 2. All graphs we will consider have the same vertices as F.
+ 2. All graphs we will consider have the same vertices as F
+ and none have self-edges.
3. G = Closure(F) is the graph of cliques whose edge weights
are the shortest paths in F, one clique for each connected
not essential and is therefore unnecessary.
END
+ # `
- printf DEBUG "spr %s before %d\n", $what, scalar($g->edges());
+ printf $debugfh "spr %s before %d\n", $what, scalar($g->edges());
my $result= Graph::Undirected->new();
foreach my $edge_ac ($g->edges()) {
+ $result->add_vertex($edge_ac->[0]); # just in case
+ next if $edge_ac->[0] eq $edge_ac->[1];
my $edgename_ac= join ' .. ', @$edge_ac;
- printf DEBUG "spr %s edge %s\n", $what, $edgename_ac;
+ printf $debugfh "spr %s edge %s\n", $what, $edgename_ac;
my $w_ac= $g->get_edge_weight(@$edge_ac);
my $needed= 1;
foreach my $vertex_b ($g->vertices()) {
next unless defined $w_ac;
next if $w_ab + $w_bc > $w_ac;
# found path
- printf DEBUG "spr %s edge %s unnecessary %s\n",
+ printf $debugfh "spr %s edge %s unnecessary %s\n",
$what, $edgename_ac, $vertex_b;
$needed= 0;
last;
}
if ($needed) {
- printf DEBUG "spr %s edge %s essential\n", $what, $edgename_ac;
+ printf $debugfh "spr %s edge %s essential\n", $what, $edgename_ac;
$result->add_weighted_edge(@$edge_ac,$w_ac);
}
}
- printf DEBUG "spr %s result %d\n", $what, scalar($result->edges());
+ printf $debugfh "spr %s result %d\n", $what, scalar($result->edges());
my $apsp= $result->APSP_Floyd_Warshall();
foreach my $ia (sort $g->vertices()) {
$wispr= shortest_path_reduction('wi',$base);
}
+sub yppedia_ocean_fetch_start ($) {
+ my ($chart) = @_;
+ my @args= ();
+ push @args, '--chart' if $chart;
+ push @args, $ocean;
+ open OCEAN, '-|', "./yppedia-ocean-scraper", @args or die $!;
+}
+sub yppedia_ocean_fetch_done () {
+ $?=0; $!=0; close OCEAN; $? and die $?; $! and die $!;
+}
+
+sub yppedia_ocean_fetch_chart () {
+ if ($stdin_chart) {
+ run_yppedia_chart_parse('::STDIN');
+ } else {
+ yppedia_ocean_fetch_start(1);
+ run_yppedia_chart_parse('::OCEAN');
+ yppedia_ocean_fetch_done();
+ }
+}
+
+sub yppedia_ocean_fetch_text () {
+ yppedia_ocean_fetch_start(0);
+ my $arch;
+ while (<OCEAN>) {
+ chomp;
+ if (m/^ocean /) {
+ $' eq $ocean or die;
+ } elsif (m/^ /) {
+ die unless defined $arch;
+ $wtisland2arch{$'}= $arch;
+ } elsif (m/^ /) {
+ $arch= $'; # '
+ } else {
+ die;
+ }
+ }
+ yppedia_ocean_fetch_done();
+}
+
sub compare_distances () {
foreach my $ia (sort keys %dbisland2arch) {
my $na= $wiisland2node{$ia};
}
}
-parse_info_serverside();
+#========== database handling ==========
-progress("reading database");
+sub database_fetch_ocean () {
+ my ($row,$sth);
+ $sth= $dbh->prepare('SELECT islandname, archipelago FROM islands');
+ $sth->execute();
+ undef %dbisland2arch;
+ $dbdists= Graph::Undirected->new();
+ while ($row= $sth->fetchrow_hashref) {
+ print $debugfh "database-island $row->{'islandname'}".
+ " $row->{'archipelago'}\n";
+ $dbisland2arch{$row->{'islandname'}}= $row->{'archipelago'};
+ }
+ $sth= $dbh->prepare('SELECT dist, a.islandname a, b.islandname b
+ FROM dists
+ JOIN islands AS a ON dists.aiid==a.islandid
+ JOIN islands AS b ON dists.biid==b.islandid');
+ $sth->execute();
+ while ($row= $sth->fetchrow_hashref) {
+ $dbdists->add_weighted_edge($row->{'a'}, $row->{'b'}, $row->{'dist'});
+ }
+}
-db_setocean($ocean);
-db_connect();
-database_fetch_ocean();
+sub database_graph_spr () {
+ $dbspr= shortest_path_reduction('db',$dbdists);
+}
+
+sub database_do_updates () {
+ my $addisland= $dbh->prepare(<<'END')
+ INSERT OR IGNORE INTO islands (islandname, archipelago) VALUES (?, ?);
+END
+ ;
+ foreach my $island (sort keys %wiisland2node) {
+ my $wiarch= wiisland2arch($island);
+ $addisland->execute($island, $wiarch);
+ }
+
+ db_doall(<<END)
+ DELETE FROM dists;
+ DELETE FROM routes;
+END
+ ;
+ my $adddist= $dbh->prepare(<<'END')
+ INSERT INTO dists VALUES
+ ((SELECT islandid FROM islands WHERE islandname == ?),
+ (SELECT islandid FROM islands WHERE islandname == ?),
+ ?);
+END
+ ;
+ my $addroute= $dbh->prepare(<<'END')
+ INSERT INTO routes VALUES
+ ((SELECT islandid FROM islands WHERE islandname == ?),
+ (SELECT islandid FROM islands WHERE islandname == ?),
+ ?);
+END
+ ;
+ foreach my $ia (sort keys %wiisland2node) {
+ my $na= $wiisland2node{$ia};
+ foreach my $ib (sort keys %wiisland2node) {
+ my $nb= $wiisland2node{$ib};
+ my $apdist= $ia eq $ib ? 0 : widist($na,$nb);
+ die "$ia $ib" unless defined $apdist;
+ my $sprdist= $wispr->get_edge_weight($ia,$ib);
+ die "$ia $ib $apdist $sprdist" if
+ defined($sprdist) && $sprdist != $apdist;
+
+ $adddist->execute($ia,$ib,$apdist);
+ $addroute->execute($ia,$ib,$sprdist) if defined $sprdist;
+ }
+ }
+
+ # select ia.islandname, ib.islandname, d.dist from dists as d, islands as ia on d.aiid = ia.islandid, islands as ib on d.biid = ib.islandid order by ia.islandname, ib.islandname;
+
+}
+
+#========== update _ocean-*.txt ==========
-progress("computing database spr"); database_graph_spr();
+our $localtopo_path;
+
+sub localtopo_rewrite () {
+ $localtopo_path= '_ocean-'.(lc $ocean).'.txt';
+ my $fh= new IO::File "$localtopo_path.tmp", 'w';
+ print $fh "# autogenerated - do not edit\n" or die $!;
+ print $fh "ocean $ocean\n" or die $!;
+ my %arches;
+ foreach my $isle (sort keys %wtisland2arch) {
+ my $arch= $wtisland2arch{$isle};
+ push @{ $arches{$arch} }, $isle;
+ }
+ foreach my $arch (sort keys %arches) {
+ print $fh " $arch\n" or die $!;
+ foreach my $isle (@{ $arches{$arch} }) {
+ print $fh " $isle\n" or die $!;
+ }
+ }
+ print $fh "\n" or die $!;
+ close $fh or die $!;
+}
-progress("reading yppedia chart"); yppedia_chart_parse();
+sub localtopo_commit () {
+ rename "$localtopo_path.tmp", $localtopo_path or die $!;
+}
+
+#========== main program ==========
+
+parse_info_serverside();
+
+progress("fetching yppedia chart"); yppedia_ocean_fetch_chart();
progress("adding shortcuts"); yppedia_graphs_add_shortcuts();
progress("pruning boring vertices"); yppedia_graphs_prune_boring();
progress("checking yppedia graphs"); yppedia_graphs_check();
progress("setting archs from nearby"); yppedia_archs_fillbynearest();
progress("computing yppedia spr"); yppedia_graph_spr();
-progress("comparing islands"); compare_island_lists();
-progress("comparing distances"); compare_distances();
+if (!$stdin_chart) {
+ progress("fetching yppedia ocean text"); yppedia_ocean_fetch_text();
+}
+
+db_setocean($ocean);
+db_connect();
+my $iteration=0;
+for (;;) {
+ progress("reading database");
+ database_fetch_ocean();
+ progress("computing database spr"); database_graph_spr();
+
+ progress("comparing islands"); compare_island_lists();
+ progress("comparing distances"); compare_distances();
+
+ print "\n";
+ print_messages();
+
+ foreach my $k (@msgkinds) {
+ my $n= $msgkindprinted{$k};
+ next unless $n;
+ printf STDERR "*** %d%s %ss\n", $n, $iteration?' additional':'', $k;
+ }
+
+ if ($msgs{'error'}) {
+ print STDERR "*** errors, aborting update\n";
+ exit 1;
+ }
+
+ if (!%msgkindprinted) {
+ progress("updating database"); database_do_updates();
+ progress("updating _ocean-*.txt"); localtopo_rewrite();
+ if ($stdin_chart) {
+ print STDERR "*** --stdin-chart, aborting!\n";
+ exit 1;
+ }
+ progress("checking database"); db_check_referential_integrity(1);
+ progress("committing database"); $dbh->commit();
+ progress("committing _ocean-*.txt"); localtopo_commit();
+ exit 0;
+ }
+ $dbh->rollback();
+
+ my $default= !$msgkindprinted{'warning'};
+ printf STDERR "*** confirm update %s ? ", $default?'(y/n)':'(n/y)';
+
+ if ($stdin_chart) {
+ printf STDERR "[--stdin-chart]\n";
+ exit 1;
+ }
+
+ $!=0; my $result= <STDIN>; defined $result or die $!;
+ $result =~ s/\s//g;
+ $result= $default?'y':'n' if !length $result;
+ $result= $result =~ m/^y/i;
+
+ if (!$result) {
+ printf STDERR "*** updated abandoned at your request\n";
+ exit 1;
+ }
+
+ print "\n";
+ undef %msgkindprinted;
+ $iteration++;
+}
print_messages();