| blib/lib/Game/HexDescribe/Utils.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % |
| statement | 170 | 668 | 25.4 |
| branch | 70 | 326 | 21.4 |
| condition | 28 | 161 | 17.3 |
| subroutine | 19 | 47 | 40.4 |
| pod | 37 | 38 | 97.3 |
| total | 324 | 1240 | 26.1 |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | #!/usr/bin/env perl | ||||||
| 2 | # Copyright (C) 2018–2022 Alex Schroeder |
||||||
| 3 | # | ||||||
| 4 | # This program is free software: you can redistribute it and/or modify it under | ||||||
| 5 | # the terms of the GNU Affero General Public License as published by the Free | ||||||
| 6 | # Software Foundation, either version 3 of the License, or (at your option) any | ||||||
| 7 | # later version. | ||||||
| 8 | # | ||||||
| 9 | # This program is distributed in the hope that it will be useful, but WITHOUT | ||||||
| 10 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | ||||||
| 11 | # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more | ||||||
| 12 | # details. | ||||||
| 13 | # | ||||||
| 14 | # You should have received a copy of the GNU Affero General Public License along | ||||||
| 15 | # with this program. If not, see |
||||||
| 16 | |||||||
| 17 | =encoding utf8 | ||||||
| 18 | |||||||
| 19 | =head1 NAME | ||||||
| 20 | |||||||
| 21 | Game::HexDescribe::Utils - utilities to use the Hex Describe data | ||||||
| 22 | |||||||
| 23 | =head1 DESCRIPTION | ||||||
| 24 | |||||||
| 25 | L |
||||||
| 26 | create the description of a map. This package contains the functions used to | ||||||
| 27 | access the information outside the web application framework. | ||||||
| 28 | |||||||
| 29 | =cut | ||||||
| 30 | |||||||
| 31 | package Game::HexDescribe::Utils; | ||||||
| 32 | require Exporter; | ||||||
| 33 | our @ISA = qw(Exporter); | ||||||
| 34 | our @EXPORT_OK = qw(init markdown describe_text list_tables parse_table load_table | ||||||
| 35 | describe_map parse_map load_map); | ||||||
| 36 | 1 | 1 | 491 | use Text::Autoformat; | |||
| 1 | 18564 | ||||||
| 1 | 79 | ||||||
| 37 | 1 | 1 | 424 | use Game::HexDescribe::Log; | |||
| 1 | 2 | ||||||
| 1 | 27 | ||||||
| 38 | 1 | 1 | 5 | use Modern::Perl; | |||
| 1 | 2 | ||||||
| 1 | 9 | ||||||
| 39 | 1 | 1 | 158 | use Mojo::URL; | |||
| 1 | 2 | ||||||
| 1 | 10 | ||||||
| 40 | 1 | 1 | 23 | use Mojo::File; | |||
| 1 | 2 | ||||||
| 1 | 35 | ||||||
| 41 | 1 | 1 | 5 | use List::Util qw(shuffle); | |||
| 1 | 2 | ||||||
| 1 | 55 | ||||||
| 42 | 1 | 1 | 381 | use Array::Utils qw(intersect); | |||
| 1 | 301 | ||||||
| 1 | 58 | ||||||
| 43 | 1 | 1 | 7 | use Encode qw(decode_utf8); | |||
| 1 | 2 | ||||||
| 1 | 40 | ||||||
| 44 | 1 | 1 | 5 | use utf8; | |||
| 1 | 10 | ||||||
| 1 | 8 | ||||||
| 45 | |||||||
| 46 | my $log = Game::HexDescribe::Log->get; | ||||||
| 47 | |||||||
| 48 | our $face_generator_url; | ||||||
| 49 | our $text_mapper_url; | ||||||
| 50 | |||||||
| 51 | =item list_tables($dir) | ||||||
| 52 | |||||||
| 53 | This function returns the table names in $dir. These are based on the following | ||||||
| 54 | filename convention: "$dir/hex-describe-$name-table.txt". | ||||||
| 55 | |||||||
| 56 | =cut | ||||||
| 57 | |||||||
| 58 | sub list_tables { | ||||||
| 59 | 0 | 0 | 1 | 0 | my $dir = shift; | ||
| 60 | 0 | 0 | $log->debug("Looking for maps in the contrib directory: $dir"); | ||||
| 61 | 0 | 0 | my @names = map { $_->basename('.txt') } Mojo::File->new($dir)->list->each; | ||||
| 0 | 0 | ||||||
| 62 | 0 | 0 | 0 | return grep { $_ } map { $1 if /^hex-describe-(.*)-table$/ } @names; | |||
| 0 | 0 | ||||||
| 0 | 0 | ||||||
| 63 | } | ||||||
| 64 | |||||||
| 65 | =item load_table($name, $dir) | ||||||
| 66 | |||||||
| 67 | This function returns the unparsed table from the filename | ||||||
| 68 | "$dir/hex-describe-$name-table.txt". | ||||||
| 69 | |||||||
| 70 | =cut | ||||||
| 71 | |||||||
| 72 | sub load_table { | ||||||
| 73 | 2 | 2 | 1 | 37 | my ($name, $dir) = @_; | ||
| 74 | 2 | 17 | $log->debug("Looking for table '$name' in the contrib directory '$dir'"); | ||||
| 75 | 2 | 27 | my $file = Mojo::File->new("$dir/hex-describe-$name-table.txt"); | ||||
| 76 | 2 | 50 | 28 | return decode_utf8($file->slurp) if -e $file; | |||
| 77 | } | ||||||
| 78 | |||||||
| 79 | =item load_map($name, $dir) | ||||||
| 80 | |||||||
| 81 | This function returns the unparsed map from the filename | ||||||
| 82 | "$dir/hex-describe-$name-map.txt". | ||||||
| 83 | |||||||
| 84 | =cut | ||||||
| 85 | |||||||
| 86 | sub load_map { | ||||||
| 87 | 1 | 1 | 1 | 254 | my ($name, $dir) = @_; | ||
| 88 | 1 | 7 | $log->debug("Looking for map in the contrib directory: $dir"); | ||||
| 89 | 1 | 20 | my $file = Mojo::File->new("$dir/hex-describe-$name-map.txt"); | ||||
| 90 | 1 | 50 | 17 | return decode_utf8($file->slurp) if -e $file; | |||
| 91 | } | ||||||
| 92 | |||||||
| 93 | =item parse_table | ||||||
| 94 | |||||||
| 95 | This parses the random tables. This is also where *bold* gets translated to | ||||||
| 96 | HTML. We also do some very basic checking of references. If we refer to another | ||||||
| 97 | table in square brackets we check whether we've seen such a table. | ||||||
| 98 | |||||||
| 99 | Table data is a reference to a hash of hashes. The key to the first hash is the | ||||||
| 100 | name of the table; the key to the second hash is "total" for the number of | ||||||
| 101 | options and "lines" for a reference to a list of hashes with two keys, "count" | ||||||
| 102 | (the weight of this lines) and "text" (the text of this line). | ||||||
| 103 | |||||||
| 104 | A table like the following: | ||||||
| 105 | |||||||
| 106 | ;tab | ||||||
| 107 | 1,a | ||||||
| 108 | 2,b | ||||||
| 109 | |||||||
| 110 | Would be: | ||||||
| 111 | |||||||
| 112 | $table_data->{tab}->{total} == 3 | ||||||
| 113 | $table_data->{tab}->{lines}->[0]->{count} == 1 | ||||||
| 114 | $table_data->{tab}->{lines}->[0]->{text} eq "a" | ||||||
| 115 | $table_data->{tab}->{lines}->[1]->{count} == 2 | ||||||
| 116 | $table_data->{tab}->{lines}->[1]->{text} eq "b" | ||||||
| 117 | |||||||
| 118 | =cut | ||||||
| 119 | |||||||
| 120 | my $dice_re = qr/^(save )?(?:(\d+)d(\d+)(?:x(\d+))?(?:([+-]\d+))?(?:>=(-?\d+))?(?:<=(-?\d+))?|(\d+))(?: as (.+))?$/; | ||||||
| 121 | my $math_re = qr/^(save )?([-+*\/%()0-9]+)(?: as (.+))?$/; | ||||||
| 122 | |||||||
| 123 | sub parse_table { | ||||||
| 124 | 2 | 2 | 1 | 6 | my $text = shift; | ||
| 125 | 2 | 7733 | $log->debug("parse_table: parsing " . length($text) . " characters"); | ||||
| 126 | 2 | 33 | my $data = {}; | ||||
| 127 | 2 | 7 | my $words = "[^\[\]\n]*"; | ||||
| 128 | 2 | 4 | my (%aliases, $key, $c, $t); | ||||
| 129 | 2 | 54380 | for my $line (split(/\r?\n/, $text)) { | ||||
| 130 | 80954 | 100 | 100 | 405093 | if ($line =~ /^;([^#\r\n]+)/) { | ||
| 100 | 66 | ||||||
| 50 | |||||||
| 131 | 4134 | 6874 | $key = $1; | ||||
| 132 | 4134 | 50 | 9171 | $log->warn("parse_table: reset '$key'") if exists $data->{$key}; | |||
| 133 | 4134 | 11759 | $data->{$key} = {}; # reset, don't merge | ||||
| 134 | } elsif ($key and ($c, $t) = $line =~ /^(\d+),(.*)/) { | ||||||
| 135 | 71936 | 120269 | $t =~ s/\*\*(.*?)\*\*/$1<\/strong>/g; | ||||
| 136 | 71936 | 98132 | $t =~ s/\*(.*?)\*/$1<\/em>/g; | ||||
| 137 | 71936 | 132356 | my %h = (text => $t); | ||||
| 138 | 71936 | 100 | 118030 | if ($c == 0) { | |||
| 139 | 12 | 25 | $h{unique} = 1; | ||||
| 140 | 12 | 62 | $c = 1; | ||||
| 141 | } | ||||||
| 142 | 71936 | 109398 | $h{count} = $c; | ||||
| 143 | 71936 | 105537 | $data->{$key}->{total} += $c; | ||||
| 144 | 71936 | 76763 | push(@{$data->{$key}->{lines}}, \%h); | ||||
| 71936 | 135672 | ||||||
| 145 | # [foo as bar] | ||||||
| 146 | 71936 | 178130 | for my $alias ($h{text} =~ /\[$words as ($words)\]/g) { | ||||
| 147 | 0 | 0 | $aliases{$alias} = 1; | ||||
| 148 | } | ||||||
| 149 | # [foo [baz] quux as bar] (one level of indirection allowed | ||||||
| 150 | 71936 | 210102 | for my $alias ($h{text} =~ /\[$words\[$words\]$words as ($words)\]/g) { | ||||
| 151 | 0 | 0 | $aliases{$alias} = 1; | ||||
| 152 | } | ||||||
| 153 | } elsif ($line ne '' and $line !~ /^\s*#/) { | ||||||
| 154 | 0 | 0 | $log->warn("unknown line type: '$line'"); | ||||
| 155 | } | ||||||
| 156 | } | ||||||
| 157 | # check tables | ||||||
| 158 | 2 | 5459 | for my $table (keys %$data) { | ||||
| 159 | 4134 | 4924 | for my $line (@{$data->{$table}->{lines}}) { | ||||
| 4134 | 11094 | ||||||
| 160 | 71936 | 157463 | for my $subtable ($line->{text} =~ /\[($words)\]/g) { | ||||
| 161 | 0 | 0 | 0 | next if index($subtable, '|') != -1; | |||
| 162 | 0 | 0 | 0 | next if $subtable =~ /$dice_re/; | |||
| 163 | 0 | 0 | 0 | next if $subtable =~ /$math_re/; | |||
| 164 | 0 | 0 | 0 | next if $subtable =~ /^redirect https?:/; | |||
| 165 | 0 | 0 | 0 | 0 | next if $subtable =~ /^names for (.*)/ and $data->{"name for $1"}; | ||
| 166 | 0 | 0 | 0 | 0 | next if $subtable =~ /^(?:capitalize|titlecase|highlightcase|normalize-elvish) (.*)/ and $data->{$1}; | ||
| 167 | 0 | 0 | 0 | next if $subtable =~ /^adjacent hex$/; # experimental | |||
| 168 | 0 | 0 | 0 | 0 | next if $subtable =~ /^same (.*)/ and ($data->{$1} or $aliases{$1} or $1 eq 'adjacent hex'); | ||
| 0 | |||||||
| 169 | 0 | 0 | 0 | 0 | next if $subtable =~ /^(?:here|nearby|other|append|later|with|and|save|store) (.+?)( as (.+))?$/ and $data->{$1}; | ||
| 170 | 0 | 0 | 0 | $subtable = $1 if $subtable =~ /^(.+) as (.+)/; | |||
| 171 | $log->error("Error in table $table: subtable $subtable is missing") | ||||||
| 172 | 0 | 0 | 0 | unless $data->{$subtable}; | |||
| 173 | } | ||||||
| 174 | } | ||||||
| 175 | } | ||||||
| 176 | 2 | 2190 | return $data; | ||||
| 177 | } | ||||||
| 178 | |||||||
| 179 | =item init | ||||||
| 180 | |||||||
| 181 | When starting a description, we need to initialize our data. There are two | ||||||
| 182 | global data structures beyond the map. | ||||||
| 183 | |||||||
| 184 | B<$extra> is a reference to a hash of lists of hashes used to keep common data | ||||||
| 185 | per line. In this context, lines are linear structures like rivers or trails on | ||||||
| 186 | the map. The first hash uses the hex coordinates as a key. This gets you the | ||||||
| 187 | list of hashes, one per line going through this hex. Each of these hashes uses | ||||||
| 188 | the key "type" to indicate the type of line, "line" for the raw data (for | ||||||
| 189 | debugging), and later "name" will be used to name these lines. | ||||||
| 190 | |||||||
| 191 | $extra->{"0101"}->[0]->{"type"} eq "river" | ||||||
| 192 | |||||||
| 193 | B<%names> is just a hash of names. It is used for all sorts of things. When | ||||||
| 194 | using the reference C |
||||||
| 195 | will be a key in this hash. When using the reference C |
||||||
| 196 | then "name for forest foo: 0101" and will be set for every hex sharing that | ||||||
| 197 | name. | ||||||
| 198 | |||||||
| 199 | $names{"name for a bugbear band1"} eq "Long Fangs" | ||||||
| 200 | $names{"name for forest foo: 0101"} eq "Dark Wood" | ||||||
| 201 | |||||||
| 202 | Note that for C, C |
||||||
| 203 | |||||||
| 204 | B<%locals> is a hash of all the "normal" table lookups encountered so far. It is | ||||||
| 205 | is reset for every paragraph. To refer to a previous result, start a reference | ||||||
| 206 | with the word "same". This doesn't work for references to adjacent hexes, dice | ||||||
| 207 | rolls, or names. Here's an example: | ||||||
| 208 | |||||||
| 209 | ;village boss | ||||||
| 210 | 1,[man] is the village boss. They call him Big [same man]. | ||||||
| 211 | 1,[woman] is the village boss. They call her Big [same woman]. | ||||||
| 212 | |||||||
| 213 | Thus: | ||||||
| 214 | |||||||
| 215 | $locals{man} eq "Alex" | ||||||
| 216 | |||||||
| 217 | B<%globals> is a hash of hashes of all the table lookups beginning with the word | ||||||
| 218 | "here" per hex. In a second phase, all the references starting with the word | ||||||
| 219 | "nearby" will be resolved using these. Here's an example: | ||||||
| 220 | |||||||
| 221 | ;ingredient | ||||||
| 222 | 1,fey moss | ||||||
| 223 | 1,blue worms | ||||||
| 224 | ;forest | ||||||
| 225 | 3,There is nothing here but trees. | ||||||
| 226 | 1,You find [here ingredient]. | ||||||
| 227 | ;village | ||||||
| 228 | 1,The alchemist needs [nearby ingredient]. | ||||||
| 229 | |||||||
| 230 | Some of the forest hexes will have one of the two possible ingredients and the | ||||||
| 231 | village alchemist will want one of the nearby ingredients. Currently, there is a | ||||||
| 232 | limitation in place: we can only resolve the references starting with the word | ||||||
| 233 | "nearby" when everything else is done. This means that at that point, references | ||||||
| 234 | starting with the word "same" will no longer work since C<%locals> will no | ||||||
| 235 | longer be set. | ||||||
| 236 | |||||||
| 237 | Thus: | ||||||
| 238 | |||||||
| 239 | $globals->{ingredient}->{"0101"} eq "fey moss" | ||||||
| 240 | |||||||
| 241 | =cut | ||||||
| 242 | |||||||
| 243 | my $extra; | ||||||
| 244 | my %names; | ||||||
| 245 | my %locals; | ||||||
| 246 | my $globals; | ||||||
| 247 | |||||||
| 248 | sub init { | ||||||
| 249 | 1 | 1 | 1 | 3 | %names = (); | ||
| 250 | 1 | 745 | %locals = (); | ||||
| 251 | 1 | 3 | $globals = undef; | ||||
| 252 | 1 | 3 | $extra = undef; | ||||
| 253 | } | ||||||
| 254 | |||||||
| 255 | =item parse_map_data | ||||||
| 256 | |||||||
| 257 | This does basic parsing of hexes on the map as produced by Text Mapper, for | ||||||
| 258 | example: | ||||||
| 259 | |||||||
| 260 | 0101 dark-green trees village | ||||||
| 261 | |||||||
| 262 | =cut | ||||||
| 263 | |||||||
| 264 | sub parse_map_data { | ||||||
| 265 | 0 | 0 | 1 | 0 | my $map = shift; | ||
| 266 | 0 | 0 | my $map_data; | ||||
| 267 | 0 | 0 | 0 | 0 | if ($map and $map->isa('Mojo::Upload')) { | ||
| 268 | 0 | 0 | $map = $map->slurp(); | ||||
| 269 | }; | ||||||
| 270 | 0 | 0 | for my $hex (split(/\r?\n/, $map)) { | ||||
| 271 | 0 | 0 | 0 | if (my ($x, $y) = $hex =~ /^(\d\d)(\d\d)\s*empty$/cg) { | |||
| 0 | |||||||
| 272 | # skip | ||||||
| 273 | } elsif (($x, $y) = $hex =~ /^(\d\d)(\d\d)\s+/cg) { | ||||||
| 274 | 0 | 0 | my @types = ("system"); # Traveller | ||||
| 275 | 0 | 0 | 0 | while($hex =~ /\G([a-z]="[^"]+")\s*/cg or $hex =~ /(\S+)/cg) { | |||
| 276 | 0 | 0 | push(@types, $1); | ||||
| 277 | } | ||||||
| 278 | 0 | 0 | $map_data->{"$x$y"} = \@types; | ||||
| 279 | } | ||||||
| 280 | } | ||||||
| 281 | 0 | 0 | return $map_data; | ||||
| 282 | } | ||||||
| 283 | |||||||
| 284 | =item parse_map_lines | ||||||
| 285 | |||||||
| 286 | This does basic parsing of linear structures on the map as produced by Text | ||||||
| 287 | Mapper, for example: | ||||||
| 288 | |||||||
| 289 | 0302-0101 trail | ||||||
| 290 | |||||||
| 291 | We use C |
||||||
| 292 | |||||||
| 293 | =cut | ||||||
| 294 | |||||||
| 295 | my $line_re = qr/^(\d\d\d\d(?:-\d\d\d\d)+)\s+(\S+)/m; | ||||||
| 296 | |||||||
| 297 | sub parse_map_lines { | ||||||
| 298 | 0 | 0 | 1 | 0 | my $map = shift; | ||
| 299 | 0 | 0 | my @lines; | ||||
| 300 | 0 | 0 | while ($map =~ /$line_re/g) { | ||||
| 301 | 0 | 0 | my ($line, $type) = ($1, $2); | ||||
| 302 | 0 | 0 | my @points = compute_missing_points(split(/-/, $line)); | ||||
| 303 | 0 | 0 | push(@lines, [$type, @points]); | ||||
| 304 | } | ||||||
| 305 | 0 | 0 | return \@lines; | ||||
| 306 | } | ||||||
| 307 | |||||||
| 308 | =item process_map_merge_lines | ||||||
| 309 | |||||||
| 310 | As we process lines, we want to do two things: if a hex is part of a linear | ||||||
| 311 | structure, we want to add the B |
||||||
| 312 | following hex and river, we want to add "river" to the terrain features of 0101: | ||||||
| 313 | |||||||
| 314 | 0801-0802-0703-0602-0503-0402-0302-0201-0101-0100 river | ||||||
| 315 | |||||||
| 316 | The (virtual) result: | ||||||
| 317 | |||||||
| 318 | 0101 dark-green trees village river | ||||||
| 319 | |||||||
| 320 | Furthermore, given another river like the following, we want to merge these | ||||||
| 321 | where they meet (at 0302): | ||||||
| 322 | |||||||
| 323 | 0701-0601-0501-0401-0302-0201-0101-0100 river | ||||||
| 324 | |||||||
| 325 | Again, the (virtual) result: | ||||||
| 326 | |||||||
| 327 | 0302 dark-green trees town river river-merge | ||||||
| 328 | |||||||
| 329 | If you look at the default map, here are some interesting situations: | ||||||
| 330 | |||||||
| 331 | A river starts at 0906 but it immediately merges with the river starting at 1005 | ||||||
| 332 | thus it should be dropped entirely. | ||||||
| 333 | |||||||
| 334 | A trail starts at 0206 and passes through 0305 on the way to 0404 but it | ||||||
| 335 | shouldn't end at 0305 just because there's also a trail starting at 0305 going | ||||||
| 336 | north to 0302. | ||||||
| 337 | |||||||
| 338 | =cut | ||||||
| 339 | |||||||
| 340 | sub process_map_merge_lines { | ||||||
| 341 | 0 | 0 | 1 | 0 | my $map_data = shift; | ||
| 342 | 0 | 0 | my $lines = shift; | ||||
| 343 | 0 | 0 | for my $line (@$lines) { | ||||
| 344 | 0 | 0 | my $type = $line->[0]; | ||||
| 345 | 0 | 0 | my %data = (type => $type, line => $line); | ||||
| 346 | # $log->debug("New $type..."); | ||||||
| 347 | 0 | 0 | my $start = 1; | ||||
| 348 | COORD: | ||||||
| 349 | 0 | 0 | for my $i (1 .. $#$line) { | ||||
| 350 | 0 | 0 | my $coord = $line->[$i]; | ||||
| 351 | # don't add data for hexes outside the map | ||||||
| 352 | 0 | 0 | 0 | last unless $map_data->{$coord}; | |||
| 353 | # don't start a line going in the same direction as an existing line in | ||||||
| 354 | # the same hex (e.g. 0906) but also don't stop a line if it runs into a | ||||||
| 355 | # merge and continues (e.g. 0305) | ||||||
| 356 | 0 | 0 | my $same_dir = 0; | ||||
| 357 | 0 | 0 | for my $line2 (grep { $_->{type} eq $type } @{$extra->{$coord}}) { | ||||
| 0 | 0 | ||||||
| 0 | 0 | ||||||
| 358 | 0 | 0 | 0 | if (same_direction($coord, $line, $line2->{line})) { | |||
| 359 | # $log->debug("... at $coord, @$line and @{$line2->{line}} go in the same direction"); | ||||||
| 360 | 0 | 0 | $same_dir = 1; | ||||
| 361 | 0 | 0 | last; | ||||
| 362 | } | ||||||
| 363 | } | ||||||
| 364 | 0 | 0 | 0 | 0 | if ($start and $same_dir) { | ||
| 365 | # $log->debug("... skipping"); | ||||||
| 366 | 0 | 0 | last COORD; | ||||
| 367 | } | ||||||
| 368 | # add type to the hex description, add "$type-merge" when | ||||||
| 369 | # running into an existing one | ||||||
| 370 | 0 | 0 | my $merged; | ||||
| 371 | 0 | 0 | 0 | if (not grep { $_ eq $type } @{$map_data->{$coord}}) { | |||
| 0 | 0 | 0 | |||||
| 0 | 0 | ||||||
| 372 | # $log->debug("...$type leading into $coord"); | ||||||
| 373 | 0 | 0 | push(@{$map_data->{$coord}}, $type); | ||||
| 0 | 0 | ||||||
| 374 | 0 | 0 | } elsif (not grep { $_ eq "$type-merge" } @{$map_data->{$coord}}) { | ||||
| 0 | 0 | ||||||
| 375 | 0 | 0 | $merged = $same_dir; # skip the rest of the line, if same dir | ||||
| 376 | # $log->debug("...noted merge into existing $type at $coord"); | ||||||
| 377 | 0 | 0 | push(@{$map_data->{$coord}}, "$type-merge"); | ||||
| 0 | 0 | ||||||
| 378 | } else { | ||||||
| 379 | 0 | 0 | $merged = $same_dir; # skip the rest of the line, if same dir | ||||
| 380 | # $log->debug("...leads into existing $type merge at $coord"); | ||||||
| 381 | } | ||||||
| 382 | 0 | 0 | $start = 0; | ||||
| 383 | # all hexes along a line share this hash | ||||||
| 384 | 0 | 0 | push(@{$extra->{$coord}}, \%data); | ||||
| 0 | 0 | ||||||
| 385 | # if a river merges into another, don't add any hexes downriver | ||||||
| 386 | 0 | 0 | 0 | last if $merged; | |||
| 387 | } | ||||||
| 388 | } | ||||||
| 389 | } | ||||||
| 390 | |||||||
| 391 | =item process_map_start_lines | ||||||
| 392 | |||||||
| 393 | As we process lines, we also want to note the start of lines: sources of rivers, | ||||||
| 394 | the beginning of trails. Thus, given the following hex and river, we want to add | ||||||
| 395 | "river-start" to the terrain features of 0801: | ||||||
| 396 | |||||||
| 397 | 0801-0802-0703-0602-0503-0402-0302-0201-0101-0100 river | ||||||
| 398 | |||||||
| 399 | Adds a river to the hex: | ||||||
| 400 | |||||||
| 401 | 0801 light-grey mountain river river-start | ||||||
| 402 | |||||||
| 403 | But note that we don't want to do this where linear structures have merged. If a | ||||||
| 404 | trail ends at a town and merges with other trails there, it doesn't "start" | ||||||
| 405 | there. It can only be said to start somewhere if no other linear structure | ||||||
| 406 | starts there. | ||||||
| 407 | |||||||
| 408 | In case we're not talking about trails and rivers but things like routes from A | ||||||
| 409 | to B, it might be important to note the fact. Therefore, both ends of the line | ||||||
| 410 | get a "river-end" (if a river). | ||||||
| 411 | |||||||
| 412 | =cut | ||||||
| 413 | |||||||
| 414 | sub process_map_start_lines { | ||||||
| 415 | 0 | 0 | 1 | 0 | my $map_data = shift; | ||
| 416 | 0 | 0 | my $lines = shift; | ||||
| 417 | # add "$type-start" to the first and last hex of a line, unless it is a merge | ||||||
| 418 | 0 | 0 | for my $line (@$lines) { | ||||
| 419 | 0 | 0 | my $type = $line->[0]; | ||||
| 420 | 0 | 0 | for my $coord ($line->[1], $line->[$#$line]) { | ||||
| 421 | # ends get marked either way | ||||||
| 422 | 0 | 0 | 0 | push(@{$map_data->{$coord}}, "$type-end") unless grep { $_ eq "$type-end" } @{$map_data->{$coord}}; | |||
| 0 | 0 | ||||||
| 0 | 0 | ||||||
| 0 | 0 | ||||||
| 423 | # skip hexes outside the map | ||||||
| 424 | 0 | 0 | 0 | last unless $map_data->{$coord}; | |||
| 425 | # skip merges | ||||||
| 426 | 0 | 0 | 0 | last if grep { $_ eq "$type-merge" } @{$map_data->{$coord}}; | |||
| 0 | 0 | ||||||
| 0 | 0 | ||||||
| 427 | # add start | ||||||
| 428 | 0 | 0 | push(@{$map_data->{$coord}}, "$type-start"); | ||||
| 0 | 0 | ||||||
| 429 | } | ||||||
| 430 | } | ||||||
| 431 | } | ||||||
| 432 | |||||||
| 433 | =item parse_map | ||||||
| 434 | |||||||
| 435 | This calls all the map parsing and processing functions we just talked about. | ||||||
| 436 | |||||||
| 437 | =cut | ||||||
| 438 | |||||||
| 439 | sub parse_map { | ||||||
| 440 | 0 | 0 | 1 | 0 | my $map = shift; | ||
| 441 | 0 | 0 | my $map_data = parse_map_data($map); | ||||
| 442 | 0 | 0 | my $lines = parse_map_lines($map); | ||||
| 443 | # longest rivers first | ||||||
| 444 | 0 | 0 | @$lines = sort { @$b <=> @$a } @$lines; | ||||
| 0 | 0 | ||||||
| 445 | # for my $line (@$lines) { | ||||||
| 446 | # $log->debug("@$line"); | ||||||
| 447 | # } | ||||||
| 448 | 0 | 0 | process_map_merge_lines($map_data, $lines); | ||||
| 449 | 0 | 0 | process_map_start_lines($map_data, $lines); | ||||
| 450 | # for my $coord (sort keys %$map_data) { | ||||||
| 451 | # $log->debug(join(" ", $coord, @{$map_data->{$coord}})); | ||||||
| 452 | # } | ||||||
| 453 | 0 | 0 | return $map_data; | ||||
| 454 | } | ||||||
| 455 | |||||||
| 456 | =item pick_description | ||||||
| 457 | |||||||
| 458 | Pick a description from a given table. In the example above, pick a random | ||||||
| 459 | number between 1 and 3 and then go through the list, addin up counts until you | ||||||
| 460 | hit that number. | ||||||
| 461 | |||||||
| 462 | If the result picked is unique, remove it from the list. That is, set it's count | ||||||
| 463 | to 0 such that it won't ever get picked again. | ||||||
| 464 | |||||||
| 465 | =cut | ||||||
| 466 | |||||||
| 467 | sub pick_description { | ||||||
| 468 | 295 | 295 | 1 | 395 | my $h = shift; | ||
| 469 | 295 | 383 | my $total = $h->{total}; | ||||
| 470 | 295 | 333 | my $lines = $h->{lines}; | ||||
| 471 | 295 | 506 | my $roll = int(rand($total)) + 1; | ||||
| 472 | 295 | 318 | my $i = 0; | ||||
| 473 | 295 | 423 | for my $line (@$lines) { | ||||
| 474 | 1106 | 1648 | $i += $line->{count}; | ||||
| 475 | 1106 | 100 | 1613 | if ($i >= $roll) { | |||
| 476 | 295 | 50 | 395 | if ($line->{unique}) { | |||
| 477 | 0 | 0 | $h->{total} -= $line->{count}; | ||||
| 478 | 0 | 0 | $line->{count} = 0; | ||||
| 479 | } | ||||||
| 480 | 295 | 618 | return $line->{text}; | ||||
| 481 | } | ||||||
| 482 | } | ||||||
| 483 | 0 | 0 | $log->error("picked nothing"); | ||||
| 484 | 0 | 0 | return ''; | ||||
| 485 | } | ||||||
| 486 | |||||||
| 487 | =item resolve_redirect | ||||||
| 488 | |||||||
| 489 | This handles the special redirect syntax: request an URL and if the response | ||||||
| 490 | code is a 301 or 302, take the location header in the response and return it. | ||||||
| 491 | |||||||
| 492 | If the environment variable C |
||||||
| 493 | resolved and the empty string is returned. | ||||||
| 494 | |||||||
| 495 | =cut | ||||||
| 496 | |||||||
| 497 | sub resolve_redirect { | ||||||
| 498 | # If you install this tool on a server using HTTPS, then some browsers will | ||||||
| 499 | # make sure that including resources from other servers will not work. | ||||||
| 500 | 10 | 10 | 1 | 13 | my $url = shift; | ||
| 501 | 10 | 12 | my $redirects = shift; | ||||
| 502 | 10 | 50 | 33 | 60 | return '' unless $redirects and not $ENV{HEX_DESCRIBE_OFFLINE}; | ||
| 503 | # Special case because table writers probably used the default face generator URL | ||||||
| 504 | 0 | 0 | 0 | $url =~ s!^https://campaignwiki\.org/face!$face_generator_url! if $face_generator_url; | |||
| 505 | 0 | 0 | 0 | $url =~ s!^https://campaignwiki\.org/text-mapper!$text_mapper_url! if $text_mapper_url; | |||
| 506 | 0 | 0 | my $ua = Mojo::UserAgent->new; | ||||
| 507 | 0 | 0 | my $res = eval { $ua->get($url)->result }; | ||||
| 0 | 0 | ||||||
| 508 | 0 | 0 | 0 | 0 | if (not $res) { | ||
| 0 | |||||||
| 509 | 0 | 0 | my $warning = $@; | ||||
| 510 | 0 | 0 | chomp($warning); | ||||
| 511 | 0 | 0 | $log->warn("connecting to $url: $warning"); | ||||
| 512 | 0 | 0 | return ""; | ||||
| 513 | } elsif ($res->code == 301 or $res->code == 302) { | ||||||
| 514 | 0 | 0 | return Mojo::URL->new($res->headers->location) | ||||
| 515 | ->base(Mojo::URL->new($url)) | ||||||
| 516 | ->to_abs; | ||||||
| 517 | } | ||||||
| 518 | 0 | 0 | $log->info("resolving redirect for $url did not result in a redirection"); | ||||
| 519 | 0 | 0 | return $url; | ||||
| 520 | } | ||||||
| 521 | |||||||
| 522 | =item pick | ||||||
| 523 | |||||||
| 524 | This function picks the appropriate table given a particular word (usually a map | ||||||
| 525 | feature such as "forest" or "river"). | ||||||
| 526 | |||||||
| 527 | This is where I |
||||||
| 528 | |||||||
| 529 | 0101 dark-green trees village river trail | ||||||
| 530 | |||||||
| 531 | Remember that parsing the map added more terrain than was noted on the map | ||||||
| 532 | itself. Our function will get called for each of these words, Let's assume it | ||||||
| 533 | will get called for "dark-green". Before checking whether a table called | ||||||
| 534 | "dark-green" exists, we want to check whether any of the other words provide | ||||||
| 535 | enough context to pick a more specific table. Thus, we will check "trees | ||||||
| 536 | dark-green", "village dark-green", "river dark-green" and "trail dark-green" | ||||||
| 537 | before checking for "dark-green". | ||||||
| 538 | |||||||
| 539 | If such a table exists in C<$table_data>, we call C |
||||||
| 540 | text from the table and then we go through the text and call C |
||||||
| 541 | resolve any table references in square brackets. | ||||||
| 542 | |||||||
| 543 | Remember that rules for the remaining words are still being called. Thus, if you | ||||||
| 544 | write a table for "trees dark-green" (which is going to be picked in preference | ||||||
| 545 | to "dark-green"), then there should be no table for "trees" because that's the | ||||||
| 546 | next word that's going to be processed! | ||||||
| 547 | |||||||
| 548 | =cut | ||||||
| 549 | |||||||
| 550 | sub pick { | ||||||
| 551 | 295 | 295 | 1 | 370 | my $map_data = shift; | ||
| 552 | 295 | 325 | my $table_data = shift; | ||||
| 553 | 295 | 366 | my $level = shift; | ||||
| 554 | 295 | 351 | my $coordinates = shift; | ||||
| 555 | 295 | 334 | my $words = shift; | ||||
| 556 | 295 | 354 | my $word = shift; | ||||
| 557 | 295 | 323 | my $redirects = shift; | ||||
| 558 | 295 | 308 | my $text; | ||||
| 559 | # Make sure we're testing all the context combinations first. Thus, if $words | ||||||
| 560 | # is [ "mountains" white" "chaos"] and $word is "mountains", we want to test | ||||||
| 561 | # "white mountains", "cold mountains" and "mountains", in this order. | ||||||
| 562 | 295 | 445 | for my $context (grep( { $_ ne $word } @$words), $word) { | ||||
| 295 | 712 | ||||||
| 563 | 296 | 100 | 476 | my $key = ($context eq $word ? $word : "$context $word"); | |||
| 564 | # $log->debug("$coordinates: looking for a $key table") if $coordinates eq "0109"; | ||||||
| 565 | 296 | 100 | 674 | if ($table_data->{$key}) { | |||
| 566 | 295 | 573 | $text = pick_description($table_data->{$key}); | ||||
| 567 | # $log->debug("$coordinates → $key → $text"); | ||||||
| 568 | 295 | 463 | my $seed = int(rand(~0)); # maxint | ||||
| 569 | 295 | 471 | $text =~ s/\[\[redirect (https:.*?)\]\]/my $url = $1; $url =~ s!\$seed!$seed!; resolve_redirect($url, $redirects)/ge; | ||||
| 10 | 20 | ||||||
| 10 | 40 | ||||||
| 10 | 22 | ||||||
| 570 | # this makes sure we recursively resolve all references, in order, because | ||||||
| 571 | # we keep rescanning from the beginning | ||||||
| 572 | 295 | 347 | my $last = $text; | ||||
| 573 | 295 | 862 | while ($text =~ s/\[([^][]*)\]/describe($map_data,$table_data,$level+1,$coordinates,[$1], $redirects)/e) { | ||||
| 409 | 1231 | ||||||
| 574 | 409 | 50 | 815 | if ($last eq $text) { | |||
| 575 | 0 | 0 | $log->error("Infinite loop: $text"); | ||||
| 576 | 0 | 0 | last; | ||||
| 577 | } | ||||||
| 578 | 409 | 1686 | $last = $text; | ||||
| 579 | }; | ||||||
| 580 | 295 | 408 | last; | ||||
| 581 | } | ||||||
| 582 | } | ||||||
| 583 | # $log->debug("$word → $text ") if $text; | ||||||
| 584 | 295 | 479 | return $text; | ||||
| 585 | } | ||||||
| 586 | |||||||
| 587 | =item describe | ||||||
| 588 | |||||||
| 589 | This is where all the references get resolved. We handle references to dice | ||||||
| 590 | rolls, the normal recursive table lookup, and all the special rules for names | ||||||
| 591 | that get saved once they have been determined both globally or per terrain | ||||||
| 592 | features. Please refer to the tutorial on the help page for the various | ||||||
| 593 | features. | ||||||
| 594 | |||||||
| 595 | =cut | ||||||
| 596 | |||||||
| 597 | sub describe { | ||||||
| 598 | 419 | 419 | 1 | 541 | my $map_data = shift; | ||
| 599 | 419 | 437 | my $table_data = shift; | ||||
| 600 | 419 | 484 | my $level = shift; | ||||
| 601 | 419 | 469 | my $coordinates = shift; | ||||
| 602 | 419 | 455 | my $words = shift; | ||||
| 603 | 419 | 470 | my $redirects = shift; | ||||
| 604 | 419 | 50 | 659 | $log->error("Recursion level $level exceeds 20 in $coordinates (@$words)!") if $level > 20; | |||
| 605 | 419 | 50 | 568 | return '' if $level > 20; | |||
| 606 | 419 | 50 | 580 | if ($level == 1) { | |||
| 607 | 0 | 0 | %locals = (); # reset once per paragraph | ||||
| 608 | 0 | 0 | for my $word (@$words) { | ||||
| 609 | 0 | 0 | 0 | 0 | if ($word =~ /^([a-z]+)="(.*)"/ or | ||
| 610 | $word =~ /(.*)-(\d+)$/) { | ||||||
| 611 | # assigments in the form uwp=“777777” assign “777777” to “uwp” | ||||||
| 612 | # words in the form law-5 assign “5” to “law” | ||||||
| 613 | 0 | 0 | $locals{$1} = $2; | ||||
| 614 | } else { | ||||||
| 615 | 0 | 0 | $locals{$word} = 1; | ||||
| 616 | } | ||||||
| 617 | } | ||||||
| 618 | } | ||||||
| 619 | 419 | 437 | my @descriptions; | ||||
| 620 | 419 | 623 | for my $word (@$words) { | ||||
| 621 | # valid dice rolls: 1d6, 1d6+1, 1d6x10, 1d6x10+1 | ||||||
| 622 | 419 | 100 | 0 | 6686 | if (my ($just_save, $n, $d, $m, $p, $min, $max, $c, $save_as) = $word =~ /$dice_re/) { | ||
| 50 | 0 | ||||||
| 50 | 0 | ||||||
| 50 | 0 | ||||||
| 50 | |||||||
| 50 | |||||||
| 100 | |||||||
| 50 | |||||||
| 50 | |||||||
| 50 | |||||||
| 100 | |||||||
| 50 | |||||||
| 100 | |||||||
| 50 | |||||||
| 50 | |||||||
| 50 | |||||||
| 50 | |||||||
| 50 | |||||||
| 50 | |||||||
| 50 | |||||||
| 0 | |||||||
| 0 | |||||||
| 623 | 30 | 44 | my $r = 0; | ||||
| 624 | 30 | 50 | 42 | if ($c) { | |||
| 625 | 0 | 0 | $r = $c; | ||||
| 626 | } else { | ||||||
| 627 | 30 | 59 | for(my $i = 0; $i < $n; $i++) { | ||||
| 628 | 40 | 114 | $r += int(rand($d)) + 1; | ||||
| 629 | } | ||||||
| 630 | 30 | 100 | 75 | $r *= $m||1; | |||
| 631 | 30 | 100 | 69 | $r += $p||0; | |||
| 632 | 30 | 50 | 33 | 55 | $r = $min if defined $min and $r < $min; | ||
| 633 | 30 | 50 | 33 | 53 | $r = $max if defined $max and $r > $max; | ||
| 634 | } | ||||||
| 635 | # $log->debug("rolling dice: $word = $r"); | ||||||
| 636 | 30 | 100 | 54 | $locals{$save_as} = $r if $save_as; | |||
| 637 | 30 | 50 | 79 | push(@descriptions, $r) unless $just_save; | |||
| 638 | } elsif (my ($save, $expression, $as) = $word =~ /$math_re/) { | ||||||
| 639 | 0 | 0 | my $r = eval($expression); | ||||
| 640 | 0 | 0 | 0 | $locals{$as} = $r if $as; | |||
| 641 | 0 | 0 | 0 | push(@descriptions, $r) unless $save; | |||
| 642 | } elsif ($word =~ /^(\S+)\?\|\|(.*)/) { | ||||||
| 643 | # [a?||b] return b if a is defined, or nothing | ||||||
| 644 | 0 | 0 | 0 | push(@descriptions, $2) if $locals{$1}; | |||
| 645 | } elsif ($word =~ /^!(\S+)\|\|(.*)/) { | ||||||
| 646 | # [!a||b] return b if a is undefined | ||||||
| 647 | 0 | 0 | 0 | push(@descriptions, $2) if not $locals{$1}; | |||
| 648 | } elsif (index($word, "||") != -1) { | ||||||
| 649 | # [a||b] returns a if defined, otherwise b | ||||||
| 650 | 0 | 0 | for my $html (split(/\|\|/, $word)) { | ||||
| 651 | 0 | 0 | my $copy = $html; | ||||
| 652 | 0 | 0 | $copy =~ s/<.*?>|…//g; # strip tags, e.g. span elements, and ellipsis | ||||
| 653 | 0 | 0 | 0 | if ($copy =~ /\S/) { | |||
| 654 | 0 | 0 | push(@descriptions, $html); | ||||
| 655 | 0 | 0 | last; | ||||
| 656 | } | ||||||
| 657 | } | ||||||
| 658 | } elsif (index($word, "|") != -1) { | ||||||
| 659 | # [a|b] returns one of a or b | ||||||
| 660 | 0 | 0 | push(@descriptions, one(split(/\|/, $word))); | ||||
| 661 | } elsif ($word =~ /^name for an? /) { | ||||||
| 662 | # for global things like factions, dukes | ||||||
| 663 | 2 | 5 | my $name = $names{$word}; | ||||
| 664 | # $log->debug("memoized: $word is $name") if $name; | ||||||
| 665 | 2 | 50 | 8 | return $name if $name; | |||
| 666 | 2 | 18 | $name = pick($map_data, $table_data, $level, $coordinates, $words, $word, $redirects); | ||||
| 667 | 2 | 50 | 7 | next unless $name; | |||
| 668 | 2 | 5 | $names{$word} = $name; | ||||
| 669 | # $log->debug("$word is $name"); | ||||||
| 670 | 2 | 3 | push(@descriptions, $name); | ||||
| 671 | } elsif ($word =~ /^names for (\S+)/) { | ||||||
| 672 | 0 | 0 | my $key = $1; # "river" | ||||
| 673 | # $log->debug("Looking at $key for $coordinates..."); | ||||||
| 674 | 0 | 0 | 0 | if (my @lines = grep { $_->{type} eq $key } @{$extra->{$coordinates}}) { | |||
| 0 | 0 | ||||||
| 0 | 0 | ||||||
| 675 | # $log->debug("...@lines"); | ||||||
| 676 | # make sure all the lines (rivers, trails) are named | ||||||
| 677 | 0 | 0 | my @names = (); | ||||
| 678 | 0 | 0 | for my $line (@lines) { | ||||
| 679 | 0 | 0 | my $name = $line->{name}; | ||||
| 680 | 0 | 0 | 0 | if (not $name) { | |||
| 681 | 0 | 0 | 0 | $name ||= pick($map_data, $table_data, $level, $coordinates, $words, "name for $key", $redirects); | |||
| 682 | 0 | 0 | $line->{name} = $name; | ||||
| 683 | } | ||||||
| 684 | 0 | 0 | push(@names, $name); | ||||
| 685 | } | ||||||
| 686 | 0 | 0 | my $list; | ||||
| 687 | 0 | 0 | 0 | if (@names > 2) { | |||
| 0 | |||||||
| 688 | 0 | 0 | $list = join(", ", @names[0 .. $#names-1], "and " . $names[-1]); | ||||
| 689 | } elsif (@names == 2) { | ||||||
| 690 | 0 | 0 | $list = join(" and ", @names); | ||||
| 691 | } else { | ||||||
| 692 | 0 | 0 | $log->error("$coordinates has merge but just one line (@lines)"); | ||||
| 693 | 0 | 0 | $list = shift(@names); | ||||
| 694 | } | ||||||
| 695 | 0 | 0 | 0 | $log->error("$coordinates uses merging rule without names") unless $list; | |||
| 696 | 0 | 0 | 0 | next unless $list; | |||
| 697 | 0 | 0 | push(@descriptions, $list); | ||||
| 698 | } | ||||||
| 699 | } elsif ($word =~ /^name for (\S+)/) { | ||||||
| 700 | 0 | 0 | my $key = $1; # "white" or "river" | ||||
| 701 | # $log->debug("Looking at $key for $coordinates..."); | ||||||
| 702 | 0 | 0 | 0 | if (my @lines = grep { $_->{type} eq $key } @{$extra->{$coordinates}}) { | |||
| 0 | 0 | ||||||
| 0 | 0 | ||||||
| 703 | # for rivers and the like: "name for river" | ||||||
| 704 | 0 | 0 | for my $line (@lines) { | ||||
| 705 | # $log->debug("Looking at $word for $coordinates..."); | ||||||
| 706 | 0 | 0 | my $name = $line->{name}; | ||||
| 707 | # $log->debug("... we already have a name: $name") if $name; | ||||||
| 708 | # if a type appears twice for a hex, this returns the same name for all of them | ||||||
| 709 | 0 | 0 | 0 | return $name if $name; | |||
| 710 | 0 | 0 | $name = pick($map_data, $table_data, $level, $coordinates, $words, $word, $redirects); | ||||
| 711 | # $log->debug("... we picked a new name: $name") if $name; | ||||||
| 712 | 0 | 0 | 0 | next unless $name; | |||
| 713 | 0 | 0 | push(@descriptions, $name); | ||||
| 714 | 0 | 0 | $line->{name} = $name; | ||||
| 715 | 0 | 0 | $globals->{$key}->{$_} = $name for @{$line->{line}}[1..$#{$line->{line}}]; | ||||
| 0 | 0 | ||||||
| 0 | 0 | ||||||
| 716 | # name the first one without a name, don't keep adding names | ||||||
| 717 | 0 | 0 | last; | ||||
| 718 | } | ||||||
| 719 | } else { | ||||||
| 720 | # regular features: "name for white big mountain" | ||||||
| 721 | 0 | 0 | my $name = $names{"$word: $coordinates"}; # "name for white big mountain: 0101" | ||||
| 722 | # $log->debug("$word for $coordinates is $name") if $name; | ||||||
| 723 | 0 | 0 | 0 | return $name if $name; | |||
| 724 | 0 | 0 | $name = pick($map_data, $table_data, $level, $coordinates, $words, $word, $redirects); | ||||
| 725 | # $log->debug("new $word for $coordinates is $name") if $name; | ||||||
| 726 | 0 | 0 | 0 | next unless $name; | |||
| 727 | 0 | 0 | $names{"$word: $coordinates"} = $name; | ||||
| 728 | 0 | 0 | push(@descriptions, $name); | ||||
| 729 | 0 | 0 | 0 | spread_name($map_data, $coordinates, $word, $key, $name) if %$map_data; | |||
| 730 | } | ||||||
| 731 | } elsif ($word eq 'adjacent hex') { | ||||||
| 732 | # experimental | ||||||
| 733 | 0 | 0 | 0 | my $location = $coordinates eq 'no map' ? 'somewhere' : one(neighbours($map_data, $coordinates)); | |||
| 734 | 0 | 0 | $locals{$word} = $location; | ||||
| 735 | 0 | 0 | return $location; | ||||
| 736 | } elsif ($word =~ /^(?:nearby|other|later) ./) { | ||||||
| 737 | # skip on the first pass | ||||||
| 738 | 1 | 7 | return "「$word」"; | ||||
| 739 | } elsif ($word =~ /^append (.*)/) { | ||||||
| 740 | 0 | 0 | my $text = pick($map_data, $table_data, $level, $coordinates, $words, $1, $redirects); | ||||
| 741 | # remember it's legitimate to have no result for a table | ||||||
| 742 | 0 | 0 | 0 | next unless $text; | |||
| 743 | 0 | 0 | $locals{$word} = $text; | ||||
| 744 | 0 | 0 | push(@descriptions, "「append $text」"); | ||||
| 745 | } elsif ($word =~ /^same (.+)/) { | ||||||
| 746 | 81 | 159 | my $key = $1; | ||||
| 747 | 81 | 50 | 33 | 291 | return $locals{$key}->[0] if exists $locals{$key} and ref($locals{$key}) eq 'ARRAY'; | ||
| 748 | 81 | 50 | 341 | return $locals{$key} if exists $locals{$key}; | |||
| 749 | 0 | 0 | 0 | 0 | return $globals->{$key}->{global} if $globals->{$key} and $globals->{$key}->{global}; | ||
| 750 | 0 | 0 | $log->warn("[same $key] is undefined for $coordinates, attempt picking a new one"); | ||||
| 751 | 0 | 0 | my $text = pick($map_data, $table_data, $level, $coordinates, $words, $key, $redirects); | ||||
| 752 | 0 | 0 | 0 | if ($text) { | |||
| 753 | 0 | 0 | $locals{$key} = $text; | ||||
| 754 | 0 | 0 | push(@descriptions, $text . "*"); | ||||
| 755 | } else { | ||||||
| 756 | 0 | 0 | $log->error("[$key] is undefined for $coordinates"); | ||||
| 757 | 0 | 0 | push(@descriptions, "…"); | ||||
| 758 | } | ||||||
| 759 | } elsif ($word =~ /^(?:(here|global) )?with (.+?)(?: as (.+))?$/) { | ||||||
| 760 | 0 | 0 | my ($where, $key, $alias) = ($1, $2, $3); | ||||
| 761 | 0 | 0 | my $text = pick($map_data, $table_data, $level, $coordinates, $words, $key, $redirects); | ||||
| 762 | 0 | 0 | 0 | next unless $text; | |||
| 763 | 0 | 0 | $locals{$key} = [$text]; # start a new list | ||||
| 764 | 0 | 0 | 0 | $locals{$alias} = $text if $alias; | |||
| 765 | 0 | 0 | 0 | 0 | $globals->{$key}->{$coordinates} = $text if $where and $where eq 'here'; | ||
| 766 | 0 | 0 | 0 | 0 | $globals->{$alias}->{$coordinates} = $text if $where and $where eq 'here' and $alias; | ||
| 0 | |||||||
| 767 | 0 | 0 | 0 | 0 | $globals->{$key}->{global} = $text if $where and $where eq 'global'; | ||
| 768 | 0 | 0 | 0 | 0 | $globals->{$alias}->{global} = $text if $where and $where eq 'global' and $alias; | ||
| 0 | |||||||
| 769 | 0 | 0 | push(@descriptions, $text); | ||||
| 770 | } elsif ($word =~ /^(?:(here|global) )?and (.+?)(?: as (.+))?$/) { | ||||||
| 771 | 0 | 0 | my ($where, $key, $alias) = ($1, $2, $3); | ||||
| 772 | 0 | 0 | my $found = 0; | ||||
| 773 | # limited attempts to find a unique entry for an existing list (instead of | ||||||
| 774 | # modifying the data structures) | ||||||
| 775 | 0 | 0 | for (1 .. 20) { | ||||
| 776 | 0 | 0 | my $text = pick($map_data, $table_data, $level, $coordinates, $words, $key, $redirects); | ||||
| 777 | 0 | 0 | 0 | $log->warn("[and $key] is used before [with $key] is done in $coordinates") if ref $locals{$key} ne 'ARRAY'; | |||
| 778 | 0 | 0 | 0 | $locals{$key} = [$text] if ref $locals{$key} ne 'ARRAY'; | |||
| 779 | 0 | 0 | 0 | 0 | next if not $text or grep { $text eq $_ } @{$locals{$key}}; | ||
| 0 | 0 | ||||||
| 0 | 0 | ||||||
| 780 | 0 | 0 | push(@{$locals{$key}}, $text); | ||||
| 0 | 0 | ||||||
| 781 | 0 | 0 | push(@descriptions, $text); | ||||
| 782 | 0 | 0 | 0 | $locals{$alias} = $text if $alias; | |||
| 783 | 0 | 0 | 0 | 0 | $globals->{$key}->{$coordinates} = $text if $where and $where eq 'here'; | ||
| 784 | 0 | 0 | 0 | 0 | $globals->{$alias}->{$coordinates} = $text if $where and $where eq 'here' and $alias; | ||
| 0 | |||||||
| 785 | 0 | 0 | 0 | 0 | $globals->{$key}->{global} = $text if $where and $where eq 'global'; | ||
| 786 | 0 | 0 | 0 | 0 | $globals->{$alias}->{global} = $text if $where and $where eq 'global' and $alias; | ||
| 0 | |||||||
| 787 | 0 | 0 | $found = 1; | ||||
| 788 | 0 | 0 | last; | ||||
| 789 | } | ||||||
| 790 | 0 | 0 | 0 | if (not $found) { | |||
| 791 | 0 | 0 | $log->warn("[and $key] not unique in $coordinates"); | ||||
| 792 | 0 | 0 | push(@descriptions, "…"); | ||||
| 793 | } | ||||||
| 794 | } elsif ($word =~ /^capitalize (.+)/) { | ||||||
| 795 | 0 | 0 | my $key = $1; | ||||
| 796 | 0 | 0 | my $text = pick($map_data, $table_data, $level, $coordinates, $words, $key, $redirects); | ||||
| 797 | 0 | 0 | 0 | next unless $text; | |||
| 798 | 0 | 0 | $locals{$key} = $text; | ||||
| 799 | 0 | 0 | push(@descriptions, ucfirst $text); | ||||
| 800 | } elsif ($word =~ /^titlecase (.+)/) { | ||||||
| 801 | 0 | 0 | my $key = $1; | ||||
| 802 | 0 | 0 | my $text = pick($map_data, $table_data, $level, $coordinates, $words, $key, $redirects); | ||||
| 803 | 0 | 0 | 0 | next unless $text; | |||
| 804 | 0 | 0 | $locals{$key} = $text; | ||||
| 805 | 0 | 0 | push(@descriptions, autoformat($text, { case => 'titlecase' })); | ||||
| 806 | } elsif ($word =~ /^highlightcase (.+)/) { | ||||||
| 807 | 0 | 0 | my $key = $1; | ||||
| 808 | 0 | 0 | my $text = pick($map_data, $table_data, $level, $coordinates, $words, $key, $redirects); | ||||
| 809 | 0 | 0 | 0 | next unless $text; | |||
| 810 | 0 | 0 | $locals{$key} = $text; | ||||
| 811 | 0 | 0 | push(@descriptions, autoformat($text, { case => 'highlight' })); | ||||
| 812 | } elsif ($word =~ /^normalize-elvish (.+)/) { | ||||||
| 813 | 0 | 0 | my $key = $1; | ||||
| 814 | 0 | 0 | my $text = normalize_elvish($key); | ||||
| 815 | 0 | 0 | 0 | next unless $text; | |||
| 816 | 0 | 0 | $locals{$key} = $text; | ||||
| 817 | 0 | 0 | push(@descriptions, $text); | ||||
| 818 | } elsif ($word =~ /^(?:(here|global) )?(?:(save|store|quote) )?(.+?)(?: as (.+))?$/) { | ||||||
| 819 | 305 | 802 | my ($where, $action, $key, $alias) = ($1, $2, $3, $4); | ||||
| 820 | 305 | 373 | my $text; | ||||
| 821 | 305 | 100 | 66 | 550 | if (not $action or $action eq "save") { | ||
| 822 | # no action and save are with lookup | ||||||
| 823 | 293 | 513 | $text = pick($map_data, $table_data, $level, $coordinates, $words, $key, $redirects); | ||||
| 824 | } else { | ||||||
| 825 | # quote and store are without lookup | ||||||
| 826 | 12 | 18 | $text = $key; | ||||
| 827 | } | ||||||
| 828 | 305 | 100 | 503 | next unless $text; | |||
| 829 | 229 | 423 | $locals{$key} = $text; | ||||
| 830 | 229 | 100 | 316 | $locals{$alias} = $text if $alias; | |||
| 831 | 229 | 100 | 66 | 354 | $globals->{$key}->{$coordinates} = $text if $where and $where eq 'here'; | ||
| 832 | 229 | 50 | 66 | 373 | $globals->{$alias}->{$coordinates} = $text if $where and $where eq 'here' and $alias; | ||
| 66 | |||||||
| 833 | 229 | 50 | 66 | 328 | $globals->{$key}->{global} = $text if $where and $where eq 'global'; | ||
| 834 | 229 | 50 | 66 | 363 | $globals->{$alias}->{global} = $text if $where and $where eq 'global' and $alias; | ||
| 33 | |||||||
| 835 | 229 | 100 | 66 | 549 | push(@descriptions, $text) if not $action or $action eq "quote"; | ||
| 836 | } elsif ($level > 1 and not exists $table_data->{$word} and not $locals{$word}) { | ||||||
| 837 | # on level one, many terrain types do not exist (e.g. river-start) | ||||||
| 838 | 0 | 0 | $log->error("unknown table for $coordinates/$level: $word"); | ||||
| 839 | } elsif ($level > 1 and not $table_data->{$word} and not $locals{$word}) { | ||||||
| 840 | # on level one, many terrain types do not exist (e.g. river-start) | ||||||
| 841 | 0 | 0 | $log->error("empty table for $coordinates/$level: $word"); | ||||
| 842 | } else { | ||||||
| 843 | 0 | 0 | my $text = pick($map_data, $table_data, $level, $coordinates, $words, $word, $redirects); | ||||
| 844 | # remember it's legitimate to have no result for a table, and remember we | ||||||
| 845 | # cannot use a local with the same name that's defined because sometimes | ||||||
| 846 | # locals are simply defined as "1" since they start out as "words" and I | ||||||
| 847 | # don't want to make "1" a special case to ignore, here | ||||||
| 848 | 0 | 0 | 0 | next unless defined $text; | |||
| 849 | 0 | 0 | $locals{$word} = $text; | ||||
| 850 | 0 | 0 | push(@descriptions, $text); | ||||
| 851 | } | ||||||
| 852 | } | ||||||
| 853 | 337 | 1234 | return join(' ', @descriptions); | ||||
| 854 | } | ||||||
| 855 | |||||||
| 856 | =item describe_text | ||||||
| 857 | |||||||
| 858 | This function does what C |
||||||
| 859 | coordinates. | ||||||
| 860 | |||||||
| 861 | =cut | ||||||
| 862 | |||||||
| 863 | sub describe_text { | ||||||
| 864 | 1 | 1 | 1 | 5 | my $input = shift; | ||
| 865 | 1 | 3 | my $table_data = shift; | ||||
| 866 | 1 | 2 | my $redirects = shift; | ||||
| 867 | 1 | 2 | my @descriptions; | ||||
| 868 | 1 | 5 | init(); | ||||
| 869 | 1 | 20 | for my $text (split(/\r?\n/, $input)) { | ||||
| 870 | # recusion level 2 makes sure we don't reset %locals | ||||||
| 871 | 10 | 45 | $text =~ s/\[(.*?)\]/describe({},$table_data,2,"no map",[$1],$redirects)/ge; | ||||
| 10 | 32 | ||||||
| 872 | 10 | 32 | push(@descriptions, process($text, $redirects)); | ||||
| 873 | 10 | 49 | %locals = (); # reset once per paragraph | ||||
| 874 | } | ||||||
| 875 | 1 | 7 | return \@descriptions; | ||||
| 876 | } | ||||||
| 877 | |||||||
| 878 | =item normalize_elvish | ||||||
| 879 | |||||||
| 880 | We do some post-processing of words, inspired by these two web pages, but using | ||||||
| 881 | our own replacements. | ||||||
| 882 | http://sindarinlessons.weebly.com/37---how-to-make-names-1.html | ||||||
| 883 | http://sindarinlessons.weebly.com/38---how-to-make-names-2.html | ||||||
| 884 | |||||||
| 885 | =cut | ||||||
| 886 | |||||||
| 887 | sub normalize_elvish { | ||||||
| 888 | 0 | 0 | 1 | 0 | my $original = shift; | ||
| 889 | 0 | 0 | my $name = $original; | ||||
| 890 | |||||||
| 891 | 0 | 0 | $name =~ s/(.) \1/$1/g; | ||||
| 892 | 0 | 0 | $name =~ s/d t/d/g; | ||||
| 893 | 0 | 0 | $name =~ s/a ui/au/g; | ||||
| 894 | 0 | 0 | $name =~ s/nd m/dhm/g; | ||||
| 895 | 0 | 0 | $name =~ s/n?d w/dhw/g; | ||||
| 896 | 0 | 0 | $name =~ s/r gw/rw/g; | ||||
| 897 | 0 | 0 | $name =~ s/^nd/d/; | ||||
| 898 | 0 | 0 | $name =~ s/^ng/g/; | ||||
| 899 | 0 | 0 | $name =~ s/th n?d/d/g; | ||||
| 900 | 0 | 0 | $name =~ s/dh dr/dhr/g; | ||||
| 901 | 0 | 0 | $name =~ s/ //g; | ||||
| 902 | |||||||
| 903 | 0 | 0 | $name =~ tr/âêîôûŷ/aeioúi/; | ||||
| 904 | 0 | 0 | $name =~ s/ll$/l/; | ||||
| 905 | 0 | 0 | $name =~ s/ben$/wen/g; | ||||
| 906 | 0 | 0 | $name =~ s/bwi$/wi/; | ||||
| 907 | 0 | 0 | $name =~ s/[^aeiouúi]ndil$/dil/g; | ||||
| 908 | 0 | 0 | $name =~ s/ae/aë/g; | ||||
| 909 | 0 | 0 | $name =~ s/ea/ëa/g; | ||||
| 910 | |||||||
| 911 | 0 | 0 | $name = ucfirst($name); | ||||
| 912 | |||||||
| 913 | # $log->debug("Elvish normalize: $original → $name"); | ||||||
| 914 | 0 | 0 | return $name; | ||||
| 915 | } | ||||||
| 916 | |||||||
| 917 | =item process | ||||||
| 918 | |||||||
| 919 | We do some post-processing after the description has been assembled: we move all | ||||||
| 920 | the IMG tags in a SPAN element with class "images". This makes it easier to lay | ||||||
| 921 | out the result using CSS. | ||||||
| 922 | |||||||
| 923 | =cut | ||||||
| 924 | |||||||
| 925 | sub process { | ||||||
| 926 | 10 | 10 | 1 | 13 | my $text = shift; | ||
| 927 | 10 | 12 | my $images = shift; | ||||
| 928 | 10 | 50 | 19 | if ($images) { | |||
| 929 | 10 | 105 | $text =~ s/( |
||||
| 930 | } else { | ||||||
| 931 | 0 | 0 | $text =~ s/( |
||||
| 932 | } | ||||||
| 933 | # fix whilespace at the end of spans | ||||||
| 934 | 10 | 172 | $text =~ s/\s+<\/span>/<\/span> /g; | ||||
| 935 | # strip empty paragraphs | ||||||
| 936 | 10 | 43 | $text =~ s/ \s*<\/p>//g; |
||||
| 937 | 10 | 39 | $text =~ s/ \s* / /g; |
||||
| 938 | # strip other empty elements | ||||||
| 939 | 10 | 38 | $text =~ s/<\/em>//g; | ||||
| 940 | 10 | 25 | return $text; | ||||
| 941 | } | ||||||
| 942 | |||||||
| 943 | =item resolve_appends | ||||||
| 944 | |||||||
| 945 | This removes text marked for appending and adds it at the end of a hex | ||||||
| 946 | description. This modifies the third parameter, C<$descriptions>. | ||||||
| 947 | |||||||
| 948 | =cut | ||||||
| 949 | |||||||
| 950 | sub resolve_appends { | ||||||
| 951 | 0 | 0 | 1 | my $map_data = shift; | |||
| 952 | 0 | my $table_data = shift; | |||||
| 953 | 0 | my $descriptions = shift; | |||||
| 954 | 0 | my $redirects = shift; | |||||
| 955 | 0 | my $text; | |||||
| 956 | 0 | for my $coord (keys %$descriptions) { | |||||
| 957 | 0 | while ($descriptions->{$coord} =~ s/「append ([^][」]*)」/$text = $1; ""/e) { | |||||
| 0 | |||||||
| 0 | |||||||
| 958 | 0 | $descriptions->{$coord} .= ' ' . $text; | |||||
| 959 | } | ||||||
| 960 | } | ||||||
| 961 | } | ||||||
| 962 | |||||||
| 963 | =item resolve_nearby | ||||||
| 964 | |||||||
| 965 | We have nearly everything resolved except for references starting with the word | ||||||
| 966 | "nearby" because these require all of the other data to be present. This | ||||||
| 967 | modifies the third parameter, C<$descriptions>. | ||||||
| 968 | |||||||
| 969 | =cut | ||||||
| 970 | |||||||
| 971 | sub resolve_nearby { | ||||||
| 972 | 0 | 0 | 1 | my $map_data = shift; | |||
| 973 | 0 | my $table_data = shift; | |||||
| 974 | 0 | my $descriptions = shift; | |||||
| 975 | 0 | my $redirects = shift; | |||||
| 976 | 0 | for my $coord (keys %$descriptions) { | |||||
| 977 | $descriptions->{$coord} =~ | ||||||
| 978 | 0 | 0 | s/「nearby ([^][」]*)」/closest($map_data,$table_data,$coord,$1, $redirects) or '…'/ge | ||||
| 979 | 0 | for 1 .. 2; # two levels deep of 「nearby ...」 | |||||
| 980 | 0 | $descriptions->{$coord} =~ s!( \(\d+\))!$1!g; # fixup | |||||
| 981 | } | ||||||
| 982 | } | ||||||
| 983 | |||||||
| 984 | =item closest | ||||||
| 985 | |||||||
| 986 | This picks the closest instance of whatever we're looking for, but not from the | ||||||
| 987 | same coordinates, obviously. | ||||||
| 988 | |||||||
| 989 | =cut | ||||||
| 990 | |||||||
| 991 | sub closest { | ||||||
| 992 | 0 | 0 | 1 | my $map_data = shift; | |||
| 993 | 0 | my $table_data = shift; | |||||
| 994 | 0 | my $coordinates = shift; | |||||
| 995 | 0 | my $key = shift; | |||||
| 996 | 0 | my $redirects = shift; | |||||
| 997 | 0 | 0 | my @coordinates = grep { $_ ne $coordinates and $_ ne 'global' } keys %{$globals->{$key}}; | ||||
| 0 | |||||||
| 0 | |||||||
| 998 | 0 | 0 | if (not @coordinates) { | ||||
| 999 | 0 | $log->info("Did not find any hex with $key ($coordinates)"); | |||||
| 1000 | 0 | return "…"; | |||||
| 1001 | } | ||||||
| 1002 | 0 | 0 | if ($coordinates !~ /^\d+$/) { | ||||
| 1003 | # if $coordinates is "TOP" or "END" or something like that, we cannot get | ||||||
| 1004 | # the closest one and we need to return a random one | ||||||
| 1005 | 0 | my $random = one(@coordinates); | |||||
| 1006 | 0 | return $globals->{$key}->{$random} | |||||
| 1007 | . qq{ ($random)}; # see resolve_later! | ||||||
| 1008 | } else { | ||||||
| 1009 | 0 | @coordinates = sort { distance($coordinates, $a) <=> distance($coordinates, $b) } @coordinates; | |||||
| 0 | |||||||
| 1010 | # the first one is the closest | ||||||
| 1011 | 0 | return $globals->{$key}->{$coordinates[0]} | |||||
| 1012 | . qq{ ($coordinates[0])}; # see resolve_later! | ||||||
| 1013 | } | ||||||
| 1014 | } | ||||||
| 1015 | |||||||
| 1016 | =item distance | ||||||
| 1017 | |||||||
| 1018 | Returns the distance between two hexes. Either provide two coordinates (strings | ||||||
| 1019 | in the form "0101", "0102") or four numbers (1, 1, 1, 2). | ||||||
| 1020 | |||||||
| 1021 | =cut | ||||||
| 1022 | |||||||
| 1023 | sub distance { | ||||||
| 1024 | 0 | 0 | 1 | my ($x1, $y1, $x2, $y2) = @_; | |||
| 1025 | 0 | 0 | if (@_ == 2) { | ||||
| 1026 | 0 | ($x1, $y1, $x2, $y2) = map { xy($_) } @_; | |||||
| 0 | |||||||
| 1027 | } | ||||||
| 1028 | # transform the coordinate system into a decent system with one axis tilted by | ||||||
| 1029 | # 60° | ||||||
| 1030 | 0 | $y1 = $y1 - POSIX::ceil($x1/2); | |||||
| 1031 | 0 | $y2 = $y2 - POSIX::ceil($x2/2); | |||||
| 1032 | 0 | 0 | if ($x1 > $x2) { | ||||
| 1033 | # only consider moves from left to right and transpose start and | ||||||
| 1034 | # end point to make it so | ||||||
| 1035 | 0 | my ($t1, $t2) = ($x1, $y1); | |||||
| 1036 | 0 | ($x1, $y1) = ($x2, $y2); | |||||
| 1037 | 0 | ($x2, $y2) = ($t1, $t2); | |||||
| 1038 | } | ||||||
| 1039 | 0 | 0 | if ($y2>=$y1) { | ||||
| 1040 | # if it the move has a downwards component add Δx and Δy | ||||||
| 1041 | 0 | return $x2-$x1 + $y2-$y1; | |||||
| 1042 | } else { | ||||||
| 1043 | # else just take the larger of Δx and Δy | ||||||
| 1044 | 0 | 0 | return $x2-$x1 > $y1-$y2 ? $x2-$x1 : $y1-$y2; | ||||
| 1045 | } | ||||||
| 1046 | } | ||||||
| 1047 | |||||||
| 1048 | =item resolve_other | ||||||
| 1049 | |||||||
| 1050 | This is a second phase. We have nearly everything resolved except for references | ||||||
| 1051 | starting with the word "other" because these require all of the other data to | ||||||
| 1052 | be present. This modifies the third parameter, C<$descriptions>. | ||||||
| 1053 | |||||||
| 1054 | =cut | ||||||
| 1055 | |||||||
| 1056 | sub resolve_other { | ||||||
| 1057 | 0 | 0 | 1 | my $map_data = shift; | |||
| 1058 | 0 | my $table_data = shift; | |||||
| 1059 | 0 | my $descriptions = shift; | |||||
| 1060 | 0 | my $redirects = shift; | |||||
| 1061 | 0 | for my $coord (keys %$descriptions) { | |||||
| 1062 | 0 | $descriptions->{$coord} =~ | |||||
| 1063 | 0 | 0 | s/「other ([^][」]*)」/some_other($map_data,$table_data,$coord,$1, $redirects) or '…'/ge; | ||||
| 1064 | 0 | $descriptions->{$coord} =~ s!( \(\d+\))!$1!g; # fixup | |||||
| 1065 | } | ||||||
| 1066 | } | ||||||
| 1067 | |||||||
| 1068 | =item some_other | ||||||
| 1069 | |||||||
| 1070 | This picks some other instance of whatever we're looking for, irrespective of distance. | ||||||
| 1071 | |||||||
| 1072 | =cut | ||||||
| 1073 | |||||||
| 1074 | sub some_other { | ||||||
| 1075 | 0 | 0 | 1 | my $map_data = shift; | |||
| 1076 | 0 | my $table_data = shift; | |||||
| 1077 | 0 | my $coordinates = shift; | |||||
| 1078 | 0 | my $key = shift; | |||||
| 1079 | 0 | my $redirects = shift; | |||||
| 1080 | # make sure we don't pick the same location! | ||||||
| 1081 | 0 | my @coordinates = grep !/$coordinates/, keys %{$globals->{$key}}; | |||||
| 0 | |||||||
| 1082 | 0 | 0 | if (not @coordinates) { | ||||
| 1083 | 0 | $log->info("Did not find any other hex with $key"); | |||||
| 1084 | 0 | return "…"; | |||||
| 1085 | } | ||||||
| 1086 | # just pick a random one | ||||||
| 1087 | 0 | my $other = one(@coordinates); | |||||
| 1088 | 0 | return $globals->{$key}->{$other} | |||||
| 1089 | . qq{ ($other)}; # see resolve_later! | ||||||
| 1090 | } | ||||||
| 1091 | |||||||
| 1092 | |||||||
| 1093 | =item resolve_later | ||||||
| 1094 | |||||||
| 1095 | This is a second phase. We have nearly everything resolved except for references | ||||||
| 1096 | starting with the word "later" because these require all of the other data to be | ||||||
| 1097 | present. This modifies the third parameter, C<$descriptions>. Use this for | ||||||
| 1098 | recursive lookup involving "nearby" and "other". | ||||||
| 1099 | |||||||
| 1100 | This also takes care of hex references introduced by "nearby" and "other". This | ||||||
| 1101 | is also why we need to take extra care to call C |
||||||
| 1102 | we want to search and replace: these hex references contain parenthesis! | ||||||
| 1103 | |||||||
| 1104 | =cut | ||||||
| 1105 | |||||||
| 1106 | sub resolve_later { | ||||||
| 1107 | 0 | 0 | 1 | my $map_data = shift; | |||
| 1108 | 0 | my $table_data = shift; | |||||
| 1109 | 0 | my $descriptions = shift; | |||||
| 1110 | 0 | my $redirects = shift; | |||||
| 1111 | 0 | for my $coord (keys %$descriptions) { | |||||
| 1112 | 0 | while ($descriptions->{$coord} =~ /「later ([^][」]*)」/) { | |||||
| 1113 | 0 | my $words = $1; | |||||
| 1114 | 0 | my ($ref) = $words =~ m!( \(.*\))!; | |||||
| 1115 | 0 | 0 | $ref //= ''; # but why should it ever be empty? | ||||
| 1116 | 0 | my $key = $words; | |||||
| 1117 | 0 | my $re = quotemeta($ref); | |||||
| 1118 | 0 | 0 | $key =~ s/$re// if $ref; | ||||
| 1119 | 0 | $re = quotemeta($words); | |||||
| 1120 | 0 | my $result = $descriptions->{$coord} =~ | |||||
| 1121 | 0 | 0 | s/「later $re」/describe($map_data,$table_data,1,$coord,[$key], $redirects) . $ref or '…'/ge; | ||||
| 1122 | 0 | 0 | if (not $result) { | ||||
| 1123 | 0 | $log->error("Could not resolve later reference in '$words'"); | |||||
| 1124 | 0 | last; # avoid infinite loops! | |||||
| 1125 | } | ||||||
| 1126 | } | ||||||
| 1127 | } | ||||||
| 1128 | } | ||||||
| 1129 | |||||||
| 1130 | =item describe_map | ||||||
| 1131 | |||||||
| 1132 | This is one of the top entry points: it simply calls C |
||||||
| 1133 | in C<$map_data> and calls C |
||||||
| 1134 | into a new hash where the hex coordinates are the key and the generated | ||||||
| 1135 | description is the value. | ||||||
| 1136 | |||||||
| 1137 | =cut | ||||||
| 1138 | |||||||
| 1139 | sub describe_map { | ||||||
| 1140 | 0 | 0 | 1 | my $map_data = shift; | |||
| 1141 | 0 | my $table_data = shift; | |||||
| 1142 | 0 | my $redirects = shift; | |||||
| 1143 | 0 | my %descriptions; | |||||
| 1144 | # first, add special rule for TOP and END keys which the description template knows | ||||||
| 1145 | 0 | for my $coords (qw(TOP END)) { | |||||
| 1146 | # with redirects means we keep images | ||||||
| 1147 | 0 | my $description = | |||||
| 1148 | process(describe($map_data, $table_data, 1, | ||||||
| 1149 | $coords, [$coords], $redirects), $redirects); | ||||||
| 1150 | # only set the TOP and END key if there is a description | ||||||
| 1151 | 0 | 0 | $descriptions{$coords} = $description if $description; | ||||
| 1152 | } | ||||||
| 1153 | # shuffle sort the coordinates so that it is reproducibly random | ||||||
| 1154 | 0 | for my $coord (shuffle sort keys %$map_data) { | |||||
| 1155 | # with redirects means we keep images | ||||||
| 1156 | my $description = | ||||||
| 1157 | process(describe($map_data, $table_data, 1, | ||||||
| 1158 | 0 | $coord, $map_data->{$coord}, $redirects), $redirects); | |||||
| 1159 | # only set the description if there is one (empty hexes are not listed) | ||||||
| 1160 | 0 | 0 | $descriptions{$coord} = $description if $description; | ||||
| 1161 | } | ||||||
| 1162 | 0 | resolve_nearby($map_data, $table_data, \%descriptions, $redirects); | |||||
| 1163 | 0 | resolve_other($map_data, $table_data, \%descriptions, $redirects); | |||||
| 1164 | 0 | resolve_later($map_data, $table_data, \%descriptions, $redirects); | |||||
| 1165 | # as append might include the items above, it must come last | ||||||
| 1166 | 0 | resolve_appends($map_data, $table_data, \%descriptions, $redirects); | |||||
| 1167 | 0 | return \%descriptions; | |||||
| 1168 | } | ||||||
| 1169 | |||||||
| 1170 | =item add_labels | ||||||
| 1171 | |||||||
| 1172 | This function is used after generating the descriptions to add the new names of | ||||||
| 1173 | rivers and trails to the existing map. | ||||||
| 1174 | |||||||
| 1175 | =cut | ||||||
| 1176 | |||||||
| 1177 | sub add_labels { | ||||||
| 1178 | 0 | 0 | 1 | my $map = shift; | |||
| 1179 | 0 | $map =~ s/$line_re/get_label($1,$2)/ge; | |||||
| 0 | |||||||
| 1180 | 0 | return $map; | |||||
| 1181 | } | ||||||
| 1182 | |||||||
| 1183 | =item get_label | ||||||
| 1184 | |||||||
| 1185 | This function returns the name of a line. | ||||||
| 1186 | |||||||
| 1187 | =cut | ||||||
| 1188 | |||||||
| 1189 | sub get_label { | ||||||
| 1190 | 0 | 0 | 1 | my $map_line = shift; | |||
| 1191 | 0 | my $type = shift; | |||||
| 1192 | 0 | my @hexes = split(/-/, $map_line); | |||||
| 1193 | LINE: | ||||||
| 1194 | 0 | for my $line (@{$extra->{$hexes[0]}}) { | |||||
| 0 | |||||||
| 1195 | 0 | 0 | next unless $line->{type} eq $type; | ||||
| 1196 | 0 | for my $hex (@hexes) { | |||||
| 1197 | 0 | my @line = @{$line->{line}}; | |||||
| 0 | |||||||
| 1198 | 0 | 0 | next LINE unless grep(/$hex/, @line); | ||||
| 1199 | } | ||||||
| 1200 | 0 | my $label = $line->{name}; | |||||
| 1201 | 0 | return qq{$map_line $type "$label"}; | |||||
| 1202 | } | ||||||
| 1203 | 0 | return qq{$map_line $type}; | |||||
| 1204 | } | ||||||
| 1205 | |||||||
| 1206 | =item xy | ||||||
| 1207 | |||||||
| 1208 | This is a helper function to turn "0101" into ("01", "01") which is equivalent | ||||||
| 1209 | to (1, 1). | ||||||
| 1210 | |||||||
| 1211 | =cut | ||||||
| 1212 | |||||||
| 1213 | sub xy { | ||||||
| 1214 | 0 | 0 | 1 | my $coordinates = shift; | |||
| 1215 | 0 | return (substr($coordinates, 0, 2), substr($coordinates, 2)); | |||||
| 1216 | } | ||||||
| 1217 | |||||||
| 1218 | =item coordinates | ||||||
| 1219 | |||||||
| 1220 | This is a helper function to turn (1, 1) back into "0101". | ||||||
| 1221 | |||||||
| 1222 | =cut | ||||||
| 1223 | |||||||
| 1224 | sub coordinates { | ||||||
| 1225 | 0 | 0 | 1 | my ($x, $y) = @_; | |||
| 1226 | 0 | return sprintf("%02d%02d", $x, $y); | |||||
| 1227 | } | ||||||
| 1228 | |||||||
| 1229 | =item neighbour | ||||||
| 1230 | |||||||
| 1231 | This is a helper function that takes the coordinates of a hex, a reference like | ||||||
| 1232 | [1,1] or regular coordinates like "0101", and a direction from 0 to 5, and | ||||||
| 1233 | returns the coordinates of the neighbouring hex in that direction. | ||||||
| 1234 | |||||||
| 1235 | =cut | ||||||
| 1236 | |||||||
| 1237 | my $delta = [[[-1, 0], [ 0, -1], [+1, 0], [+1, +1], [ 0, +1], [-1, +1]], # x is even | ||||||
| 1238 | [[-1, -1], [ 0, -1], [+1, -1], [+1, 0], [ 0, +1], [-1, 0]]]; # x is odd | ||||||
| 1239 | |||||||
| 1240 | sub neighbour { | ||||||
| 1241 | # $hex is [x,y] or "0101" and $i is a number 0 .. 5 | ||||||
| 1242 | 0 | 0 | 1 | my ($hex, $i) = @_; | |||
| 1243 | 0 | 0 | $hex = [xy($hex)] unless ref $hex; | ||||
| 1244 | # return is a string like "0102" | ||||||
| 1245 | 0 | return coordinates( | |||||
| 1246 | $hex->[0] + $delta->[$hex->[0] % 2]->[$i]->[0], | ||||||
| 1247 | $hex->[1] + $delta->[$hex->[0] % 2]->[$i]->[1]); | ||||||
| 1248 | } | ||||||
| 1249 | |||||||
| 1250 | =item neighbours | ||||||
| 1251 | |||||||
| 1252 | This is a helper function that takes map_data and the coordinates of a hex, a | ||||||
| 1253 | reference like [1,1] or regular coordinates like "0101", and returns a list of | ||||||
| 1254 | existing neighbours, or the string "[…]". This makes a difference at the edge of | ||||||
| 1255 | the map. | ||||||
| 1256 | |||||||
| 1257 | =cut | ||||||
| 1258 | |||||||
| 1259 | sub neighbours { | ||||||
| 1260 | 0 | 0 | 1 | my $map_data = shift; | |||
| 1261 | 0 | my $hex = shift; | |||||
| 1262 | 0 | my @neighbours; | |||||
| 1263 | 0 | 0 | $hex = [xy($hex)] unless ref $hex; | ||||
| 1264 | 0 | for my $i (0 .. 5) { | |||||
| 1265 | 0 | my $neighbour = neighbour($hex, $i); | |||||
| 1266 | # $log->debug($neighbour); | ||||||
| 1267 | 0 | 0 | push(@neighbours, $neighbour) if $map_data->{$neighbour}; | ||||
| 1268 | } | ||||||
| 1269 | 0 | 0 | return "..." unless @neighbours; | ||||
| 1270 | 0 | return @neighbours; | |||||
| 1271 | } | ||||||
| 1272 | |||||||
| 1273 | =item one | ||||||
| 1274 | |||||||
| 1275 | This is a helper function that picks a random element from a list. This works | ||||||
| 1276 | both for actual lists and for references to lists. | ||||||
| 1277 | |||||||
| 1278 | =cut | ||||||
| 1279 | |||||||
| 1280 | sub one { | ||||||
| 1281 | 0 | 0 | 1 | my @arr = @_; | |||
| 1282 | 0 | 0 | 0 | @arr = @{$arr[0]} if @arr == 1 and ref $arr[0] eq 'ARRAY'; | |||
| 0 | |||||||
| 1283 | 0 | return $arr[int(rand(scalar @arr))]; | |||||
| 1284 | } | ||||||
| 1285 | |||||||
| 1286 | =item one_step_to | ||||||
| 1287 | |||||||
| 1288 | Given a hex to start from, check all directions and figure out which neighbour | ||||||
| 1289 | is closer to your destination. Return the coordinates of this neighbour. | ||||||
| 1290 | |||||||
| 1291 | =cut | ||||||
| 1292 | |||||||
| 1293 | sub one_step_to { | ||||||
| 1294 | 0 | 0 | 1 | my $from = shift; | |||
| 1295 | 0 | my $to = shift; | |||||
| 1296 | 0 | my ($min, $best); | |||||
| 1297 | 0 | for my $i (0 .. 5) { | |||||
| 1298 | # make a new guess | ||||||
| 1299 | 0 | my ($x, $y) = ($from->[0] + $delta->[$from->[0] % 2]->[$i]->[0], | |||||
| 1300 | $from->[1] + $delta->[$from->[0] % 2]->[$i]->[1]); | ||||||
| 1301 | 0 | my $d = ($to->[0] - $x) * ($to->[0] - $x) | |||||
| 1302 | + ($to->[1] - $y) * ($to->[1] - $y); | ||||||
| 1303 | 0 | 0 | 0 | if (!defined($min) || $d < $min) { | |||
| 1304 | 0 | $min = $d; | |||||
| 1305 | 0 | $best = [$x, $y]; | |||||
| 1306 | } | ||||||
| 1307 | } | ||||||
| 1308 | 0 | return $best; | |||||
| 1309 | } | ||||||
| 1310 | |||||||
| 1311 | =item compute_missing_points | ||||||
| 1312 | |||||||
| 1313 | Return a list of coordinates in string form. Thus, given a list like ("0302", | ||||||
| 1314 | "0101") it will return ("0302", "0201", "0101"). | ||||||
| 1315 | |||||||
| 1316 | =cut | ||||||
| 1317 | |||||||
| 1318 | sub compute_missing_points { | ||||||
| 1319 | 0 | 0 | 1 | my @result = ($_[0]); # "0101" not [01,02] | |||
| 1320 | 0 | my @points = map { [xy($_)] } @_; | |||||
| 0 | |||||||
| 1321 | # $log->debug("Line: " . join(", ", map { coordinates(@$_) } @points)); | ||||||
| 1322 | 0 | my $from = shift(@points); | |||||
| 1323 | 0 | while (@points) { | |||||
| 1324 | # $log->debug("Going from " . coordinates(@$from) . " to " . coordinates(@{$points[0]})); | ||||||
| 1325 | 0 | $from = one_step_to($from, $points[0]); | |||||
| 1326 | 0 | 0 | 0 | shift(@points) if $from->[0] == $points[0]->[0] and $from->[1] == $points[0]->[1]; | |||
| 1327 | 0 | push(@result, coordinates(@$from)); | |||||
| 1328 | } | ||||||
| 1329 | 0 | return @result; | |||||
| 1330 | } | ||||||
| 1331 | |||||||
| 1332 | =item same_direction | ||||||
| 1333 | |||||||
| 1334 | Given two linear structures and a point of contact, return 1 if the these | ||||||
| 1335 | objects go in the same direction on way or the other. | ||||||
| 1336 | |||||||
| 1337 | =cut | ||||||
| 1338 | |||||||
| 1339 | sub same_direction { | ||||||
| 1340 | 0 | 0 | 1 | my $coord = shift; | |||
| 1341 | 0 | my $line1 = shift; | |||||
| 1342 | 0 | my $line2 = shift; | |||||
| 1343 | # $log->debug("same_direction: $coord, @$line1 and @$line2"); | ||||||
| 1344 | # this code assumes that a line starts with $type at index 0 | ||||||
| 1345 | 0 | my $j; | |||||
| 1346 | 0 | for my $i (1 .. $#$line1) { | |||||
| 1347 | 0 | 0 | if ($line1->[$i] eq $coord) { | ||||
| 1348 | 0 | $j = $i; | |||||
| 1349 | 0 | last; | |||||
| 1350 | } | ||||||
| 1351 | } | ||||||
| 1352 | # $log->debug("same_direction: $coord has index $j in @$line1"); | ||||||
| 1353 | 0 | for my $i1 ($j - 1, $j + 1) { | |||||
| 1354 | 0 | 0 | 0 | next if $i1 == 0 or $i1 > $#$line1; | |||
| 1355 | 0 | my $next = $line1->[$i1]; | |||||
| 1356 | 0 | for my $i2 (1 .. $#$line2) { | |||||
| 1357 | 0 | 0 | if ($line2->[$i2] eq $coord) { | ||||
| 1358 | 0 | 0 | 0 | if ($line2->[$i2-1] and $next eq $line2->[$i2-1] | |||
| 0 | |||||||
| 0 | |||||||
| 1359 | or $line2->[$i2+1] and $next eq $line2->[$i2+1]) { | ||||||
| 1360 | # $log->debug("same direction at $coord: @$line1 and @$line2"); | ||||||
| 1361 | 0 | return 1; | |||||
| 1362 | } | ||||||
| 1363 | } | ||||||
| 1364 | } | ||||||
| 1365 | } | ||||||
| 1366 | 0 | return 0; | |||||
| 1367 | } | ||||||
| 1368 | |||||||
| 1369 | =item spread_name | ||||||
| 1370 | |||||||
| 1371 | This function is used to spread a name along terrain features. | ||||||
| 1372 | |||||||
| 1373 | =cut | ||||||
| 1374 | |||||||
| 1375 | sub spread_name { | ||||||
| 1376 | 0 | 0 | 1 | my $map_data = shift; | |||
| 1377 | 0 | my $coordinates = shift; | |||||
| 1378 | 0 | my $word = shift; # "name for white big mountain" | |||||
| 1379 | 0 | my $key = shift; # "white" | |||||
| 1380 | 0 | my @keys = split(/\//, $key); # ("white") | |||||
| 1381 | 0 | my $name = shift; # "Vesuv" | |||||
| 1382 | 0 | my %seen = ($coordinates => 1); | |||||
| 1383 | 0 | $globals->{$key}->{$coordinates} = $name; | |||||
| 1384 | # $log->debug("$word: $coordinates = $name"); | ||||||
| 1385 | 0 | my @queue = map { neighbour($coordinates, $_) } 0..5; | |||||
| 0 | |||||||
| 1386 | 0 | while (@queue) { | |||||
| 1387 | # $log->debug("Working on the first item of @queue"); | ||||||
| 1388 | 0 | my $coord = shift(@queue); | |||||
| 1389 | 0 | 0 | 0 | next if $seen{$coord} or not $map_data->{$coord}; | |||
| 1390 | 0 | $seen{$coord} = 1; | |||||
| 1391 | 0 | 0 | if (intersect(@keys, @{$map_data->{$coord}})) { | ||||
| 0 | |||||||
| 1392 | $log->error("$word for $coord is already something else") | ||||||
| 1393 | 0 | 0 | if $names{"$word for $coord"}; | ||||
| 1394 | 0 | $names{"$word: $coord"} = $name; # "name for white big mountain: 0102" | |||||
| 1395 | # $log->debug("$coord: $name for @keys"); | ||||||
| 1396 | 0 | $globals->{$_}->{$coord} = $name for @keys; | |||||
| 1397 | # $log->debug("$word: $coord = $name"); | ||||||
| 1398 | 0 | push(@queue, map { neighbour($coord, $_) } 0..5); | |||||
| 0 | |||||||
| 1399 | } | ||||||
| 1400 | } | ||||||
| 1401 | } | ||||||
| 1402 | |||||||
| 1403 | =item markdown | ||||||
| 1404 | |||||||
| 1405 | This allows us to generate Markdown output. | ||||||
| 1406 | |||||||
| 1407 | =cut | ||||||
| 1408 | |||||||
| 1409 | sub markdown { | ||||||
| 1410 | 0 | 0 | 1 | my $descriptions = shift; | |||
| 1411 | 0 | 0 | my $separator = shift || "\n\n---\n\n"; | ||||
| 1412 | my @paragraphs = map { | ||||||
| 1413 | # remove inline images | ||||||
| 1414 | 0 | s! |
|||||
| 0 | |||||||
| 1415 | # empty spans left after img has been removed | ||||||
| 1416 | 0 | s!]*>\s*!!g; | |||||
| 1417 | # remaining spans result in Japanese brackets around their text | ||||||
| 1418 | 0 | s!]*>\s*!「!g; | |||||
| 1419 | 0 | s!\s*!」!g; | |||||
| 1420 | # emphasis | ||||||
| 1421 | 0 | s!?(strong|b)>!**!g; | |||||
| 1422 | 0 | s!?(em|i)>!*!g; | |||||
| 1423 | 0 | s!?u>!_!g; | |||||
| 1424 | # remove links but leave their text | ||||||
| 1425 | 0 | s!?a\b[^>]*>!!g; | |||||
| 1426 | # closing paragraph tags are optional | ||||||
| 1427 | 0 | s!!!g; | |||||
| 1428 | # paragraph breaks | ||||||
| 1429 | 0 | s! |
|||||
| 1430 | # blockquotes | ||||||
| 1431 | 0 | s!(.*?)!local $_ = $1; s/^/\n> /g; $_!gem; |
|||||
| 0 | |||||||
| 0 | |||||||
| 0 | |||||||
| 1432 | # unreplaced references (nearby, other, later) | ||||||
| 1433 | 0 | s!(.*?)!$1!g; | |||||
| 1434 | # return what's left | ||||||
| 1435 | 0 | markdown_lists($_); | |||||
| 1436 | } @$descriptions; | ||||||
| 1437 | 0 | return join($separator, @paragraphs); | |||||
| 1438 | } | ||||||
| 1439 | |||||||
| 1440 | sub markdown_lists { | ||||||
| 1441 | 0 | 0 | 0 | $_ = shift; | |||
| 1442 | 0 | my ($str, @list); | |||||
| 1443 | 0 | for (split(/(<.*?>)/)) { | |||||
| 1444 | 0 | 0 | if (/^ |
||||
| 0 | 0 | ||||||
| 0 | 0 | ||||||
| 0 | |||||||
| 0 | |||||||
| 1445 | 0 | elsif (/^ |
|||||
| 0 | |||||||
| 1446 | 0 | elsif (/^ |
|||||
| 1447 | 0 | elsif (/^<\/(ol|ul)>$/) { shift(@list) } | |||||
| 1448 | 0 | 0 | elsif (/^<\/li>$/) { $str .= "\n" unless $str =~ /\n$/ } | ||||
| 1449 | 0 | else { $str .= $_ } | |||||
| 1450 | } | ||||||
| 1451 | 0 | return $str; | |||||
| 1452 | } | ||||||
| 1453 | |||||||
| 1454 | 1; |