File Coverage

blib/lib/Game/TextMapper/Schroeder/Alpine.pm
Criterion Covered Total %
statement 499 553 90.2
branch 198 250 79.2
condition 87 123 70.7
subroutine 50 51 98.0
pod 0 28 0.0
total 834 1005 82.9


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   11 use Game::TextMapper::Log;
  1         2  
  1         27  
49 1     1   4 use Modern::Perl '2018';
  1         3  
  1         5  
50 1     1   115 use Mojo::Base -base;
  1         2  
  1         6  
51 1     1   630 use Role::Tiny::With;
  1         276  
  1         49  
52             with 'Game::TextMapper::Schroeder::Base';
53 1     1   6 use List::Util 'shuffle';
  1         2  
  1         8778  
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         3 my $altitude = shift;
69 2         4 my $count = shift;
70 2         7 my $current_altitude = shift;
71 2         8 my @queue;
72             # place some peaks and put them in a queue
73 2         8 for (1 .. $count) {
74             # try to find an empty hex
75 14         21 for (1 .. 6) {
76 14         24 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       33 next if $altitude->{$coordinates};
80 14         23 $altitude->{$coordinates} = $current_altitude;
81 14         40 $log->debug("placed $current_altitude at $coordinates");
82 14         90 push(@queue, $coordinates);
83 14         30 last;
84             }
85             }
86 2         19 return @queue;
87             }
88              
89             sub grow_mountains {
90 2     2 0 4 my $self = shift;
91 2         3 my $altitude = shift;
92 2         6 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         7 while (@queue) {
96 600         1813 my $coordinates = shift @queue;
97 600         977 my $current_altitude = $altitude->{$coordinates};
98 600 50       1017 next unless $current_altitude > 0;
99             # pick some random neighbors based on variable steepness
100 600         1324 my $n = $self->steepness;
101             # round up based on fraction
102 600 50       2705 $n += 1 if rand() < $n - int($n);
103 600         764 $n = int($n);
104 600 50       874 next if $n < 1;
105 600         1010 for (1 .. $n) {
106             # try to find an empty neighbor; abort after six attempts
107 1800         3856 for (1 .. 6) {
108 8651         25774 my ($x, $y) = $self->neighbor($coordinates, $self->random_neighbor());
109 8651 100       17685 next unless $self->legal($x, $y);
110 7861         72718 my $other = coordinates($x, $y);
111             # if this is taken, look further
112 7861 100       16543 if ($altitude->{$other}) {
113 7675         14338 ($x, $y) = $self->neighbor2($coordinates, $self->random_neighbor2());
114 7675 100       15984 next unless $self->legal($x, $y);
115 6656         60716 $other = coordinates($x, $y);
116             # if this is also taken, try again
117 6656 100       18287 next if $altitude->{$other};
118             }
119             # if we found an empty neighbor, set its altitude
120 586 50       1036 $altitude->{$other} = $current_altitude > 0 ? $current_altitude - 1 : 0;
121 586         940 push(@queue, $other);
122 586         1139 last;
123             }
124             }
125             }
126             }
127              
128             sub fix_altitude {
129 2     2 0 7 my $self = shift;
130 2         5 my $altitude = shift;
131             # go through all the hexes
132 2         279 for my $coordinates (sort keys %$altitude) {
133             # find hexes that we missed and give them the height of a random neighbor
134 600 50       922 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         19 my ($world, $altitude) = @_;
156 2         10 my @queue = $self->place_peak($altitude, $self->peaks, $self->peak);
157 2         15 $self->grow_mountains($altitude, @queue);
158 2         46 $self->fix_altitude($altitude);
159             # note height for debugging purposes
160 2         191 for my $coordinates (sort keys %$altitude) {
161 600         1403 $world->{$coordinates} = "height$altitude->{$coordinates}";
162             }
163             }
164              
165             sub bumpiness {
166 2     2 0 8 my ($self, $world, $altitude) = @_;
167 2         11 for (1 .. $self->bumps) {
168 14         43 for my $delta (-$self->bump, $self->bump) {
169             # six attempts to try and find a good hex
170 28         95 for (1 .. 6) {
171 30         64 my $x = int(rand($self->width)) + 1;
172 30         133 my $y = int(rand($self->height)) + 1;
173 30         124 my $coordinates = coordinates($x, $y);
174 30         51 my $current_altitude = $altitude->{$coordinates} + $delta;
175 30 100 66     82 next if $current_altitude > 10 or $current_altitude < 0;
176             # bump it up or down
177 28         40 $altitude->{$coordinates} = $current_altitude;
178 28         55 $world->{$coordinates} = "height$altitude->{$coordinates} zone";
179 28         96 $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     235 if ($delta < -1 or $delta > 1) {
182 28         67 my $delta = $delta - $delta / abs($delta);
183 28         64 for my $i ($self->neighbors()) {
184 140         791 my ($x, $y) = $self->neighbor($coordinates, $i);
185 140 100       273 next unless $self->legal($x, $y);
186 134         1368 my $other = coordinates($x, $y);
187 134         263 $current_altitude = $altitude->{$other} + $delta;
188 134 100 66     354 next if $current_altitude > 10 or $current_altitude < 0;
189 133         171 $altitude->{$other} = $current_altitude;
190 133         250 $world->{$other} = "height$altitude->{$other} zone";
191 133         377 $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         202 last;
196             }
197             }
198             }
199             }
200              
201             sub water {
202 2     2 0 4 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         157 for my $coordinates (sort keys %$altitude) {
207 600 50       1327 next if $altitude->{$coordinates} <= $self->bottom;
208             # note preferred water flow by identifying lower lying neighbors
209 600         2307 my ($lowest, $direction);
210             # look at neighbors in random order
211             NEIGHBOR:
212 600         1156 for my $i (shuffle $self->neighbors()) {
213 3000         9802 my ($x, $y) = $self->neighbor($coordinates, $i);
214 3000         6480 my $legal = $self->legal($x, $y);
215 3000         27998 my $other = coordinates($x, $y);
216 3000 100 100     9913 next if $legal and $altitude->{$other} > $altitude->{$coordinates};
217             # don't point head on to another arrow
218 2180 100 100     6668 next if $legal and $water->{$other} and $water->{$other} == ($i-3) % 6;
      100        
219             # don't point into loops
220 2077         4611 my %loop = ($coordinates => 1, $other => 1);
221 2077         2881 my $next = $other;
222 2077         6840 $log->debug("Loop detection starting with $coordinates and $other");
223 2077         12553 while ($next) {
224             # no water flow known is also good;
225 3687   100     12789 $log->debug("water for $next: " . ($water->{$next} || "none"));
226 3687 100       20146 last unless defined $water->{$next};
227 1923         4314 ($x, $y) = $self->neighbor($next, $water->{$next});
228             # leaving the map is good
229 1923         4807 $log->debug("legal for $next: " . $self->legal($x, $y));
230 1923 100       27703 last unless $self->legal($x, $y);
231 1677         16091 $next = coordinates($x, $y);
232             # skip this neighbor if this is a loop
233 1677   100     7647 $log->debug("is $next in a loop? " . ($loop{$next} || "no"));
234 1677 100       9975 next NEIGHBOR if $loop{$next};
235 1610         3203 $loop{$next} = 1;
236             }
237 2010 100 66     10166 if (not defined $direction
      66        
      100        
      66        
238             or not $legal and $altitude->{$coordinates} < $lowest
239             or $legal and $altitude->{$other} < $lowest) {
240 798 100       1574 $lowest = $legal ? $altitude->{$other} : $altitude->{$coordinates};
241 798         937 $direction = $i;
242 798         1930 $log->debug("Set lowest to $lowest ($direction)");
243             }
244             }
245 600 100       1601 if (defined $direction) {
246 588         1054 $water->{$coordinates} = $direction;
247             $world->{$coordinates} =~ s/arrow\d/arrow$water->{$coordinates}/
248 588 50       2176 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         42 for my $coordinates (keys %$altitude) {
258 600 100       1158 if ($altitude->{$coordinates} >= 10) {
    100          
    100          
259 18         37 $world->{$coordinates} = "white mountains";
260             } elsif ($altitude->{$coordinates} >= 9) {
261 51         69 $world->{$coordinates} = "white mountain";
262             } elsif ($altitude->{$coordinates} >= 8) {
263 116         176 $world->{$coordinates} = "light-grey mountain";
264             }
265             }
266             }
267              
268             sub ocean {
269 2     2 0 6 my $self = shift;
270 2         6 my ($world, $altitude) = @_;
271 2         201 for my $coordinates (sort keys %$altitude) {
272 600 50       2077 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 7 my $self = shift;
288 2         7 my ($world, $altitude, $water) = @_;
289             # any areas without water flow are lakes
290 2         210 for my $coordinates (sort keys %$altitude) {
291 600 100 66     1064 if (not defined $water->{$coordinates}
292             and $world->{$coordinates} ne "ocean") {
293 12         17 $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 7 my ($self, $world, $altitude, $water, $flow, $dry) = @_;
301 2         44 for my $coordinates (keys %$altitude) {
302             # don't turn lakes into swamps and skip bogs
303 600 100       1520 next if $world->{$coordinates} =~ /ocean|water|swamp|grass/;
304             # swamps require a river
305 543 100       824 next unless $flow->{$coordinates};
306             # no swamps when there is a canyon
307 264 100       479 next if $dry->{$coordinates};
308             # look at the neighbor the water would flow to
309 249         493 my ($x, $y) = $self->neighbor($coordinates, $water->{$coordinates});
310             # skip if water flows off the map
311 249 100       467 next unless $self->legal($x, $y);
312 222         1956 my $other = coordinates($x, $y);
313             # skip if water flows downhill
314 222 100       503 next if $altitude->{$coordinates} > $altitude->{$other};
315             # if there was no lower neighbor, this is a swamp
316 65 100       102 if ($altitude->{$coordinates} >= 6) {
317 28         112 $world->{$coordinates} =~ s/height\d+/grey swamp/;
318             } else {
319 37         143 $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         5 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         59 my @lakes = shuffle sort grep { not defined $water->{$_} } keys %$world;
  600         855  
334 2 50       26 return unless @lakes;
335 2         4 my $start = shift(@lakes);
336 2         4 my @candidates = ($start);
337 2         5 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 130   100     470 ($altitude->{$a}||0) <=> ($altitude->{$b}||0)
  6092   100     11896  
344             } shuffle @candidates;
345 130         595 $log->debug("Candidates @candidates");
346 130         708 my $coordinates;
347             do {
348 140         466 $coordinates = shift(@candidates);
349 130   66     154 } until not $coordinates or not $seen{$coordinates};
350 130 50       235 last unless $coordinates;
351 130         197 $seen{$coordinates} = 1;
352 130         360 $log->debug("Looking at $coordinates");
353 130 100 66     723 if ($self->legal($coordinates) and $world->{$coordinates} ne "ocean") {
354             # if we're still on the map, check all the unknown neighbors
355 118         1259 my $from = $coordinates;
356 118         220 for my $i ($self->neighbors()) {
357 514         929 my $to = coordinates($self->neighbor($from, $i));
358 514 100       1067 next if $seen{$to};
359 388         1046 $log->debug("Adding $to to our candidates");
360 388         2249 $flow{$to} = $from;
361             # adding to the front as we keep pushing forward (I hope)
362 388         650 push(@candidates, $to);
363             }
364 118         249 next;
365             }
366 12         213 $log->debug("We left the map at $coordinates");
367 12         77 my $to = $coordinates;
368 12         18 my $from = $flow{$to};
369 12         24 while ($from) {
370 75         169 my $i = $self->direction($from, $to);
371 75 100 100     328 if (not defined $water->{$from}
372             or $water->{$from} != $i) {
373 47         147 $log->debug("Arrow for $from now points to $to");
374 47         261 $water->{$from} = $i;
375             $world->{$from} =~ s/arrow\d/arrow$i/
376 47 100       294 or $world->{$from} .= " arrow$i";
377             } else {
378 28         74 $log->debug("Arrow for $from already points $to");
379             }
380 75         221 $to = $from;
381 75         154 $from = $flow{$to};
382             }
383             # pick the next lake
384             do {
385 12         19 $start = shift(@lakes);
386 12 100       35 $log->debug("Next lake is $start") if $start;
387 12   66     17 } until not $start or not defined $water->{$start};
388 12 100       96 last unless $start;
389 10         47 %seen = %flow = ();
390 10         34 @candidates = ($start);
391             }
392             }
393              
394             sub rivers {
395 2     2 0 12 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       306 $world->{$_} = "light-grey forest-hill" unless $world->{$_} =~ /mountain|swamp|grass|water|ocean/;
399 117         297 $flow->{$_} = [$_]
400             } sort grep {
401             # these are the potential starting places: up in the mountains below the
402             # ice, or lakes
403 2         51 ($altitude->{$_} == 7 or $altitude->{$_} == 8
404             or $world->{$_} =~ /water/ and $altitude->{$_} > $self->bottom)
405             and not $flow->{$_}
406 600 50 66     2588 and $world->{$_} !~ /dry/;
      66        
407             } keys %$altitude;
408 2         48 $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         8 while (@$growing) {
414             # warn "Rivers: " . @growing . "\n";
415             # pick a random growing river and grow it
416 888         1561 my $n = int(rand(scalar @$growing));
417 888         1149 my $river = $growing->[$n];
418             # warn "Picking @$river\n";
419 888         1082 my $coordinates = $river->[-1];
420 888         1021 my $end = 1;
421 888 100       1431 if (defined $water->{$coordinates}) {
422 776         1775 my $other = coordinates($self->neighbor($coordinates, $water->{$coordinates}));
423 776 50       7297 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 776 100       1618 if (ref $flow->{$other}) {
426             # warn "Prepending @$river to @{$flow->{$other}}\n";
427             # prepend the current river to the other river
428 5         7 unshift(@{$flow->{$other}}, @$river);
  5         16  
429             # move the source marker
430 5         11 $flow->{$river->[0]} = $flow->{$other};
431 5         6 $flow->{$other} = 1;
432             # and remove the current river from the growing list
433 5         16 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 771         1213 $flow->{$coordinates} = 1;
438 771         2135 push(@$river, $other);
439             }
440             } else {
441             # stop growing this river
442             # warn "Stopped river: @$river\n" if grep(/0914/, @$river);
443 112         265 push(@$rivers, splice(@$growing, $n, 1));
444             }
445             }
446             }
447              
448             sub canyons {
449 2     2 0 7 my $self = shift;
450 2         6 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         3 my %seen;
455 2         6 for my $river (@$rivers) {
456 112         174 my $last = $river->[0];
457 112         143 my $current_altitude = $altitude->{$last};
458 112         360 $log->debug("Looking at @$river ($current_altitude)");
459 112         542 for my $coordinates (@$river) {
460 734         1485 $log->debug("Looking at $coordinates");
461 734 100       3532 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 30 50       51 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 30         35 $log->debug("We've seen the rest: @{$seen{$coordinates}}");
  30         80  
484 30         147 last;
485             }
486             # no canyons through water!
487 704 100 100     1823 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 15 100       52 push(@$canyon, $last) unless @$canyon;
492 15         27 push(@$canyon, $coordinates);
493 15 50       32 $world->{$coordinates} .= " zone" unless $dry->{$coordinates};
494 15         24 $dry->{$coordinates} = 1;
495 15         48 $log->debug("Growing canyon @$canyon");
496 15         81 $seen{$coordinates} = $canyon;
497             } else {
498             # if we just left a canyon, append the current step
499 689 100       964 if (@$canyon) {
500 7         14 push(@$canyon, $coordinates);
501 7         12 push(@$canyons, $canyon);
502 7         26 $log->debug("Looking at river @$river");
503 7         47 $log->debug("Canyon @$canyon");
504 7         34 $canyon = [];
505 7         13 last;
506             }
507             # not digging a canyon
508 682         816 $last = $coordinates;
509 682         940 $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 279     279 0 463 my ($self, $coordinates, $world, $altitude, $dry) = @_;
534 279         358 my @candidates;
535 279 100       1104 push(@candidates, $coordinates) if $world->{$coordinates} !~ /mountain|hill|water|ocean|swamp|grass/;
536 279         656 my $n = $self->arid;
537             # fractions are allowed
538 279 50       1118 $n += 1 if rand() < $self->arid - int($self->arid);
539 279         1311 $n = int($n);
540 279         769 $log->debug("Arid: $n");
541 279 50       1463 if ($n >= 1) {
542 279         578 for my $i ($self->neighbors()) {
543 1396         3058 my ($x, $y) = $self->neighbor($coordinates, $i);
544 1396 100       2743 next unless $self->legal($x, $y);
545 1307         11638 my $other = coordinates($x, $y);
546 1307 100       2771 next if $dry->{$other};
547 1288 100       2351 next if $altitude->{$coordinates} < $altitude->{$other}; # distance of one unless higher
548 859 100       3364 push(@candidates, $other) if $world->{$other} !~ /mountain|hill|water|ocean|swamp|grass/;
549             }
550             }
551 279 50       606 if ($n >= 2) {
552 279         554 for my $i ($self->neighbors2()) {
553 2792         6930 my ($x, $y) = $self->neighbor2($coordinates, $i);
554 2792 100       5333 next unless $self->legal($x, $y);
555 2444         21533 my $other = coordinates($x, $y);
556 2444 100       5522 next if $altitude->{$coordinates} <= $altitude->{$other}; # distance of two only if lower
557 696         808 my $ok = 0;
558 696         1302 for my $m ($self->neighbors()) {
559 2227         4361 my ($mx, $my) = $self->neighbor($coordinates, $m);
560 2227 100       4269 next unless $self->legal($mx, $my);
561 2192         19505 my $midway = coordinates($mx, $my);
562 2192 100       4086 next if $dry->{$midway};
563 2181 100       4014 next if $self->distance($midway, $other) != 1;
564 718 100       1531 next if $altitude->{$coordinates} < $altitude->{$midway};
565 686 100       1211 next if $altitude->{$midway} < $altitude->{$other};
566 662         764 $ok = 1;
567 662         872 last;
568             }
569 696 100       1229 next unless $ok;
570 662 100       2845 push(@candidates, $other) if $world->{$other} !~ /mountain|hill|water|ocean|swamp|grass/;
571             }
572             }
573 279         1492 $log->debug("forest growth: $coordinates: @candidates");
574 279         2123 for $coordinates (@candidates) {
575 772 100       1452 if ($altitude->{$coordinates} >= 7) {
    100          
    100          
576 117         224 $world->{$coordinates} = "light-green fir-forest";
577             } elsif ($altitude->{$coordinates} >= 6) {
578 326         535 $world->{$coordinates} = "green fir-forest";
579             } elsif ($altitude->{$coordinates} >= 4) {
580 311         576 $world->{$coordinates} = "green forest";
581             } else {
582 18         48 $world->{$coordinates} = "dark-green forest";
583             }
584             }
585             }
586              
587             sub forests {
588 2     2 0 8 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         48 for my $coordinates (keys %$flow) {
592 294 100       555 next if $dry->{$coordinates};
593 279         499 $self->grow_forest($coordinates, $world, $altitude, $dry);
594             }
595             }
596              
597             sub winds {
598 2     2 0 4 my $self = shift;
599 2         6 my ($world, $altitude, $water, $flow) = @_;
600 2   33     9 my $wind = $self->wind // $self->random_neighbor;
601 2         15 $world->{"0101"} .= " wind" . $self->reverse($wind);
602 2         46 for my $coordinates (keys %$altitude) {
603             # limit ourselves to altitude 7 and 8
604 600 100 100     1428 next if $altitude->{$coordinates} < 7 or $altitude->{$coordinates} > 8;
605             # look at the neighbor the water would flow to
606 269         556 my ($x, $y) = $self->neighbor($coordinates, $wind);
607             # skip if off the map
608 269 100       566 next unless $self->legal($x, $y);
609 260         2614 my $other = coordinates($x, $y);
610             # skip if the other hex is lower
611 260 100       645 next if $altitude->{$coordinates} > $altitude->{$other};
612             # if the other hex was higher, this land is dry
613 160         488 $log->debug("$coordinates is dry because of $other");
614 160         1028 $world->{$coordinates} .= " dry zone"; # use label for debugging
615             }
616             }
617              
618             sub bogs {
619 2     2 0 6 my $self = shift;
620 2         5 my ($world, $altitude, $water) = @_;
621 2         55 for my $coordinates (keys %$altitude) {
622             # limit ourselves to altitude 7
623 600 100       1145 next if $altitude->{$coordinates} != 7;
624             # don't turn lakes into bogs
625 153 100       455 next if $world->{$coordinates} =~ /water|ocean/;
626             # look at the neighbor the water would flow to
627 149         341 my ($x, $y) = $self->neighbor($coordinates, $water->{$coordinates});
628             # skip if water flows off the map
629 149 100       328 next unless $self->legal($x, $y);
630 139         1311 my $other = coordinates($x, $y);
631             # skip if water flows downhill
632 139 100       363 next if $altitude->{$coordinates} > $altitude->{$other};
633             # if there was no lower neighbor, this is a bog
634 45         225 $world->{$coordinates} =~ s/height\d+/grey swamp/;
635             }
636             }
637              
638             sub dry {
639 2     2 0 7 my ($self, $world, $altitude) = @_;
640 2         4 my @dry;
641 2         208 for my $coordinates (shuffle sort keys %$world) {
642 600 100       1585 if ($world->{$coordinates} !~ /mountain|hill|water|ocean|swamp|grass|forest|firs|trees/) {
643 30 100       54 if ($altitude->{$coordinates} >= 7) {
644 8         15 $world->{$coordinates} = "light-grey grass";
645             } else {
646 22         28 $world->{$coordinates} = "light-green bushes";
647 22         38 push(@dry, $coordinates);
648             }
649             }
650             }
651 2 50       22 return unless @dry;
652             # dry some of them up
653 2         14 my @seeds = @dry[0..@dry/4];
654 2         6 for my $coordinates (@seeds) {
655 7         38 $self->drier($world, $coordinates);
656 7         18 for my $i ($self->neighbors()) {
657 32         98 my ($x, $y) = $self->neighbor($coordinates, $i);
658 32 100       61 next unless $self->legal($x, $y);
659 23         239 my $other = coordinates($x, $y);
660 23         45 $self->drier($world, $other);
661             }
662             }
663             }
664              
665             sub drier {
666 30     30 0 44 my ($self, $world, $coordinates) = @_;
667             $world->{$coordinates} =~ s/light-green bushes/light-green grass/
668             or $world->{$coordinates} =~ s/light-green grass/dust grass/
669             or $world->{$coordinates} =~ s/dust grass/dust hill/
670 30 100 100     140 or $world->{$coordinates} =~ s/dust hill/dust desert/;
      66        
671             }
672              
673             sub settlements {
674 4     4 0 12 my $self = shift;
675 4         11 my ($world, $flow) = @_;
676 4         23 my @settlements;
677 4         15 my $max = $self->height * $self->width;
678             # do not match forest-hill
679 4         156 my @candidates = shuffle sort grep { $world->{$_} =~ /\b(fir-forest|forest(?!-hill))\b/ } keys %$world;
  1800         3073  
680 4         101 @candidates = $self->remove_closer_than(2, @candidates);
681 4 100       43 @candidates = @candidates[0 .. int($max/10 - 1)] if @candidates > $max/10;
682 4         19 push(@settlements, @candidates);
683 4         13 for my $coordinates (@candidates) {
684             $world->{$coordinates} =~ s/fir-forest/firs thorp/
685 67 100       214 or $world->{$coordinates} =~ s/forest(?!-hill)/trees thorp/;
686             }
687 4 100       135 @candidates = shuffle sort grep { $world->{$_} =~ /(?{$_}} keys %$world;
  1800         3234  
688 4         71 @candidates = $self->remove_closer_than(5, @candidates);
689 4 50       26 @candidates = @candidates[0 .. int($max/20 - 1)] if @candidates > $max/20;
690 4         12 push(@settlements, @candidates);
691 4         10 for my $coordinates (@candidates) {
692 10         26 $world->{$coordinates} =~ s/forest/trees village/;
693             }
694 4 100       125 @candidates = shuffle sort grep { $world->{$_} =~ /(?{$_} } keys %$world;
  1800         3140  
695 4         57 @candidates = $self->remove_closer_than(10, @candidates);
696 4 50       19 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
697 4         8 push(@settlements, @candidates);
698 4         8 for my $coordinates (@candidates) {
699 5         16 $world->{$coordinates} =~ s/forest/trees town/;
700             }
701 4         120 @candidates = shuffle sort grep { $world->{$_} =~ /white mountain\b/ } keys %$world;
  1800         2527  
702 4         61 @candidates = $self->remove_closer_than(10, @candidates);
703 4 50       19 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
704 4         9 push(@settlements, @candidates);
705 4         18 for my $coordinates (@candidates) {
706 6         26 $world->{$coordinates} =~ s/white mountain\b/white mountain law/;
707             }
708 4         126 @candidates = shuffle sort grep { $world->{$_} =~ /swamp/ } keys %$world;
  1800         2624  
709 4         62 @candidates = $self->remove_closer_than(10, @candidates);
710 4 50       26 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
711 4         11 push(@settlements, @candidates);
712 4         10 for my $coordinates (@candidates) {
713 5         18 $world->{$coordinates} =~ s/swamp/swamp2 chaos/;
714             }
715 4         9 for my $coordinates (@settlements) {
716 93         215 for my $i ($self->neighbors()) {
717 429         764 my ($x, $y) = $self->neighbor($coordinates, $i);
718 429         772 my $other = coordinates($x, $y);
719 429 100 100     1596 next unless $world->{$other} and $world->{$other} =~ /water|ocean/;
720             # bump ports one size category up
721 15         32 $world->{$coordinates} =~ s/large-town/city port/;
722 15         30 $world->{$coordinates} =~ s/town/large-town port/;
723 15         26 $world->{$coordinates} =~ s/village/town port/;
724             # no bumps for thorps
725 15         26 last;
726             }
727             }
728 4         15 for my $coordinates (@settlements) {
729             # thorps and villages don't cut enough wood; make sure to get both "green" and "dark-green"
730 93 100       209 $world->{$coordinates} =~ s/\S*green trees/light-soil/ if $world->{$coordinates} =~ /large-town|city/;
731 93 100       178 $world->{$coordinates} =~ s/\S*green trees/soil/ if $world->{$coordinates} =~ / town/;
732             }
733 4         32 return @settlements;
734             }
735              
736             sub trails {
737 4     4 0 11 my $self = shift;
738 4         10 my ($altitude, $settlements) = @_;
739             # look for a neighbor that is as low as possible and nearby
740 4         7 my %trails;
741 4         28 my @from = shuffle @$settlements;
742 4         29 my @to = shuffle @$settlements;
743 4         10 for my $from (@from) {
744 93         395 my ($best, $best_distance, $best_altitude);
745 93         128 for my $to (@to) {
746 3725 100       5653 next if $from eq $to;
747 3632         6130 my $distance = $self->distance($from, $to);
748 3632         11289 $log->debug("Considering $from-$to: distance $distance, altitude " . $altitude->{$to});
749 3632 100 100     20489 if ($distance <= 3
      100        
      100        
      100        
750             and (not $best_distance or $distance <= $best_distance)
751             and (not $best or $altitude->{$to} < $best_altitude)) {
752 143         191 $best = $to;
753 143         181 $best_altitude = $altitude->{$best};
754 143         202 $best_distance = $distance;
755             }
756             }
757 93 100       156 next if not $best;
758             # skip if it already exists in the other direction
759 86 100       174 next if $trails{"$best-$from"};
760 75         213 $trails{"$from-$best"} = 1;
761 75         161 $log->debug("Trail $from-$best");
762             }
763 4         56 return keys %trails;
764             }
765              
766             sub cliffs {
767 2     2 0 6 my $self = shift;
768 2         6 my ($world, $altitude) = @_;
769 2         15 my @neighbors = $self->neighbors();
770             # hexes with altitude difference bigger than 1 have cliffs
771 2         45 for my $coordinates (keys %$world) {
772 600 50       1468 next if $altitude->{$coordinates} <= $self->bottom;
773 600         2104 for my $i (@neighbors) {
774 3000         6052 my ($x, $y) = $self->neighbor($coordinates, $i);
775 3000 100       5419 next unless $self->legal($x, $y);
776 2762         24263 my $other = coordinates($x, $y);
777 2762 100       6513 if ($altitude->{$coordinates} - $altitude->{$other} >= 2) {
778 155 100       261 if (@neighbors == 6) {
779 95         218 $world->{$coordinates} .= " cliff$i";
780             } else { # square
781 60         128 $world->{$coordinates} .= " cliffs$i";
782             }
783             }
784             }
785             }
786             }
787              
788             sub marshlands {
789 2     2 0 8 my ($self, $world, $altitude, $rivers) = @_;
790 2         3 my %seen;
791 2         6 for my $river (@$rivers) {
792 112         168 my $last = $river->[0];
793 112         128 for my $coordinates (@$river) {
794 407 100       677 last if $seen{$coordinates}; # we've been here before
795 325         461 $seen{$coordinates} = 1;
796 325 100       512 next unless exists $altitude->{$coordinates}; # rivers ending off the map
797 295 50       453 if ($altitude->{$coordinates} <= $self->bottom) {
798 0 0 0     0 if ($altitude->{$coordinates} == $self->bottom
      0        
799             and $world->{$coordinates} =~ /water|ocean/
800             and $altitude->{$coordinates} == $altitude->{$last} - 1) {
801 0         0 $world->{$coordinates} = "blue-green swamp";
802             } else {
803 0         0 $world->{$coordinates} =~ s/ocean/water/;
804 0         0 delete $seen{$coordinates};
805 0         0 last;
806             }
807             }
808 295         960 $last = $coordinates;
809             }
810             }
811             }
812              
813             sub generate {
814 2     2 0 9 my ($self, $world, $altitude, $water, $rivers, $settlements, $trails, $canyons, $step) = @_;
815             # $flow indicates that there is actually a river in this hex
816 2         4 my $flow = {};
817             # $dry indicates that is a river in this hex, but it cut itself a canyon
818 2         3 my $dry = {};
819             my @code = (
820 2     2   18 sub { $self->flat($altitude);
821 2         20 $self->altitude($world, $altitude); },
822 2     2   16 sub { $self->bumpiness($world, $altitude); },
823 2     2   29 sub { $self->mountains($world, $altitude); },
824 2     2   16 sub { $self->ocean($world, $altitude); },
825 2     2   17 sub { $self->water($world, $altitude, $water); },
826 2     2   47 sub { $self->lakes($world, $altitude, $water); },
827 2     2   20 sub { $self->flood($world, $altitude, $water); },
828 2     2   31 sub { $self->bogs($world, $altitude, $water); },
829 2     2   33 sub { $self->winds($world, $altitude, $water); },
830 2     2   18 sub { $self->rivers($world, $altitude, $water, $flow, $rivers); },
831 2     2   18 sub { $self->canyons($world, $altitude, $rivers, $canyons, $dry); },
832 2     2   22 sub { $self->swamps($world, $altitude, $water, $flow, $dry); },
833 2     2   22 sub { $self->forests($world, $altitude, $flow, $dry); },
834 2     2   27 sub { $self->dry($world, $altitude); },
835 2     2   17 sub { $self->cliffs($world, $altitude); },
836 2     2   24 sub { push(@$settlements, $self->settlements($world, $flow)); },
837 2     2   20 sub { push(@$trails, $self->trails($altitude, $settlements)); },
838 2     2   25 sub { $self->marshlands($world, $altitude, $rivers); },
839             # make sure you look at "alpine_document.html.ep" if you change this list!
840             # make sure you look at '/alpine/document' if you add to this list!
841 2         68 );
842              
843             # $step 0 runs all the code; note that we can't simply cache those results
844             # because we need to start over with the same seed!
845 2         4 my $i = 1;
846 2         7 while (@code) {
847 36         153 shift(@code)->();
848 36 50       427 return if $step == $i++;
849 36         111 $self->fixup($world, $altitude, $i);
850             }
851             }
852              
853             # Remove temporary markers that won't be needed in the next step
854             sub fixup {
855 36     36 0 73 my ($self, $world, $altitude, $step, $last) = @_;
856             # When documenting or debugging, water flow arrows are no longer needed when
857             # the rivers are added.
858 36 100       100 if ($step >= 10) {
859 20         654 for my $coordinates (keys %$world) {
860 6000         8193 $world->{$coordinates} =~ s/ arrow\d//;
861             }
862             }
863             # Wind direction is only shown once.
864 36         236 $world->{"0101"} =~ s/ wind\d//;
865             # Remove zone markers.
866 36         843 for my $coordinates (keys %$world) {
867 10800         14524 $world->{$coordinates} =~ s/ zone//;
868             }
869             }
870              
871             sub generate_map {
872 2     2 0 1274 my $self = shift;
873              
874             # The parameters turn into class variables.
875 2   50     21 $self->width(shift // 30);
876 2   50     41 $self->height(shift // 10);
877 2   50     43 $self->steepness(shift // 3);
878 2   33     15 $self->peaks(shift // int($self->width * $self->height / 40));
879 2   50     46 $self->peak(shift // 10);
880 2   33     16 $self->bumps(shift // int($self->width * $self->height / 40));
881 2   50     38 $self->bump(shift // 2);
882 2   50     20 $self->bottom(shift // 0);
883 2   50     24 $self->arid(shift // 2);
884 2         16 $self->wind(shift); # or random
885 2   33     14 my $seed = shift||time;
886 2         4 my $url = shift;
887 2   50     7 my $step = shift||0;
888              
889             # For documentation purposes, I want to be able to set the pseudo-random
890             # number seed using srand and rely on rand to reproduce the same sequence of
891             # pseudo-random numbers for the same seed. The key point to remember is that
892             # the keys function will return keys in random order. So if we look over the
893             # result of keys, we need to look at the code in the loop: If order is
894             # important, that wont do. We need to sort the keys. If we want the keys to be
895             # pseudo-shuffled, use shuffle sort keys.
896 2         5 srand($seed);
897              
898             # Keys for all hashes are coordinates such as "0101".
899             # %world is the description with values such as "green forest".
900             # %altitude is the altitude with values such as 3.
901             # %water is the preferred direction water would take with values such as 0.
902             # (north west); 0 means we need to use "if defined".
903             # @rivers are the rivers with values such as ["0102", "0202"].
904             # @settlements are are the locations of settlements such as "0101".
905             # @trails are the trails connecting these with values as "0102-0202".
906             # $step is how far we want map generation to go where 0 means all the way.
907 2         10 my ($world, $altitude, $water, $rivers, $settlements, $trails, $canyons) =
908             ({}, {}, {}, [], [], [], []);
909 2         89 $self->generate($world, $altitude, $water, $rivers, $settlements, $trails, $canyons, $step);
910              
911             # When documenting or debugging, add altitude as a label.
912 2 50       8 if ($step > 0) {
913 0         0 for my $coordinates (keys %$world) {
914 0         0 $world->{$coordinates} .= ' "' . $altitude->{$coordinates} . '"';
915             }
916             }
917              
918 2         7 local $" = "-"; # list items separated by -
919 2         4 my @lines;
920 2         188 push(@lines, map { $_ . " " . $world->{$_} } sort keys %$world);
  600         1049  
921 2         38 push(@lines, map { "$_ trail" } @$trails);
  71         118  
922 2         8 push(@lines, map { "@$_ river" } @$rivers);
  112         283  
923 2         32 push(@lines, map { "@$_ canyon" } @$canyons); # after rivers
  7         24  
924 2         6 push(@lines, "include gnomeyland.txt");
925              
926             # when documenting or debugging, add some more lines at the end
927 2 50       7 if ($step > 0) {
928             # visualize height
929             push(@lines,
930             map {
931 0         0 my $n = int(25.5 * $_);
  0         0  
932 0         0 qq{height$_ attributes fill="rgb($n,$n,$n)"};
933             } (0 .. 10));
934             # visualize water flow
935 0         0 push(@lines, $self->arrows());
936             }
937              
938 2         6 push(@lines, "# Seed: $seed");
939 2 50       17 push(@lines, "# Documentation: " . $url) if $url;
940 2         1324 my $map = join("\n", @lines);
941 2         413 return $map;
942             }
943              
944             1;