File Coverage

blib/lib/Game/TextMapper/Schroeder/Island.pm
Criterion Covered Total %
statement 138 147 93.8
branch 59 70 84.2
condition 9 14 64.2
subroutine 20 20 100.0
pod 0 7 0.0
total 226 258 87.6


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 11     11   83 use Game::TextMapper::Log;
  11         29  
  11         539  
47 11     11   67 use Modern::Perl '2018';
  11         23  
  11         91  
48 11     11   3899 use Mojo::Base 'Game::TextMapper::Schroeder::Alpine';
  11         27  
  11         152  
49 11     11   2414 use Role::Tiny::With;
  11         24  
  11         1088  
50             with 'Game::TextMapper::Schroeder::Base';
51 11     11   92 use List::Util qw'shuffle min max';
  11         21  
  11         44221  
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 4     4 0 13 my $self = shift;
62 4         14 my ($world, $altitude) = @_;
63 4         1375 for my $coordinates (sort keys %$altitude) {
64 2400 100       12313 if ($altitude->{$coordinates} <= $self->bottom) {
65 2145         9050 my $ocean = 1;
66 2145         5300 for my $i ($self->neighbors()) {
67 10694         51543 my ($x, $y) = $self->neighbor($coordinates, $i);
68 10694         26102 my $legal = $self->legal($x, $y);
69 10694         120093 my $other = coordinates($x, $y);
70 10694 100 100     34411 next if not $legal or $altitude->{$other} <= $self->bottom;
71 893         4807 $ocean = 0;
72             }
73 2145 100       14684 $world->{$coordinates} = $ocean ? "ocean" : "water";
74             }
75             }
76             }
77              
78             sub change {
79 128     128 0 240 my $self = shift;
80 128 100       517 return if $self->hotspot->[0] > $self->width - 2 * $self->radius;
81 64         1169 my $world = shift;
82 64         134 my $altitude = shift;
83             # advance hotspot
84 64 100       269 if (rand() < 0.2) {
85 13         49 $self->hotspot->[0] += 1.5 * $self->radius;
86             } else {
87 51         154 $self->hotspot->[0]++;
88             }
89 64 100       438 if (rand() < 0.5) {
90 40 100       127 if (rand() > $self->hotspot->[1] / $self->height) {
91 22         234 $self->hotspot->[1]++;
92             } else {
93 18         180 $self->hotspot->[1]--;
94             }
95             }
96             # figure out who goes up and who goes down, if the hotspot is active
97 64         227 my %hot;
98 64         241 for my $x (max(1, $self->hotspot->[0] - $self->radius) .. min($self->width, $self->hotspot->[0] + $self->radius)) {
99 565         3791 for my $y (max(1, $self->hotspot->[1] - $self->radius) .. min($self->height, $self->hotspot->[1] + $self->radius)) {
100 5085 100       19271 if ($self->distance($x, $y, @{$self->hotspot}) <= $self->radius) {
  5085         11714  
101 3134         14174 my $coordinates = coordinates($x, $y);
102 3134         14589 $hot{$coordinates} = 1;
103             }
104             }
105             }
106             # change the land
107 64         14563 for my $coordinates (keys %$altitude) {
108 38400         76778 my $change = 0;
109 38400 100       74694 if ($hot{$coordinates}) {
110             # on the hotspot the land rises
111 3134 100       6068 $change = 1 if rand() < 0.2;
112             } else {
113             # off the hotspot the land sinks
114 35266 100       65577 $change = -1 if rand() < 0.2;
115             }
116 38400 100       88246 next unless $change;
117             # rising from the ocean atop the hotspot
118 7748         18173 $altitude->{$coordinates} += $change;
119 7748 100       17590 $altitude->{$coordinates} = $self->bottom if $altitude->{$coordinates} < $self->bottom;
120 7748 50       59437 $altitude->{$coordinates} = $self->top if $altitude->{$coordinates} > $self->top;
121             }
122             # land with higher neighbours on the hotspot goes up
123 64         2774 for my $coordinates (keys %hot) {
124 3134         4521 my $change = 0;
125 3134         7560 for my $i ($self->neighbors()) {
126 3165         6935 my ($x, $y) = $self->neighbor($coordinates, $i);
127 3165 100       7334 next unless $self->legal($x, $y);
128 3134         44045 my $other = coordinates($x, $y);
129 3134 100       9071 $change = 1 if $altitude->{$other} - $altitude->{$coordinates} > 1;
130 3134         5390 last;
131             }
132 3134 100       7341 $altitude->{$coordinates}++ if $change;
133             }
134             # note height for debugging purposes
135 64         6117 for my $coordinates (keys %$altitude) {
136 38400         85108 $world->{$coordinates} = "height$altitude->{$coordinates}";
137             }
138             }
139              
140             sub forests {
141 4     4 0 29 my $self = shift;
142 4         14 my ($world, $altitude) = @_;
143             # higher up is forests
144 4         243 for my $coordinates (keys %$altitude) {
145 2400 100       4643 next unless $altitude->{$coordinates}; # skip ocean
146 255 100       777 next if $world->{$coordinates} =~ /mountain|lake/;
147 195 100       374 if ($altitude->{$coordinates} == 1) {
    100          
    50          
    0          
    0          
148 144         266 $world->{$coordinates} = "light-grey bushes";
149             } elsif ($altitude->{$coordinates} == 2) {
150 45         78 $world->{$coordinates} = "light-green trees";
151             } elsif ($altitude->{$coordinates} == 3) {
152 6         17 $world->{$coordinates} = "green forest";
153             } elsif ($altitude->{$coordinates} == 4) {
154 0         0 $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 4     4 0 12 my $self = shift;
163 4         16 my ($world, $altitude) = @_;
164             # any areas surrounded by higher land is a lake
165             HEX:
166 4         1306 for my $coordinates (sort keys %$altitude) {
167 2400         5600 for my $i ($self->neighbors()) {
168 2921         6781 my ($x, $y) = $self->neighbor($coordinates, $i);
169 2921 100       8045 next unless $self->legal($x, $y);
170 2740         30913 my $other = coordinates($x, $y);
171 2740 100       8844 next HEX if $altitude->{$other} == 0;
172 361 100       955 next HEX if $altitude->{$coordinates} > $altitude->{$other};
173             }
174 4         50 $world->{$coordinates} = "green lake";
175             }
176             }
177              
178             sub islands {
179 4     4 0 13 my $self = shift;
180 4         15 my ($world, $altitude) = @_;
181             # any areas surrounded by water is an island
182             HEX:
183 4         1289 for my $coordinates (sort keys %$altitude) {
184 2400 100       5098 next if $altitude->{$coordinates} == 0;
185 255         572 for my $i ($self->neighbors()) {
186 711         1439 my ($x, $y) = $self->neighbor($coordinates, $i);
187 711 100       1445 next unless $self->legal($x, $y);
188 710         7436 my $other = coordinates($x, $y);
189 710 100       1907 next HEX if $altitude->{$other} > 0;
190             }
191 58         195 $world->{$coordinates} = "water mountains";
192             }
193             }
194              
195             sub generate {
196 4     4 0 10 my $self = shift;
197 4         15 my ($world, $altitude, $settlements, $trails, $step) = @_;
198             # %flow indicates that there is actually a river in this hex
199 4         10 my $flow = {};
200              
201 4         17 $self->hotspot([int($self->radius / 2), int($self->height / 3 + rand() * $self->height / 3)]);
202              
203 4     4   123 my @code = (sub { $self->flat($altitude) });
  4         136  
204 4         16 for (1 .. $self->width - 2 * $self->radius) {
205 128     128   480 push(@code, sub { $self->change($world, $altitude) });
  128         454  
206             }
207 4     4   21 push(@code, sub { $self->ocean($world, $altitude) });
  4         60  
208              
209             push(@code,
210 4     4   62 sub { $self->lakes($world, $altitude); },
211 4     4   60 sub { $self->islands($world, $altitude); },
212 4     4   77 sub { $self->forests($world, $altitude); },
213 4     4   66 sub { push(@$settlements, $self->settlements($world, $flow)); },
214 4     4   67 sub { push(@$trails, $self->trails($altitude, $settlements)); },
215 4         53 );
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 4         12 my $i = 1;
220 4         21 while (@code) {
221 156         576 shift(@code)->();
222 156 50       4575 return if $step == $i++;
223             }
224             }
225              
226             sub generate_map {
227 4     4 0 1254 my $self = shift;
228             # The parameters turn into class variables.
229 4   50     63 $self->width(shift // 40);
230 4   50     95 $self->height(shift // 15);
231 4   50     64 $self->radius(shift // 4);
232 4   66     41 my $seed = shift||time;
233 4         13 my $url = shift;
234 4   50     25 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 4         17 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 4         20 my ($world, $altitude, $settlements, $trails) =
252             ({}, {}, [], []);
253 4         39 $self->generate($world, $altitude, $settlements, $trails, $step);
254              
255             # when documenting or debugging, do this before collecting lines
256 4 50       18 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 4         16 local $" = "-"; # list items separated by -
266 4         8 my @lines;
267 4         1322 push(@lines, map { $_ . " " . $world->{$_} } sort keys %$world);
  2400         5332  
268 4         230 push(@lines, map { "$_ trail" } @$trails);
  1         6  
269 4         13 push(@lines, "include gnomeyland.txt");
270              
271             # when documenting or debugging, add some more lines at the end
272 4 50       22 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 4         17 push(@lines, "# Seed: $seed");
284 4 100       34 push(@lines, "# Documentation: " . $url) if $url;
285 4         1262 my $map = join("\n", @lines);
286 4         1764 return $map;
287             }
288              
289             1;