File Coverage

blib/lib/Game/TextMapper/Schroeder/Island.pm
Criterion Covered Total %
statement 139 147 94.5
branch 60 70 85.7
condition 8 14 57.1
subroutine 20 20 100.0
pod 0 7 0.0
total 227 258 87.9


line stmt bran cond sub pod time code
1             # Copyright (C) 2009-2021 Alex Schroeder
2             #
3             # This program is free software: you can redistribute it and/or modify it under
4             # the terms of the GNU Affero General Public License as published by the Free
5             # Software Foundation, either version 3 of the License, or (at your option) any
6             # later version.
7             #
8             # This program is distributed in the hope that it will be useful, but WITHOUT
9             # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10             # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
11             # details.
12             #
13             # You should have received a copy of the GNU Affero General Public License along
14             # with this program. If not, see .
15              
16             =encoding utf8
17              
18             =head1 NAME
19              
20             Game::TextMapper::Schroeder::Island - generate an island chain
21              
22             =head1 DESCRIPTION
23              
24             This creates an island chain in an ocean, based on the idea of a hotspot moving
25             across the map. All regions atop the hotspot get raised at random; all regions
26             outside the hotspot are eroded at random. This leaves a chain of ever smaller
27             islands behind.
28              
29             The rest of the code, river formation and all that, is based on the Alpine
30             algorithm and therefore it also requires the use of roles.
31              
32             return Game::TextMapper::Schroeder::Island
33             ->with_roles('Game::TextMapper::Schroeder::Square')->new()
34             ->generate_map(@params);
35              
36             =head1 SEE ALSO
37              
38             L
39             L
40             L
41             L
42              
43             =cut
44              
45             package Game::TextMapper::Schroeder::Island;
46 1     1   7 use Game::TextMapper::Log;
  1         3  
  1         30  
47 1     1   6 use Modern::Perl '2018';
  1         2  
  1         11  
48 1     1   228 use Mojo::Base 'Game::TextMapper::Schroeder::Alpine';
  1         2  
  1         18  
49 1     1   161 use Role::Tiny::With;
  1         3  
  1         84  
50             with 'Game::TextMapper::Schroeder::Base';
51 1     1   7 use List::Util qw'shuffle min max';
  1         3  
  1         2368  
