File Coverage

blib/lib/Game/TextMapper/Schroeder/Alpine.pm
Criterion Covered Total %
statement 510 552 92.3
branch 201 248 81.0
condition 92 123 74.8
subroutine 50 51 98.0
pod 0 28 0.0
total 853 1002 85.1


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   8 use Game::TextMapper::Log;
  1         3  
  1         34  
49 1     1   5 use Modern::Perl '2018';
  1         3  
  1         10  
50 1     1   206 use Mojo::Base -base;
  1         4  
  1         9  
51 1     1   628 use Role::Tiny::With;
  1         371  
  1         69  
52             with 'Game::TextMapper::Schroeder::Base';
53 1     1   6 use List::Util 'shuffle';
  1         3  
  1         8871  
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 33 my $self = shift;
68 2         5 my $altitude = shift;
69 2         3 my $count = shift;
70 2         6 my $current_altitude = shift;
71 2         5 my @queue;
72             # place some peaks and put them in a queue
73 2         7 for (1 .. $count) {
74             # try to find an empty hex
75 14         23 for (1 .. 6) {
76 14         25 my $x = int(rand($self->width)) + 1;
77 14         76 my $y = int(rand($self->height)) + 1;
78 14         165 my $coordinates = coordinates($x, $y);
79 14 50       34 next if $altitude->{$coordinates};
80 14         23 $altitude->{$coordinates} = $current_altitude;
81 14         49 $log->debug("placed $current_altitude at $coordinates");
82 14         88 push(@queue, $coordinates);
83 14         23 last;
84             }
85             }
86 2         16 return @queue;
87             }
88              
89             sub grow_mountains {
90 2     2 0 5 my $self = shift;
91 2         5 my $altitude = shift;
92 2         7 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         1676 my $coordinates = shift @queue;
97 600         996 my $current_altitude = $altitude->{$coordinates};
98 600 50       1046 next unless $current_altitude > 0;
99             # pick some random neighbors based on variable steepness
100 600         1174 my $n = $self->steepness;
101             # round up based on fraction
102 600 50       2686 $n += 1 if rand() < $n - int($n);
103 600         805 $n = int($n);
104 600 50       977 next if $n < 1;
105 600         947 for (1 .. $n) {
106             # try to find an empty neighbor; abort after six attempts
107 1800         3857 for (1 .. 6) {
108 8573         23134 my ($x, $y) = $self->neighbor($coordinates, $self->random_neighbor());
109 8573 100       16661 next unless $self->legal($x, $y);
110 7865         70662 my $other = coordinates($x, $y);
111             # if this is taken, look further
112 7865 100       15412 if ($altitude->{$other}) {
113 7650         13394 ($x, $y) = $self->neighbor2($coordinates, $self->random_neighbor2());
114 7650 100       14762 next unless $self->legal($x, $y);
115 6639         60152 $other = coordinates($x, $y);
116             # if this is also taken, try again
117 6639 100       16459 next if $altitude->{$other};
118             }
119             # if we found an empty neighbor, set its altitude
120 586 50       1041 $altitude->{$other} = $current_altitude > 0 ? $current_altitude - 1 : 0;
121 586         908 push(@queue, $other);
122 586         1061 last;
123             }
124             }
125             }
126             }
127              
128             sub fix_altitude {
129 2     2 0 6 my $self = shift;
130 2         4 my $altitude = shift;
131             # go through all the hexes
132 2         259 for my $coordinates (sort keys %$altitude) {
133             # find hexes that we missed and give them the height of a random neighbor
134 600 50       890 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 7 my $self = shift;
155 2         5 my ($world, $altitude) = @_;
156 2         17 my @queue = $self->place_peak($altitude, $self->peaks, $self->peak);
157 2         17 $self->grow_mountains($altitude, @queue);
158 2         52 $self->fix_altitude($altitude);
159             # note height for debugging purposes
160 2         170 for my $coordinates (sort keys %$altitude) {
161 600         1334 $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         47 for my $delta (-$self->bump, $self->bump) {
169             # six attempts to try and find a good hex
170 28         112 for (1 .. 6) {
171 30         58 my $x = int(rand($self->width)) + 1;
172 30         136 my $y = int(rand($self->height)) + 1;
173 30         119 my $coordinates = coordinates($x, $y);
174 30         67 my $current_altitude = $altitude->{$coordinates} + $delta;
175 30 100 66     90 next if $current_altitude > 10 or $current_altitude < 0;
176             # bump it up or down
177 28         43 $altitude->{$coordinates} = $current_altitude;
178 28         61 $world->{$coordinates} = "height$altitude->{$coordinates} zone";
179 28         94 $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     201 if ($delta < -1 or $delta > 1) {
182 28         50 my $delta = $delta - $delta / abs($delta);
183 28         70 for my $i ($self->neighbors()) {
184 140         689 my ($x, $y) = $self->neighbor($coordinates, $i);
185 140 100       278 next unless $self->legal($x, $y);
186 131         1151 my $other = coordinates($x, $y);
187 131         237 $current_altitude = $altitude->{$other} + $delta;
188 131 100 66     305 next if $current_altitude > 10 or $current_altitude < 0;
189 130         177 $altitude->{$other} = $current_altitude;
190 130         240 $world->{$other} = "height$altitude->{$other} zone";
191 130         314 $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         194 last;
196             }
197             }
198             }
199             }
200              
201             sub water {
202 2     2 0 5 my $self = shift;
203 2         5 my ($world, $altitude, $water) = @_;
204             # reset in case we run this twice
205             # go through all the hexes
206 2         173 for my $coordinates (sort keys %$altitude) {
207 600 50       1397 next if $altitude->{$coordinates} <= $self->bottom;
208             # note preferred water flow by identifying lower lying neighbors
209 600         2495 my ($lowest, $direction);
210             # look at neighbors in random order
211             NEIGHBOR:
212 600         1097 for my $i (shuffle $self->neighbors()) {
213 3000         8475 my ($x, $y) = $self->neighbor($coordinates, $i);
214 3000         5861 my $legal = $self->legal($x, $y);
215 3000         25691 my $other = coordinates($x, $y);
216 3000 100 100     9308 next if $legal and $altitude->{$other} > $altitude->{$coordinates};
217             # don't point head on to another arrow
218 2145 100 100     6216 next if $legal and $water->{$other} and $water->{$other} == ($i-3) % 6;
      100        
219             # don't point into loops
220 2028         4200 my %loop = ($coordinates => 1, $other => 1);
221 2028         2537 my $next = $other;
222 2028         5542 $log->debug("Loop detection starting with $coordinates and $other");
223 2028         11725 while ($next) {
224             # no water flow known is also good;
225 3252   100     10417 $log->debug("water for $next: " . ($water->{$next} || "none"));
226 3252 100       16328 last unless defined $water->{$next};
227 1523         2759 ($x, $y) = $self->neighbor($next, $water->{$next});
228             # leaving the map is good
229 1523         3590 $log->debug("legal for $next: " . $self->legal($x, $y));
230 1523 100       20283 last unless $self->legal($x, $y);
231 1292         11475 $next = coordinates($x, $y);
232             # skip this neighbor if this is a loop
233 1292   100     4968 $log->debug("is $next in a loop? " . ($loop{$next} || "no"));
234 1292 100       7236 next NEIGHBOR if $loop{$next};
235 1224         2426 $loop{$next} = 1;
236             }
237 1960 100 66     8745 if (not defined $direction
      66        
      100        
      66        
238             or not $legal and $altitude->{$coordinates} < $lowest
239             or $legal and $altitude->{$other} < $lowest) {
240 780 100       1410 $lowest = $legal ? $altitude->{$other} : $altitude->{$coordinates};
241 780         887 $direction = $i;
242 780         1619 $log->debug("Set lowest to $lowest ($direction)");
243             }
244             }
245 600 100       1323 if (defined $direction) {
246 578         1032 $water->{$coordinates} = $direction;
247             $world->{$coordinates} =~ s/arrow\d/arrow$water->{$coordinates}/
248 578 50       2145 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         53 for my $coordinates (keys %$altitude) {
258 600 100       1128 if ($altitude->{$coordinates} >= 10) {
    100          
    100          
259 21         41 $world->{$coordinates} = "white mountains";
260             } elsif ($altitude->{$coordinates} >= 9) {
261 46         65 $world->{$coordinates} = "white mountain";
262             } elsif ($altitude->{$coordinates} >= 8) {
263 105         131 $world->{$coordinates} = "light-grey mountain";
264             }
265             }
266             }
267              
268             sub ocean {
269 2     2 0 11 my $self = shift;
270 2         6 my ($world, $altitude) = @_;
271 2         192 for my $coordinates (sort keys %$altitude) {
272 600 50       1910 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 9 my $self = shift;
288 2         6 my ($world, $altitude, $water) = @_;
289             # any areas without water flow are lakes
290 2         215 for my $coordinates (sort keys %$altitude) {
291 600 100 66     1110 if (not defined $water->{$coordinates}
292             and $world->{$coordinates} ne "ocean") {
293 22         39 $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 10 my ($self, $world, $altitude, $water, $flow, $dry) = @_;
301 2         56 for my $coordinates (keys %$altitude) {
302             # don't turn lakes into swamps and skip bogs
303 600 100       1590 next if $world->{$coordinates} =~ /ocean|water|swamp|grass/;
304             # swamps require a river
305 522 100       884 next unless $flow->{$coordinates};
306             # no swamps when there is a canyon
307 229 100       354 next if $dry->{$coordinates};
308             # look at the neighbor the water would flow to
309 200         414 my ($x, $y) = $self->neighbor($coordinates, $water->{$coordinates});
310             # skip if water flows off the map
311 200 100       409 next unless $self->legal($x, $y);
312 174         1624 my $other = coordinates($x, $y);
313             # skip if water flows downhill
314 174 100       436 next if $altitude->{$coordinates} > $altitude->{$other};
315             # if there was no lower neighbor, this is a swamp
316 42 100       94 if ($altitude->{$coordinates} >= 6) {
317 26         112 $world->{$coordinates} =~ s/height\d+/grey swamp/;
318             } else {
319 16         70 $world->{$coordinates} =~ s/height\d+/dark-grey swamp/;
320             }
321             }
322             }
323              
324             sub flood {
325 2     2 0 6 my $self = shift;
326 2         8 my ($world, $altitude, $water) = @_;
327             # backtracking information: $from = $flow{$to}
328 2         6 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         50 my @lakes = shuffle sort grep { not defined $water->{$_} } keys %$world;
  600         828  
334 2 50       22 return unless @lakes;
335 2         7 my $start = shift(@lakes);
336 2         6 my @candidates = ($start);
337 2         15 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 210   100     746 ($altitude->{$a}||0) <=> ($altitude->{$b}||0)
  10303   100     20349  
344             } shuffle @candidates;
345 210         896 $log->debug("Candidates @candidates");
346 210         1179 my $coordinates;
347             do {
348 227         741 $coordinates = shift(@candidates);
349 210   66     253 } until not $coordinates or not $seen{$coordinates};
350 210 50       345 last unless $coordinates;
351 210         304 $seen{$coordinates} = 1;
352 210         583 $log->debug("Looking at $coordinates");
353 210 100 66     1185 if ($self->legal($coordinates) and $world->{$coordinates} ne "ocean") {
354             # if we're still on the map, check all the unknown neighbors
355 191         2052 my $from = $coordinates;
356 191         363 for my $i ($self->neighbors()) {
357 856         1453 my $to = coordinates($self->neighbor($from, $i));
358 856 100       1685 next if $seen{$to};
359 644         1648 $log->debug("Adding $to to our candidates");
360 644         3572 $flow{$to} = $from;
361             # adding to the front as we keep pushing forward (I hope)
362 644         1093 push(@candidates, $to);
363             }
364 191         409 next;
365             }
366 19         208 $log->debug("We left the map at $coordinates");
367 19         110 my $to = $coordinates;
368 19         39 my $from = $flow{$to};
369 19         64 while ($from) {
370 136         303 my $i = $self->direction($from, $to);
371 136 100 100     559 if (not defined $water->{$from}
372             or $water->{$from} != $i) {
373 79         232 $log->debug("Arrow for $from now points to $to");
374 79         443 $water->{$from} = $i;
375             $world->{$from} =~ s/arrow\d/arrow$i/
376 79 100       593 or $world->{$from} .= " arrow$i";
377             } else {
378 57         166 $log->debug("Arrow for $from already points $to");
379             }
380 136         456 $to = $from;
381 136         270 $from = $flow{$to};
382             }
383             # pick the next lake
384             do {
385 22         61 $start = shift(@lakes);
386 22 100       77 $log->debug("Next lake is $start") if $start;
387 19   100     36 } until not $start or not defined $water->{$start};
388 19 100       176 last unless $start;
389 17         132 %seen = %flow = ();
390 17         75 @candidates = ($start);
391             }
392             }
393              
394             sub rivers {
395 2     2 0 11 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 117 100       334 $world->{$_} = "light-grey forest-hill" unless $world->{$_} =~ /mountain|swamp|grass|water|ocean/;
399 117         315 $flow->{$_} = [$_]
400             } sort grep {
401             # these are the potential starting places: up in the mountains below the
402             # ice, or lakes
403 2         53 ($altitude->{$_} == 7 or $altitude->{$_} == 8
404             or $world->{$_} =~ /water/ and $altitude->{$_} > $self->bottom)
405             and not $flow->{$_}
406 600 50 100     2532 and $world->{$_} !~ /dry/;
      66        
407             } keys %$altitude;
408 2         46 $self->grow_rivers(\@growing, $water, $flow, $rivers);
409             }
410              
411             sub grow_rivers {
412 2     2 0 8 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 778         1297 my $n = int(rand(scalar @$growing));
417 778         1020 my $river = $growing->[$n];
418             # warn "Picking @$river\n";
419 778         937 my $coordinates = $river->[-1];
420 778         838 my $end = 1;
421 778 100       1317 if (defined $water->{$coordinates}) {
422 673         1309 my $other = coordinates($self->neighbor($coordinates, $water->{$coordinates}));
423 673 50       5713 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 673 100       1475 if (ref $flow->{$other}) {
426             # warn "Prepending @$river to @{$flow->{$other}}\n";
427             # prepend the current river to the other river
428 12         21 unshift(@{$flow->{$other}}, @$river);
  12         34  
429             # move the source marker
430 12         27 $flow->{$river->[0]} = $flow->{$other};
431 12         16 $flow->{$other} = 1;
432             # and remove the current river from the growing list
433 12         39 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 661         969 $flow->{$coordinates} = 1;
438 661         1621 push(@$river, $other);
439             }
440             } else {
441             # stop growing this river
442             # warn "Stopped river: @$river\n" if grep(/0914/, @$river);
443 105         247 push(@$rivers, splice(@$growing, $n, 1));
444             }
445             }
446             }
447              
448             sub canyons {
449 2     2 0 8 my $self = shift;
450 2         10 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         6 my %seen;
455 2         7 for my $river (@$rivers) {
456 105         155 my $last = $river->[0];
457 105         148 my $current_altitude = $altitude->{$last};
458 105         390 $log->debug("Looking at @$river ($current_altitude)");
459 105         522 for my $coordinates (@$river) {
460 507         1044 $log->debug("Looking at $coordinates");
461 507 100       2603 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 38 100       71 if (@$canyon) {
466 1         4 my @other = @{$seen{$coordinates}};
  1         8  
467 1 50       8 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 1         14 $log->debug("Canyon @$canyon of river @$river stumbled upon existing canyon @other at $coordinates");
472 1         8 while (@other) {
473 2         6 my $other = shift(@other);
474 2 100       7 next if $other ne $coordinates;
475 1         4 push(@$canyon, $other, @other);
476 1         4 last;
477             }
478 1         8 $log->debug("Canyon @$canyon");
479 1         6 push(@$canyons, $canyon);
480             }
481 1         3 $canyon = [];
482             }
483 38         45 $log->debug("We've seen the rest: @{$seen{$coordinates}}");
  38         109  
484 38         214 last;
485             }
486             # no canyons through water!
487 469 100 100     1391 if ($altitude->{$coordinates} and $current_altitude < $altitude->{$coordinates}
      100        
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 37 100       78 push(@$canyon, $last) unless @$canyon;
492 37         54 push(@$canyon, $coordinates);
493 37 50       84 $world->{$coordinates} .= " zone" unless $dry->{$coordinates};
494 37         64 $dry->{$coordinates} = 1;
495 37         110 $log->debug("Growing canyon @$canyon");
496 37         199 $seen{$coordinates} = $canyon;
497             } else {
498             # if we just left a canyon, append the current step
499 432 100       617 if (@$canyon) {
500 15         27 push(@$canyon, $coordinates);
501 15         22 push(@$canyons, $canyon);
502 15         87 $log->debug("Looking at river @$river");
503 15         110 $log->debug("Canyon @$canyon");
504 15         68 $canyon = [];
505 15         36 last;
506             }
507             # not digging a canyon
508 417         475 $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 241     241 0 460 my ($self, $coordinates, $world, $altitude, $dry) = @_;
534 241         290 my @candidates;
535 241 100       1273 push(@candidates, $coordinates) if $world->{$coordinates} !~ /mountain|hill|water|ocean|swamp|grass/;
536 241         598 my $n = $self->arid;
537             # fractions are allowed
538 241 50       1086 $n += 1 if rand() < $self->arid - int($self->arid);
539 241         1235 $n = int($n);
540 241         690 $log->debug("Arid: $n");
541 241 50       1354 if ($n >= 1) {
542 241         556 for my $i ($self->neighbors()) {
543 1198         2621 my ($x, $y) = $self->neighbor($coordinates, $i);
544 1198 100       2359 next unless $self->legal($x, $y);
545 1107         10077 my $other = coordinates($x, $y);
546 1107 100       2288 next if $dry->{$other};
547 1034 100       2235 next if $altitude->{$coordinates} < $altitude->{$other}; # distance of one unless higher
548 650 100       2808 push(@candidates, $other) if $world->{$other} !~ /mountain|hill|water|ocean|swamp|grass/;
549             }
550             }
551 241 50       621 if ($n >= 2) {
552 241         542 for my $i ($self->neighbors2()) {
553 2396         6148 my ($x, $y) = $self->neighbor2($coordinates, $i);
554 2396 100       4682 next unless $self->legal($x, $y);
555 2049         18498 my $other = coordinates($x, $y);
556 2049 100       5483 next if $altitude->{$coordinates} <= $altitude->{$other}; # distance of two only if lower
557 576         728 my $ok = 0;
558 576         1120 for my $m ($self->neighbors()) {
559 1758         3556 my ($mx, $my) = $self->neighbor($coordinates, $m);
560 1758 100       3434 next unless $self->legal($mx, $my);
561 1692         15538 my $midway = coordinates($mx, $my);
562 1692 100       3348 next if $dry->{$midway};
563 1562 100       2822 next if $self->distance($midway, $other) != 1;
564 573 100       1192 next if $altitude->{$coordinates} < $altitude->{$midway};
565 540 100       961 next if $altitude->{$midway} < $altitude->{$other};
566 501         572 $ok = 1;
567 501         662 last;
568             }
569 576 100       1020 next unless $ok;
570 501 100       2389 push(@candidates, $other) if $world->{$other} !~ /mountain|hill|water|ocean|swamp|grass/;
571             }
572             }
573 241         1715 $log->debug("forest growth: $coordinates: @candidates");
574 241         1778 for $coordinates (@candidates) {
575 591 100       1222 if ($altitude->{$coordinates} >= 7) {
    100          
    100          
576 129         286 $world->{$coordinates} = "light-green fir-forest";
577             } elsif ($altitude->{$coordinates} >= 6) {
578 297         631 $world->{$coordinates} = "green fir-forest";
579             } elsif ($altitude->{$coordinates} >= 4) {
580 131         284 $world->{$coordinates} = "green forest";
581             } else {
582 34         67 $world->{$coordinates} = "dark-green forest";
583             }
584             }
585             }
586              
587             sub forests {
588 2     2 0 11 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         44 for my $coordinates (keys %$flow) {
592 278 100       591 next if $dry->{$coordinates};
593 241         473 $self->grow_forest($coordinates, $world, $altitude, $dry);
594             }
595             }
596              
597             sub winds {
598 2     2 0 6 my $self = shift;
599 2         6 my ($world, $altitude, $water, $flow) = @_;
600 2   66     10 my $wind = $self->wind // $self->random_neighbor;
601 2         47 $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     1470 next if $altitude->{$coordinates} < 7 or $altitude->{$coordinates} > 8;
605             # look at the neighbor the water would flow to
606 275         475 my ($x, $y) = $self->neighbor($coordinates, $wind);
607             # skip if off the map
608 275 100       587 next unless $self->legal($x, $y);
609 265         2349 my $other = coordinates($x, $y);
610             # skip if the other hex is lower
611 265 100       597 next if $altitude->{$coordinates} > $altitude->{$other};
612             # if the other hex was higher, this land is dry
613 173         447 $log->debug("$coordinates is dry because of $other");
614 173         1042 $world->{$coordinates} .= " dry zone"; # use label for debugging
615             }
616             }
617              
618             sub bogs {
619 2     2 0 8 my $self = shift;
620 2         8 my ($world, $altitude, $water) = @_;
621 2         56 for my $coordinates (keys %$altitude) {
622             # limit ourselves to altitude 7
623 600 100       1218 next if $altitude->{$coordinates} != 7;
624             # don't turn lakes into bogs
625 170 100       438 next if $world->{$coordinates} =~ /water|ocean/;
626             # look at the neighbor the water would flow to
627 163         354 my ($x, $y) = $self->neighbor($coordinates, $water->{$coordinates});
628             # skip if water flows off the map
629 163 100       310 next unless $self->legal($x, $y);
630 150         1381 my $other = coordinates($x, $y);
631             # skip if water flows downhill
632 150 100       342 next if $altitude->{$coordinates} > $altitude->{$other};
633             # if there was no lower neighbor, this is a bog
634 56         241 $world->{$coordinates} =~ s/height\d+/grey swamp/;
635             }
636             }
637              
638             sub dry {
639 2     2 0 10 my ($self, $world, $altitude) = @_;
640 2         5 my @dry;
641 2         235 for my $coordinates (shuffle sort keys %$world) {
642 600 100       1585 if ($world->{$coordinates} !~ /mountain|hill|water|ocean|swamp|grass|forest|firs|trees/) {
643 56 100       121 if ($altitude->{$coordinates} >= 7) {
644 11         19 $world->{$coordinates} = "light-grey grass";
645             } else {
646 45         58 $world->{$coordinates} = "light-green bushes";
647 45         76 push(@dry, $coordinates);
648             }
649             }
650             }
651             # dry some of them up
652 2         40 my @seeds = @dry[0..@dry/4];
653 2         9 for my $coordinates (@seeds) {
654 12         43 $self->drier($world, $coordinates);
655 12         36 for my $i ($self->neighbors()) {
656 60         168 my ($x, $y) = $self->neighbor($coordinates, $i);
657 60 100       113 next unless $self->legal($x, $y);
658 49         457 my $other = coordinates($x, $y);
659 49         82 $self->drier($world, $other);
660             }
661             }
662             }
663              
664             sub drier {
665 61     61 0 97 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 61 100 100     280 or $world->{$coordinates} =~ s/dust hill/dust desert/;
      100        
670             }
671              
672             sub settlements {
673 4     4 0 12 my $self = shift;
674 4         16 my ($world, $flow) = @_;
675 4         8 my @settlements;
676 4         41 my $max = $self->height * $self->width;
677             # do not match forest-hill
678 4         178 my @candidates = shuffle sort grep { $world->{$_} =~ /\b(fir-forest|forest(?!-hill))\b/ } keys %$world;
  1800         3145  
679 4         134 @candidates = $self->remove_closer_than(2, @candidates);
680 4 100       41 @candidates = @candidates[0 .. int($max/10 - 1)] if @candidates > $max/10;
681 4         19 push(@settlements, @candidates);
682 4         12 for my $coordinates (@candidates) {
683             $world->{$coordinates} =~ s/fir-forest/firs thorp/
684 67 100       295 or $world->{$coordinates} =~ s/forest(?!-hill)/trees thorp/;
685             }
686 4 100       197 @candidates = shuffle sort grep { $world->{$_} =~ /(?{$_}} keys %$world;
  1800         3411  
687 4         71 @candidates = $self->remove_closer_than(5, @candidates);
688 4 50       42 @candidates = @candidates[0 .. int($max/20 - 1)] if @candidates > $max/20;
689 4         11 push(@settlements, @candidates);
690 4         17 for my $coordinates (@candidates) {
691 7         27 $world->{$coordinates} =~ s/forest/trees village/;
692             }
693 4 100       126 @candidates = shuffle sort grep { $world->{$_} =~ /(?{$_} } keys %$world;
  1800         3277  
694 4         67 @candidates = $self->remove_closer_than(10, @candidates);
695 4 50       22 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
696 4         13 push(@settlements, @candidates);
697 4         13 for my $coordinates (@candidates) {
698 4         16 $world->{$coordinates} =~ s/forest/trees town/;
699             }
700 4         127 @candidates = shuffle sort grep { $world->{$_} =~ /white mountain\b/ } keys %$world;
  1800         2596  
701 4         67 @candidates = $self->remove_closer_than(10, @candidates);
702 4 50       23 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
703 4         13 push(@settlements, @candidates);
704 4         9 for my $coordinates (@candidates) {
705 6         34 $world->{$coordinates} =~ s/white mountain\b/white mountain law/;
706             }
707 4         142 @candidates = shuffle sort grep { $world->{$_} =~ /swamp/ } keys %$world;
  1800         2742  
708 4         70 @candidates = $self->remove_closer_than(10, @candidates);
709 4 50       43 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
710 4         16 push(@settlements, @candidates);
711 4         11 for my $coordinates (@candidates) {
712 7         42 $world->{$coordinates} =~ s/swamp/swamp2 chaos/;
713             }
714 4         16 for my $coordinates (@settlements) {
715 91         191 for my $i ($self->neighbors()) {
716 410         831 my ($x, $y) = $self->neighbor($coordinates, $i);
717 410         762 my $other = coordinates($x, $y);
718 410 100 100     1792 next unless $world->{$other} and $world->{$other} =~ /water|ocean/;
719             # bump ports one size category up
720 19         56 $world->{$coordinates} =~ s/large-town/city port/;
721 19         47 $world->{$coordinates} =~ s/town/large-town port/;
722 19         40 $world->{$coordinates} =~ s/village/town port/;
723             # no bumps for thorps
724 19         41 last;
725             }
726             }
727 4         16 for my $coordinates (@settlements) {
728             # thorps and villages don't cut enough wood; make sure to get both "green" and "dark-green"
729 91 100       226 $world->{$coordinates} =~ s/\S*green trees/light-soil/ if $world->{$coordinates} =~ /large-town|city/;
730 91 100       201 $world->{$coordinates} =~ s/\S*green trees/soil/ if $world->{$coordinates} =~ / town/;
731             }
732 4         69 return @settlements;
733             }
734              
735             sub trails {
736 4     4 0 13 my $self = shift;
737 4         14 my ($altitude, $settlements) = @_;
738             # look for a neighbor that is as low as possible and nearby
739 4         8 my %trails;
740 4         47 my @from = shuffle @$settlements;
741 4         25 my @to = shuffle @$settlements;
742 4         12 for my $from (@from) {
743 91         388 my ($best, $best_distance, $best_altitude);
744 91         153 for my $to (@to) {
745 3555 100       5569 next if $from eq $to;
746 3464         6084 my $distance = $self->distance($from, $to);
747 3464         11036 $log->debug("Considering $from-$to: distance $distance, altitude " . $altitude->{$to});
748 3464 100 100     20009 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 124         160 $best = $to;
752 124         183 $best_altitude = $altitude->{$best};
753 124         180 $best_distance = $distance;
754             }
755             }
756 91 100       175 next if not $best;
757             # skip if it already exists in the other direction
758 84 100       265 next if $trails{"$best-$from"};
759 63         283 $trails{"$from-$best"} = 1;
760 63         180 $log->debug("Trail $from-$best");
761             }
762 4         123 return keys %trails;
763             }
764              
765             sub cliffs {
766 2     2 0 5 my $self = shift;
767 2         8 my ($world, $altitude) = @_;
768 2         12 my @neighbors = $self->neighbors();
769             # hexes with altitude difference bigger than 1 have cliffs
770 2         41 for my $coordinates (keys %$world) {
771 600 50       1674 next if $altitude->{$coordinates} <= $self->bottom;
772 600         2364 for my $i (@neighbors) {
773 3000         6031 my ($x, $y) = $self->neighbor($coordinates, $i);
774 3000 100       5515 next unless $self->legal($x, $y);
775 2762         24757 my $other = coordinates($x, $y);
776 2762 100       7343 if ($altitude->{$coordinates} - $altitude->{$other} >= 2) {
777 186 100       344 if (@neighbors == 6) {
778 116         361 $world->{$coordinates} .= " cliff$i";
779             } else { # square
780 70         279 $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         4 my %seen;
790 2         7 for my $river (@$rivers) {
791 105         177 my $last = $river->[0];
792 105         139 for my $coordinates (@$river) {
793 385 100       655 last if $seen{$coordinates}; # we've been here before
794 310         498 $seen{$coordinates} = 1;
795 310 100       556 next unless exists $altitude->{$coordinates}; # rivers ending off the map
796 280 50       464 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 280         972 $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         5 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   17 sub { $self->flat($altitude);
820 2         37 $self->altitude($world, $altitude); },
821 2     2   18 sub { $self->bumpiness($world, $altitude); },
822 2     2   31 sub { $self->mountains($world, $altitude); },
823 2     2   22 sub { $self->ocean($world, $altitude); },
824 2     2   25 sub { $self->water($world, $altitude, $water); },
825 2     2   46 sub { $self->lakes($world, $altitude, $water); },
826 2     2   24 sub { $self->flood($world, $altitude, $water); },
827 2     2   29 sub { $self->bogs($world, $altitude, $water); },
828 2     2   33 sub { $self->winds($world, $altitude, $water); },
829 2     2   29 sub { $self->rivers($world, $altitude, $water, $flow, $rivers); },
830 2     2   37 sub { $self->canyons($world, $altitude, $rivers, $canyons, $dry); },
831 2     2   48 sub { $self->swamps($world, $altitude, $water, $flow, $dry); },
832 2     2   34 sub { $self->forests($world, $altitude, $flow, $dry); },
833 2     2   45 sub { $self->dry($world, $altitude); },
834 2     2   36 sub { $self->cliffs($world, $altitude); },
835 2     2   53 sub { push(@$settlements, $self->settlements($world, $flow)); },
836 2     2   43 sub { push(@$trails, $self->trails($altitude, $settlements)); },
837 2     2   47 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         76 );
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         9 while (@code) {
846 36         173 shift(@code)->();
847 36 50       634 return if $step == $i++;
848 36         178 $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 107 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       134 if ($step >= 10) {
858 20         992 for my $coordinates (keys %$world) {
859 6000         8856 $world->{$coordinates} =~ s/ arrow\d//;
860             }
861             }
862             # Wind direction is only shown once.
863 36         286 $world->{"0101"} =~ s/ wind\d//;
864             # Remove zone markers.
865 36         966 for my $coordinates (keys %$world) {
866 10800         14269 $world->{$coordinates} =~ s/ zone//;
867             }
868             }
869              
870             sub generate_map {
871 2     2 0 1327 my $self = shift;
872              
873             # The parameters turn into class variables.
874 2   50     25 $self->width(shift // 30);
875 2   50     41 $self->height(shift // 10);
876 2   50     27 $self->steepness(shift // 3);
877 2   33     22 $self->peaks(shift // int($self->width * $self->height / 40));
878 2   50     52 $self->peak(shift // 10);
879 2   33     20 $self->bumps(shift // int($self->width * $self->height / 40));
880 2   50     42 $self->bump(shift // 2);
881 2   50     25 $self->bottom(shift // 0);
882 2   50     27 $self->arid(shift // 2);
883 2         39 $self->wind(shift); # or random
884 2   33     14 my $seed = shift||time;
885 2         5 my $url = shift;
886 2   50     9 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         8 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         10 my ($world, $altitude, $water, $rivers, $settlements, $trails, $canyons) =
907             ({}, {}, {}, [], [], [], []);
908 2         18 $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         10 local $" = "-"; # list items separated by -
918 2         5 my @lines;
919 2         182 push(@lines, map { $_ . " " . $world->{$_} } sort keys %$world);
  600         1115  
920 2         43 push(@lines, map { "$_ trail" } @$trails);
  60         114  
921 2         10 push(@lines, map { "@$_ river" } @$rivers);
  105         292  
922 2         13 push(@lines, map { "@$_ canyon" } @$canyons); # after rivers
  16         44  
923 2         7 push(@lines, "include gnomeyland.txt");
924              
925             # when documenting or debugging, add some more lines at the end
926 2 50       14 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         10 push(@lines, "# Seed: $seed");
938 2 50       27 push(@lines, "# Documentation: " . $url) if $url;
939 2         1613 my $map = join("\n", @lines);
940 2         577 return $map;
941             }
942              
943             1;