File Coverage

blib/lib/Game/TextMapper/Schroeder/Alpine.pm
Criterion Covered Total %
statement 498 552 90.2
branch 197 248 79.4
condition 90 123 73.1
subroutine 50 51 98.0
pod 0 28 0.0
total 835 1002 83.3


line stmt bran cond sub pod time code
1             # Copyright (C) 2009-2021 Alex Schroeder
2             #
3             # This program is free software: you can redistribute it and/or modify it under
4             # the terms of the GNU Affero General Public License as published by the Free
5             # Software Foundation, either version 3 of the License, or (at your option) any
6             # later version.
7             #
8             # This program is distributed in the hope that it will be useful, but WITHOUT
9             # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10             # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
11             # details.
12             #
13             # You should have received a copy of the GNU Affero General Public License along
14             # with this program. If not, see .
15              
16             =encoding utf8
17              
18             =head1 NAME
19              
20             Game::TextMapper::Alpine - generate an alpine landscape
21              
22             =head1 DESCRIPTION
23              
24             This fills the map with some mountains and then traces the water flow down to
25             the sea and off the map. With water, forests grow; but if the area remains at
26             the same altitude, swamps form.
27              
28             Settlements are placed at random in the habitable zones, but far enough from
29             each other, and connected by trails.
30              
31             In order to support hex and square maps, this class uses roles to implement
32             coordinates, neighbours, and all that. This is why you need to specify the role
33             before creating an instance of this class:
34              
35             return Game::TextMapper::Schroeder::Alpine
36             ->with_roles('Game::TextMapper::Schroeder::Hex')->new()
37             ->generate_map(@params);
38              
39             =head1 SEE ALSO
40              
41             L
42             L
43             L
44              
45             =cut
46              
47             package Game::TextMapper::Schroeder::Alpine;
48 1     1   7 use Game::TextMapper::Log;
  1         2  
  1         35  
49 1     1   5 use Modern::Perl '2018';
  1         2  
  1         6  
50 1     1   142 use Mojo::Base -base;
  1         2  
  1         6  
51 1     1   594 use Role::Tiny::With;
  1         218  
  1         52  
52             with 'Game::TextMapper::Schroeder::Base';
53 1     1   5 use List::Util 'shuffle';
  1         3  
  1         8189  
