File Coverage

blib/lib/Game/TextMapper/Schroeder/Alpine.pm
Criterion Covered Total %
statement 550 597 92.1
branch 222 280 79.2
condition 87 144 60.4
subroutine 52 53 98.1
pod 0 29 0.0
total 911 1103 82.5


line stmt bran cond sub pod time code
1             # Copyright (C) 2009-2023 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 11     11   113 use Game::TextMapper::Log;
  11         21  
  11         505  
49 11     11   64 use Modern::Perl '2018';
  11         21  
  11         130  
50 11     11   3697 use Mojo::Base -base;
  11         23  
  11         88  
51 11     11   6695 use Role::Tiny::With;
  11         2720  
  11         750  
52             with 'Game::TextMapper::Schroeder::Base';
53 11     11   65 use List::Util 'shuffle';
  11         19  
  11         126270  
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 'climate';
65             has 'wind';
66              
67             sub place_peak {
68 4     4 0 49 my $self = shift;
69 4         9 my $altitude = shift;
70 4         11 my $count = shift;
71 4         24 my ($min, $max) = split(/-/, shift);
72 4   33     42 $max //= $min;
73             # max altitude exactly once
74 4         15 my $x = int(rand($self->width)) + 1;
75 4         40 my $y = int(rand($self->height)) + 1;
76 4         36 my $coordinates = coordinates($x, $y);
77 4         14 $altitude->{$coordinates} = $max;
78 4         38 $log->debug("placed max $max at $coordinates");
79             # prepare distribution
80 4         54 my @distribution;
81 4         9 my $n = 1;
82 4         20 for my $i (0 .. $max - $min) {
83 4         9 push(@distribution, $n);
84 4         42 $n *= 2;
85             }
86             # this is the "die size"
87 4         12 $n = $distribution[$#distribution];
88 4         27 $log->debug("@distribution");
89 4         28 my @queue;
90             # place some peaks and put them in a queue
91 4         13 for (1 .. $count) {
92             # try to find an empty hex
93 28         59 for (1 .. 6) {
94 29         76 my $x = int(rand($self->width)) + 1;
95 29         182 my $y = int(rand($self->height)) + 1;
96 29         145 my $coordinates = coordinates($x, $y);
97 29 100       89 next if $altitude->{$coordinates};
98 28         50 my $r = rand($n);
99 28         168 $log->debug(" rolled $r");
100 28         206 for my $i (0 .. $#distribution) {
101 28         118 $log->debug(" $r < $distribution[$i]");
102 28 50       231 if ($r < $distribution[$i]) {
103 28         58 $altitude->{$coordinates} = $max - $i;
104 28         119 $log->debug("placed $altitude->{$coordinates} at $coordinates");
105 28         193 push(@queue, $coordinates);
106 28         50 last;
107             }
108             }
109 28         88 last;
110             }
111             }
112 4         28 return @queue;
113             }
114              
115             sub grow_mountains {
116 4     4 0 11 my $self = shift;
117 4         11 my $altitude = shift;
118 4         19 my @queue = @_;
119             # go through the queue and add adjacent lower altitude hexes, if possible; the
120             # hexes added are to the end of the queue
121 4         14 while (@queue) {
122 1196         3205 my $coordinates = shift @queue;
123 1196         1891 my $current_altitude = $altitude->{$coordinates};
124 1196 50       1976 next unless $current_altitude > 0;
125             # pick some random neighbors based on variable steepness
126 1196         2318 my $n = $self->steepness;
127             # round up based on fraction
128 1196 50       4568 $n += 1 if rand() < $n - int($n);
129 1196         1376 $n = int($n);
130 1196 50       1814 next if $n < 1;
131 1196         1977 for (1 .. $n) {
132             # try to find an empty neighbor; make more attempts if we're looking for
133             # more neighbours
134 3588         7716 for my $attempt (1 .. 3 + $n) {
135 17365         46350 my ($x, $y) = $self->neighbor($coordinates, $self->random_neighbor());
136 17365 100       29156 next unless $self->legal($x, $y);
137 15760         126677 my $other = coordinates($x, $y);
138             # if this is taken, look further
139 15760 100       28702 if ($altitude->{$other}) {
140 15262         26547 ($x, $y) = $self->neighbor2($coordinates, $self->random_neighbor2());
141 15262 100       25944 next unless $self->legal($x, $y);
142 13190         103711 $other = coordinates($x, $y);
143             # if this is also taken, try again – but if we've already had four
144             # attempts, jump!
145 13190 100       24223 if ($altitude->{$other}) {
146 12520 100       18890 $coordinates = $other if $attempt > 4;
147 12520         19261 next;
148             }
149             }
150             # if we found an empty neighbor, set its altitude
151 1168 50       2048 $altitude->{$other} = $current_altitude > 0 ? $current_altitude - 1 : 0;
152 1168         1783 push(@queue, $other);
153 1168         2093 last;
154             }
155             }
156             }
157             }
158              
159             sub fix_altitude {
160 4     4 0 12 my $self = shift;
161 4         8 my $altitude = shift;
162             # go through all the hexes
163 4         636 for my $coordinates (sort keys %$altitude) {
164             # find hexes that we missed and give them the height of a random neighbor
165 1200 50       1951 if (not defined $altitude->{$coordinates}) {
166             # warn "identified a hex that was skipped: $coordinates\n";
167             # try to find a suitable neighbor
168 0         0 for (1 .. 6) {
169 0         0 my ($x, $y) = $self->neighbor($coordinates, $self->random_neighbor());
170 0 0       0 next unless $self->legal($x, $y);
171 0         0 my $other = coordinates($x, $y);
172 0 0       0 next unless defined $altitude->{$other};
173 0         0 $altitude->{$coordinates} = $altitude->{$other};
174 0         0 last;
175             }
176             # if we didn't find one in the last six attempts, just make it hole in the ground
177 0 0       0 if (not defined $altitude->{$coordinates}) {
178 0         0 $altitude->{$coordinates} = 0;
179             }
180             }
181             }
182             }
183              
184             sub altitude {
185 4     4 0 13 my $self = shift;
186 4         14 my ($world, $altitude) = @_;
187 4         22 my @queue = $self->place_peak($altitude, $self->peaks, $self->peak);
188 4         53 $self->grow_mountains($altitude, @queue);
189 4         76 $self->fix_altitude($altitude);
190             # note height for debugging purposes
191 4         474 for my $coordinates (sort keys %$altitude) {
192 1200         2607 $world->{$coordinates} = "height$altitude->{$coordinates}";
193             }
194             }
195              
196             sub bumpiness {
197 4     4 0 17 my ($self, $world, $altitude) = @_;
198 4         30 for (1 .. $self->bumps) {
199 28         97 for my $delta (-$self->bump, $self->bump) {
200             # six attempts to try and find a good hex
201 56         287 for (1 .. 6) {
202 58         113 my $x = int(rand($self->width)) + 1;
203 58         311 my $y = int(rand($self->height)) + 1;
204 58         227 my $coordinates = coordinates($x, $y);
205 58         149 my $current_altitude = $altitude->{$coordinates} + $delta;
206 58 100 66     165 next if $current_altitude > 10 or $current_altitude < 0;
207             # bump it up or down
208 56         82 $altitude->{$coordinates} = $current_altitude;
209 56         103 $world->{$coordinates} = "height$altitude->{$coordinates} zone";
210 56         242 $log->debug("bumped altitude of $coordinates by $delta to $current_altitude");
211             # if the bump was +2 or -2, bump the neighbours by +1 or -1
212 56 50 66     413 if ($delta < -1 or $delta > 1) {
213 56         136 my $delta = $delta - $delta / abs($delta);
214 56         151 for my $i ($self->neighbors()) {
215 280         1683 my ($x, $y) = $self->neighbor($coordinates, $i);
216 280 100       563 next unless $self->legal($x, $y);
217 252         2441 my $other = coordinates($x, $y);
218 252         494 $current_altitude = $altitude->{$other} + $delta;
219 252 100 66     639 next if $current_altitude > 10 or $current_altitude < 0;
220 251         392 $altitude->{$other} = $current_altitude;
221 251         435 $world->{$other} = "height$altitude->{$other} zone";
222 251         700 $log->debug("$i bumped altitude of $other by $delta to $current_altitude");
223             }
224             }
225             # if we have found a good hex, don't go through all the other attempts
226 56         435 last;
227             }
228             }
229             }
230             }
231              
232             sub water {
233 4     4 0 10 my $self = shift;
234 4         13 my ($world, $altitude, $water) = @_;
235             # reset in case we run this twice
236             # go through all the hexes
237 4         468 for my $coordinates (sort keys %$altitude) {
238 1200 50       3049 next if $altitude->{$coordinates} <= $self->bottom;
239             # note preferred water flow by identifying lower lying neighbors
240 1200         4260 my ($lowest, $direction);
241             # look at neighbors in random order
242             NEIGHBOR:
243 1200         2664 for my $i (shuffle $self->neighbors()) {
244 6000         18718 my ($x, $y) = $self->neighbor($coordinates, $i);
245 6000         11450 my $legal = $self->legal($x, $y);
246 6000         51892 my $other = coordinates($x, $y);
247 6000 100 100     18836 next if $legal and $altitude->{$other} > $altitude->{$coordinates};
248             # don't point head on to another arrow
249 4416 100 100     14100 next if $legal and $water->{$other} and $water->{$other} == ($i-3) % 6;
      100        
250             # don't point into loops
251 4208         9207 my %loop = ($coordinates => 1, $other => 1);
252 4208         5469 my $next = $other;
253 4208         12232 $log->debug("Loop detection starting with $coordinates and $other");
254 4208         23801 while ($next) {
255             # no water flow known is also good;
256 7606   100     26823 $log->debug("water for $next: " . ($water->{$next} || "none"));
257 7606 100       39458 last unless defined $water->{$next};
258 4002         8498 ($x, $y) = $self->neighbor($next, $water->{$next});
259             # leaving the map is good
260 4002         8450 $log->debug("legal for $next: " . $self->legal($x, $y));
261 4002 100       54161 last unless $self->legal($x, $y);
262 3541         32142 $next = coordinates($x, $y);
263             # skip this neighbor if this is a loop
264 3541   100     14301 $log->debug("is $next in a loop? " . ($loop{$next} || "no"));
265 3541 100       19201 next NEIGHBOR if $loop{$next};
266 3398         7261 $loop{$next} = 1;
267             }
268 4065 100 66     21192 if (not defined $direction
      66        
      100        
      66        
269             or not $legal and $altitude->{$coordinates} < $lowest
270             or $legal and $altitude->{$other} < $lowest) {
271 1558 100       3064 $lowest = $legal ? $altitude->{$other} : $altitude->{$coordinates};
272 1558         1940 $direction = $i;
273 1558         3449 $log->debug("Set lowest to $lowest ($direction)");
274             }
275             }
276 1200 100       2880 if (defined $direction) {
277 1161         2249 $water->{$coordinates} = $direction;
278             $world->{$coordinates} =~ s/arrow\d/arrow$water->{$coordinates}/
279 1161 50       4637 or $world->{$coordinates} .= " arrow$water->{$coordinates}";
280             }
281             }
282             }
283              
284             sub mountains {
285 4     4 0 19 my $self = shift;
286 4         11 my ($world, $altitude) = @_;
287             # place the types
288 4         130 for my $coordinates (keys %$altitude) {
289 1200 100       2532 if ($altitude->{$coordinates} >= 10) {
    100          
    100          
290 35         45 $world->{$coordinates} = "white mountains";
291             } elsif ($altitude->{$coordinates} >= 9) {
292 98         133 $world->{$coordinates} = "white mountain";
293             } elsif ($altitude->{$coordinates} >= 8) {
294 248         350 $world->{$coordinates} = "light-grey mountain";
295             }
296             }
297             }
298              
299             sub ocean {
300 4     4 0 12 my $self = shift;
301 4         11 my ($world, $altitude) = @_;
302 4         535 for my $coordinates (sort keys %$altitude) {
303 1200 50       3913 if ($altitude->{$coordinates} <= $self->bottom) {
304 0         0 my $ocean = 1;
305 0         0 for my $i ($self->neighbors()) {
306 0         0 my ($x, $y) = $self->neighbor($coordinates, $i);
307 0 0       0 next unless $self->legal($x, $y);
308 0         0 my $other = coordinates($x, $y);
309 0 0       0 next if $altitude->{$other} <= $self->bottom;
310 0         0 $ocean = 0;
311             }
312 0 0       0 $world->{$coordinates} = $ocean ? "ocean" : "water";
313             }
314             }
315             }
316              
317             sub lakes {
318 4     4 0 12 my $self = shift;
319 4         13 my ($world, $altitude, $water) = @_;
320             # any areas without water flow are lakes
321 4         574 for my $coordinates (sort keys %$altitude) {
322 1200 100 66     2586 if (not defined $water->{$coordinates}
323             and $world->{$coordinates} ne "ocean") {
324 39         83 $world->{$coordinates} = "water";
325             }
326             }
327             }
328              
329             sub swamps {
330             # any area with water flowing to a neighbor at the same altitude is a swamp
331 4     4 0 26 my ($self, $world, $altitude, $water, $flow, $dry) = @_;
332 4         111 for my $coordinates (keys %$altitude) {
333             # don't turn lakes into swamps and skip bogs
334 1200 100       3123 next if $world->{$coordinates} =~ /ocean|water|swamp|grass/;
335             # swamps require a river
336 1033 100       1777 next unless $flow->{$coordinates};
337             # no swamps when there is a canyon
338 423 100       694 next if $dry->{$coordinates};
339             # look at the neighbor the water would flow to
340 366         808 my ($x, $y) = $self->neighbor($coordinates, $water->{$coordinates});
341             # skip if water flows off the map
342 366 100       743 next unless $self->legal($x, $y);
343 314         2904 my $other = coordinates($x, $y);
344             # skip if water flows downhill
345 314 100       775 next if $altitude->{$coordinates} > $altitude->{$other};
346             # if there was no lower neighbor, this is a swamp
347 80 100       137 if ($altitude->{$coordinates} >= 6) {
348 67         276 $world->{$coordinates} =~ s/height\d+/grey swamp/;
349             } else {
350 13         63 $world->{$coordinates} =~ s/height\d+/dark-grey swamp/;
351             }
352             }
353             }
354              
355             sub flood {
356 4     4 0 13 my $self = shift;
357 4         12 my ($world, $altitude, $water) = @_;
358             # backtracking information: $from = $flow{$to}
359 4         11 my %flow;
360             # allow easy skipping
361             my %seen;
362             # start with a list of hexes to look at; as always, keys is a source of
363             # randomness that's independent of srand which is why we shuffle sort
364 4         119 my @lakes = shuffle sort grep { not defined $water->{$_} } keys %$world;
  1200         2050  
365 4 50       75 return unless @lakes;
366 4         10 my $start = shift(@lakes);
367 4         11 my @candidates = ($start);
368 4         15 while (@candidates) {
369             # Prefer candidates outside the map with altitude 0; reshuffle because
370             # candidates at the same height are all equal and early or late discoveries
371             # should not matter (not shuffling means it matters whether candidates are
372             # pushed or unshifted because this is a stable sort)
373             @candidates = sort {
374 408   100     3059 ($altitude->{$a}||0) <=> ($altitude->{$b}||0)
  27808   100     76509  
375             } shuffle @candidates;
376 408         2820 $log->debug("Candidates @candidates");
377 408         3486 my $coordinates;
378             do {
379 478         2156 $coordinates = shift(@candidates);
380 408   66     642 } until not $coordinates or not $seen{$coordinates};
381 408 50       895 last unless $coordinates;
382 408         909 $seen{$coordinates} = 1;
383 408         1315 $log->debug("Looking at $coordinates");
384 408 100 66     3050 if ($self->legal($coordinates) and $world->{$coordinates} ne "ocean") {
385             # if we're still on the map, check all the unknown neighbors
386 376         5613 my $from = $coordinates;
387 376         1131 for my $i ($self->neighbors()) {
388 1768         4217 my $to = coordinates($self->neighbor($from, $i));
389 1768 100       4459 next if $seen{$to};
390 1296         4049 $log->debug("Adding $to to our candidates");
391 1296         9201 $flow{$to} = $from;
392             # adding to the front as we keep pushing forward (I hope)
393 1296         2822 push(@candidates, $to);
394             }
395 376         1178 next;
396             }
397 32         394 $log->debug("We left the map at $coordinates");
398 32         186 my $to = $coordinates;
399 32         68 my $from = $flow{$to};
400 32         83 while ($from) {
401 219         626 my $i = $self->direction($from, $to);
402 219 100 100     962 if (not defined $water->{$from}
403             or $water->{$from} != $i) {
404 135         475 $log->debug("Arrow for $from now points to $to");
405 135         1051 $water->{$from} = $i;
406             $world->{$from} =~ s/arrow\d/arrow$i/
407 135 100       1027 or $world->{$from} .= " arrow$i";
408             } else {
409 84         317 $log->debug("Arrow for $from already points $to");
410             }
411 219         863 $to = $from;
412 219         630 $from = $flow{$to};
413             }
414             # pick the next lake
415             do {
416 39         155 $start = shift(@lakes);
417 39 100       154 $log->debug("Next lake is $start") if $start;
418 32   100     56 } until not $start or not defined $water->{$start};
419 32 100       335 last unless $start;
420 28         251 %seen = %flow = ();
421 28         167 @candidates = ($start);
422             }
423             }
424              
425             sub rivers {
426 4     4 0 18 my ($self, $world, $altitude, $water, $flow, $rivers) = @_;
427             # $flow are the sources points of rivers, or 1 if a river flows through them
428             my @growing = map {
429 246 100       651 $world->{$_} = "light-grey forest-hill" unless $world->{$_} =~ /mountain|swamp|grass|water|ocean/;
430 246         619 $flow->{$_} = [$_]
431             } sort grep {
432             # these are the potential starting places: up in the mountains below the
433             # ice, or lakes
434 4         120 ($altitude->{$_} == 7 or $altitude->{$_} == 8
435             or $world->{$_} =~ /water/ and $altitude->{$_} > $self->bottom)
436             and not $flow->{$_}
437 1200 50 66     5119 and $world->{$_} !~ /dry/;
      66        
438             } keys %$altitude;
439 4         85 $self->grow_rivers(\@growing, $water, $flow, $rivers);
440             }
441              
442             sub grow_rivers {
443 4     4 0 15 my ($self, $growing, $water, $flow, $rivers) = @_;
444 4         14 while (@$growing) {
445             # warn "Rivers: " . @growing . "\n";
446             # pick a random growing river and grow it
447 1456         2480 my $n = int(rand(scalar @$growing));
448 1456         1882 my $river = $growing->[$n];
449             # warn "Picking @$river\n";
450 1456         1899 my $coordinates = $river->[-1];
451 1456         1679 my $end = 1;
452 1456 100       2514 if (defined $water->{$coordinates}) {
453 1240         2728 my $other = coordinates($self->neighbor($coordinates, $water->{$coordinates}));
454 1240 50       13007 die "Adding $other leads to an infinite loop in river @$river\n" if grep /$other/, @$river;
455             # if we flowed into a hex with a river
456 1240 100       2535 if (ref $flow->{$other}) {
457             # warn "Prepending @$river to @{$flow->{$other}}\n";
458             # prepend the current river to the other river
459 30         42 unshift(@{$flow->{$other}}, @$river);
  30         78  
460             # move the source marker
461 30         67 $flow->{$river->[0]} = $flow->{$other};
462 30         45 $flow->{$other} = 1;
463             # and remove the current river from the growing list
464 30         116 splice(@$growing, $n, 1);
465             # warn "Flow at $river->[0]: @{$flow->{$river->[0]}}\n";
466             # warn "Flow at $other: $flow->{$other}\n";
467             } else {
468 1210         1887 $flow->{$coordinates} = 1;
469 1210         2920 push(@$river, $other);
470             }
471             } else {
472             # stop growing this river
473             # warn "Stopped river: @$river\n" if grep(/0914/, @$river);
474 216         525 push(@$rivers, splice(@$growing, $n, 1));
475             }
476             }
477             }
478              
479             sub canyons {
480 4     4 0 12 my $self = shift;
481 4         14 my ($world, $altitude, $rivers, $canyons, $dry) = @_;
482             # using a reference to an array so that we can leave pointers in the %seen hash
483 4         13 my $canyon = [];
484             # remember which canyon flows through which hex
485 4         14 my %seen;
486 4         13 for my $river (@$rivers) {
487 216         354 my $last = $river->[0];
488 216         343 my $current_altitude = $altitude->{$last};
489 216         784 $log->debug("Looking at @$river ($current_altitude)");
490 216         1097 for my $coordinates (@$river) {
491 1041         2425 $log->debug("Looking at $coordinates");
492 1041 100       5221 if ($seen{$coordinates}) {
493             # the rest of this river was already looked at, so there is no need to
494             # do the rest of this river; if we're in a canyon, prepend it to the one
495             # we just found before ending
496 90 100       157 if (@$canyon) {
497 3         169 my @other = @{$seen{$coordinates}};
  3         16  
498 3 100       14 if ($other[0] eq $canyon->[-1]) {
499 1         8 $log->debug("Canyon @$canyon of river @$river merging with @other at $coordinates");
500 1         7 unshift(@{$seen{$coordinates}}, @$canyon[0 .. @$canyon - 2]);
  1         4  
501             } else {
502 2         21 $log->debug("Canyon @$canyon of river @$river stumbled upon existing canyon @other at $coordinates");
503 2         18 while (@other) {
504 5         10 my $other = shift(@other);
505 5 100       16 next if $other ne $coordinates;
506 2         9 push(@$canyon, $other, @other);
507 2         5 last;
508             }
509 2         13 $log->debug("Canyon @$canyon");
510 2         15 push(@$canyons, $canyon);
511             }
512 3         8 $canyon = [];
513             }
514 90         149 $log->debug("We've seen the rest: @{$seen{$coordinates}}");
  90         333  
515 90         488 last;
516             }
517             # no canyons through water!
518 951 100 100     2963 if ($altitude->{$coordinates} and $current_altitude < $altitude->{$coordinates}
      100        
519             and $world->{$coordinates} !~ /water|ocean/) {
520             # river is digging a canyon; if this not the start of the river and it
521             # is the start of a canyon, prepend the last step
522 73 100       163 push(@$canyon, $last) unless @$canyon;
523 73         121 push(@$canyon, $coordinates);
524 73 50       185 $world->{$coordinates} .= " zone" unless $dry->{$coordinates};
525 73         192 $dry->{$coordinates} = 1;
526 73         231 $log->debug("Growing canyon @$canyon");
527 73         444 $seen{$coordinates} = $canyon;
528             } else {
529             # if we just left a canyon, append the current step
530 878 100       1334 if (@$canyon) {
531 28         63 push(@$canyon, $coordinates);
532 28         43 push(@$canyons, $canyon);
533 28         118 $log->debug("Looking at river @$river");
534 28         208 $log->debug("Canyon @$canyon");
535 28         158 $canyon = [];
536 28         53 last;
537             }
538             # not digging a canyon
539 850         1065 $last = $coordinates;
540 850         1277 $current_altitude = $altitude->{$coordinates};
541             }
542             }
543             }
544             }
545              
546             sub wet {
547 0     0 0 0 my $self = shift;
548             # a hex is wet if there is a river, a swamp or a forest within 2 hexes
549 0         0 my ($coordinates, $world, $flow) = @_;
550 0         0 for my $i ($self->neighbors()) {
551 0         0 my ($x, $y) = $self->neighbor($coordinates, $i);
552 0         0 my $other = coordinates($x, $y);
553 0 0       0 return 0 if $flow->{$other};
554             }
555 0         0 for my $i ($self->neighbors2()) {
556 0         0 my ($x, $y) = $self->neighbor2($coordinates, $i);
557 0         0 my $other = coordinates($x, $y);
558 0 0       0 return 0 if $flow->{$other};
559             }
560 0         0 return 1;
561             }
562              
563             sub grow_forest {
564 445     445 0 745 my ($self, $coordinates, $world, $altitude, $dry) = @_;
565 445         493 my @candidates;
566 445 100       1950 push(@candidates, $coordinates) if $world->{$coordinates} !~ /mountain|hill|water|ocean|swamp|grass/;
567 445         963 my $n = $self->arid;
568             # fractions are allowed
569 445 50       1603 $n += 1 if rand() < $self->arid - int($self->arid);
570 445         2160 $n = int($n);
571 445         1204 $log->debug("Arid: $n");
572 445 50       2270 if ($n >= 1) {
573 445         896 for my $i ($self->neighbors()) {
574 2226         4730 my ($x, $y) = $self->neighbor($coordinates, $i);
575 2226 100       3655 next unless $self->legal($x, $y);
576 2032         16367 my $other = coordinates($x, $y);
577 2032 100       3441 next if $dry->{$other};
578 1911 100       3804 next if $altitude->{$coordinates} < $altitude->{$other}; # distance of one unless higher
579 1250 100       4687 push(@candidates, $other) if $world->{$other} !~ /mountain|hill|water|ocean|swamp|grass/;
580             }
581             }
582 445 50       1049 if ($n >= 2) {
583 445         882 for my $i ($self->neighbors2()) {
584 4452         10947 my ($x, $y) = $self->neighbor2($coordinates, $i);
585 4452 100       7303 next unless $self->legal($x, $y);
586 3752         29758 my $other = coordinates($x, $y);
587 3752 100       8148 next if $altitude->{$coordinates} <= $altitude->{$other}; # distance of two only if lower
588 934         1193 my $ok = 0;
589 934         1770 for my $m ($self->neighbors()) {
590 2909         5830 my ($mx, $my) = $self->neighbor($coordinates, $m);
591 2909 100       4944 next unless $self->legal($mx, $my);
592 2749         22384 my $midway = coordinates($mx, $my);
593 2749 100       4802 next if $dry->{$midway};
594 2579 100       4733 next if $self->distance($midway, $other) != 1;
595 944 100       1988 next if $altitude->{$coordinates} < $altitude->{$midway};
596 879 100       1641 next if $altitude->{$midway} < $altitude->{$other};
597 827         981 $ok = 1;
598 827         1106 last;
599             }
600 934 100       1558 next unless $ok;
601 827 100       3740 push(@candidates, $other) if $world->{$other} !~ /mountain|hill|water|ocean|swamp|grass/;
602             }
603             }
604 445         2419 $log->debug("forest growth: $coordinates: @candidates");
605 445         2865 for $coordinates (@candidates) {
606 973 100       1831 if ($altitude->{$coordinates} >= 7) {
    100          
    50          
607 250         514 $world->{$coordinates} = "light-green fir-forest";
608             } elsif ($altitude->{$coordinates} >= 6) {
609 504         1012 $world->{$coordinates} = "green fir-forest";
610             } elsif ($altitude->{$coordinates} >= 4) {
611 219         471 $world->{$coordinates} = "green forest";
612             } else {
613 0         0 $world->{$coordinates} = "dark-green forest";
614             }
615             }
616             }
617              
618             sub forests {
619 4     4 0 16 my ($self, $world, $altitude, $flow, $dry) = @_;
620             # Empty hexes with a river flowing through them (and nearby hexes) are forest
621             # filled valleys.
622 4         63 for my $coordinates (keys %$flow) {
623 518 100       1013 next if $dry->{$coordinates};
624 445         830 $self->grow_forest($coordinates, $world, $altitude, $dry);
625             }
626             }
627              
628             sub winds {
629 4     4 0 12 my $self = shift;
630 4         13 my ($world, $altitude, $water, $flow) = @_;
631 4   66     25 my $wind = $self->wind // $self->random_neighbor;
632 4         26 $world->{"0101"} .= " wind" . $self->reverse($wind);
633 4         147 for my $coordinates (keys %$altitude) {
634             # limit ourselves to altitude 7 and 8
635 1200 100 100     3200 next if $altitude->{$coordinates} < 7 or $altitude->{$coordinates} > 8;
636             # look at the neighbor the water would flow to
637 636         1130 my ($x, $y) = $self->neighbor($coordinates, $wind);
638             # skip if off the map
639 636 100       1147 next unless $self->legal($x, $y);
640 578         4873 my $other = coordinates($x, $y);
641             # skip if the other hex is lower
642 578 100       1323 next if $altitude->{$coordinates} > $altitude->{$other};
643             # if the other hex was higher, this land is dry
644 423         1044 $log->debug("$coordinates is dry because of $other");
645 423         2498 $world->{$coordinates} .= " dry zone"; # use label for debugging
646             }
647             }
648              
649             sub bogs {
650 4     4 0 11 my $self = shift;
651 4         13 my ($world, $altitude, $water) = @_;
652 4         138 for my $coordinates (keys %$altitude) {
653             # limit ourselves to altitude 7
654 1200 100       2291 next if $altitude->{$coordinates} != 7;
655             # don't turn lakes into bogs
656 388 100       1058 next if $world->{$coordinates} =~ /water|ocean/;
657             # look at the neighbor the water would flow to
658 384         993 my ($x, $y) = $self->neighbor($coordinates, $water->{$coordinates});
659             # skip if water flows off the map
660 384 100       800 next unless $self->legal($x, $y);
661 353         3288 my $other = coordinates($x, $y);
662             # skip if water flows downhill
663 353 100       860 next if $altitude->{$coordinates} > $altitude->{$other};
664             # if there was no lower neighbor, this is a bog
665 128         592 $world->{$coordinates} =~ s/height\d+/grey swamp/;
666             }
667             }
668              
669             sub dry {
670 4     4 0 12 my ($self, $world, $altitude, $rivers) = @_;
671 4         8 my @dry;
672 4         464 for my $coordinates (shuffle sort keys %$world) {
673 1200 100       2643 if ($world->{$coordinates} !~ /mountain|hill|water|ocean|swamp|grass|forest|firs|trees/) {
674 142 100       215 if ($altitude->{$coordinates} >= 7) {
675 46         61 $world->{$coordinates} = "light-grey grass";
676             } else {
677 96         104 $world->{$coordinates} = "light-green bushes";
678 96         127 push(@dry, $coordinates);
679             }
680             }
681             }
682              
683             BUSHES:
684 4         42 for my $coordinates (@dry) {
685 96         180 for my $i ($self->neighbors()) {
686 311         587 my ($x, $y) = $self->neighbor($coordinates, $i);
687 311 100       424 next unless $self->legal($x, $y);
688 257         1703 my $other = coordinates($x, $y);
689 257 100       621 next BUSHES if $world->{$other} =~ /forest|firs|trees|swamp/;
690             }
691 45 100       109 if ($altitude->{$coordinates} >= 5) {
    50          
692 42         96 $world->{$coordinates} =~ s/light-green bushes/light-grey grass/;
693             } elsif ($altitude->{$coordinates} >= 3) {
694 3         16 $world->{$coordinates} =~ s/light-green bushes/grey grass/;
695             } else {
696 0         0 $world->{$coordinates} =~ s/light-green bushes/dark-grey grass/;
697             }
698             }
699              
700             GRASS:
701 4         10 for my $coordinates (@dry) {
702 96 100       184 next if $self->with_river($rivers, $coordinates);
703 85         165 for my $i ($self->neighbors()) {
704 217         417 my ($x, $y) = $self->neighbor($coordinates, $i);
705 217 100       320 next unless $self->legal($x, $y);
706 182         1325 my $other = coordinates($x, $y);
707 182 100       641 next GRASS if $world->{$other} !~ /grass|desert|water/;
708             }
709 23 50       61 if ($altitude->{$coordinates} >= 3) {
710 23         88 $world->{$coordinates} =~ s/(light-|dark-)?grey grass/light-grey desert/;
711             } else {
712 0         0 $world->{$coordinates} =~ s/(light-|dark-)?grey grass/dust desert/;
713             }
714             }
715             }
716              
717             sub with_river {
718 96     96 0 137 my ($self, $rivers, $coordinates) = @_;
719 96         113 for my $river (@$rivers) {
720 4565 100       4960 return 1 if grep { $coordinates eq $_ } (@$river);
  30901         37155  
721             }
722             }
723              
724             sub settlements {
725 8     8 0 19 my $self = shift;
726 8         20 my ($world, $flow) = @_;
727 8         24 my @settlements;
728 8         37 my $max = $self->height * $self->width;
729             # do not match forest-hill
730 8         337 my @candidates = shuffle sort grep { $world->{$_} =~ /\b(fir-forest|forest(?!-hill))\b/ } keys %$world;
  3600         5824  
731 8         225 @candidates = $self->remove_closer_than(2, @candidates);
732 8 100       82 @candidates = @candidates[0 .. int($max/10 - 1)] if @candidates > $max/10;
733 8         49 push(@settlements, @candidates);
734 8         20 for my $coordinates (@candidates) {
735             $world->{$coordinates} =~ s/fir-forest/firs thorp/
736 130 100       488 or $world->{$coordinates} =~ s/forest(?!-hill)/trees thorp/;
737             }
738 8 100       370 @candidates = shuffle sort grep { $world->{$_} =~ /(?{$_}} keys %$world;
  3600         6609  
739 8         164 @candidates = $self->remove_closer_than(5, @candidates);
740 8 50       35 @candidates = @candidates[0 .. int($max/20 - 1)] if @candidates > $max/20;
741 8         39 push(@settlements, @candidates);
742 8         20 for my $coordinates (@candidates) {
743 11         36 $world->{$coordinates} =~ s/forest/trees village/;
744             }
745 8 100       279 @candidates = shuffle sort grep { $world->{$_} =~ /(?{$_} } keys %$world;
  3600         6262  
746 8         166 @candidates = $self->remove_closer_than(10, @candidates);
747 8 50       39 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
748 8         26 push(@settlements, @candidates);
749 8         21 for my $coordinates (@candidates) {
750 5         24 $world->{$coordinates} =~ s/forest/trees town/;
751             }
752 8         292 @candidates = shuffle sort grep { $world->{$_} =~ /white mountain\b/ } keys %$world;
  3600         5113  
753 8         169 @candidates = $self->remove_closer_than(10, @candidates);
754 8 50       53 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
755 8         19 push(@settlements, @candidates);
756 8         28 for my $coordinates (@candidates) {
757 15         54 $world->{$coordinates} =~ s/white mountain\b/white mountain law/;
758             }
759 8         289 @candidates = shuffle sort grep { $world->{$_} =~ /swamp/ } keys %$world;
  3600         5227  
760 8         137 @candidates = $self->remove_closer_than(10, @candidates);
761 8 50       50 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
762 8         36 push(@settlements, @candidates);
763 8         20 for my $coordinates (@candidates) {
764 15         45 $world->{$coordinates} =~ s/swamp/swamp2 chaos/;
765             }
766 8         22 for my $coordinates (@settlements) {
767 176         381 for my $i ($self->neighbors()) {
768 777         1302 my ($x, $y) = $self->neighbor($coordinates, $i);
769 777         1289 my $other = coordinates($x, $y);
770 777 100 100     2770 next unless $world->{$other} and $world->{$other} =~ /water|ocean/;
771             # bump ports one size category up
772 50         133 $world->{$coordinates} =~ s/large-town/city port/;
773 50         82 $world->{$coordinates} =~ s/town/large-town port/;
774 50         116 $world->{$coordinates} =~ s/village/town port/;
775             # no bumps for thorps
776 50         91 last;
777             }
778             }
779 8         19 for my $coordinates (@settlements) {
780             # thorps and villages don't cut enough wood; make sure to get both "green" and "dark-green"
781 176 100       321 $world->{$coordinates} =~ s/\S*green trees/light-soil/ if $world->{$coordinates} =~ /large-town|city/;
782 176 100       305 $world->{$coordinates} =~ s/\S*green trees/soil/ if $world->{$coordinates} =~ / town/;
783             }
784 8         77 return @settlements;
785             }
786              
787             sub trails {
788 8     8 0 18 my $self = shift;
789 8         17 my ($altitude, $settlements) = @_;
790             # look for a neighbor that is as low as possible and nearby
791 8         19 my %trails;
792 8         81 my @from = shuffle @$settlements;
793 8         60 my @to = shuffle @$settlements;
794 8         29 for my $from (@from) {
795 176         675 my ($best, $best_distance, $best_altitude);
796 176         238 for my $to (@to) {
797 6920 100       10118 next if $from eq $to;
798 6744         10442 my $distance = $self->distance($from, $to);
799 6744         17604 $log->debug("Considering $from-$to: distance $distance, altitude " . $altitude->{$to});
800 6744 100 100     33154 if ($distance <= 3
      100        
      100        
      100        
801             and (not $best_distance or $distance <= $best_distance)
802             and (not $best or $altitude->{$to} < $best_altitude)) {
803 210         250 $best = $to;
804 210         303 $best_altitude = $altitude->{$best};
805 210         302 $best_distance = $distance;
806             }
807             }
808 176 100       320 next if not $best;
809             # skip if it already exists in the other direction
810 169 100       337 next if $trails{"$best-$from"};
811 136         397 $trails{"$from-$best"} = 1;
812 136         274 $log->debug("Trail $from-$best");
813             }
814 8         220 return keys %trails;
815             }
816              
817             sub cliffs {
818 4     4 0 11 my $self = shift;
819 4         10 my ($world, $altitude) = @_;
820 4         15 my @neighbors = $self->neighbors();
821             # hexes with altitude difference bigger than 1 have cliffs
822 4         83 for my $coordinates (keys %$world) {
823 1200 50       2509 next if $altitude->{$coordinates} <= $self->bottom;
824 1200         3298 for my $i (@neighbors) {
825 6000         10444 my ($x, $y) = $self->neighbor($coordinates, $i);
826 6000 100       8817 next unless $self->legal($x, $y);
827 5524         37798 my $other = coordinates($x, $y);
828 5524 100       10397 if ($altitude->{$coordinates} - $altitude->{$other} >= 2) {
829 300 100       410 if (@neighbors == 6) {
830 202         427 $world->{$coordinates} .= " cliff$i";
831             } else { # square
832 98         196 $world->{$coordinates} .= " cliffs$i";
833             }
834             }
835             }
836             }
837             }
838              
839             sub marshlands {
840 4     4 0 12 my ($self, $world, $altitude, $rivers) = @_;
841 4         10 my %seen;
842 4         13 for my $river (@$rivers) {
843 216         319 my $last = $river->[0];
844 216         265 for my $coordinates (@$river) {
845 735 100       1163 last if $seen{$coordinates}; # we've been here before
846 583         929 $seen{$coordinates} = 1;
847 583 100       865 next unless exists $altitude->{$coordinates}; # rivers ending off the map
848 519 50       737 if ($altitude->{$coordinates} <= $self->bottom) {
849 0 0 0     0 if ($altitude->{$coordinates} == $self->bottom
      0        
850             and $world->{$coordinates} =~ /water|ocean/
851             and $altitude->{$coordinates} == $altitude->{$last} - 1) {
852 0         0 $world->{$coordinates} = "blue-green swamp";
853             } else {
854 0         0 $world->{$coordinates} =~ s/ocean/water/;
855 0         0 delete $seen{$coordinates};
856 0         0 last;
857             }
858             }
859 519         1525 $last = $coordinates;
860             }
861             }
862             }
863              
864             sub desertification {
865 4     4 0 14 my ($self, $world, $altitude, $rivers) = @_;
866 4 50       31 return unless $self->climate eq 'desert';
867 0         0 for my $coordinates (keys %$world) {
868 0 0       0 if ($self->with_river($rivers, $coordinates)) {
869             $world->{$coordinates} =~ s/light-grey/light-green/
870 0 0       0 or $world->{$coordinates} =~ s/dark-green/green/
871             } else {
872             $world->{$coordinates} =~ s/light-green bushes/rock bushes/
873             or $world->{$coordinates} =~ s/light-grey grass/rock bush/
874             or $world->{$coordinates} =~ s/dark-grey grass/dark-soil bush/
875             or $world->{$coordinates} =~ s/^grey grass/rock bush/
876             or $altitude->{$coordinates} >= 4 and $world->{$coordinates} =~ s/light-grey desert/dark-soil desert/
877 0 0 0     0 or $altitude->{$coordinates} >= 2 and $world->{$coordinates} =~ s/(dust|light-grey) desert/light-grey desert/
      0        
      0        
      0        
      0        
      0        
878             }
879             $world->{$coordinates} =~ s/dark-grey swamp2?/light-green bushes/
880             or $world->{$coordinates} =~ s/^grey swamp2?/light-grey bushes/
881             or $world->{$coordinates} =~ s/fir-forest/trees/
882 0 0 0     0 or $world->{$coordinates} =~ s/firs/trees/;
      0        
883             }
884             }
885              
886             sub generate {
887 4     4 0 17 my ($self, $world, $altitude, $water, $rivers, $settlements, $trails, $canyons, $step) = @_;
888             # $flow indicates that there is actually a river in this hex
889 4         8 my $flow = {};
890             # $dry indicates that is a river in this hex, but it cut itself a canyon
891 4         10 my $dry = {};
892             my @code = (
893 4     4   33 sub { $self->flat($altitude);
894 4         43 $self->altitude($world, $altitude); },
895 4     4   40 sub { $self->bumpiness($world, $altitude); },
896 4     4   45 sub { $self->mountains($world, $altitude); },
897 4     4   39 sub { $self->ocean($world, $altitude); },
898 4     4   38 sub { $self->water($world, $altitude, $water); },
899 4     4   68 sub { $self->lakes($world, $altitude, $water); },
900 4     4   47 sub { $self->flood($world, $altitude, $water); },
901 4     4   73 sub { $self->bogs($world, $altitude, $water); },
902 4     4   50 sub { $self->winds($world, $altitude, $water); },
903 4     4   88 sub { $self->rivers($world, $altitude, $water, $flow, $rivers); },
904 4     4   87 sub { $self->canyons($world, $altitude, $rivers, $canyons, $dry); },
905 4     4   54 sub { $self->swamps($world, $altitude, $water, $flow, $dry); },
906 4     4   58 sub { $self->forests($world, $altitude, $flow, $dry); },
907 4     4   66 sub { $self->dry($world, $altitude, $rivers); },
908 4     4   41 sub { $self->cliffs($world, $altitude); },
909 4     4   64 sub { push(@$settlements, $self->settlements($world, $flow)); },
910 4     4   48 sub { push(@$trails, $self->trails($altitude, $settlements)); },
911 4     4   57 sub { $self->marshlands($world, $altitude, $rivers); },
912 4     4   72 sub { $self->desertification($world, $altitude, $rivers); },
913             # make sure you look at "alpine_document.html.ep" if you change this list!
914             # make sure you look at '/alpine/document' if you add to this list!
915 4         153 );
916              
917             # $step 0 runs all the code; note that we can't simply cache those results
918             # because we need to start over with the same seed!
919 4         11 my $i = 1;
920 4         14 while (@code) {
921 76         312 shift(@code)->();
922 76 50       1195 return if $step == $i++;
923 76         343 $self->fixup($world, $altitude, $i);
924             }
925             }
926              
927             # Remove temporary markers that won't be needed in the next step
928             sub fixup {
929 76     76 0 194 my ($self, $world, $altitude, $step, $last) = @_;
930             # When documenting or debugging, water flow arrows are no longer needed when
931             # the rivers are added.
932 76 100       191 if ($step >= 10) {
933 44         1781 for my $coordinates (keys %$world) {
934 13200         19125 $world->{$coordinates} =~ s/ arrow\d//;
935             }
936             }
937             # Wind direction is only shown once.
938 76         740 $world->{"0101"} =~ s/ wind\d//;
939             # Remove zone markers.
940 76         2187 for my $coordinates (keys %$world) {
941 22800         33502 $world->{$coordinates} =~ s/ zone//;
942             }
943             }
944              
945             sub generate_map {
946 4     4 0 1141 my $self = shift;
947              
948             # The parameters turn into class variables.
949 4   50     54 $self->width(shift // 30);
950 4   50     79 $self->height(shift // 10);
951 4   50     55 $self->steepness(shift // 3);
952 4   33     38 $self->peaks(shift // int($self->width * $self->height / 40));
953 4   50     99 $self->peak(shift // 10);
954 4   33     37 $self->bumps(shift // int($self->width * $self->height / 40));
955 4   50     124 $self->bump(shift // 2);
956 4   50     54 $self->bottom(shift // 0);
957 4   50     49 $self->arid(shift // 2);
958 4 50       44 $self->climate(shift ? 'desert' : 'temperate');
959 4         38 $self->wind(shift); # or random
960 4   66     34 my $seed = shift||time;
961 4         9 my $url = shift;
962 4   50     22 my $step = shift||0;
963              
964             # For documentation purposes, I want to be able to set the pseudo-random
965             # number seed using srand and rely on rand to reproduce the same sequence of
966             # pseudo-random numbers for the same seed. The key point to remember is that
967             # the keys function will return keys in random order. So if we look over the
968             # result of keys, we need to look at the code in the loop: If order is
969             # important, that wont do. We need to sort the keys. If we want the keys to be
970             # pseudo-shuffled, use shuffle sort keys.
971 4         14 srand($seed);
972              
973             # Keys for all hashes are coordinates such as "0101".
974             # %world is the description with values such as "green forest".
975             # %altitude is the altitude with values such as 3.
976             # %water is the preferred direction water would take with values such as 0.
977             # (north west); 0 means we need to use "if defined".
978             # @rivers are the rivers with values such as ["0102", "0202"].
979             # @settlements are are the locations of settlements such as "0101".
980             # @trails are the trails connecting these with values as "0102-0202".
981             # $step is how far we want map generation to go where 0 means all the way.
982 4         20 my ($world, $altitude, $water, $rivers, $settlements, $trails, $canyons) =
983             ({}, {}, {}, [], [], [], []);
984 4         33 $self->generate($world, $altitude, $water, $rivers, $settlements, $trails, $canyons, $step);
985              
986             # When documenting or debugging, add altitude as a label.
987 4 50       19 if ($step > 0) {
988 0         0 for my $coordinates (keys %$world) {
989 0         0 $world->{$coordinates} .= ' "' . $altitude->{$coordinates} . '"';
990             }
991             }
992              
993 4         12 local $" = "-"; # list items separated by -
994 4         10 my @lines;
995 4         409 push(@lines, map { $_ . " " . $world->{$_} } sort keys %$world);
  1200         2004  
996 4         77 push(@lines, map { "$_ trail" } @$trails);
  131         213  
997 4         15 push(@lines, map { "@$_ river" } @$rivers);
  216         457  
998 4         17 push(@lines, map { "@$_ canyon" } @$canyons); # after rivers
  30         74  
999 4         9 push(@lines, "include gnomeyland.txt");
1000              
1001             # when documenting or debugging, add some more lines at the end
1002 4 50       17 if ($step > 0) {
1003             # visualize height
1004             push(@lines,
1005             map {
1006 0         0 my $n = int(25.5 * $_);
  0         0  
1007 0         0 qq{height$_ attributes fill="rgb($n,$n,$n)"};
1008             } (0 .. 10));
1009             # visualize water flow
1010 0         0 push(@lines, $self->arrows());
1011             }
1012              
1013 4         14 push(@lines, "# Seed: $seed");
1014 4 100       27 push(@lines, "# Documentation: " . $url) if $url;
1015 4         1469 my $map = join("\n", @lines);
1016 4 100       1093 return $map, $self if wantarray;
1017 2         477 return $map;
1018             }
1019              
1020             1;