File Coverage

blib/lib/Game/TextMapper/Schroeder/Alpine.pm
Criterion Covered Total %
statement 537 598 89.8
branch 218 280 77.8
condition 87 144 60.4
subroutine 52 53 98.1
pod 0 29 0.0
total 894 1104 80.9


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         29  
  11         522  
49 11     11   81 use Modern::Perl '2018';
  11         24  
  11         131  
50 11     11   4031 use Mojo::Base -base;
  11         30  
  11         93  
51 11     11   8328 use Role::Tiny::With;
  11         3757  
  11         917  
52             with 'Game::TextMapper::Schroeder::Base';
53 11     11   89 use List::Util 'shuffle';
  11         25  
  11         179130  
54              
55             my $log = Game::TextMapper::Log->get;
56              
57             has 'steepness';
58             has 'peaks';
59             has 'peak';
60             has 'peak_min';
61             has 'bumps';
62             has 'bump';
63             has 'bottom';
64             has 'arid';
65             has 'climate';
66             has 'wind';
67              
68             sub place_peak {
69 4     4 0 82 my $self = shift;
70 4         12 my $altitude = shift;
71 4         10 my $count = shift;
72 4         9 my $min = shift;
73 4         8 my $max = shift;
74             # max altitude exactly once
75 4         16 my $x = int(rand($self->width)) + 1;
76 4         45 my $y = int(rand($self->height)) + 1;
77 4         47 my $coordinates = coordinates($x, $y);
78 4         32 $altitude->{$coordinates} = $max;
79 4         46 $log->debug("placed max $max at $coordinates");
80             # prepare distribution
81 4         56 my @distribution;
82 4         11 my $n = 1;
83 4         17 for my $i (0 .. $max - $min) {
84 4         12 push(@distribution, $n);
85 4         13 $n *= 2;
86             }
87             # this is the "die size"
88 4         11 $n = $distribution[$#distribution];
89 4         34 $log->debug("@distribution");
90 4         32 my @queue;
91             # place some peaks and put them in a queue
92 4         13 for (1 .. $count) {
93             # try to find an empty hex
94 28         54 for (1 .. 6) {
95 29         67 my $x = int(rand($self->width)) + 1;
96 29         138 my $y = int(rand($self->height)) + 1;
97 29         151 my $coordinates = coordinates($x, $y);
98 29 100       78 next if $altitude->{$coordinates};
99 28         48 my $r = rand($n);
100 28         153 $log->debug(" rolled $r");
101 28         175 for my $i (0 .. $#distribution) {
102 28         115 $log->debug(" $r < $distribution[$i]");
103 28 50       163 if ($r < $distribution[$i]) {
104 28         51 $altitude->{$coordinates} = $max - $i;
105 28         92 $log->debug("placed $altitude->{$coordinates} at $coordinates");
106 28         190 push(@queue, $coordinates);
107 28         48 last;
108             }
109             }
110 28         50 last;
111             }
112             }
113 4         35 return @queue;
114             }
115              
116             sub grow_mountains {
117 4     4 0 11 my $self = shift;
118 4         15 my $altitude = shift;
119 4         16 my @queue = @_;
120             # go through the queue and add adjacent lower altitude hexes, if possible; the
121             # hexes added are to the end of the queue
122 4         18 while (@queue) {
123 1196         4145 my $coordinates = shift @queue;
124 1196         2737 my $current_altitude = $altitude->{$coordinates};
125 1196 50       2481 next unless $current_altitude > 0;
126             # pick some random neighbors based on variable steepness
127 1196         3276 my $n = $self->steepness;
128             # round up based on fraction
129 1196 50       6544 $n += 1 if rand() < $n - int($n);
130 1196         1883 $n = int($n);
131 1196 50       2372 next if $n < 1;
132 1196         2864 for (1 .. $n) {
133             # try to find an empty neighbor; make more attempts if we're looking for
134             # more neighbours
135 3588         10677 for my $attempt (1 .. 3 + $n) {
136 17318         64046 my ($x, $y) = $self->neighbor($coordinates, $self->random_neighbor());
137 17318 100       40088 next unless $self->legal($x, $y);
138 15762         180259 my $other = coordinates($x, $y);
139             # if this is taken, look further
140 15762 100       39794 if ($altitude->{$other}) {
141 15300         37627 ($x, $y) = $self->neighbor2($coordinates, $self->random_neighbor2());
142 15300 100       37731 next unless $self->legal($x, $y);
143 13348         150528 $other = coordinates($x, $y);
144             # if this is also taken, try again – but if we've already had four
145             # attempts, jump!
146 13348 100       34425 if ($altitude->{$other}) {
147 12642 100       25927 $coordinates = $other if $attempt > 4;
148 12642         28694 next;
149             }
150             }
151             # if we found an empty neighbor, set its altitude
152 1168 50       2572 $altitude->{$other} = $current_altitude > 0 ? $current_altitude - 1 : 0;
153 1168         2168 push(@queue, $other);
154 1168         2870 last;
155             }
156             }
157             }
158             }
159              
160             sub fix_altitude {
161 4     4 0 12 my $self = shift;
162 4         23 my $altitude = shift;
163             # go through all the hexes
164 4         862 for my $coordinates (sort keys %$altitude) {
165             # find hexes that we missed and give them the height of a random neighbor
166 1200 50       2545 if (not defined $altitude->{$coordinates}) {
167             # warn "identified a hex that was skipped: $coordinates\n";
168             # try to find a suitable neighbor
169 0         0 for (1 .. 6) {
170 0         0 my ($x, $y) = $self->neighbor($coordinates, $self->random_neighbor());
171 0 0       0 next unless $self->legal($x, $y);
172 0         0 my $other = coordinates($x, $y);
173 0 0       0 next unless defined $altitude->{$other};
174 0         0 $altitude->{$coordinates} = $altitude->{$other};
175 0         0 last;
176             }
177             # if we didn't find one in the last six attempts, just make it hole in the ground
178 0 0       0 if (not defined $altitude->{$coordinates}) {
179 0         0 $altitude->{$coordinates} = 0;
180             }
181             }
182             }
183             }
184              
185             sub altitude {
186 4     4 0 13 my $self = shift;
187 4         31 my ($world, $altitude) = @_;
188 4         26 my @queue = $self->place_peak($altitude, $self->peaks, $self->peak_min, $self->peak);
189 4         68 $self->grow_mountains($altitude, @queue);
190 4         64 $self->fix_altitude($altitude);
191             # note height for debugging purposes
192 4         648 for my $coordinates (sort keys %$altitude) {
193 1200         3453 $world->{$coordinates} = "height$altitude->{$coordinates}";
194             }
195             }
196              
197             sub bumpiness {
198 4     4 0 17 my ($self, $world, $altitude) = @_;
199 4         27 for (1 .. $self->bumps) {
200 28         114 for my $delta (-$self->bump, $self->bump) {
201             # six attempts to try and find a good hex
202 56         258 for (1 .. 6) {
203 58         163 my $x = int(rand($self->width)) + 1;
204 58         343 my $y = int(rand($self->height)) + 1;
205 58         330 my $coordinates = coordinates($x, $y);
206 58         163 my $current_altitude = $altitude->{$coordinates} + $delta;
207 58 100 66     201 next if $current_altitude > 10 or $current_altitude < 0;
208             # bump it up or down
209 56         105 $altitude->{$coordinates} = $current_altitude;
210 56         126 $world->{$coordinates} = "height$altitude->{$coordinates} zone";
211 56         250 $log->debug("bumped altitude of $coordinates by $delta to $current_altitude");
212             # if the bump was +2 or -2, bump the neighbours by +1 or -1
213 56 50 66     481 if ($delta < -1 or $delta > 1) {
214 56         130 my $delta = $delta - $delta / abs($delta);
215 56         184 for my $i ($self->neighbors()) {
216 280         1989 my ($x, $y) = $self->neighbor($coordinates, $i);
217 280 100       643 next unless $self->legal($x, $y);
218 256         3105 my $other = coordinates($x, $y);
219 256         543 $current_altitude = $altitude->{$other} + $delta;
220 256 100 66     765 next if $current_altitude > 10 or $current_altitude < 0;
221 254         411 $altitude->{$other} = $current_altitude;
222 254         555 $world->{$other} = "height$altitude->{$other} zone";
223 254         893 $log->debug("$i bumped altitude of $other by $delta to $current_altitude");
224             }
225             }
226             # if we have found a good hex, don't go through all the other attempts
227 56         535 last;
228             }
229             }
230             }
231             }
232              
233             sub water {
234 4     4 0 12 my $self = shift;
235 4         16 my ($world, $altitude, $water) = @_;
236             # reset in case we run this twice
237             # go through all the hexes
238 4         603 for my $coordinates (sort keys %$altitude) {
239 1200 50       3822 next if $altitude->{$coordinates} <= $self->bottom;
240             # note preferred water flow by identifying lower lying neighbors
241 1200         5706 my ($lowest, $direction);
242             # look at neighbors in random order
243             NEIGHBOR:
244 1200         3498 for my $i (shuffle $self->neighbors()) {
245 6000         24046 my ($x, $y) = $self->neighbor($coordinates, $i);
246 6000         14900 my $legal = $self->legal($x, $y);
247 6000         66933 my $other = coordinates($x, $y);
248 6000 100 100     33669 next if $legal and $altitude->{$other} > $altitude->{$coordinates};
249             # don't point head on to another arrow
250 4407 100 100     17748 next if $legal and $water->{$other} and $water->{$other} == ($i-3) % 6;
      100        
251             # don't point into loops
252 4149         11914 my %loop = ($coordinates => 1, $other => 1);
253 4149         6509 my $next = $other;
254 4149         16793 $log->debug("Loop detection starting with $coordinates and $other");
255 4149         31313 while ($next) {
256             # no water flow known is also good;
257 7457   100     32855 $log->debug("water for $next: " . ($water->{$next} || "none"));
258 7457 100       47423 last unless defined $water->{$next};
259 3978         11409 ($x, $y) = $self->neighbor($next, $water->{$next});
260             # leaving the map is good
261 3978         10534 $log->debug("legal for $next: " . $self->legal($x, $y));
262 3978 100       66761 last unless $self->legal($x, $y);
263 3462         38492 $next = coordinates($x, $y);
264             # skip this neighbor if this is a loop
265 3462   100     18131 $log->debug("is $next in a loop? " . ($loop{$next} || "no"));
266 3462 100       24392 next NEIGHBOR if $loop{$next};
267 3308         8594 $loop{$next} = 1;
268             }
269 3995 100 66     26453 if (not defined $direction
      66        
      100        
      66        
270             or not $legal and $altitude->{$coordinates} < $lowest
271             or $legal and $altitude->{$other} < $lowest) {
272 1560 100       3750 $lowest = $legal ? $altitude->{$other} : $altitude->{$coordinates};
273 1560         2401 $direction = $i;
274 1560         4252 $log->debug("Set lowest to $lowest ($direction)");
275             }
276             }
277 1200 100       3676 if (defined $direction) {
278 1158         2868 $water->{$coordinates} = $direction;
279             $world->{$coordinates} =~ s/arrow\d/arrow$water->{$coordinates}/
280 1158 50       5964 or $world->{$coordinates} .= " arrow$water->{$coordinates}";
281             }
282             }
283             }
284              
285             sub mountains {
286 4     4 0 12 my $self = shift;
287 4         26 my ($world, $altitude) = @_;
288             # place the types
289 4         130 for my $coordinates (keys %$altitude) {
290 1200 100       3255 if ($altitude->{$coordinates} >= 10) {
    100          
    100          
291 39         77 $world->{$coordinates} = "white mountains";
292             } elsif ($altitude->{$coordinates} >= 9) {
293 97         191 $world->{$coordinates} = "white mountain";
294             } elsif ($altitude->{$coordinates} >= 8) {
295 227         471 $world->{$coordinates} = "light-grey mountain";
296             }
297             }
298             }
299              
300             sub ocean {
301 4     4 0 11 my $self = shift;
302 4         14 my ($world, $altitude) = @_;
303 4         586 for my $coordinates (sort keys %$altitude) {
304 1200 50       5131 if ($altitude->{$coordinates} <= $self->bottom) {
305 0         0 my $ocean = 1;
306 0         0 for my $i ($self->neighbors()) {
307 0         0 my ($x, $y) = $self->neighbor($coordinates, $i);
308 0 0       0 next unless $self->legal($x, $y);
309 0         0 my $other = coordinates($x, $y);
310 0 0       0 next if $altitude->{$other} <= $self->bottom;
311 0         0 $ocean = 0;
312             }
313 0 0       0 $world->{$coordinates} = $ocean ? "ocean" : "water";
314             }
315             }
316             }
317              
318             sub lakes {
319 4     4 0 14 my $self = shift;
320 4         13 my ($world, $altitude, $water) = @_;
321             # any areas without water flow are lakes
322 4         588 for my $coordinates (sort keys %$altitude) {
323 1200 100 66     2571 if (not defined $water->{$coordinates}
324             and $world->{$coordinates} ne "ocean") {
325 42         75 $world->{$coordinates} = "water";
326             }
327             }
328             }
329              
330             sub swamps {
331             # any area with water flowing to a neighbor at the same altitude is a swamp
332 4     4 0 17 my ($self, $world, $altitude, $water, $flow, $dry) = @_;
333 4         108 for my $coordinates (keys %$altitude) {
334             # don't turn lakes into swamps and skip bogs
335 1200 100       4457 next if $world->{$coordinates} =~ /ocean|water|swamp|grass/;
336             # swamps require a river
337 1042 100       2185 next unless $flow->{$coordinates};
338             # no swamps when there is a canyon
339 460 100       1007 next if $dry->{$coordinates};
340             # look at the neighbor the water would flow to
341 418         1304 my ($x, $y) = $self->neighbor($coordinates, $water->{$coordinates});
342             # skip if water flows off the map
343 418 100       970 next unless $self->legal($x, $y);
344 360         4412 my $other = coordinates($x, $y);
345             # skip if water flows downhill
346 360 100       1158 next if $altitude->{$coordinates} > $altitude->{$other};
347             # if there was no lower neighbor, this is a swamp
348 103 100       532 if ($altitude->{$coordinates} >= 6) {
349 69         378 $world->{$coordinates} =~ s/height\d+/grey swamp/;
350             } else {
351 34         230 $world->{$coordinates} =~ s/height\d+/dark-grey swamp/;
352             }
353             }
354             }
355              
356             sub flood {
357 4     4 0 15 my $self = shift;
358 4         14 my ($world, $altitude, $water) = @_;
359             # backtracking information: $from = $flow{$to}
360 4         14 my %flow;
361             # allow easy skipping
362             my %seen;
363             # start with a list of hexes to look at; as always, keys is a source of
364             # randomness that's independent of srand which is why we shuffle sort
365 4         123 my @lakes = shuffle sort grep { not defined $water->{$_} } keys %$world;
  1200         1912  
366 4 50       72 return unless @lakes;
367 4         10 my $start = shift(@lakes);
368 4         12 my @candidates = ($start);
369 4         14 while (@candidates) {
370             # Prefer candidates outside the map with altitude 0; reshuffle because
371             # candidates at the same height are all equal and early or late discoveries
372             # should not matter (not shuffling means it matters whether candidates are
373             # pushed or unshifted because this is a stable sort)
374             @candidates = sort {
375 356   100     2421 ($altitude->{$a}||0) <=> ($altitude->{$b}||0)
  22097   100     53267  
376             } shuffle @candidates;
377 356         2068 $log->debug("Candidates @candidates");
378 356         2443 my $coordinates;
379             do {
380 388         1508 $coordinates = shift(@candidates);
381 356   66     496 } until not $coordinates or not $seen{$coordinates};
382 356 50       669 last unless $coordinates;
383 356         606 $seen{$coordinates} = 1;
384 356         948 $log->debug("Looking at $coordinates");
385 356 100 66     2293 if ($self->legal($coordinates) and $world->{$coordinates} ne "ocean") {
386             # if we're still on the map, check all the unknown neighbors
387 321         4102 my $from = $coordinates;
388 321         825 for my $i ($self->neighbors()) {
389 1582         3259 my $to = coordinates($self->neighbor($from, $i));
390 1582 100       3675 next if $seen{$to};
391 1208         3648 $log->debug("Adding $to to our candidates");
392 1208         8029 $flow{$to} = $from;
393             # adding to the front as we keep pushing forward (I hope)
394 1208         2425 push(@candidates, $to);
395             }
396 321         840 next;
397             }
398 35         371 $log->debug("We left the map at $coordinates");
399 35         203 my $to = $coordinates;
400 35         65 my $from = $flow{$to};
401 35         86 while ($from) {
402 215         584 my $i = $self->direction($from, $to);
403 215 100 100     826 if (not defined $water->{$from}
404             or $water->{$from} != $i) {
405 138         475 $log->debug("Arrow for $from now points to $to");
406 138         985 $water->{$from} = $i;
407             $world->{$from} =~ s/arrow\d/arrow$i/
408 138 100       1031 or $world->{$from} .= " arrow$i";
409             } else {
410 77         231 $log->debug("Arrow for $from already points $to");
411             }
412 215         773 $to = $from;
413 215         565 $from = $flow{$to};
414             }
415             # pick the next lake
416             do {
417 42         112 $start = shift(@lakes);
418 42 100       150 $log->debug("Next lake is $start") if $start;
419 35   100     58 } until not $start or not defined $water->{$start};
420 35 100       327 last unless $start;
421 31         235 %seen = %flow = ();
422 31         136 @candidates = ($start);
423             }
424             }
425              
426             sub rivers {
427 4     4 0 17 my ($self, $world, $altitude, $water, $flow, $rivers) = @_;
428             # $flow are the sources points of rivers, or 1 if a river flows through them
429             my @growing = map {
430 231 100       674 $world->{$_} = "light-grey forest-hill" unless $world->{$_} =~ /mountain|swamp|grass|water|ocean/;
431 231         602 $flow->{$_} = [$_]
432             } sort grep {
433             # these are the potential starting places: up in the mountains below the
434             # ice, or lakes
435 4         111 ($altitude->{$_} == 7 or $altitude->{$_} == 8
436             or $world->{$_} =~ /water/ and $altitude->{$_} > $self->bottom)
437             and not $flow->{$_}
438 1200 50 100     6009 and $world->{$_} !~ /dry/;
      66        
439             } keys %$altitude;
440 4         94 $self->grow_rivers(\@growing, $water, $flow, $rivers);
441             }
442              
443             sub grow_rivers {
444 4     4 0 17 my ($self, $growing, $water, $flow, $rivers) = @_;
445 4         48 while (@$growing) {
446             # warn "Rivers: " . @growing . "\n";
447             # pick a random growing river and grow it
448 1406         2584 my $n = int(rand(scalar @$growing));
449 1406         2014 my $river = $growing->[$n];
450             # warn "Picking @$river\n";
451 1406         2044 my $coordinates = $river->[-1];
452 1406         1866 my $end = 1;
453 1406 100       2834 if (defined $water->{$coordinates}) {
454 1204         3225 my $other = coordinates($self->neighbor($coordinates, $water->{$coordinates}));
455 1204 50       14699 die "Adding $other leads to an infinite loop in river @$river\n" if grep /$other/, @$river;
456             # if we flowed into a hex with a river
457 1204 100       2950 if (ref $flow->{$other}) {
458             # warn "Prepending @$river to @{$flow->{$other}}\n";
459             # prepend the current river to the other river
460 29         41 unshift(@{$flow->{$other}}, @$river);
  29         82  
461             # move the source marker
462 29         79 $flow->{$river->[0]} = $flow->{$other};
463 29         45 $flow->{$other} = 1;
464             # and remove the current river from the growing list
465 29         102 splice(@$growing, $n, 1);
466             # warn "Flow at $river->[0]: @{$flow->{$river->[0]}}\n";
467             # warn "Flow at $other: $flow->{$other}\n";
468             } else {
469 1175         1969 $flow->{$coordinates} = 1;
470 1175         3253 push(@$river, $other);
471             }
472             } else {
473             # stop growing this river
474             # warn "Stopped river: @$river\n" if grep(/0914/, @$river);
475 202         596 push(@$rivers, splice(@$growing, $n, 1));
476             }
477             }
478             }
479              
480             sub canyons {
481 4     4 0 15 my $self = shift;
482 4         16 my ($world, $altitude, $rivers, $canyons, $dry) = @_;
483             # using a reference to an array so that we can leave pointers in the %seen hash
484 4         21 my $canyon = [];
485             # remember which canyon flows through which hex
486 4         34 my %seen;
487 4         13 for my $river (@$rivers) {
488 202         365 my $last = $river->[0];
489 202         392 my $current_altitude = $altitude->{$last};
490 202         808 $log->debug("Looking at @$river ($current_altitude)");
491 202         1213 for my $coordinates (@$river) {
492 1078         2773 $log->debug("Looking at $coordinates");
493 1078 100       6270 if ($seen{$coordinates}) {
494             # the rest of this river was already looked at, so there is no need to
495             # do the rest of this river; if we're in a canyon, prepend it to the one
496             # we just found before ending
497 57 50       117 if (@$canyon) {
498 0         0 my @other = @{$seen{$coordinates}};
  0         0  
499 0 0       0 if ($other[0] eq $canyon->[-1]) {
500 0         0 $log->debug("Canyon @$canyon of river @$river merging with @other at $coordinates");
501 0         0 unshift(@{$seen{$coordinates}}, @$canyon[0 .. @$canyon - 2]);
  0         0  
502             } else {
503 0         0 $log->debug("Canyon @$canyon of river @$river stumbled upon existing canyon @other at $coordinates");
504 0         0 while (@other) {
505 0         0 my $other = shift(@other);
506 0 0       0 next if $other ne $coordinates;
507 0         0 push(@$canyon, $other, @other);
508 0         0 last;
509             }
510 0         0 $log->debug("Canyon @$canyon");
511 0         0 push(@$canyons, $canyon);
512             }
513 0         0 $canyon = [];
514             }
515 57         81 $log->debug("We've seen the rest: @{$seen{$coordinates}}");
  57         226  
516 57         330 last;
517             }
518             # no canyons through water!
519 1021 100 100     3378 if ($altitude->{$coordinates} and $current_altitude < $altitude->{$coordinates}
      66        
520             and $world->{$coordinates} !~ /water|ocean/) {
521             # river is digging a canyon; if this not the start of the river and it
522             # is the start of a canyon, prepend the last step
523 46 100       130 push(@$canyon, $last) unless @$canyon;
524 46         75 push(@$canyon, $coordinates);
525 46 50       141 $world->{$coordinates} .= " zone" unless $dry->{$coordinates};
526 46         87 $dry->{$coordinates} = 1;
527 46         144 $log->debug("Growing canyon @$canyon");
528 46         253 $seen{$coordinates} = $canyon;
529             } else {
530             # if we just left a canyon, append the current step
531 975 100       1757 if (@$canyon) {
532 21         49 push(@$canyon, $coordinates);
533 21         57 push(@$canyons, $canyon);
534 21         98 $log->debug("Looking at river @$river");
535 21         225 $log->debug("Canyon @$canyon");
536 21         115 $canyon = [];
537 21         53 last;
538             }
539             # not digging a canyon
540 954         1430 $last = $coordinates;
541 954         1612 $current_altitude = $altitude->{$coordinates};
542             }
543             }
544             }
545             }
546              
547             sub wet {
548 0     0 0 0 my $self = shift;
549             # a hex is wet if there is a river, a swamp or a forest within 2 hexes
550 0         0 my ($coordinates, $world, $flow) = @_;
551 0         0 for my $i ($self->neighbors()) {
552 0         0 my ($x, $y) = $self->neighbor($coordinates, $i);
553 0         0 my $other = coordinates($x, $y);
554 0 0       0 return 0 if $flow->{$other};
555             }
556 0         0 for my $i ($self->neighbors2()) {
557 0         0 my ($x, $y) = $self->neighbor2($coordinates, $i);
558 0         0 my $other = coordinates($x, $y);
559 0 0       0 return 0 if $flow->{$other};
560             }
561 0         0 return 1;
562             }
563              
564             sub grow_forest {
565 492     492 0 1078 my ($self, $coordinates, $world, $altitude, $dry) = @_;
566 492         684 my @candidates;
567 492 100       2870 push(@candidates, $coordinates) if $world->{$coordinates} !~ /mountain|hill|water|ocean|swamp|grass/;
568 492         1353 my $n = $self->arid;
569             # fractions are allowed
570 492 50       2562 $n += 1 if rand() < $self->arid - int($self->arid);
571 492         2972 $n = int($n);
572 492         1767 $log->debug("Arid: $n");
573 492 50       3240 if ($n >= 1) {
574 492         1331 for my $i ($self->neighbors()) {
575 2500         7301 my ($x, $y) = $self->neighbor($coordinates, $i);
576 2500 100       5642 next unless $self->legal($x, $y);
577 2285         24433 my $other = coordinates($x, $y);
578 2285 100       5291 next if $dry->{$other};
579 2199 100       5410 next if $altitude->{$coordinates} < $altitude->{$other}; # distance of one unless higher
580 1489 100       7672 push(@candidates, $other) if $world->{$other} !~ /mountain|hill|water|ocean|swamp|grass/;
581             }
582             }
583 492 50       1510 if ($n >= 2) {
584 492         1330 for my $i ($self->neighbors2()) {
585 5000         16365 my ($x, $y) = $self->neighbor2($coordinates, $i);
586 5000 100       11069 next unless $self->legal($x, $y);
587 4239         45431 my $other = coordinates($x, $y);
588 4239 100       12327 next if $altitude->{$coordinates} <= $altitude->{$other}; # distance of two only if lower
589 1142         1698 my $ok = 0;
590 1142         2949 for my $m ($self->neighbors()) {
591 3356         8726 my ($mx, $my) = $self->neighbor($coordinates, $m);
592 3356 100       7621 next unless $self->legal($mx, $my);
593 3220         35278 my $midway = coordinates($mx, $my);
594 3220 100       7604 next if $dry->{$midway};
595 3093 100       7191 next if $self->distance($midway, $other) != 1;
596 1167 100       3091 next if $altitude->{$coordinates} < $altitude->{$midway};
597 1097 100       2399 next if $altitude->{$midway} < $altitude->{$other};
598 1041         1509 $ok = 1;
599 1041         1730 last;
600             }
601 1142 100       2437 next unless $ok;
602 1041 100       6521 push(@candidates, $other) if $world->{$other} !~ /mountain|hill|water|ocean|swamp|grass/;
603             }
604             }
605 492         3711 $log->debug("forest growth: $coordinates: @candidates");
606 492         4112 for $coordinates (@candidates) {
607 1138 100       2817 if ($altitude->{$coordinates} >= 7) {
    100          
    100          
608 288         740 $world->{$coordinates} = "light-green fir-forest";
609             } elsif ($altitude->{$coordinates} >= 6) {
610 579         1308 $world->{$coordinates} = "green fir-forest";
611             } elsif ($altitude->{$coordinates} >= 4) {
612 260         722 $world->{$coordinates} = "green forest";
613             } else {
614 11         47 $world->{$coordinates} = "dark-green forest";
615             }
616             }
617             }
618              
619             sub forests {
620 4     4 0 16 my ($self, $world, $altitude, $flow, $dry) = @_;
621             # Empty hexes with a river flowing through them (and nearby hexes) are forest
622             # filled valleys.
623 4         59 for my $coordinates (keys %$flow) {
624 538 100       1248 next if $dry->{$coordinates};
625 492         1134 $self->grow_forest($coordinates, $world, $altitude, $dry);
626             }
627             }
628              
629             sub winds {
630 4     4 0 12 my $self = shift;
631 4         61 my ($world, $altitude, $water, $flow) = @_;
632 4   66     26 my $wind = $self->wind // $self->random_neighbor;
633 4         25 $world->{"0101"} .= " wind" . $self->reverse($wind);
634 4         138 for my $coordinates (keys %$altitude) {
635             # limit ourselves to altitude 7 and 8
636 1200 100 100     3474 next if $altitude->{$coordinates} < 7 or $altitude->{$coordinates} > 8;
637             # look at the neighbor the water would flow to
638 596         1400 my ($x, $y) = $self->neighbor($coordinates, $wind);
639             # skip if off the map
640 596 100       1287 next unless $self->legal($x, $y);
641 574         5887 my $other = coordinates($x, $y);
642             # skip if the other hex is lower
643 574 100       1468 next if $altitude->{$coordinates} > $altitude->{$other};
644             # if the other hex was higher, this land is dry
645 391         1265 $log->debug("$coordinates is dry because of $other");
646 391         2712 $world->{$coordinates} .= " dry zone"; # use label for debugging
647             }
648             }
649              
650             sub bogs {
651 4     4 0 14 my $self = shift;
652 4         13 my ($world, $altitude, $water) = @_;
653 4         121 for my $coordinates (keys %$altitude) {
654             # limit ourselves to altitude 7
655 1200 100       2719 next if $altitude->{$coordinates} != 7;
656             # don't turn lakes into bogs
657 369 100       1150 next if $world->{$coordinates} =~ /water|ocean/;
658             # look at the neighbor the water would flow to
659 356         1062 my ($x, $y) = $self->neighbor($coordinates, $water->{$coordinates});
660             # skip if water flows off the map
661 356 100       794 next unless $self->legal($x, $y);
662 329         3694 my $other = coordinates($x, $y);
663             # skip if water flows downhill
664 329 100       995 next if $altitude->{$coordinates} > $altitude->{$other};
665             # if there was no lower neighbor, this is a bog
666 116         687 $world->{$coordinates} =~ s/height\d+/grey swamp/;
667             }
668             }
669              
670             sub dry {
671 4     4 0 39 my ($self, $world, $altitude, $rivers) = @_;
672 4         13 my @dry;
673 4         764 for my $coordinates (shuffle sort keys %$world) {
674 1200 100       4422 if ($world->{$coordinates} !~ /mountain|hill|water|ocean|swamp|grass|forest|firs|trees/) {
675 115 100       283 if ($altitude->{$coordinates} >= 7) {
676 38         93 $world->{$coordinates} = "light-grey grass";
677             } else {
678 77         146 $world->{$coordinates} = "light-green bushes";
679 77         223 push(@dry, $coordinates);
680             }
681             }
682             }
683              
684             BUSHES:
685 4         91 for my $coordinates (@dry) {
686 77         304 for my $i ($self->neighbors()) {
687 192         599 my ($x, $y) = $self->neighbor($coordinates, $i);
688 192 100       557 next unless $self->legal($x, $y);
689 171         2567 my $other = coordinates($x, $y);
690 171 100       1191 next BUSHES if $world->{$other} =~ /forest|firs|trees|swamp/;
691             }
692 18 100       123 if ($altitude->{$coordinates} >= 5) {
    50          
693 17         106 $world->{$coordinates} =~ s/light-green bushes/light-grey grass/;
694             } elsif ($altitude->{$coordinates} >= 3) {
695 1         8 $world->{$coordinates} =~ s/light-green bushes/grey grass/;
696             } else {
697 0         0 $world->{$coordinates} =~ s/light-green bushes/dark-grey grass/;
698             }
699             }
700              
701             GRASS:
702 4         31 for my $coordinates (@dry) {
703 77 100       218 next if $self->with_river($rivers, $coordinates);
704 60         264 for my $i ($self->neighbors()) {
705 109         270 my ($x, $y) = $self->neighbor($coordinates, $i);
706 109 100       236 next unless $self->legal($x, $y);
707 96         1088 my $other = coordinates($x, $y);
708 96 100       704 next GRASS if $world->{$other} !~ /grass|desert|water/;
709             }
710 4 50       17 if ($altitude->{$coordinates} >= 3) {
711 4         26 $world->{$coordinates} =~ s/(light-|dark-)?grey grass/light-grey desert/;
712             } else {
713 0         0 $world->{$coordinates} =~ s/(light-|dark-)?grey grass/dust desert/;
714             }
715             }
716             }
717              
718             sub with_river {
719 77     77 0 139 my ($self, $rivers, $coordinates) = @_;
720 77         127 for my $river (@$rivers) {
721 2948 100       4356 return 1 if grep { $coordinates eq $_ } (@$river);
  18914         31236  
722             }
723             }
724              
725             sub settlements {
726 8     8 0 53 my $self = shift;
727 8         59 my ($world, $flow) = @_;
728 8         20 my @settlements;
729 8         47 my $max = $self->height * $self->width;
730             # do not match forest-hill
731 8         439 my @candidates = shuffle sort grep { $world->{$_} =~ /\b(fir-forest|forest(?!-hill))\b/ } keys %$world;
  3600         7578  
732 8         290 @candidates = $self->remove_closer_than(2, @candidates);
733 8 100       92 @candidates = @candidates[0 .. int($max/10 - 1)] if @candidates > $max/10;
734 8         55 push(@settlements, @candidates);
735 8         29 for my $coordinates (@candidates) {
736             $world->{$coordinates} =~ s/fir-forest/firs thorp/
737 129 100       658 or $world->{$coordinates} =~ s/forest(?!-hill)/trees thorp/;
738             }
739 8 100       514 @candidates = shuffle sort grep { $world->{$_} =~ /(?{$_}} keys %$world;
  3600         8466  
740 8         393 @candidates = $self->remove_closer_than(5, @candidates);
741 8 50       55 @candidates = @candidates[0 .. int($max/20 - 1)] if @candidates > $max/20;
742 8         26 push(@settlements, @candidates);
743 8         27 for my $coordinates (@candidates) {
744 18         90 $world->{$coordinates} =~ s/forest/trees village/;
745             }
746 8 100       390 @candidates = shuffle sort grep { $world->{$_} =~ /(?{$_} } keys %$world;
  3600         8788  
747 8         266 @candidates = $self->remove_closer_than(10, @candidates);
748 8 50       48 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
749 8         23 push(@settlements, @candidates);
750 8         24 for my $coordinates (@candidates) {
751 6         38 $world->{$coordinates} =~ s/forest/trees town/;
752             }
753 8         396 @candidates = shuffle sort grep { $world->{$_} =~ /white mountain\b/ } keys %$world;
  3600         6910  
754 8         292 @candidates = $self->remove_closer_than(10, @candidates);
755 8 50       54 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
756 8         27 push(@settlements, @candidates);
757 8         25 for my $coordinates (@candidates) {
758 11         93 $world->{$coordinates} =~ s/white mountain\b/white mountain law/;
759             }
760 8         401 @candidates = shuffle sort grep { $world->{$_} =~ /swamp/ } keys %$world;
  3600         7312  
761 8         237 @candidates = $self->remove_closer_than(10, @candidates);
762 8 50       69 @candidates = @candidates[0 .. int($max/40 - 1)] if @candidates > $max/40;
763 8         28 push(@settlements, @candidates);
764 8         29 for my $coordinates (@candidates) {
765 17         74 $world->{$coordinates} =~ s/swamp/swamp2 chaos/;
766             }
767 8         31 for my $coordinates (@settlements) {
768 181         589 for my $i ($self->neighbors()) {
769 820         2213 my ($x, $y) = $self->neighbor($coordinates, $i);
770 820         1909 my $other = coordinates($x, $y);
771 820 100 100     4679 next unless $world->{$other} and $world->{$other} =~ /water|ocean/;
772             # bump ports one size category up
773 42         123 $world->{$coordinates} =~ s/large-town/city port/;
774 42         114 $world->{$coordinates} =~ s/town/large-town port/;
775 42         123 $world->{$coordinates} =~ s/village/town port/;
776             # no bumps for thorps
777 42         126 last;
778             }
779             }
780 8         29 for my $coordinates (@settlements) {
781             # thorps and villages don't cut enough wood; make sure to get both "green" and "dark-green"
782 181 100       556 $world->{$coordinates} =~ s/\S*green trees/light-soil/ if $world->{$coordinates} =~ /large-town|city/;
783 181 100       482 $world->{$coordinates} =~ s/\S*green trees/soil/ if $world->{$coordinates} =~ / town/;
784             }
785 8         125 return @settlements;
786             }
787              
788             sub trails {
789 8     8 0 38 my $self = shift;
790 8         24 my ($altitude, $settlements) = @_;
791             # look for a neighbor that is as low as possible and nearby
792 8         18 my %trails;
793 8         130 my @from = shuffle @$settlements;
794 8         58 my @to = shuffle @$settlements;
795 8         24 for my $from (@from) {
796 181         909 my ($best, $best_distance, $best_altitude);
797 181         335 for my $to (@to) {
798 7459 100       14181 next if $from eq $to;
799 7278         15578 my $distance = $self->distance($from, $to);
800 7278         27614 $log->debug("Considering $from-$to: distance $distance, altitude " . $altitude->{$to});
801 7278 100 100     50637 if ($distance <= 3
      100        
      100        
      100        
802             and (not $best_distance or $distance <= $best_distance)
803             and (not $best or $altitude->{$to} < $best_altitude)) {
804 203         330 $best = $to;
805 203         392 $best_altitude = $altitude->{$best};
806 203         406 $best_distance = $distance;
807             }
808             }
809 181 100       415 next if not $best;
810             # skip if it already exists in the other direction
811 168 100       469 next if $trails{"$best-$from"};
812 134         518 $trails{"$from-$best"} = 1;
813 134         396 $log->debug("Trail $from-$best");
814             }
815 8         228 return keys %trails;
816             }
817              
818             sub cliffs {
819 4     4 0 12 my $self = shift;
820 4         11 my ($world, $altitude) = @_;
821 4         20 my @neighbors = $self->neighbors();
822             # hexes with altitude difference bigger than 1 have cliffs
823 4         108 for my $coordinates (keys %$world) {
824 1200 50       5150 next if $altitude->{$coordinates} <= $self->bottom;
825 1200         5944 for my $i (@neighbors) {
826 6000         18122 my ($x, $y) = $self->neighbor($coordinates, $i);
827 6000 100       17636 next unless $self->legal($x, $y);
828 5524         72719 my $other = coordinates($x, $y);
829 5524 100       22093 if ($altitude->{$coordinates} - $altitude->{$other} >= 2) {
830 299 100       759 if (@neighbors == 6) {
831 186         803 $world->{$coordinates} .= " cliff$i";
832             } else { # square
833 113         394 $world->{$coordinates} .= " cliffs$i";
834             }
835             }
836             }
837             }
838             }
839              
840             sub marshlands {
841 4     4 0 13 my ($self, $world, $altitude, $rivers) = @_;
842 4         12 my %seen;
843 4         17 for my $river (@$rivers) {
844 202         363 my $last = $river->[0];
845 202         317 for my $coordinates (@$river) {
846 746 100       1498 last if $seen{$coordinates}; # we've been here before
847 609         1166 $seen{$coordinates} = 1;
848 609 100       1192 next unless exists $altitude->{$coordinates}; # rivers ending off the map
849 544 50       1014 if ($altitude->{$coordinates} <= $self->bottom) {
850 0 0 0     0 if ($altitude->{$coordinates} == $self->bottom
      0        
851             and $world->{$coordinates} =~ /water|ocean/
852             and $altitude->{$coordinates} == $altitude->{$last} - 1) {
853 0         0 $world->{$coordinates} = "blue-green swamp";
854             } else {
855 0         0 $world->{$coordinates} =~ s/ocean/water/;
856 0         0 delete $seen{$coordinates};
857 0         0 last;
858             }
859             }
860 544         2031 $last = $coordinates;
861             }
862             }
863             }
864              
865             sub desertification {
866 4     4 0 35 my ($self, $world, $altitude, $rivers) = @_;
867 4 50       26 return unless $self->climate eq 'desert';
868 0         0 for my $coordinates (keys %$world) {
869 0 0       0 if ($self->with_river($rivers, $coordinates)) {
870             $world->{$coordinates} =~ s/light-grey/light-green/
871 0 0       0 or $world->{$coordinates} =~ s/dark-green/green/
872             } else {
873             $world->{$coordinates} =~ s/light-green bushes/rock bushes/
874             or $world->{$coordinates} =~ s/light-grey grass/rock bush/
875             or $world->{$coordinates} =~ s/dark-grey grass/dark-soil bush/
876             or $world->{$coordinates} =~ s/^grey grass/rock bush/
877             or $altitude->{$coordinates} >= 4 and $world->{$coordinates} =~ s/light-grey desert/dark-soil desert/
878 0 0 0     0 or $altitude->{$coordinates} >= 2 and $world->{$coordinates} =~ s/(dust|light-grey) desert/light-grey desert/
      0        
      0        
      0        
      0        
      0        
879             }
880             $world->{$coordinates} =~ s/dark-grey swamp2?/light-green bushes/
881             or $world->{$coordinates} =~ s/^grey swamp2?/light-grey bushes/
882             or $world->{$coordinates} =~ s/fir-forest/trees/
883 0 0 0     0 or $world->{$coordinates} =~ s/firs/trees/;
      0        
884             }
885             }
886              
887             sub generate {
888 4     4 0 109 my ($self, $world, $altitude, $water, $rivers, $settlements, $trails, $canyons, $step) = @_;
889             # $flow indicates that there is actually a river in this hex
890 4         12 my $flow = {};
891             # $dry indicates that is a river in this hex, but it cut itself a canyon
892 4         8 my $dry = {};
893             my @code = (
894 4     4   36 sub { $self->flat($altitude);
895 4         63 $self->altitude($world, $altitude); },
896 4     4   70 sub { $self->bumpiness($world, $altitude); },
897 4     4   47 sub { $self->mountains($world, $altitude); },
898 4     4   62 sub { $self->ocean($world, $altitude); },
899 4     4   46 sub { $self->water($world, $altitude, $water); },
900 4     4   92 sub { $self->lakes($world, $altitude, $water); },
901 4     4   66 sub { $self->flood($world, $altitude, $water); },
902 4     4   61 sub { $self->bogs($world, $altitude, $water); },
903 4     4   54 sub { $self->winds($world, $altitude, $water); },
904 4     4   50 sub { $self->rivers($world, $altitude, $water, $flow, $rivers); },
905 4     4   61 sub { $self->canyons($world, $altitude, $rivers, $canyons, $dry); },
906 4     4   70 sub { $self->swamps($world, $altitude, $water, $flow, $dry); },
907 4     4   53 sub { $self->forests($world, $altitude, $flow, $dry); },
908 4     4   60 sub { $self->dry($world, $altitude, $rivers); },
909 4     4   47 sub { $self->cliffs($world, $altitude); },
910 4     4   64 sub { push(@$settlements, $self->settlements($world, $flow)); },
911 4     4   63 sub { push(@$trails, $self->trails($altitude, $settlements)); },
912 4     4   64 sub { $self->marshlands($world, $altitude, $rivers); },
913 4     4   47 sub { $self->desertification($world, $altitude, $rivers); },
914             # make sure you look at "alpine_document.html.ep" if you change this list!
915             # make sure you look at '/alpine/document' if you add to this list!
916 4         220 );
917              
918             # $step 0 runs all the code; note that we can't simply cache those results
919             # because we need to start over with the same seed!
920 4         13 my $i = 1;
921 4         18 while (@code) {
922 76         339 shift(@code)->();
923 76 50       1311 return if $step == $i++;
924 76         351 $self->fixup($world, $altitude, $i);
925             }
926             }
927              
928             # Remove temporary markers that won't be needed in the next step
929             sub fixup {
930 76     76 0 218 my ($self, $world, $altitude, $step, $last) = @_;
931             # When documenting or debugging, water flow arrows are no longer needed when
932             # the rivers are added.
933 76 100       219 if ($step >= 10) {
934 44         2207 for my $coordinates (keys %$world) {
935 13200         23793 $world->{$coordinates} =~ s/ arrow\d//;
936             }
937             }
938             # Wind direction is only shown once.
939 76         906 $world->{"0101"} =~ s/ wind\d//;
940             # Remove zone markers.
941 76         2561 for my $coordinates (keys %$world) {
942 22800         40010 $world->{$coordinates} =~ s/ zone//;
943             }
944             }
945              
946             sub generate_map {
947 4     4 0 1110 my $self = shift;
948              
949             # The parameters turn into class variables.
950 4   50     73 $self->width(shift // 30);
951 4   50     81 $self->height(shift // 10);
952 4   50     55 $self->steepness(shift // 3);
953 4   33     46 $self->peaks(shift // int($self->width * $self->height / 40));
954 4   50     98 $self->peak(shift // 10);
955 4   33     34 $self->peak_min(shift // $self->peak);
956 4   33     63 $self->bumps(shift // int($self->width * $self->height / 40));
957 4   50     92 $self->bump(shift // 2);
958 4   50     53 $self->bottom(shift // 0);
959 4   50     62 $self->arid(shift // 2);
960 4 50       50 $self->climate(shift ? 'desert' : 'temperate');
961 4         43 $self->wind(shift); # or random
962 4   66     43 my $seed = shift||time;
963 4         11 my $url = shift;
964 4   50     21 my $step = shift||0;
965              
966             # For documentation purposes, I want to be able to set the pseudo-random
967             # number seed using srand and rely on rand to reproduce the same sequence of
968             # pseudo-random numbers for the same seed. The key point to remember is that
969             # the keys function will return keys in random order. So if we look over the
970             # result of keys, we need to look at the code in the loop: If order is
971             # important, that wont do. We need to sort the keys. If we want the keys to be
972             # pseudo-shuffled, use shuffle sort keys.
973 4         14 srand($seed);
974              
975             # Keys for all hashes are coordinates such as "0101".
976             # %world is the description with values such as "green forest".
977             # %altitude is the altitude with values such as 3.
978             # %water is the preferred direction water would take with values such as 0.
979             # (north west); 0 means we need to use "if defined".
980             # @rivers are the rivers with values such as ["0102", "0202"].
981             # @settlements are are the locations of settlements such as "0101".
982             # @trails are the trails connecting these with values as "0102-0202".
983             # $step is how far we want map generation to go where 0 means all the way.
984 4         24 my ($world, $altitude, $water, $rivers, $settlements, $trails, $canyons) =
985             ({}, {}, {}, [], [], [], []);
986 4         37 $self->generate($world, $altitude, $water, $rivers, $settlements, $trails, $canyons, $step);
987              
988             # When documenting or debugging, add altitude as a label.
989 4 50       24 if ($step > 0) {
990 0         0 for my $coordinates (keys %$world) {
991 0         0 $world->{$coordinates} .= ' "' . $altitude->{$coordinates} . '"';
992             }
993             }
994              
995 4         13 local $" = "-"; # list items separated by -
996 4         10 my @lines;
997 4         603 push(@lines, map { $_ . " " . $world->{$_} } sort keys %$world);
  1200         2571  
998 4         116 push(@lines, map { "$_ trail" } @$trails);
  132         228  
999 4         18 push(@lines, map { "@$_ river" } @$rivers);
  202         625  
1000 4         22 push(@lines, map { "@$_ canyon" } @$canyons); # after rivers
  21         62  
1001 4         12 push(@lines, "include gnomeyland.txt");
1002              
1003             # when documenting or debugging, add some more lines at the end
1004 4 50       18 if ($step > 0) {
1005             # visualize height
1006             push(@lines,
1007             map {
1008 0         0 my $n = int(25.5 * $_);
  0         0  
1009 0         0 qq{height$_ attributes fill="rgb($n,$n,$n)"};
1010             } (0 .. 10));
1011             # visualize water flow
1012 0         0 push(@lines, $self->arrows());
1013             }
1014              
1015 4         14 push(@lines, "# Seed: $seed");
1016 4 100       28 push(@lines, "# Documentation: " . $url) if $url;
1017 4         1791 my $map = join("\n", @lines);
1018 4 100       1201 return $map, $self if wantarray;
1019 2         560 return $map;
1020             }
1021              
1022             1;