File Coverage

blib/lib/Game/TextMapper/Schroeder/Alpine.pm
Criterion Covered Total %
statement 523 578 90.4
branch 217 278 78.0
condition 85 141 60.2
subroutine 52 53 98.1
pod 0 29 0.0
total 877 1079 81.2


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   88 use Game::TextMapper::Log;
  11         27  
  11         583  
49 11     11   150 use Modern::Perl '2018';
  11         29  
  11         108  
50 11     11   4870 use Mojo::Base -base;
  11         30  
  11         126  
51 11     11   11272 use Role::Tiny::With;
  11         3842  
  11         1007  
52             with 'Game::TextMapper::Schroeder::Base';
53 11     11   87 use List::Util 'shuffle';
  11         29  
  11         226839  
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 58 my $self = shift;
69 4         8 my $altitude = shift;
70 4         10 my $count = shift;
71 4         11 my $current_altitude = shift;
72 4         7 my @queue;
73             # place some peaks and put them in a queue
74 4         16 for (1 .. $count) {
75             # try to find an empty hex
76 28         52 for (1 .. 6) {
77 30         74 my $x = int(rand($self->width)) + 1;
78 30         160 my $y = int(rand($self->height)) + 1;
79 30         150 my $coordinates = coordinates($x, $y);
80 30 100       97 next if $altitude->{$coordinates};
81 28         51 $altitude->{$coordinates} = $current_altitude;
82 28         117 $log->debug("placed $current_altitude at $coordinates");
83 28         267 push(@queue, $coordinates);
84 28         56 last;
85             }
86             }
87 4         25 return @queue;
88             }
89              
90             sub grow_mountains {
91 4     4 0 11 my $self = shift;
92 4         9 my $altitude = shift;
93 4         31 my @queue = @_;
94             # go through the queue and add adjacent lower altitude hexes, if possible; the
95             # hexes added are to the end of the queue
96 4         17 while (@queue) {
97 1200         4044 my $coordinates = shift @queue;
98 1200         2599 my $current_altitude = $altitude->{$coordinates};
99 1200 50       2344 next unless $current_altitude > 0;
100             # pick some random neighbors based on variable steepness
101 1200         3265 my $n = $self->steepness;
102             # round up based on fraction
103 1200 50       6502 $n += 1 if rand() < $n - int($n);
104 1200         1817 $n = int($n);
105 1200 50       2282 next if $n < 1;
106 1200         2678 for (1 .. $n) {
107             # try to find an empty neighbor; make more attempts if we're looking for
108             # more neighbours
109 3600         10172 for my $attempt (1 .. 3 + $n) {
110 17371         61174 my ($x, $y) = $self->neighbor($coordinates, $self->random_neighbor());
111 17371 100       38836 next unless $self->legal($x, $y);
112 15833         181522 my $other = coordinates($x, $y);
113             # if this is taken, look further
114 15833 100       37836 if ($altitude->{$other}) {
115 15345         36810 ($x, $y) = $self->neighbor2($coordinates, $self->random_neighbor2());
116 15345 100       37691 next unless $self->legal($x, $y);
117 13359         147547 $other = coordinates($x, $y);
118             # if this is also taken, try again – but if we've already had four
119             # attempts, jump!
120 13359 100       34206 if ($altitude->{$other}) {
121 12675 100       26131 $coordinates = $other if $attempt > 4;
122 12675         26444 next;
123             }
124             }
125             # if we found an empty neighbor, set its altitude
126 1172 50       2780 $altitude->{$other} = $current_altitude > 0 ? $current_altitude - 1 : 0;
127 1172         2248 push(@queue, $other);
128 1172         2729 last;
129             }
130             }
131             }
132             }
133              
134             sub fix_altitude {
135 4     4 0 10 my $self = shift;
136 4         11 my $altitude = shift;
137             # go through all the hexes
138 4         792 for my $coordinates (sort keys %$altitude) {
139             # find hexes that we missed and give them the height of a random neighbor
140 1200 50       2227 if (not defined $altitude->{$coordinates}) {
141             # warn "identified a hex that was skipped: $coordinates\n";
142             # try to find a suitable neighbor
143 0         0 for (1 .. 6) {
144 0         0 my ($x, $y) = $self->neighbor($coordinates, $self->random_neighbor());
145 0 0       0 next unless $self->legal($x, $y);
146 0         0 my $other = coordinates($x, $y);
147 0 0       0 next unless defined $altitude->{$other};
148 0         0 $altitude->{$coordinates} = $altitude->{$other};
149 0         0 last;
150             }
151             # if we didn't find one in the last six attempts, just make it hole in the ground
152 0 0       0 if (not defined $altitude->{$coordinates}) {
153 0         0 $altitude->{$coordinates} = 0;
154             }
155             }
156             }
157             }
158              
159             sub altitude {
160 4     4 0 10 my $self = shift;
161 4         11 my ($world, $altitude) = @_;
162 4         21 my @queue = $self->place_peak($altitude, $self->peaks, $self->peak);
163 4         35 $self->grow_mountains($altitude, @queue);
164 4         88 $self->fix_altitude($altitude);
165             # note height for debugging purposes
166 4         573 for my $coordinates (sort keys %$altitude) {
167 1200         3006 $world->{$coordinates} = "height$altitude->{$coordinates}";
168             }
169             }
170              
171             sub bumpiness {
172 4     4 0 13 my ($self, $world, $altitude) = @_;
173 4         25 for (1 .. $self->bumps) {
174 28         102 for my $delta (-$self->bump, $self->bump) {
175             # six attempts to try and find a good hex
176 56         239 for (1 .. 6) {
177 58         152 my $x = int(rand($self->width)) + 1;
178 58         372 my $y = int(rand($self->height)) + 1;
179 58         296 my $coordinates = coordinates($x, $y);
180 58         123 my $current_altitude = $altitude->{$coordinates} + $delta;
181 58 100 66     195 next if $current_altitude > 10 or $current_altitude < 0;
182             # bump it up or down
183 56         125 $altitude->{$coordinates} = $current_altitude;
184 56         126 $world->{$coordinates} = "height$altitude->{$coordinates} zone";
185 56         227 $log->debug("bumped altitude of $coordinates by $delta to $current_altitude");
186             # if the bump was +2 or -2, bump the neighbours by +1 or -1
187 56 50 66     510 if ($delta < -1 or $delta > 1) {
188 56         150 my $delta = $delta - $delta / abs($delta);
189 56         177 for my $i ($self->neighbors()) {
190 280         1845 my ($x, $y) = $self->neighbor($coordinates, $i);
191 280 100       655 next unless $self->legal($x, $y);
192 256         2785 my $other = coordinates($x, $y);
193 256         509 $current_altitude = $altitude->{$other} + $delta;
194 256 100 66     743 next if $current_altitude > 10 or $current_altitude < 0;
195 255         440 $altitude->{$other} = $current_altitude;
196 255         517 $world->{$other} = "height$altitude->{$other} zone";
197 255         823 $log->debug("$i bumped altitude of $other by $delta to $current_altitude");
198             }
199             }
200             # if we have found a good hex, don't go through all the other attempts
201 56         506 last;
202             }
203             }
204             }
205             }
206              
207             sub water {
208 4     4 0 12 my $self = shift;
209 4         11 my ($world, $altitude, $water) = @_;
210             # reset in case we run this twice
211             # go through all the hexes
212 4         463 for my $coordinates (sort keys %$altitude) {
213 1200 50       3367 next if $altitude->{$coordinates} <= $self->bottom;
214             # note preferred water flow by identifying lower lying neighbors
215 1200         4969 my ($lowest, $direction);
216             # look at neighbors in random order
217             NEIGHBOR:
218 1200         2981 for my $i (shuffle $self->neighbors()) {
219 6000         20195 my ($x, $y) = $self->neighbor($coordinates, $i);
220 6000         12664 my $legal = $self->legal($x, $y);
221 6000         56985 my $other = coordinates($x, $y);
222 6000 100 100     26677 next if $legal and $altitude->{$other} > $altitude->{$coordinates};
223             # don't point head on to another arrow
224 4408 100 100     16043 next if $legal and $water->{$other} and $water->{$other} == ($i-3) % 6;
      100        
225             # don't point into loops
226 4135         11648 my %loop = ($coordinates => 1, $other => 1);
227 4135         5957 my $next = $other;
228 4135         13556 $log->debug("Loop detection starting with $coordinates and $other");
229 4135         26792 while ($next) {
230             # no water flow known is also good;
231 6690   100     25862 $log->debug("water for $next: " . ($water->{$next} || "none"));
232 6690 100       37673 last unless defined $water->{$next};
233 3156         7225 ($x, $y) = $self->neighbor($next, $water->{$next});
234             # leaving the map is good
235 3156         7019 $log->debug("legal for $next: " . $self->legal($x, $y));
236 3156 100       47132 last unless $self->legal($x, $y);
237 2737         25816 $next = coordinates($x, $y);
238             # skip this neighbor if this is a loop
239 2737   100     12073 $log->debug("is $next in a loop? " . ($loop{$next} || "no"));
240 2737 100       16344 next NEIGHBOR if $loop{$next};
241 2555         5833 $loop{$next} = 1;
242             }
243 3953 100 66     21825 if (not defined $direction
      66        
      100        
      66        
244             or not $legal and $altitude->{$coordinates} < $lowest
245             or $legal and $altitude->{$other} < $lowest) {
246 1576 100       3358 $lowest = $legal ? $altitude->{$other} : $altitude->{$coordinates};
247 1576         2270 $direction = $i;
248 1576         4158 $log->debug("Set lowest to $lowest ($direction)");
249             }
250             }
251 1200 100       3256 if (defined $direction) {
252 1160         2771 $water->{$coordinates} = $direction;
253             $world->{$coordinates} =~ s/arrow\d/arrow$water->{$coordinates}/
254 1160 50       5137 or $world->{$coordinates} .= " arrow$water->{$coordinates}";
255             }
256             }
257             }
258              
259             sub mountains {
260 4     4 0 11 my $self = shift;
261 4         27 my ($world, $altitude) = @_;
262             # place the types
263 4         118 for my $coordinates (keys %$altitude) {
264 1200 100       2994 if ($altitude->{$coordinates} >= 10) {
    100          
    100          
265 34         76 $world->{$coordinates} = "white mountains";
266             } elsif ($altitude->{$coordinates} >= 9) {
267 96         156 $world->{$coordinates} = "white mountain";
268             } elsif ($altitude->{$coordinates} >= 8) {
269 253         411 $world->{$coordinates} = "light-grey mountain";
270             }
271             }
272             }
273              
274             sub ocean {
275 4     4 0 8 my $self = shift;
276 4         14 my ($world, $altitude) = @_;
277 4         560 for my $coordinates (sort keys %$altitude) {
278 1200 50       4235 if ($altitude->{$coordinates} <= $self->bottom) {
279 0         0 my $ocean = 1;
280 0         0 for my $i ($self->neighbors()) {
281 0         0 my ($x, $y) = $self->neighbor($coordinates, $i);
282 0 0       0 next unless $self->legal($x, $y);
283 0         0 my $other = coordinates($x, $y);
284 0 0       0 next if $altitude->{$other} <= $self->bottom;
285 0         0 $ocean = 0;
286             }
287 0 0       0 $world->{$coordinates} = $ocean ? "ocean" : "water";
288             }
289             }
290             }
291              
292             sub lakes {
293 4     4 0 15 my $self = shift;
294 4         15 my ($world, $altitude, $water) = @_;
295             # any areas without water flow are lakes
296 4         706 for my $coordinates (sort keys %$altitude) {
297 1200 100 66     2994 if (not defined $water->{$coordinates}
298             and $world->{$coordinates} ne "ocean") {
299 40         131 $world->{$coordinates} = "water";
300             }
301             }
302             }
303              
304             sub swamps {
305             # any area with water flowing to a neighbor at the same altitude is a swamp
306 4     4 0 18 my ($self, $world, $altitude, $water, $flow, $dry) = @_;
307 4         129 for my $coordinates (keys %$altitude) {
308             # don't turn lakes into swamps and skip bogs
309 1200 100       4173 next if $world->{$coordinates} =~ /ocean|water|swamp|grass/;
310             # swamps require a river
311 998 100       1976 next unless $flow->{$coordinates};
312             # no swamps when there is a canyon
313 460 100       901 next if $dry->{$coordinates};
314             # look at the neighbor the water would flow to
315 414         1109 my ($x, $y) = $self->neighbor($coordinates, $water->{$coordinates});
316             # skip if water flows off the map
317 414 100       878 next unless $self->legal($x, $y);
318 357         3905 my $other = coordinates($x, $y);
319             # skip if water flows downhill
320 357 100       1091 next if $altitude->{$coordinates} > $altitude->{$other};
321             # if there was no lower neighbor, this is a swamp
322 90 100       194 if ($altitude->{$coordinates} >= 6) {
323 65         329 $world->{$coordinates} =~ s/height\d+/grey swamp/;
324             } else {
325 25         153 $world->{$coordinates} =~ s/height\d+/dark-grey swamp/;
326             }
327             }
328             }
329              
330             sub flood {
331 4     4 0 12 my $self = shift;
332 4         16 my ($world, $altitude, $water) = @_;
333             # backtracking information: $from = $flow{$to}
334 4         10 my %flow;
335             # allow easy skipping
336             my %seen;
337             # start with a list of hexes to look at; as always, keys is a source of
338             # randomness that's independent of srand which is why we shuffle sort
339 4         138 my @lakes = shuffle sort grep { not defined $water->{$_} } keys %$world;
  1200         2293  
340 4 50       81 return unless @lakes;
341 4         14 my $start = shift(@lakes);
342 4         14 my @candidates = ($start);
343 4         15 while (@candidates) {
344             # Prefer candidates outside the map with altitude 0; reshuffle because
345             # candidates at the same height are all equal and early or late discoveries
346             # should not matter (not shuffling means it matters whether candidates are
347             # pushed or unshifted because this is a stable sort)
348             @candidates = sort {
349 448   100     3569 ($altitude->{$a}||0) <=> ($altitude->{$b}||0)
  36187   100     109598  
350             } shuffle @candidates;
351 448         3242 $log->debug("Candidates @candidates");
352 448         3862 my $coordinates;
353             do {
354 490         2453 $coordinates = shift(@candidates);
355 448   66     778 } until not $coordinates or not $seen{$coordinates};
356 448 50       1017 last unless $coordinates;
357 448         1090 $seen{$coordinates} = 1;
358 448         1592 $log->debug("Looking at $coordinates");
359 448 100 66     3625 if ($self->legal($coordinates) and $world->{$coordinates} ne "ocean") {
360             # if we're still on the map, check all the unknown neighbors
361 413         6666 my $from = $coordinates;
362 413         1269 for my $i ($self->neighbors()) {
363 1992         5023 my $to = coordinates($self->neighbor($from, $i));
364 1992 100       5450 next if $seen{$to};
365 1503         5399 $log->debug("Adding $to to our candidates");
366 1503         11978 $flow{$to} = $from;
367             # adding to the front as we keep pushing forward (I hope)
368 1503         3647 push(@candidates, $to);
369             }
370 413         1313 next;
371             }
372 35         520 $log->debug("We left the map at $coordinates");
373 35         261 my $to = $coordinates;
374 35         90 my $from = $flow{$to};
375 35         137 while ($from) {
376 277         769 my $i = $self->direction($from, $to);
377 277 100 100     1330 if (not defined $water->{$from}
378             or $water->{$from} != $i) {
379 168         686 $log->debug("Arrow for $from now points to $to");
380 168         2019 $water->{$from} = $i;
381             $world->{$from} =~ s/arrow\d/arrow$i/
382 168 100       1431 or $world->{$from} .= " arrow$i";
383             } else {
384 109         488 $log->debug("Arrow for $from already points $to");
385             }
386 277         1230 $to = $from;
387 277         815 $from = $flow{$to};
388             }
389             # pick the next lake
390             do {
391 40         136 $start = shift(@lakes);
392 40 100       211 $log->debug("Next lake is $start") if $start;
393 35   100     66 } until not $start or not defined $water->{$start};
394 35 100       415 last unless $start;
395 31         287 %seen = %flow = ();
396 31         182 @candidates = ($start);
397             }
398             }
399              
400             sub rivers {
401 4     4 0 19 my ($self, $world, $altitude, $water, $flow, $rivers) = @_;
402             # $flow are the sources points of rivers, or 1 if a river flows through them
403             my @growing = map {
404 253 100       1009 $world->{$_} = "light-grey forest-hill" unless $world->{$_} =~ /mountain|swamp|grass|water|ocean/;
405 253         745 $flow->{$_} = [$_]
406             } sort grep {
407             # these are the potential starting places: up in the mountains below the
408             # ice, or lakes
409 4         151 ($altitude->{$_} == 7 or $altitude->{$_} == 8
410             or $world->{$_} =~ /water/ and $altitude->{$_} > $self->bottom)
411             and not $flow->{$_}
412 1200 50 66     6947 and $world->{$_} !~ /dry/;
      66        
413             } keys %$altitude;
414 4         113 $self->grow_rivers(\@growing, $water, $flow, $rivers);
415             }
416              
417             sub grow_rivers {
418 4     4 0 17 my ($self, $growing, $water, $flow, $rivers) = @_;
419 4         26 while (@$growing) {
420             # warn "Rivers: " . @growing . "\n";
421             # pick a random growing river and grow it
422 1648         3270 my $n = int(rand(scalar @$growing));
423 1648         2676 my $river = $growing->[$n];
424             # warn "Picking @$river\n";
425 1648         2632 my $coordinates = $river->[-1];
426 1648         2436 my $end = 1;
427 1648 100       3391 if (defined $water->{$coordinates}) {
428 1424         3782 my $other = coordinates($self->neighbor($coordinates, $water->{$coordinates}));
429 1424 50       18413 die "Adding $other leads to an infinite loop in river @$river\n" if grep /$other/, @$river;
430             # if we flowed into a hex with a river
431 1424 100       3478 if (ref $flow->{$other}) {
432             # warn "Prepending @$river to @{$flow->{$other}}\n";
433             # prepend the current river to the other river
434 29         44 unshift(@{$flow->{$other}}, @$river);
  29         103  
435             # move the source marker
436 29         75 $flow->{$river->[0]} = $flow->{$other};
437 29         48 $flow->{$other} = 1;
438             # and remove the current river from the growing list
439 29         116 splice(@$growing, $n, 1);
440             # warn "Flow at $river->[0]: @{$flow->{$river->[0]}}\n";
441             # warn "Flow at $other: $flow->{$other}\n";
442             } else {
443 1395         2478 $flow->{$coordinates} = 1;
444 1395         4112 push(@$river, $other);
445             }
446             } else {
447             # stop growing this river
448             # warn "Stopped river: @$river\n" if grep(/0914/, @$river);
449 224         672 push(@$rivers, splice(@$growing, $n, 1));
450             }
451             }
452             }
453              
454             sub canyons {
455 4     4 0 12 my $self = shift;
456 4         13 my ($world, $altitude, $rivers, $canyons, $dry) = @_;
457             # using a reference to an array so that we can leave pointers in the %seen hash
458 4         10 my $canyon = [];
459             # remember which canyon flows through which hex
460 4         12 my %seen;
461 4         11 for my $river (@$rivers) {
462 224         387 my $last = $river->[0];
463 224         456 my $current_altitude = $altitude->{$last};
464 224         933 $log->debug("Looking at @$river ($current_altitude)");
465 224         1222 for my $coordinates (@$river) {
466 1178         2956 $log->debug("Looking at $coordinates");
467 1178 100       6791 if ($seen{$coordinates}) {
468             # the rest of this river was already looked at, so there is no need to
469             # do the rest of this river; if we're in a canyon, prepend it to the one
470             # we just found before ending
471 82 100       165 if (@$canyon) {
472 2         4 my @other = @{$seen{$coordinates}};
  2         10  
473 2 50       35 if ($other[0] eq $canyon->[-1]) {
474 2         21 $log->debug("Canyon @$canyon of river @$river merging with @other at $coordinates");
475 2         13 unshift(@{$seen{$coordinates}}, @$canyon[0 .. @$canyon - 2]);
  2         14  
476             } else {
477 0         0 $log->debug("Canyon @$canyon of river @$river stumbled upon existing canyon @other at $coordinates");
478 0         0 while (@other) {
479 0         0 my $other = shift(@other);
480 0 0       0 next if $other ne $coordinates;
481 0         0 push(@$canyon, $other, @other);
482 0         0 last;
483             }
484 0         0 $log->debug("Canyon @$canyon");
485 0         0 push(@$canyons, $canyon);
486             }
487 2         7 $canyon = [];
488             }
489 82         117 $log->debug("We've seen the rest: @{$seen{$coordinates}}");
  82         299  
490 82         527 last;
491             }
492             # no canyons through water!
493 1096 100 100     3821 if ($altitude->{$coordinates} and $current_altitude < $altitude->{$coordinates}
      66        
494             and $world->{$coordinates} !~ /water|ocean/) {
495             # river is digging a canyon; if this not the start of the river and it
496             # is the start of a canyon, prepend the last step
497 61 100       155 push(@$canyon, $last) unless @$canyon;
498 61         107 push(@$canyon, $coordinates);
499 61 50       179 $world->{$coordinates} .= " zone" unless $dry->{$coordinates};
500 61         142 $dry->{$coordinates} = 1;
501 61         209 $log->debug("Growing canyon @$canyon");
502 61         368 $seen{$coordinates} = $canyon;
503             } else {
504             # if we just left a canyon, append the current step
505 1035 100       1796 if (@$canyon) {
506 25         60 push(@$canyon, $coordinates);
507 25         47 push(@$canyons, $canyon);
508 25         131 $log->debug("Looking at river @$river");
509 25         180 $log->debug("Canyon @$canyon");
510 25         144 $canyon = [];
511 25         59 last;
512             }
513             # not digging a canyon
514 1010         1510 $last = $coordinates;
515 1010         1887 $current_altitude = $altitude->{$coordinates};
516             }
517             }
518             }
519             }
520              
521             sub wet {
522 0     0 0 0 my $self = shift;
523             # a hex is wet if there is a river, a swamp or a forest within 2 hexes
524 0         0 my ($coordinates, $world, $flow) = @_;
525 0         0 for my $i ($self->neighbors()) {
526 0         0 my ($x, $y) = $self->neighbor($coordinates, $i);
527 0         0 my $other = coordinates($x, $y);
528 0 0       0 return 0 if $flow->{$other};
529             }
530 0         0 for my $i ($self->neighbors2()) {
531 0         0 my ($x, $y) = $self->neighbor2($coordinates, $i);
532 0         0 my $other = coordinates($x, $y);
533 0 0       0 return 0 if $flow->{$other};
534             }
535 0         0 return 1;
536             }
537              
538             sub grow_forest {
539 513     513 0 1304 my ($self, $coordinates, $world, $altitude, $dry) = @_;
540 513         812 my @candidates;
541 513 100       3266 push(@candidates, $coordinates) if $world->{$coordinates} !~ /mountain|hill|water|ocean|swamp|grass/;
542 513         1387 my $n = $self->arid;
543             # fractions are allowed
544 513 50       2434 $n += 1 if rand() < $self->arid - int($self->arid);
545 513         3200 $n = int($n);
546 513         1909 $log->debug("Arid: $n");
547 513 50       3514 if ($n >= 1) {
548 513         1333 for my $i ($self->neighbors()) {
549 2540         6527 my ($x, $y) = $self->neighbor($coordinates, $i);
550 2540 100       5311 next unless $self->legal($x, $y);
551 2338         23296 my $other = coordinates($x, $y);
552 2338 100       5076 next if $dry->{$other};
553 2214 100       5311 next if $altitude->{$coordinates} < $altitude->{$other}; # distance of one unless higher
554 1459 100       7784 push(@candidates, $other) if $world->{$other} !~ /mountain|hill|water|ocean|swamp|grass/;
555             }
556             }
557 513 50       1868 if ($n >= 2) {
558 513         1401 for my $i ($self->neighbors2()) {
559 5080         14550 my ($x, $y) = $self->neighbor2($coordinates, $i);
560 5080 100       10410 next unless $self->legal($x, $y);
561 4368         43222 my $other = coordinates($x, $y);
562 4368 100       11662 next if $altitude->{$coordinates} <= $altitude->{$other}; # distance of two only if lower
563 1101         1617 my $ok = 0;
564 1101         2544 for my $m ($self->neighbors()) {
565 3409         7574 my ($mx, $my) = $self->neighbor($coordinates, $m);
566 3409 100       6821 next unless $self->legal($mx, $my);
567 3305         31855 my $midway = coordinates($mx, $my);
568 3305 100       6949 next if $dry->{$midway};
569 3096 100       6287 next if $self->distance($midway, $other) != 1;
570 1101 100       2656 next if $altitude->{$coordinates} < $altitude->{$midway};
571 1051 100       2312 next if $altitude->{$midway} < $altitude->{$other};
572 1013         1447 $ok = 1;
573 1013         1671 last;
574             }
575 1101 100       2240 next unless $ok;
576 1013 100       6214 push(@candidates, $other) if $world->{$other} !~ /mountain|hill|water|ocean|swamp|grass/;
577             }
578             }
579 513         3993 $log->debug("forest growth: $coordinates: @candidates");
580 513         4614 for $coordinates (@candidates) {
581 1066 100       2641 if ($altitude->{$coordinates} >= 7) {
    100          
    100          
582 315         784 $world->{$coordinates} = "light-green fir-forest";
583             } elsif ($altitude->{$coordinates} >= 6) {
584 457         1200 $world->{$coordinates} = "green fir-forest";
585             } elsif ($altitude->{$coordinates} >= 4) {
586 288         680 $world->{$coordinates} = "green forest";
587             } else {
588 6         29 $world->{$coordinates} = "dark-green forest";
589             }
590             }
591             }
592              
593             sub forests {
594 4     4 0 19 my ($self, $world, $altitude, $flow, $dry) = @_;
595             # Empty hexes with a river flowing through them (and nearby hexes) are forest
596             # filled valleys.
597 4         84 for my $coordinates (keys %$flow) {
598 574 100       1481 next if $dry->{$coordinates};
599 513         1198 $self->grow_forest($coordinates, $world, $altitude, $dry);
600             }
601             }
602              
603             sub winds {
604 4     4 0 13 my $self = shift;
605 4         15 my ($world, $altitude, $water, $flow) = @_;
606 4   66     25 my $wind = $self->wind // $self->random_neighbor;
607 4         36 $world->{"0101"} .= " wind" . $self->reverse($wind);
608 4         126 for my $coordinates (keys %$altitude) {
609             # limit ourselves to altitude 7 and 8
610 1200 100 100     4322 next if $altitude->{$coordinates} < 7 or $altitude->{$coordinates} > 8;
611             # look at the neighbor the water would flow to
612 682         1700 my ($x, $y) = $self->neighbor($coordinates, $wind);
613             # skip if off the map
614 682 100       2235 next unless $self->legal($x, $y);
615 650         11335 my $other = coordinates($x, $y);
616             # skip if the other hex is lower
617 650 100       1891 next if $altitude->{$coordinates} > $altitude->{$other};
618             # if the other hex was higher, this land is dry
619 455         1582 $log->debug("$coordinates is dry because of $other");
620 455         3843 $world->{$coordinates} .= " dry zone"; # use label for debugging
621             }
622             }
623              
624             sub bogs {
625 4     4 0 11 my $self = shift;
626 4         13 my ($world, $altitude, $water) = @_;
627 4         154 for my $coordinates (keys %$altitude) {
628             # limit ourselves to altitude 7
629 1200 100       7502 next if $altitude->{$coordinates} != 7;
630             # don't turn lakes into bogs
631 429 100       1698 next if $world->{$coordinates} =~ /water|ocean/;
632             # look at the neighbor the water would flow to
633 415         1238 my ($x, $y) = $self->neighbor($coordinates, $water->{$coordinates});
634             # skip if water flows off the map
635 415 100       975 next unless $self->legal($x, $y);
636 378         4814 my $other = coordinates($x, $y);
637             # skip if water flows downhill
638 378 100       1130 next if $altitude->{$coordinates} > $altitude->{$other};
639             # if there was no lower neighbor, this is a bog
640 162         975 $world->{$coordinates} =~ s/height\d+/grey swamp/;
641             }
642             }
643              
644             sub dry {
645 4     4 0 16 my ($self, $world, $altitude, $rivers) = @_;
646 4         13 my @dry;
647 4         630 for my $coordinates (shuffle sort keys %$world) {
648 1200 100       3808 if ($world->{$coordinates} !~ /mountain|hill|water|ocean|swamp|grass|forest|firs|trees/) {
649 88 100       166 if ($altitude->{$coordinates} >= 7) {
650 30         61 $world->{$coordinates} = "light-grey grass";
651             } else {
652 58         88 $world->{$coordinates} = "light-green bushes";
653 58         120 push(@dry, $coordinates);
654             }
655             }
656             }
657              
658             BUSHES:
659 4         61 for my $coordinates (@dry) {
660 58         146 for my $i ($self->neighbors()) {
661 154         442 my ($x, $y) = $self->neighbor($coordinates, $i);
662 154 100       302 next unless $self->legal($x, $y);
663 124         1282 my $other = coordinates($x, $y);
664 124 100       521 next BUSHES if $world->{$other} =~ /forest|firs|trees|swamp/;
665             }
666 15 50       54 if ($altitude->{$coordinates} >= 5) {
    0          
667 15         62 $world->{$coordinates} =~ s/light-green bushes/light-grey grass/;
668             } elsif ($altitude->{$coordinates} >= 3) {
669 0         0 $world->{$coordinates} =~ s/light-green bushes/grey grass/;
670             } else {
671 0         0 $world->{$coordinates} =~ s/light-green bushes/dark-grey grass/;
672             }
673             }
674              
675             GRASS:
676 4         14 for my $coordinates (@dry) {
677 58 100       192 next if $self->with_river($rivers, $coordinates);
678 50         253 for my $i ($self->neighbors()) {
679 95         307 my ($x, $y) = $self->neighbor($coordinates, $i);
680 95 100       219 next unless $self->legal($x, $y);
681 75         847 my $other = coordinates($x, $y);
682 75 100       504 next GRASS if $world->{$other} !~ /grass|desert|water/;
683             }
684 5 50       14 if ($altitude->{$coordinates} >= 3) {
685 5         31 $world->{$coordinates} =~ s/(light-|dark-)?grey grass/light-grey desert/;
686             } else {
687 0         0 $world->{$coordinates} =~ s/(light-|dark-)?grey grass/dust desert/;
688             }
689             }
690             }
691              
692             sub with_river {
693 58     58 0 119 my ($self, $rivers, $coordinates) = @_;
694 58         103 for my $river (@$rivers) {
695 2788 100       4111 return 1 if grep { $coordinates eq $_ } (@$river);
  19888         32222  
696             }
697             }
698              
699             sub settlements {
700 8     8 0 28 my $self = shift;
701 8         66 my ($world, $flow) = @_;
702 8         16 my @settlements;
703 8         45 my $max = $self->height * $self->width;
704             # do not match forest-hill
705 8         430 my @candidates = shuffle sort grep { $world->{$_} =~ /\b(fir-forest|forest(?!-hill))\b/ } keys %$world;
  3600         9696  
706 8         337 @candidates = $self->remove_closer_than(2, @candidates);
707 8 100       86 @candidates = @candidates[0 .. int($max/10 - 1)] if @candidates > $max/10;
708 8         44 push(@settlements, @candidates);
709 8         24 for my $coordinates (@candidates) {
710             $world->{$coordinates} =~ s/fir-forest/firs thorp/
711 126 100       730 or $world->{$coordinates} =~ s/forest(?!-hill)/trees thorp/;
712             }
713 8 100       501 @candidates = shuffle sort grep { $world->{$_} =~ /(?{$_}} keys %$world;
  3600         8561  
714 8         223 @candidates = $self->remove_closer_than(5, @candidates);
715 8 50       41 @candidates = @candidates[0 .. int($max/20 - 1)] if @candidates > $max/20;
716 8         23 push(@settlements, @candidates);
717 8         39 for my $coordinates (@candidates) {
718 13         50 $world->{$coordinates} =~ s/forest/trees village/;
719             }
720 8 100       324 @candidates = shuffle sort grep { $world->{$_} =~ /(?{$_} } keys %$world;
  3600         15241  
721 8         224 @candidates = $self->remove_closer_than(10, @candidates);
722 8 50       58 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
723 8         23 push(@settlements, @candidates);
724 8         27 for my $coordinates (@candidates) {
725 7         30 $world->{$coordinates} =~ s/forest/trees town/;
726             }
727 8         353 @candidates = shuffle sort grep { $world->{$_} =~ /white mountain\b/ } keys %$world;
  3600         6896  
728 8         210 @candidates = $self->remove_closer_than(10, @candidates);
729 8 50       47 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
730 8         24 push(@settlements, @candidates);
731 8         43 for my $coordinates (@candidates) {
732 11         69 $world->{$coordinates} =~ s/white mountain\b/white mountain law/;
733             }
734 8         387 @candidates = shuffle sort grep { $world->{$_} =~ /swamp/ } keys %$world;
  3600         7047  
735 8         211 @candidates = $self->remove_closer_than(10, @candidates);
736 8 50       52 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
737 8         26 push(@settlements, @candidates);
738 8         39 for my $coordinates (@candidates) {
739 16         73 $world->{$coordinates} =~ s/swamp/swamp2 chaos/;
740             }
741 8         24 for my $coordinates (@settlements) {
742 173         440 for my $i ($self->neighbors()) {
743 779         1708 my ($x, $y) = $self->neighbor($coordinates, $i);
744 779         1449 my $other = coordinates($x, $y);
745 779 100 100     3628 next unless $world->{$other} and $world->{$other} =~ /water|ocean/;
746             # bump ports one size category up
747 36         99 $world->{$coordinates} =~ s/large-town/city port/;
748 36         85 $world->{$coordinates} =~ s/town/large-town port/;
749 36         76 $world->{$coordinates} =~ s/village/town port/;
750             # no bumps for thorps
751 36         70 last;
752             }
753             }
754 8         31 for my $coordinates (@settlements) {
755             # thorps and villages don't cut enough wood; make sure to get both "green" and "dark-green"
756 173 100       456 $world->{$coordinates} =~ s/\S*green trees/light-soil/ if $world->{$coordinates} =~ /large-town|city/;
757 173 100       430 $world->{$coordinates} =~ s/\S*green trees/soil/ if $world->{$coordinates} =~ / town/;
758             }
759 8         104 return @settlements;
760             }
761              
762             sub trails {
763 8     8 0 24 my $self = shift;
764 8         21 my ($altitude, $settlements) = @_;
765             # look for a neighbor that is as low as possible and nearby
766 8         21 my %trails;
767 8         99 my @from = shuffle @$settlements;
768 8         55 my @to = shuffle @$settlements;
769 8         22 for my $from (@from) {
770 173         963 my ($best, $best_distance, $best_altitude);
771 173         316 for my $to (@to) {
772 6999 100       16458 next if $from eq $to;
773 6826         15256 my $distance = $self->distance($from, $to);
774 6826         25720 $log->debug("Considering $from-$to: distance $distance, altitude " . $altitude->{$to});
775 6826 100 100     52924 if ($distance <= 3
      100        
      100        
      100        
776             and (not $best_distance or $distance <= $best_distance)
777             and (not $best or $altitude->{$to} < $best_altitude)) {
778 234         365 $best = $to;
779 234         439 $best_altitude = $altitude->{$best};
780 234         448 $best_distance = $distance;
781             }
782             }
783 173 100       489 next if not $best;
784             # skip if it already exists in the other direction
785 162 100       499 next if $trails{"$best-$from"};
786 134         592 $trails{"$from-$best"} = 1;
787 134         463 $log->debug("Trail $from-$best");
788             }
789 8         239 return keys %trails;
790             }
791              
792             sub cliffs {
793 4     4 0 15 my $self = shift;
794 4         27 my ($world, $altitude) = @_;
795 4         41 my @neighbors = $self->neighbors();
796             # hexes with altitude difference bigger than 1 have cliffs
797 4         124 for my $coordinates (keys %$world) {
798 1200 50       4602 next if $altitude->{$coordinates} <= $self->bottom;
799 1200         6736 for my $i (@neighbors) {
800 6000         18468 my ($x, $y) = $self->neighbor($coordinates, $i);
801 6000 100       14702 next unless $self->legal($x, $y);
802 5524         77909 my $other = coordinates($x, $y);
803 5524 100       17863 if ($altitude->{$coordinates} - $altitude->{$other} >= 2) {
804 274 100       614 if (@neighbors == 6) {
805 163         570 $world->{$coordinates} .= " cliff$i";
806             } else { # square
807 111         500 $world->{$coordinates} .= " cliffs$i";
808             }
809             }
810             }
811             }
812             }
813              
814             sub marshlands {
815 4     4 0 18 my ($self, $world, $altitude, $rivers) = @_;
816 4         11 my %seen;
817 4         15 for my $river (@$rivers) {
818 224         425 my $last = $river->[0];
819 224         385 for my $coordinates (@$river) {
820 798 100       1921 last if $seen{$coordinates}; # we've been here before
821 638         1310 $seen{$coordinates} = 1;
822 638 100       1362 next unless exists $altitude->{$coordinates}; # rivers ending off the map
823 574 50       1276 if ($altitude->{$coordinates} <= $self->bottom) {
824 0 0 0     0 if ($altitude->{$coordinates} == $self->bottom
      0        
825             and $world->{$coordinates} =~ /water|ocean/
826             and $altitude->{$coordinates} == $altitude->{$last} - 1) {
827 0         0 $world->{$coordinates} = "blue-green swamp";
828             } else {
829 0         0 $world->{$coordinates} =~ s/ocean/water/;
830 0         0 delete $seen{$coordinates};
831 0         0 last;
832             }
833             }
834 574         2423 $last = $coordinates;
835             }
836             }
837             }
838              
839             sub desertification {
840 4     4 0 16 my ($self, $world, $altitude, $rivers) = @_;
841 4 50       29 return unless $self->climate eq 'desert';
842 0         0 for my $coordinates (keys %$world) {
843 0 0       0 if ($self->with_river($rivers, $coordinates)) {
844             $world->{$coordinates} =~ s/light-grey/light-green/
845 0 0       0 or $world->{$coordinates} =~ s/dark-green/green/
846             } else {
847             $world->{$coordinates} =~ s/light-green bushes/rock bushes/
848             or $world->{$coordinates} =~ s/light-grey grass/rock bush/
849             or $world->{$coordinates} =~ s/dark-grey grass/dark-soil bush/
850             or $world->{$coordinates} =~ s/^grey grass/rock bush/
851             or $altitude->{$coordinates} >= 4 and $world->{$coordinates} =~ s/light-grey desert/dark-soil desert/
852 0 0 0     0 or $altitude->{$coordinates} >= 2 and $world->{$coordinates} =~ s/(dust|light-grey) desert/light-grey desert/
      0        
      0        
      0        
      0        
      0        
853             }
854             $world->{$coordinates} =~ s/dark-grey swamp2?/light-green bushes/
855             or $world->{$coordinates} =~ s/^grey swamp2?/light-grey bushes/
856             or $world->{$coordinates} =~ s/fir-forest/trees/
857 0 0 0     0 or $world->{$coordinates} =~ s/firs/trees/;
      0        
858             }
859             }
860              
861             sub generate {
862 4     4 0 17 my ($self, $world, $altitude, $water, $rivers, $settlements, $trails, $canyons, $step) = @_;
863             # $flow indicates that there is actually a river in this hex
864 4         9 my $flow = {};
865             # $dry indicates that is a river in this hex, but it cut itself a canyon
866 4         10 my $dry = {};
867             my @code = (
868 4     4   38 sub { $self->flat($altitude);
869 4         46 $self->altitude($world, $altitude); },
870 4     4   41 sub { $self->bumpiness($world, $altitude); },
871 4     4   51 sub { $self->mountains($world, $altitude); },
872 4     4   38 sub { $self->ocean($world, $altitude); },
873 4     4   38 sub { $self->water($world, $altitude, $water); },
874 4     4   69 sub { $self->lakes($world, $altitude, $water); },
875 4     4   53 sub { $self->flood($world, $altitude, $water); },
876 4     4   64 sub { $self->bogs($world, $altitude, $water); },
877 4     4   55 sub { $self->winds($world, $altitude, $water); },
878 4     4   77 sub { $self->rivers($world, $altitude, $water, $flow, $rivers); },
879 4     4   56 sub { $self->canyons($world, $altitude, $rivers, $canyons, $dry); },
880 4     4   70 sub { $self->swamps($world, $altitude, $water, $flow, $dry); },
881 4     4   76 sub { $self->forests($world, $altitude, $flow, $dry); },
882 4     4   67 sub { $self->dry($world, $altitude, $rivers); },
883 4     4   61 sub { $self->cliffs($world, $altitude); },
884 4     4   106 sub { push(@$settlements, $self->settlements($world, $flow)); },
885 4     4   60 sub { push(@$trails, $self->trails($altitude, $settlements)); },
886 4     4   68 sub { $self->marshlands($world, $altitude, $rivers); },
887 4     4   63 sub { $self->desertification($world, $altitude, $rivers); },
888             # make sure you look at "alpine_document.html.ep" if you change this list!
889             # make sure you look at '/alpine/document' if you add to this list!
890 4         152 );
891              
892             # $step 0 runs all the code; note that we can't simply cache those results
893             # because we need to start over with the same seed!
894 4         97 my $i = 1;
895 4         17 while (@code) {
896 76         358 shift(@code)->();
897 76 50       1506 return if $step == $i++;
898 76         392 $self->fixup($world, $altitude, $i);
899             }
900             }
901              
902             # Remove temporary markers that won't be needed in the next step
903             sub fixup {
904 76     76 0 227 my ($self, $world, $altitude, $step, $last) = @_;
905             # When documenting or debugging, water flow arrows are no longer needed when
906             # the rivers are added.
907 76 100       247 if ($step >= 10) {
908 44         2483 for my $coordinates (keys %$world) {
909 13200         26320 $world->{$coordinates} =~ s/ arrow\d//;
910             }
911             }
912             # Wind direction is only shown once.
913 76         1027 $world->{"0101"} =~ s/ wind\d//;
914             # Remove zone markers.
915 76         2636 for my $coordinates (keys %$world) {
916 22800         44216 $world->{$coordinates} =~ s/ zone//;
917             }
918             }
919              
920             sub generate_map {
921 4     4 0 1532 my $self = shift;
922              
923             # The parameters turn into class variables.
924 4   50     56 $self->width(shift // 30);
925 4   50     78 $self->height(shift // 10);
926 4   50     60 $self->steepness(shift // 3);
927 4   33     39 $self->peaks(shift // int($self->width * $self->height / 40));
928 4   50     105 $self->peak(shift // 10);
929 4   33     40 $self->bumps(shift // int($self->width * $self->height / 40));
930 4   50     96 $self->bump(shift // 2);
931 4   50     51 $self->bottom(shift // 0);
932 4   50     51 $self->arid(shift // 2);
933 4 50       45 $self->climate(shift ? 'desert' : 'temperate');
934 4         56 $self->wind(shift); # or random
935 4   66     53 my $seed = shift||time;
936 4         11 my $url = shift;
937 4   50     23 my $step = shift||0;
938              
939             # For documentation purposes, I want to be able to set the pseudo-random
940             # number seed using srand and rely on rand to reproduce the same sequence of
941             # pseudo-random numbers for the same seed. The key point to remember is that
942             # the keys function will return keys in random order. So if we look over the
943             # result of keys, we need to look at the code in the loop: If order is
944             # important, that wont do. We need to sort the keys. If we want the keys to be
945             # pseudo-shuffled, use shuffle sort keys.
946 4         14 srand($seed);
947              
948             # Keys for all hashes are coordinates such as "0101".
949             # %world is the description with values such as "green forest".
950             # %altitude is the altitude with values such as 3.
951             # %water is the preferred direction water would take with values such as 0.
952             # (north west); 0 means we need to use "if defined".
953             # @rivers are the rivers with values such as ["0102", "0202"].
954             # @settlements are are the locations of settlements such as "0101".
955             # @trails are the trails connecting these with values as "0102-0202".
956             # $step is how far we want map generation to go where 0 means all the way.
957 4         18 my ($world, $altitude, $water, $rivers, $settlements, $trails, $canyons) =
958             ({}, {}, {}, [], [], [], []);
959 4         36 $self->generate($world, $altitude, $water, $rivers, $settlements, $trails, $canyons, $step);
960              
961             # When documenting or debugging, add altitude as a label.
962 4 50       27 if ($step > 0) {
963 0         0 for my $coordinates (keys %$world) {
964 0         0 $world->{$coordinates} .= ' "' . $altitude->{$coordinates} . '"';
965             }
966             }
967              
968 4         18 local $" = "-"; # list items separated by -
969 4         9 my @lines;
970 4         603 push(@lines, map { $_ . " " . $world->{$_} } sort keys %$world);
  1200         2825  
971 4         133 push(@lines, map { "$_ trail" } @$trails);
  133         291  
972 4         18 push(@lines, map { "@$_ river" } @$rivers);
  224         818  
973 4         27 push(@lines, map { "@$_ canyon" } @$canyons); # after rivers
  25         87  
974 4         16 push(@lines, "include gnomeyland.txt");
975              
976             # when documenting or debugging, add some more lines at the end
977 4 50       20 if ($step > 0) {
978             # visualize height
979             push(@lines,
980             map {
981 0         0 my $n = int(25.5 * $_);
  0         0  
982 0         0 qq{height$_ attributes fill="rgb($n,$n,$n)"};
983             } (0 .. 10));
984             # visualize water flow
985 0         0 push(@lines, $self->arrows());
986             }
987              
988 4         19 push(@lines, "# Seed: $seed");
989 4 100       30 push(@lines, "# Documentation: " . $url) if $url;
990 4         1838 my $map = join("\n", @lines);
991 4 100       1670 return $map, $self if wantarray;
992 2         604 return $map;
993             }
994              
995             1;