52              
53             my $log = Game::TextMapper::Log->get;
54              
55             has 'bottom' => 0;
56             has 'top' => 10;
57             has 'radius' => 5;
58             has 'hotspot';
59              
60             sub ocean {
61 2     2 0 6 my $self = shift;
62 2         7 my ($world, $altitude) = @_;
63 2         364 for my $coordinates (sort keys %$altitude) {
64 1200 100       2400 if ($altitude->{$coordinates} <= $self->bottom) {
65 1078         3664 my $ocean = 1;
66 1078         1813 for my $i ($self->neighbors()) {
67 5384         20662 my ($x, $y) = $self->neighbor($coordinates, $i);
68 5384         10063 my $legal = $self->legal($x, $y);
69 5384         45224 my $other = coordinates($x, $y);
70 5384 100 100     13679 next if not $legal or $altitude->{$other} <= $self->bottom;
71 400         1781 $ocean = 0;
72             }
73 1078 100       5797 $world->{$coordinates} = $ocean ? "ocean" : "water";
74             }
75             }
76             }
77              
78             sub change {
79 64     64 0 137 my $self = shift;
80 64 100       227 return if $self->hotspot->[0] > $self->width - 2 * $self->radius;
81 38         635 my $world = shift;
82 38         69 my $altitude = shift;
83             # advance hotspot
84 38 100       187 if (rand() < 0.2) {
85 5         12 $self->hotspot->[0] += 1.5 * $self->radius;
86             } else {
87 33         129 $self->hotspot->[0]++;
88             }
89 38 100       256 if (rand() < 0.5) {
90 21 100       79 if (rand() > $self->hotspot->[1] / $self->height) {
91 13         143 $self->hotspot->[1]++;
92             } else {
93 8         81 $self->hotspot->[1]--;
94             }
95             }
96             # figure out who goes up and who goes down, if the hotspot is active
97 38         132 my %hot;
98 38         133 for my $x (max(1, $self->hotspot->[0] - $self->radius) .. min($self->width, $self->hotspot->[0] + $self->radius)) {
99 339         1818 for my $y (max(1, $self->hotspot->[1] - $self->radius) .. min($self->height, $self->hotspot->[1] + $self->radius)) {
100 3024 100       8791 if ($self->distance($x, $y, @{$self->hotspot}) <= $self->radius) {
  3024         4978  
101 1790         6786 my $coordinates = coordinates($x, $y);
102 1790         3635 $hot{$coordinates} = 1;
103             }
104             }
105             }
106             # change the land
107 38         2601 for my $coordinates (keys %$altitude) {
108 22800         34923 my $change = 0;
109 22800 100       30495 if ($hot{$coordinates}) {
110             # on the hotspot the land rises
111 1790 100       2907 $change = 1 if rand() < 0.2;
112             } else {
113             # off the hotspot the land sinks
114 21010 100       31100 $change = -1 if rand() < 0.2;
115             }
116 22800 100       34101 next unless $change;
117             # rising from the ocean atop the hotspot
118 4496         6913 $altitude->{$coordinates} += $change;
119 4496 100       6977 $altitude->{$coordinates} = $self->bottom if $altitude->{$coordinates} < $self->bottom;
120 4496 50       24353 $altitude->{$coordinates} = $self->top if $altitude->{$coordinates} > $self->top;
121             }
122             # land with higher neighbours on the hotspot goes up
123 38         1380 for my $coordinates (keys %hot) {
124 1790         2066 my $change = 0;
125 1790         3116 for my $i ($self->neighbors()) {
126 1805         3059 my ($x, $y) = $self->neighbor($coordinates, $i);
127 1805 100       3414 next unless $self->legal($x, $y);
128 1790         16522 my $other = coordinates($x, $y);
129 1790 100       4188 $change = 1 if $altitude->{$other} - $altitude->{$coordinates} > 1;
130 1790         2302 last;
131             }
132 1790 100       3146 $altitude->{$coordinates}++ if $change;
133             }
134             # note height for debugging purposes
135 38         2984 for my $coordinates (keys %$altitude) {
136 22800         33236 $world->{$coordinates} = "height$altitude->{$coordinates}";
137             }
138             }
139              
140             sub forests {
141 2     2 0 6 my $self = shift;
142 2         7 my ($world, $altitude) = @_;
143             # higher up is forests
144 2         99 for my $coordinates (keys %$altitude) {
145 1200 100       1972 next unless $altitude->{$coordinates}; # skip ocean
146 122 100       328 next if $world->{$coordinates} =~ /mountain|lake/;
147 96 100       194 if ($altitude->{$coordinates} == 1) {
    100          
    100          
    50          
    0          
148 70         92 $world->{$coordinates} = "light-grey bushes";
149             } elsif ($altitude->{$coordinates} == 2) {
150 15         27 $world->{$coordinates} = "light-green trees";
151             } elsif ($altitude->{$coordinates} == 3) {
152 7         12 $world->{$coordinates} = "green forest";
153             } elsif ($altitude->{$coordinates} == 4) {
154 4         8 $world->{$coordinates} = "dark-green forest";
155             } elsif ($altitude->{$coordinates} > 4) {
156 0         0 $world->{$coordinates} = "dark-green mountains";
157             }
158             }
159             }
160              
161             sub lakes {
162 2     2 0 7 my $self = shift;
163 2         8 my ($world, $altitude) = @_;
164             # any areas surrounded by higher land is a lake
165             HEX:
166 2         474 for my $coordinates (sort keys %$altitude) {
167 1200         2200 for my $i ($self->neighbors()) {
168 1458         2613 my ($x, $y) = $self->neighbor($coordinates, $i);
169 1458 100       2746 next unless $self->legal($x, $y);
170 1368         12456 my $other = coordinates($x, $y);
171 1368 100       3647 next HEX if $altitude->{$other} == 0;
172 184 100       408 next HEX if $altitude->{$coordinates} > $altitude->{$other};
173             }
174 3         21 $world->{$coordinates} = "green lake";
175             }
176             }
177              
178             sub islands {
179 2     2 0 7 my $self = shift;
180 2         6 my ($world, $altitude) = @_;
181             # any areas surrounded by water is an island
182             HEX:
183 2         404 for my $coordinates (sort keys %$altitude) {
184 1200 100       2064 next if $altitude->{$coordinates} == 0;
185 122         231 for my $i ($self->neighbors()) {
186 315         560 my ($x, $y) = $self->neighbor($coordinates, $i);
187 315 100       612 next unless $self->legal($x, $y);
188 314         2777 my $other = coordinates($x, $y);
189 314 100       752 next HEX if $altitude->{$other} > 0;
190             }
191 24         86 $world->{$coordinates} = "water mountains";
192             }
193             }
194              
195             sub generate {
196 2     2 0 6 my $self = shift;
197 2         7 my ($world, $altitude, $settlements, $trails, $step) = @_;
198             # %flow indicates that there is actually a river in this hex
199 2         5 my $flow = {};
200              
201 2         8 $self->hotspot([int($self->radius / 2), int($self->height / 3 + rand() * $self->height / 3)]);
202              
203 2     2   53 my @code = (sub { $self->flat($altitude) });
  2         21  
204 2         7 for (1 .. $self->width - 2 * $self->radius) {
205 64     64   178 push(@code, sub { $self->change($world, $altitude) });
  64         210  
206             }
207 2     2   11 push(@code, sub { $self->ocean($world, $altitude) });
  2         32  
208              
209             push(@code,
210 2     2   45 sub { $self->lakes($world, $altitude); },
211 2     2   46 sub { $self->islands($world, $altitude); },
212 2     2   29 sub { $self->forests($world, $altitude); },
213 2     2   34 sub { push(@$settlements, $self->settlements($world, $flow)); },
214 2     2   34 sub { push(@$trails, $self->trails($altitude, $settlements)); },
215 2         25 );
216              
217             # $step 0 runs all the code; note that we can't simply cache those results
218             # because we need to start over with the same seed!
219 2         5 my $i = 1;
220 2         9 while (@code) {
221 78         282 shift(@code)->();
222 78 50       1719 return if $step == $i++;
223             }
224             }
225              
226             sub generate_map {
227 2     2 0 458 my $self = shift;
228             # The parameters turn into class variables.
229 2   50     22 $self->width(shift // 40);
230 2   50     42 $self->height(shift // 15);
231 2   50     31 $self->radius(shift // 4);
232 2   33     15 my $seed = shift||time;
233 2         5 my $url = shift;
234 2   50     12 my $step = shift||0;
235              
236             # For documentation purposes, I want to be able to set the pseudo-random
237             # number seed using srand and rely on rand to reproduce the same sequence of
238             # pseudo-random numbers for the same seed. The key point to remember is that
239             # the keys function will return keys in random order. So if we look over the
240             # result of keys, we need to look at the code in the loop: If order is
241             # important, that wont do. We need to sort the keys. If we want the keys to be
242             # pseudo-shuffled, use shuffle sort keys.
243 2         8 srand($seed);
244              
245             # keys for all hashes are coordinates such as "0101".
246             # %world is the description with values such as "green forest".
247             # %altitude is the altitude with values such as 3.
248             # @settlements are are the locations of settlements such as "0101"
249             # @trails are the trails connecting these with values as "0102-0202"
250             # $step is how far we want map generation to go where 0 means all the way
251 2         9 my ($world, $altitude, $settlements, $trails) =
252             ({}, {}, [], []);
253 2         17 $self->generate($world, $altitude, $settlements, $trails, $step);
254              
255             # when documenting or debugging, do this before collecting lines
256 2 50       24 if ($step > 0) {
257             # add a height label at the very end
258 0 0       0 if ($step) {
259 0         0 for my $coordinates (keys %$world) {
260 0         0 $world->{$coordinates} .= ' "' . $altitude->{$coordinates} . '"';
261             }
262             }
263             }
264              
265 2         9 local $" = "-"; # list items separated by -
266 2         5 my @lines;
267 2         433 push(@lines, map { $_ . " " . $world->{$_} } sort keys %$world);
  1200         2215  
268 2         129 push(@lines, map { "$_ trail" } @$trails);
  3         15  
269 2         8 push(@lines, "include gnomeyland.txt");
270              
271             # when documenting or debugging, add some more lines at the end
272 2 50       11 if ($step > 0) {
273             # visualize height
274             push(@lines,
275             map {
276 0         0 my $n = int(25.5 * $_);
  0         0  
277 0         0 qq{height$_ attributes fill="rgb($n,$n,$n)"};
278             } (0 .. 10));
279             # visualize water flow
280 0         0 push(@lines, $self->arrows());
281             }
282              
283 2         8 push(@lines, "# Seed: $seed");
284 2 50       23 push(@lines, "# Documentation: " . $url) if $url;
285 2         1086 my $map = join("\n", @lines);
286 2         475 return $map;
287             }
288              
289             1;