54              
55             my $log = Game::TextMapper::Log->get;
56              
57             has 'steepness';
58             has 'peaks';
59             has 'peak';
60             has 'bumps';
61             has 'bump';
62             has 'bottom';
63             has 'arid';
64             has 'wind';
65              
66             sub place_peak {
67 2     2 0 21 my $self = shift;
68 2         142 my $altitude = shift;
69 2         5 my $count = shift;
70 2         13 my $current_altitude = shift;
71 2         4 my @queue;
72             # place some peaks and put them in a queue
73 2         6 for (1 .. $count) {
74             # try to find an empty hex
75 14         21 for (1 .. 6) {
76 14         26 my $x = int(rand($self->width)) + 1;
77 14         59 my $y = int(rand($self->height)) + 1;
78 14         55 my $coordinates = coordinates($x, $y);
79 14 50       32 next if $altitude->{$coordinates};
80 14         18 $altitude->{$coordinates} = $current_altitude;
81 14         41 $log->debug("placed $current_altitude at $coordinates");
82 14         91 push(@queue, $coordinates);
83 14         26 last;
84             }
85             }
86 2         10 return @queue;
87             }
88              
89             sub grow_mountains {
90 2     2 0 5 my $self = shift;
91 2         5 my $altitude = shift;
92 2         8 my @queue = @_;
93             # go through the queue and add adjacent lower altitude hexes, if possible; the
94             # hexes added are to the end of the queue
95 2         8 while (@queue) {
96 600         1544 my $coordinates = shift @queue;
97 600         903 my $current_altitude = $altitude->{$coordinates};
98 600 50       911 next unless $current_altitude > 0;
99             # pick some random neighbors based on variable steepness
100 600         1120 my $n = $self->steepness;
101             # round up based on fraction
102 600 50       2314 $n += 1 if rand() < $n - int($n);
103 600         693 $n = int($n);
104 600 50       887 next if $n < 1;
105 600         908 for (1 .. $n) {
106             # try to find an empty neighbor; abort after six attempts
107 1800         3522 for (1 .. 6) {
108 8677         23672 my ($x, $y) = $self->neighbor($coordinates, $self->random_neighbor());
109 8677 100       15878 next unless $self->legal($x, $y);
110 7915         71143 my $other = coordinates($x, $y);
111             # if this is taken, look further
112 7915 100       14964 if ($altitude->{$other}) {
113 7714         13351 ($x, $y) = $self->neighbor2($coordinates, $self->random_neighbor2());
114 7714 100       14664 next unless $self->legal($x, $y);
115 6738         59454 $other = coordinates($x, $y);
116             # if this is also taken, try again
117 6738 100       16161 next if $altitude->{$other};
118             }
119             # if we found an empty neighbor, set its altitude
120 586 50       983 $altitude->{$other} = $current_altitude > 0 ? $current_altitude - 1 : 0;
121 586         888 push(@queue, $other);
122 586         1009 last;
123             }
124             }
125             }
126             }
127              
128             sub fix_altitude {
129 2     2 0 4 my $self = shift;
130 2         6 my $altitude = shift;
131             # go through all the hexes
132 2         292 for my $coordinates (sort keys %$altitude) {
133             # find hexes that we missed and give them the height of a random neighbor
134 600 50       881 if (not defined $altitude->{$coordinates}) {
135             # warn "identified a hex that was skipped: $coordinates\n";
136             # try to find a suitable neighbor
137 0         0 for (1 .. 6) {
138 0         0 my ($x, $y) = $self->neighbor($coordinates, $self->random_neighbor());
139 0 0       0 next unless $self->legal($x, $y);
140 0         0 my $other = coordinates($x, $y);
141 0 0       0 next unless defined $altitude->{$other};
142 0         0 $altitude->{$coordinates} = $altitude->{$other};
143 0         0 last;
144             }
145             # if we didn't find one in the last six attempts, just make it hole in the ground
146 0 0       0 if (not defined $altitude->{$coordinates}) {
147 0         0 $altitude->{$coordinates} = 0;
148             }
149             }
150             }
151             }
152              
153             sub altitude {
154 2     2 0 5 my $self = shift;
155 2         6 my ($world, $altitude) = @_;
156 2         9 my @queue = $self->place_peak($altitude, $self->peaks, $self->peak);
157 2         15 $self->grow_mountains($altitude, @queue);
158 2         44 $self->fix_altitude($altitude);
159             # note height for debugging purposes
160 2         171 for my $coordinates (sort keys %$altitude) {
161 600         1620 $world->{$coordinates} = "height$altitude->{$coordinates}";
162             }
163             }
164              
165             sub bumpiness {
166 2     2 0 7 my ($self, $world, $altitude) = @_;
167 2         13 for (1 .. $self->bumps) {
168 14         41 for my $delta (-$self->bump, $self->bump) {
169             # six attempts to try and find a good hex
170 28         96 for (1 .. 6) {
171 29         51 my $x = int(rand($self->width)) + 1;
172 29         122 my $y = int(rand($self->height)) + 1;
173 29         114 my $coordinates = coordinates($x, $y);
174 29         49 my $current_altitude = $altitude->{$coordinates} + $delta;
175 29 100 66     76 next if $current_altitude > 10 or $current_altitude < 0;
176             # bump it up or down
177 28         45 $altitude->{$coordinates} = $current_altitude;
178 28         51 $world->{$coordinates} = "height$altitude->{$coordinates} zone";
179 28         115 $log->debug("bumped altitude of $coordinates by $delta to $current_altitude");
180             # if the bump was +2 or -2, bump the neighbours by +1 or -1
181 28 50 66     198 if ($delta < -1 or $delta > 1) {
182 28         47 my $delta = $delta - $delta / abs($delta);
183 28         67 for my $i ($self->neighbors()) {
184 140         750 my ($x, $y) = $self->neighbor($coordinates, $i);
185 140 100       251 next unless $self->legal($x, $y);
186 119         1073 my $other = coordinates($x, $y);
187 119         186 $current_altitude = $altitude->{$other} + $delta;
188 119 100 66     284 next if $current_altitude > 10 or $current_altitude < 0;
189 118         151 $altitude->{$other} = $current_altitude;
190 118         208 $world->{$other} = "height$altitude->{$other} zone";
191 118         297 $log->debug("$i bumped altitude of $other by $delta to $current_altitude");
192             }
193             }
194             # if we have found a good hex, don't go through all the other attempts
195 28         210 last;
196             }
197             }
198             }
199             }
200              
201             sub water {
202 2     2 0 4 my $self = shift;
203 2         6 my ($world, $altitude, $water) = @_;
204             # reset in case we run this twice
205             # go through all the hexes
206 2         161 for my $coordinates (sort keys %$altitude) {
207 600 50       1303 next if $altitude->{$coordinates} <= $self->bottom;
208             # note preferred water flow by identifying lower lying neighbors
209 600         2179 my ($lowest, $direction);
210             # look at neighbors in random order
211             NEIGHBOR:
212 600         1153 for my $i (shuffle $self->neighbors()) {
213 3000         8869 my ($x, $y) = $self->neighbor($coordinates, $i);
214 3000         5845 my $legal = $self->legal($x, $y);
215 3000         25441 my $other = coordinates($x, $y);
216 3000 100 100     9022 next if $legal and $altitude->{$other} > $altitude->{$coordinates};
217             # don't point head on to another arrow
218 2214 100 100     6810 next if $legal and $water->{$other} and $water->{$other} == ($i-3) % 6;
      100        
219             # don't point into loops
220 2128         4338 my %loop = ($coordinates => 1, $other => 1);
221 2128         2634 my $next = $other;
222 2128         6071 $log->debug("Loop detection starting with $coordinates and $other");
223 2128         11856 while ($next) {
224             # no water flow known is also good;
225 4059   100     13285 $log->debug("water for $next: " . ($water->{$next} || "none"));
226 4059 100       20643 last unless defined $water->{$next};
227 2338         4482 ($x, $y) = $self->neighbor($next, $water->{$next});
228             # leaving the map is good
229 2338         5407 $log->debug("legal for $next: " . $self->legal($x, $y));
230 2338 100       30627 last unless $self->legal($x, $y);
231 1983         17358 $next = coordinates($x, $y);
232             # skip this neighbor if this is a loop
233 1983   100     7681 $log->debug("is $next in a loop? " . ($loop{$next} || "no"));
234 1983 100       10851 next NEIGHBOR if $loop{$next};
235 1931         3569 $loop{$next} = 1;
236             }
237 2076 100 66     10000 if (not defined $direction
      66        
      100        
      66        
238             or not $legal and $altitude->{$coordinates} < $lowest
239             or $legal and $altitude->{$other} < $lowest) {
240 799 100       1357 $lowest = $legal ? $altitude->{$other} : $altitude->{$coordinates};
241 799         946 $direction = $i;
242 799         1748 $log->debug("Set lowest to $lowest ($direction)");
243             }
244             }
245 600 100       1276 if (defined $direction) {
246 585         1053 $water->{$coordinates} = $direction;
247             $world->{$coordinates} =~ s/arrow\d/arrow$water->{$coordinates}/
248 585 50       2094 or $world->{$coordinates} .= " arrow$water->{$coordinates}";
249             }
250             }
251             }
252              
253             sub mountains {
254 2     2 0 6 my $self = shift;
255 2         6 my ($world, $altitude) = @_;
256             # place the types
257 2         54 for my $coordinates (keys %$altitude) {
258 600 100       1124 if ($altitude->{$coordinates} >= 10) {
    100          
    100          
259 21         31 $world->{$coordinates} = "white mountains";
260             } elsif ($altitude->{$coordinates} >= 9) {
261 43         59 $world->{$coordinates} = "white mountain";
262             } elsif ($altitude->{$coordinates} >= 8) {
263 119         157 $world->{$coordinates} = "light-grey mountain";
264             }
265             }
266             }
267              
268             sub ocean {
269 2     2 0 4 my $self = shift;
270 2         6 my ($world, $altitude) = @_;
271 2         187 for my $coordinates (sort keys %$altitude) {
272 600 50       1900 if ($altitude->{$coordinates} <= $self->bottom) {
273 0         0 my $ocean = 1;
274 0         0 for my $i ($self->neighbors()) {
275 0         0 my ($x, $y) = $self->neighbor($coordinates, $i);
276 0 0       0 next unless $self->legal($x, $y);
277 0         0 my $other = coordinates($x, $y);
278 0 0       0 next if $altitude->{$other} <= $self->bottom;
279 0         0 $ocean = 0;
280             }
281 0 0       0 $world->{$coordinates} = $ocean ? "ocean" : "water";
282             }
283             }
284             }
285              
286             sub lakes {
287 2     2 0 5 my $self = shift;
288 2         7 my ($world, $altitude, $water) = @_;
289             # any areas without water flow are lakes
290 2         184 for my $coordinates (sort keys %$altitude) {
291 600 100 66     1001 if (not defined $water->{$coordinates}
292             and $world->{$coordinates} ne "ocean") {
293 15         23 $world->{$coordinates} = "water";
294             }
295             }
296             }
297              
298             sub swamps {
299             # any area with water flowing to a neighbor at the same altitude is a swamp
300 2     2 0 9 my ($self, $world, $altitude, $water, $flow, $dry) = @_;
301 2         46 for my $coordinates (keys %$altitude) {
302             # don't turn lakes into swamps and skip bogs
303 600 100       1403 next if $world->{$coordinates} =~ /ocean|water|swamp|grass/;
304             # swamps require a river
305 547 100       828 next unless $flow->{$coordinates};
306             # no swamps when there is a canyon
307 219 100       341 next if $dry->{$coordinates};
308             # look at the neighbor the water would flow to
309 191         374 my ($x, $y) = $self->neighbor($coordinates, $water->{$coordinates});
310             # skip if water flows off the map
311 191 100       354 next unless $self->legal($x, $y);
312 159         1479 my $other = coordinates($x, $y);
313             # skip if water flows downhill
314 159 100       364 next if $altitude->{$coordinates} > $altitude->{$other};
315             # if there was no lower neighbor, this is a swamp
316 52 100       91 if ($altitude->{$coordinates} >= 6) {
317 29         107 $world->{$coordinates} =~ s/height\d+/grey swamp/;
318             } else {
319 23         88 $world->{$coordinates} =~ s/height\d+/dark-grey swamp/;
320             }
321             }
322             }
323              
324             sub flood {
325 2     2 0 7 my $self = shift;
326 2         9 my ($world, $altitude, $water) = @_;
327             # backtracking information: $from = $flow{$to}
328 2         5 my %flow;
329             # allow easy skipping
330             my %seen;
331             # start with a list of hexes to look at; as always, keys is a source of
332             # randomness that's independent of srand which is why we shuffle sort
333 2         44 my @lakes = shuffle sort grep { not defined $water->{$_} } keys %$world;
  600         829  
334 2 50       24 return unless @lakes;
335 2         6 my $start = shift(@lakes);
336 2         15 my @candidates = ($start);
337 2         6 while (@candidates) {
338             # Prefer candidates outside the map with altitude 0; reshuffle because
339             # candidates at the same height are all equal and early or late discoveries
340             # should not matter (not shuffling means it matters whether candidates are
341             # pushed or unshifted because this is a stable sort)
342             @candidates = sort {
343 146   100     468 ($altitude->{$a}||0) <=> ($altitude->{$b}||0)
  10475   100     19239  
344             } shuffle @candidates;
345 146         655 $log->debug("Candidates @candidates");
346 146         802 my $coordinates;
347             do {
348 150         466 $coordinates = shift(@candidates);
349 146   66     181 } until not $coordinates or not $seen{$coordinates};
350 146 50       226 last unless $coordinates;
351 146         206 $seen{$coordinates} = 1;
352 146         369 $log->debug("Looking at $coordinates");
353 146 100 66     803 if ($self->legal($coordinates) and $world->{$coordinates} ne "ocean") {
354             # if we're still on the map, check all the unknown neighbors
355 133         1366 my $from = $coordinates;
356 133         249 for my $i ($self->neighbors()) {
357 652         1134 my $to = coordinates($self->neighbor($from, $i));
358 652 100       1258 next if $seen{$to};
359 518         1348 $log->debug("Adding $to to our candidates");
360 518         2845 $flow{$to} = $from;
361             # adding to the front as we keep pushing forward (I hope)
362 518         819 push(@candidates, $to);
363             }
364 133         278 next;
365             }
366 13         126 $log->debug("We left the map at $coordinates");
367 13         68 my $to = $coordinates;
368 13         19 my $from = $flow{$to};
369 13         27 while ($from) {
370 108         219 my $i = $self->direction($from, $to);
371 108 100 100     334 if (not defined $water->{$from}
372             or $water->{$from} != $i) {
373 53         161 $log->debug("Arrow for $from now points to $to");
374 53         290 $water->{$from} = $i;
375             $world->{$from} =~ s/arrow\d/arrow$i/
376 53 100       274 or $world->{$from} .= " arrow$i";
377             } else {
378 55         138 $log->debug("Arrow for $from already points $to");
379             }
380 108         392 $to = $from;
381 108         191 $from = $flow{$to};
382             }
383             # pick the next lake
384             do {
385 15         33 $start = shift(@lakes);
386 15 100       54 $log->debug("Next lake is $start") if $start;
387 13   100     20 } until not $start or not defined $water->{$start};
388 13 100       113 last unless $start;
389 11         56 %seen = %flow = ();
390 11         38 @candidates = ($start);
391             }
392             }
393              
394             sub rivers {
395 2     2 0 8 my ($self, $world, $altitude, $water, $flow, $rivers) = @_;
396             # $flow are the sources points of rivers, or 1 if a river flows through them
397             my @growing = map {
398 104 100       257 $world->{$_} = "light-grey forest-hill" unless $world->{$_} =~ /mountain|swamp|grass|water|ocean/;
399 104         233 $flow->{$_} = [$_]
400             } sort grep {
401             # these are the potential starting places: up in the mountains below the
402             # ice, or lakes
403 2         48 ($altitude->{$_} == 7 or $altitude->{$_} == 8
404             or $world->{$_} =~ /water/ and $altitude->{$_} > $self->bottom)
405             and not $flow->{$_}
406 600 50 100     2438 and $world->{$_} !~ /dry/;
      66        
407             } keys %$altitude;
408 2         34 $self->grow_rivers(\@growing, $water, $flow, $rivers);
409             }
410              
411             sub grow_rivers {
412 2     2 0 9 my ($self, $growing, $water, $flow, $rivers) = @_;
413 2         16 while (@$growing) {
414             # warn "Rivers: " . @growing . "\n";
415             # pick a random growing river and grow it
416 740         1299 my $n = int(rand(scalar @$growing));
417 740         937 my $river = $growing->[$n];
418             # warn "Picking @$river\n";
419 740         869 my $coordinates = $river->[-1];
420 740         816 my $end = 1;
421 740 100       1105 if (defined $water->{$coordinates}) {
422 647         1309 my $other = coordinates($self->neighbor($coordinates, $water->{$coordinates}));
423 647 50       5340 die "Adding $other leads to an infinite loop in river @$river\n" if grep /$other/, @$river;
424             # if we flowed into a hex with a river
425 647 100       1294 if (ref $flow->{$other}) {
426             # warn "Prepending @$river to @{$flow->{$other}}\n";
427             # prepend the current river to the other river
428 11         18 unshift(@{$flow->{$other}}, @$river);
  11         28  
429             # move the source marker
430 11         21 $flow->{$river->[0]} = $flow->{$other};
431 11         16 $flow->{$other} = 1;
432             # and remove the current river from the growing list
433 11         31 splice(@$growing, $n, 1);
434             # warn "Flow at $river->[0]: @{$flow->{$river->[0]}}\n";
435             # warn "Flow at $other: $flow->{$other}\n";
436             } else {
437 636         876 $flow->{$coordinates} = 1;
438 636         1555 push(@$river, $other);
439             }
440             } else {
441             # stop growing this river
442             # warn "Stopped river: @$river\n" if grep(/0914/, @$river);
443 93         215 push(@$rivers, splice(@$growing, $n, 1));
444             }
445             }
446             }
447              
448             sub canyons {
449 2     2 0 6 my $self = shift;
450 2         8 my ($world, $altitude, $rivers, $canyons, $dry) = @_;
451             # using a reference to an array so that we can leave pointers in the %seen hash
452 2         5 my $canyon = [];
453             # remember which canyon flows through which hex
454 2         5 my %seen;
455 2         6 for my $river (@$rivers) {
456 93         128 my $last = $river->[0];
457 93         128 my $current_altitude = $altitude->{$last};
458 93         296 $log->debug("Looking at @$river ($current_altitude)");
459 93         451 for my $coordinates (@$river) {
460 481         1029 $log->debug("Looking at $coordinates");
461 481 100       2288 if ($seen{$coordinates}) {
462             # the rest of this river was already looked at, so there is no need to
463             # do the rest of this river; if we're in a canyon, prepend it to the one
464             # we just found before ending
465 23 50       40 if (@$canyon) {
466 0         0 my @other = @{$seen{$coordinates}};
  0         0  
467 0 0       0 if ($other[0] eq $canyon->[-1]) {
468 0         0 $log->debug("Canyon @$canyon of river @$river merging with @other at $coordinates");
469 0         0 unshift(@{$seen{$coordinates}}, @$canyon[0 .. @$canyon - 2]);
  0         0  
470             } else {
471 0         0 $log->debug("Canyon @$canyon of river @$river stumbled upon existing canyon @other at $coordinates");
472 0         0 while (@other) {
473 0         0 my $other = shift(@other);
474 0 0       0 next if $other ne $coordinates;
475 0         0 push(@$canyon, $other, @other);
476 0         0 last;
477             }
478 0         0 $log->debug("Canyon @$canyon");
479 0         0 push(@$canyons, $canyon);
480             }
481 0         0 $canyon = [];
482             }
483 23         29 $log->debug("We've seen the rest: @{$seen{$coordinates}}");
  23         70  
484 23         115 last;
485             }
486             # no canyons through water!
487 458 100 100     1231 if ($altitude->{$coordinates} and $current_altitude < $altitude->{$coordinates}
      66        
488             and $world->{$coordinates} !~ /water|ocean/) {
489             # river is digging a canyon; if this not the start of the river and it
490             # is the start of a canyon, prepend the last step
491 30 100       65 push(@$canyon, $last) unless @$canyon;
492 30         48 push(@$canyon, $coordinates);
493 30 50       64 $world->{$coordinates} .= " zone" unless $dry->{$coordinates};
494 30         42 $dry->{$coordinates} = 1;
495 30         93 $log->debug("Growing canyon @$canyon");
496 30         156 $seen{$coordinates} = $canyon;
497             } else {
498             # if we just left a canyon, append the current step
499 428 100       638 if (@$canyon) {
500 11         22 push(@$canyon, $coordinates);
501 11         14 push(@$canyons, $canyon);
502 11         37 $log->debug("Looking at river @$river");
503 11         85 $log->debug("Canyon @$canyon");
504 11         65 $canyon = [];
505 11         20 last;
506             }
507             # not digging a canyon
508 417         501 $last = $coordinates;
509 417         597 $current_altitude = $altitude->{$coordinates};
510             }
511             }
512             }
513             }
514              
515             sub wet {
516 0     0 0 0 my $self = shift;
517             # a hex is wet if there is a river, a swamp or a forest within 2 hexes
518 0         0 my ($coordinates, $world, $flow) = @_;
519 0         0 for my $i ($self->neighbors()) {
520 0         0 my ($x, $y) = $self->neighbor($coordinates, $i);
521 0         0 my $other = coordinates($x, $y);
522 0 0       0 return 0 if $flow->{$other};
523             }
524 0         0 for my $i ($self->neighbors2()) {
525 0         0 my ($x, $y) = $self->neighbor2($coordinates, $i);
526 0         0 my $other = coordinates($x, $y);
527 0 0       0 return 0 if $flow->{$other};
528             }
529 0         0 return 1;
530             }
531              
532             sub grow_forest {
533 218     218 0 376 my ($self, $coordinates, $world, $altitude, $dry) = @_;
534 218         249 my @candidates;
535 218 100       875 push(@candidates, $coordinates) if $world->{$coordinates} !~ /mountain|hill|water|ocean|swamp|grass/;
536 218         455 my $n = $self->arid;
537             # fractions are allowed
538 218 50       817 $n += 1 if rand() < $self->arid - int($self->arid);
539 218         1038 $n = int($n);
540 218         611 $log->debug("Arid: $n");
541 218 50       1138 if ($n >= 1) {
542 218         447 for my $i ($self->neighbors()) {
543 1074         2417 my ($x, $y) = $self->neighbor($coordinates, $i);
544 1074 100       2062 next unless $self->legal($x, $y);
545 984         8840 my $other = coordinates($x, $y);
546 984 100       1887 next if $dry->{$other};
547 930 100       1664 next if $altitude->{$coordinates} < $altitude->{$other}; # distance of one unless higher
548 607 100       2263 push(@candidates, $other) if $world->{$other} !~ /mountain|hill|water|ocean|swamp|grass/;
549             }
550             }
551 218 50       491 if ($n >= 2) {
552 218         441 for my $i ($self->neighbors2()) {
553 2148         5314 my ($x, $y) = $self->neighbor2($coordinates, $i);
554 2148 100       4013 next unless $self->legal($x, $y);
555 1838         16400 my $other = coordinates($x, $y);
556 1838 100       4155 next if $altitude->{$coordinates} <= $altitude->{$other}; # distance of two only if lower
557 464         575 my $ok = 0;
558 464         875 for my $m ($self->neighbors()) {
559 1307         2569 my ($mx, $my) = $self->neighbor($coordinates, $m);
560 1307 100       2506 next unless $self->legal($mx, $my);
561 1264         11657 my $midway = coordinates($mx, $my);
562 1264 100       2476 next if $dry->{$midway};
563 1135 100       2050 next if $self->distance($midway, $other) != 1;
564 464 100       935 next if $altitude->{$coordinates} < $altitude->{$midway};
565 436 100       776 next if $altitude->{$midway} < $altitude->{$other};
566 410         467 $ok = 1;
567 410         560 last;
568             }
569 464 100       746 next unless $ok;
570 410 100       1719 push(@candidates, $other) if $world->{$other} !~ /mountain|hill|water|ocean|swamp|grass/;
571             }
572             }
573 218         1103 $log->debug("forest growth: $coordinates: @candidates");
574 218         1388 for $coordinates (@candidates) {
575 547 100       1040 if ($altitude->{$coordinates} >= 7) {
    100          
    100          
576 125         221 $world->{$coordinates} = "light-green fir-forest";
577             } elsif ($altitude->{$coordinates} >= 6) {
578 231         396 $world->{$coordinates} = "green fir-forest";
579             } elsif ($altitude->{$coordinates} >= 4) {
580 157         276 $world->{$coordinates} = "green forest";
581             } else {
582 34         75 $world->{$coordinates} = "dark-green forest";
583             }
584             }
585             }
586              
587             sub forests {
588 2     2 0 7 my ($self, $world, $altitude, $flow, $dry) = @_;
589             # Empty hexes with a river flowing through them (and nearby hexes) are forest
590             # filled valleys.
591 2         42 for my $coordinates (keys %$flow) {
592 248 100       443 next if $dry->{$coordinates};
593 218         373 $self->grow_forest($coordinates, $world, $altitude, $dry);
594             }
595             }
596              
597             sub winds {
598 2     2 0 7 my $self = shift;
599 2         6 my ($world, $altitude, $water, $flow) = @_;
600 2   33     9 my $wind = $self->wind // $self->random_neighbor;
601 2         17 $world->{"0101"} .= " wind" . $self->reverse($wind);
602 2         49 for my $coordinates (keys %$altitude) {
603             # limit ourselves to altitude 7 and 8
604 600 100 100     1357 next if $altitude->{$coordinates} < 7 or $altitude->{$coordinates} > 8;
605             # look at the neighbor the water would flow to
606 263         454 my ($x, $y) = $self->neighbor($coordinates, $wind);
607             # skip if off the map
608 263 100       479 next unless $self->legal($x, $y);
609 243         2135 my $other = coordinates($x, $y);
610             # skip if the other hex is lower
611 243 100       517 next if $altitude->{$coordinates} > $altitude->{$other};
612             # if the other hex was higher, this land is dry
613 171         453 $log->debug("$coordinates is dry because of $other");
614 171         977 $world->{$coordinates} .= " dry zone"; # use label for debugging
615             }
616             }
617              
618             sub bogs {
619 2     2 0 6 my $self = shift;
620 2         7 my ($world, $altitude, $water) = @_;
621 2         47 for my $coordinates (keys %$altitude) {
622             # limit ourselves to altitude 7
623 600 100       968 next if $altitude->{$coordinates} != 7;
624             # don't turn lakes into bogs
625 144 100       334 next if $world->{$coordinates} =~ /water|ocean/;
626             # look at the neighbor the water would flow to
627 141         281 my ($x, $y) = $self->neighbor($coordinates, $water->{$coordinates});
628             # skip if water flows off the map
629 141 100       256 next unless $self->legal($x, $y);
630 132         1173 my $other = coordinates($x, $y);
631             # skip if water flows downhill
632 132 100       293 next if $altitude->{$coordinates} > $altitude->{$other};
633             # if there was no lower neighbor, this is a bog
634 38         152 $world->{$coordinates} =~ s/height\d+/grey swamp/;
635             }
636             }
637              
638             sub dry {
639 2     2 0 10 my ($self, $world, $altitude) = @_;
640 2         4 my @dry;
641 2         189 for my $coordinates (shuffle sort keys %$world) {
642 600 100       1612 if ($world->{$coordinates} !~ /mountain|hill|water|ocean|swamp|grass|forest|firs|trees/) {
643 72 100       110 if ($altitude->{$coordinates} >= 7) {
644 13         26 $world->{$coordinates} = "light-grey grass";
645             } else {
646 59         82 $world->{$coordinates} = "light-green bushes";
647 59         87 push(@dry, $coordinates);
648             }
649             }
650             }
651             # dry some of them up
652 2         28 my @seeds = @dry[0..@dry/4];
653 2         7 for my $coordinates (@seeds) {
654 16         57 $self->drier($world, $coordinates);
655 16         37 for my $i ($self->neighbors()) {
656 82         210 my ($x, $y) = $self->neighbor($coordinates, $i);
657 82 100       146 next unless $self->legal($x, $y);
658 69         620 my $other = coordinates($x, $y);
659 69         119 $self->drier($world, $other);
660             }
661             }
662             }
663              
664             sub drier {
665 85     85 0 134 my ($self, $world, $coordinates) = @_;
666             $world->{$coordinates} =~ s/light-green bushes/light-green grass/
667             or $world->{$coordinates} =~ s/light-green grass/dust grass/
668             or $world->{$coordinates} =~ s/dust grass/dust hill/
669 85 100 100     349 or $world->{$coordinates} =~ s/dust hill/dust desert/;
      100        
670             }
671              
672             sub settlements {
673 4     4 0 12 my $self = shift;
674 4         11 my ($world, $flow) = @_;
675 4         8 my @settlements;
676 4         24 my $max = $self->height * $self->width;
677             # do not match forest-hill
678 4         162 my @candidates = shuffle sort grep { $world->{$_} =~ /\b(fir-forest|forest(?!-hill))\b/ } keys %$world;
  1800         3007  
679 4         89 @candidates = $self->remove_closer_than(2, @candidates);
680 4 100       35 @candidates = @candidates[0 .. int($max/10 - 1)] if @candidates > $max/10;
681 4         18 push(@settlements, @candidates);
682 4         11 for my $coordinates (@candidates) {
683             $world->{$coordinates} =~ s/fir-forest/firs thorp/
684 63 100       205 or $world->{$coordinates} =~ s/forest(?!-hill)/trees thorp/;
685             }
686 4 100       145 @candidates = shuffle sort grep { $world->{$_} =~ /(?{$_}} keys %$world;
  1800         3223  
687 4         58 @candidates = $self->remove_closer_than(5, @candidates);
688 4 50       17 @candidates = @candidates[0 .. int($max/20 - 1)] if @candidates > $max/20;
689 4         9 push(@settlements, @candidates);
690 4         10 for my $coordinates (@candidates) {
691 7         21 $world->{$coordinates} =~ s/forest/trees village/;
692             }
693 4 100       126 @candidates = shuffle sort grep { $world->{$_} =~ /(?{$_} } keys %$world;
  1800         3223  
694 4         71 @candidates = $self->remove_closer_than(10, @candidates);
695 4 50       20 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
696 4         9 push(@settlements, @candidates);
697 4         10 for my $coordinates (@candidates) {
698 4         13 $world->{$coordinates} =~ s/forest/trees town/;
699             }
700 4         130 @candidates = shuffle sort grep { $world->{$_} =~ /white mountain\b/ } keys %$world;
  1800         2572  
701 4         65 @candidates = $self->remove_closer_than(10, @candidates);
702 4 50       28 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
703 4         11 push(@settlements, @candidates);
704 4         12 for my $coordinates (@candidates) {
705 6         26 $world->{$coordinates} =~ s/white mountain\b/white mountain law/;
706             }
707 4         125 @candidates = shuffle sort grep { $world->{$_} =~ /swamp/ } keys %$world;
  1800         2588  
708 4         60 @candidates = $self->remove_closer_than(10, @candidates);
709 4 50       20 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
710 4         9 push(@settlements, @candidates);
711 4         9 for my $coordinates (@candidates) {
712 8         24 $world->{$coordinates} =~ s/swamp/swamp2 chaos/;
713             }
714 4         13 for my $coordinates (@settlements) {
715 88         176 for my $i ($self->neighbors()) {
716 408         735 my ($x, $y) = $self->neighbor($coordinates, $i);
717 408         748 my $other = coordinates($x, $y);
718 408 100 100     1491 next unless $world->{$other} and $world->{$other} =~ /water|ocean/;
719             # bump ports one size category up
720 11         26 $world->{$coordinates} =~ s/large-town/city port/;
721 11         21 $world->{$coordinates} =~ s/town/large-town port/;
722 11         19 $world->{$coordinates} =~ s/village/town port/;
723             # no bumps for thorps
724 11         21 last;
725             }
726             }
727 4         17 for my $coordinates (@settlements) {
728             # thorps and villages don't cut enough wood; make sure to get both "green" and "dark-green"
729 88 100       173 $world->{$coordinates} =~ s/\S*green trees/light-soil/ if $world->{$coordinates} =~ /large-town|city/;
730 88 100       177 $world->{$coordinates} =~ s/\S*green trees/soil/ if $world->{$coordinates} =~ / town/;
731             }
732 4         30 return @settlements;
733             }
734              
735             sub trails {
736 4     4 0 10 my $self = shift;
737 4         16 my ($altitude, $settlements) = @_;
738             # look for a neighbor that is as low as possible and nearby
739 4         6 my %trails;
740 4         30 my @from = shuffle @$settlements;
741 4         30 my @to = shuffle @$settlements;
742 4         25 for my $from (@from) {
743 88         374 my ($best, $best_distance, $best_altitude);
744 88         145 for my $to (@to) {
745 3646 100       5322 next if $from eq $to;
746 3558         5729 my $distance = $self->distance($from, $to);
747 3558         10804 $log->debug("Considering $from-$to: distance $distance, altitude " . $altitude->{$to});
748 3558 100 100     20215 if ($distance <= 3
      100        
      100        
      100        
749             and (not $best_distance or $distance <= $best_distance)
750             and (not $best or $altitude->{$to} < $best_altitude)) {
751 125         166 $best = $to;
752 125         180 $best_altitude = $altitude->{$best};
753 125         173 $best_distance = $distance;
754             }
755             }
756 88 100       146 next if not $best;
757             # skip if it already exists in the other direction
758 86 100       169 next if $trails{"$best-$from"};
759 72         202 $trails{"$from-$best"} = 1;
760 72         153 $log->debug("Trail $from-$best");
761             }
762 4         64 return keys %trails;
763             }
764              
765             sub cliffs {
766 2     2 0 5 my $self = shift;
767 2         7 my ($world, $altitude) = @_;
768 2         9 my @neighbors = $self->neighbors();
769             # hexes with altitude difference bigger than 1 have cliffs
770 2         67 for my $coordinates (keys %$world) {
771 600 50       1474 next if $altitude->{$coordinates} <= $self->bottom;
772 600         2103 for my $i (@neighbors) {
773 3000         6247 my ($x, $y) = $self->neighbor($coordinates, $i);
774 3000 100       5670 next unless $self->legal($x, $y);
775 2762         24005 my $other = coordinates($x, $y);
776 2762 100       6527 if ($altitude->{$coordinates} - $altitude->{$other} >= 2) {
777 121 100       202 if (@neighbors == 6) {
778 65         143 $world->{$coordinates} .= " cliff$i";
779             } else { # square
780 56         140 $world->{$coordinates} .= " cliffs$i";
781             }
782             }
783             }
784             }
785             }
786              
787             sub marshlands {
788 2     2 0 10 my ($self, $world, $altitude, $rivers) = @_;
789 2         7 my %seen;
790 2         8 for my $river (@$rivers) {
791 93         162 my $last = $river->[0];
792 93         121 for my $coordinates (@$river) {
793 343 100       589 last if $seen{$coordinates}; # we've been here before
794 281         460 $seen{$coordinates} = 1;
795 281 100       452 next unless exists $altitude->{$coordinates}; # rivers ending off the map
796 250 50       408 if ($altitude->{$coordinates} <= $self->bottom) {
797 0 0 0     0 if ($altitude->{$coordinates} == $self->bottom
      0        
798             and $world->{$coordinates} =~ /water|ocean/
799             and $altitude->{$coordinates} == $altitude->{$last} - 1) {
800 0         0 $world->{$coordinates} = "blue-green swamp";
801             } else {
802 0         0 $world->{$coordinates} =~ s/ocean/water/;
803 0         0 delete $seen{$coordinates};
804 0         0 last;
805             }
806             }
807 250         885 $last = $coordinates;
808             }
809             }
810             }
811              
812             sub generate {
813 2     2 0 9 my ($self, $world, $altitude, $water, $rivers, $settlements, $trails, $canyons, $step) = @_;
814             # $flow indicates that there is actually a river in this hex
815 2         6 my $flow = {};
816             # $dry indicates that is a river in this hex, but it cut itself a canyon
817 2         5 my $dry = {};
818             my @code = (
819 2     2   16 sub { $self->flat($altitude);
820 2         34 $self->altitude($world, $altitude); },
821 2     2   17 sub { $self->bumpiness($world, $altitude); },
822 2     2   19 sub { $self->mountains($world, $altitude); },
823 2     2   16 sub { $self->ocean($world, $altitude); },
824 2     2   15 sub { $self->water($world, $altitude, $water); },
825 2     2   35 sub { $self->lakes($world, $altitude, $water); },
826 2     2   15 sub { $self->flood($world, $altitude, $water); },
827 2     2   20 sub { $self->bogs($world, $altitude, $water); },
828 2     2   19 sub { $self->winds($world, $altitude, $water); },
829 2     2   17 sub { $self->rivers($world, $altitude, $water, $flow, $rivers); },
830 2     2   21 sub { $self->canyons($world, $altitude, $rivers, $canyons, $dry); },
831 2     2   20 sub { $self->swamps($world, $altitude, $water, $flow, $dry); },
832 2     2   15 sub { $self->forests($world, $altitude, $flow, $dry); },
833 2     2   39 sub { $self->dry($world, $altitude); },
834 2     2   20 sub { $self->cliffs($world, $altitude); },
835 2     2   38 sub { push(@$settlements, $self->settlements($world, $flow)); },
836 2     2   25 sub { push(@$trails, $self->trails($altitude, $settlements)); },
837 2     2   26 sub { $self->marshlands($world, $altitude, $rivers); },
838             # make sure you look at "alpine_document.html.ep" if you change this list!
839             # make sure you look at '/alpine/document' if you add to this list!
840 2         72 );
841              
842             # $step 0 runs all the code; note that we can't simply cache those results
843             # because we need to start over with the same seed!
844 2         5 my $i = 1;
845 2         7 while (@code) {
846 36         117 shift(@code)->();
847 36 50       383 return if $step == $i++;
848 36         114 $self->fixup($world, $altitude, $i);
849             }
850             }
851              
852             # Remove temporary markers that won't be needed in the next step
853             sub fixup {
854 36     36 0 80 my ($self, $world, $altitude, $step, $last) = @_;
855             # When documenting or debugging, water flow arrows are no longer needed when
856             # the rivers are added.
857 36 100       76 if ($step >= 10) {
858 20         652 for my $coordinates (keys %$world) {
859 6000         8168 $world->{$coordinates} =~ s/ arrow\d//;
860             }
861             }
862             # Wind direction is only shown once.
863 36         230 $world->{"0101"} =~ s/ wind\d//;
864             # Remove zone markers.
865 36         811 for my $coordinates (keys %$world) {
866 10800         13696 $world->{$coordinates} =~ s/ zone//;
867             }
868             }
869              
870             sub generate_map {
871 2     2 0 1285 my $self = shift;
872              
873             # The parameters turn into class variables.
874 2   50     24 $self->width(shift // 30);
875 2   50     37 $self->height(shift // 10);
876 2   50     25 $self->steepness(shift // 3);
877 2   33     20 $self->peaks(shift // int($self->width * $self->height / 40));
878 2   50     55 $self->peak(shift // 10);
879 2   33     17 $self->bumps(shift // int($self->width * $self->height / 40));
880 2   50     39 $self->bump(shift // 2);
881 2   50     23 $self->bottom(shift // 0);
882 2   50     23 $self->arid(shift // 2);
883 2         22 $self->wind(shift); # or random
884 2   33     13 my $seed = shift||time;
885 2         4 my $url = shift;
886 2   50     10 my $step = shift||0;
887              
888             # For documentation purposes, I want to be able to set the pseudo-random
889             # number seed using srand and rely on rand to reproduce the same sequence of
890             # pseudo-random numbers for the same seed. The key point to remember is that
891             # the keys function will return keys in random order. So if we look over the
892             # result of keys, we need to look at the code in the loop: If order is
893             # important, that wont do. We need to sort the keys. If we want the keys to be
894             # pseudo-shuffled, use shuffle sort keys.
895 2         6 srand($seed);
896              
897             # Keys for all hashes are coordinates such as "0101".
898             # %world is the description with values such as "green forest".
899             # %altitude is the altitude with values such as 3.
900             # %water is the preferred direction water would take with values such as 0.
901             # (north west); 0 means we need to use "if defined".
902             # @rivers are the rivers with values such as ["0102", "0202"].
903             # @settlements are are the locations of settlements such as "0101".
904             # @trails are the trails connecting these with values as "0102-0202".
905             # $step is how far we want map generation to go where 0 means all the way.
906 2         11 my ($world, $altitude, $water, $rivers, $settlements, $trails, $canyons) =
907             ({}, {}, {}, [], [], [], []);
908 2         17 $self->generate($world, $altitude, $water, $rivers, $settlements, $trails, $canyons, $step);
909              
910             # When documenting or debugging, add altitude as a label.
911 2 50       13 if ($step > 0) {
912 0         0 for my $coordinates (keys %$world) {
913 0         0 $world->{$coordinates} .= ' "' . $altitude->{$coordinates} . '"';
914             }
915             }
916              
917 2         7 local $" = "-"; # list items separated by -
918 2         5 my @lines;
919 2         193 push(@lines, map { $_ . " " . $world->{$_} } sort keys %$world);
  600         1070  
920 2         37 push(@lines, map { "$_ trail" } @$trails);
  70         119  
921 2         13 push(@lines, map { "@$_ river" } @$rivers);
  93         216  
922 2         10 push(@lines, map { "@$_ canyon" } @$canyons); # after rivers
  11         29  
923 2         10 push(@lines, "include gnomeyland.txt");
924              
925             # when documenting or debugging, add some more lines at the end
926 2 50       9 if ($step > 0) {
927             # visualize height
928             push(@lines,
929             map {
930 0         0 my $n = int(25.5 * $_);
  0         0  
931 0         0 qq{height$_ attributes fill="rgb($n,$n,$n)"};
932             } (0 .. 10));
933             # visualize water flow
934 0         0 push(@lines, $self->arrows());
935             }
936              
937 2         9 push(@lines, "# Seed: $seed");
938 2 50       23 push(@lines, "# Documentation: " . $url) if $url;
939 2         1419 my $map = join("\n", @lines);
940 2         473 return $map;
941             }
942              
943             1;