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