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