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/(]+?>)/$1<\/span>/g; | ||||
931 | } else { | ||||||
932 | 3 | 640 | $text =~ s/(]+?>)//g; | ||||
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!]*>!!g; | ||||
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; |