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::Smale - generate fantasy wilderness maps |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
=head1 SYNOPSIS |
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
my $text = Game::TextMapper::Smale->new |
25
|
|
|
|
|
|
|
->generate_map($width, $height, $bw); |
26
|
|
|
|
|
|
|
|
27
|
|
|
|
|
|
|
=head1 DESCRIPTION |
28
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
This generates a wilderness map based on the algorithm by Erin D. Smale. See the |
30
|
|
|
|
|
|
|
blog posts at L and |
31
|
|
|
|
|
|
|
L for more |
32
|
|
|
|
|
|
|
information. |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
Generally speaking, the idea is that the algorithm picks a random terrain for a |
35
|
|
|
|
|
|
|
hex in the middle of the map. Based on that, the surrounding hexes a bit further |
36
|
|
|
|
|
|
|
away are picked, and finally the remaining hexes are picked. This is why the |
37
|
|
|
|
|
|
|
maps vary so drastically in terrain distribution. |
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
=head1 METHODS |
40
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
Note that this module acts as a class with the C method, but none |
42
|
|
|
|
|
|
|
of the other subroutines defined are actual methods. They don't take a C<$self> |
43
|
|
|
|
|
|
|
argument. |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
=cut |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
package Game::TextMapper::Smale; |
48
|
1
|
|
|
1
|
|
8
|
use Game::TextMapper::Log; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
27
|
|
49
|
1
|
|
|
1
|
|
4
|
use Game::TextMapper::Point; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
6
|
|
50
|
1
|
|
|
1
|
|
36
|
use Modern::Perl '2018'; |
|
1
|
|
|
|
|
2
|
|
|
1
|
|
|
|
|
8
|
|
51
|
1
|
|
|
1
|
|
140
|
use Mojo::Base -base; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
15
|
|
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
my $log = Game::TextMapper::Log->get; |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
my %world = (); |
56
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
# ATLAS HEX PRIMARY TERRAIN TYPE |
58
|
|
|
|
|
|
|
# Water Swamp Desert Plains Forest Hills Mountains |
59
|
|
|
|
|
|
|
# Water P W W W W W - |
60
|
|
|
|
|
|
|
# Swamp W P - W W - - |
61
|
|
|
|
|
|
|
# Desert W - P W - W W |
62
|
|
|
|
|
|
|
# Plains S [1] S T P [4] S T - |
63
|
|
|
|
|
|
|
# Forest T [2] T - S P [5] W [8] T [11] |
64
|
|
|
|
|
|
|
# Hills W - S [3] T T [6] P [9] S |
65
|
|
|
|
|
|
|
# Mountns - - W - W [7] S [10] P [12] |
66
|
|
|
|
|
|
|
# |
67
|
|
|
|
|
|
|
# 1. Treat as coastal (beach or scrub) if adjacent to water |
68
|
|
|
|
|
|
|
# 2. 66% light forest |
69
|
|
|
|
|
|
|
# 3. 33% rocky desert or high sand dunes |
70
|
|
|
|
|
|
|
# 4. Treat as farmland in settled hexes |
71
|
|
|
|
|
|
|
# 5. 33% heavy forest |
72
|
|
|
|
|
|
|
# 6. 66% forested hills |
73
|
|
|
|
|
|
|
# 7. 66% forested mountains |
74
|
|
|
|
|
|
|
# 8. 33% forested hills |
75
|
|
|
|
|
|
|
# 9. 20% canyon or fissure (not implemented) |
76
|
|
|
|
|
|
|
# 10. 40% chance of a pass (not implemented) |
77
|
|
|
|
|
|
|
# 11. 33% forested mountains |
78
|
|
|
|
|
|
|
# 12. 20% chance of a dominating peak; 10% chance of a mountain pass (not |
79
|
|
|
|
|
|
|
# implemented); 5% volcano (not implemented) |
80
|
|
|
|
|
|
|
# |
81
|
|
|
|
|
|
|
# Notes |
82
|
|
|
|
|
|
|
# water: water |
83
|
|
|
|
|
|
|
# sand: sand or dust |
84
|
|
|
|
|
|
|
# swamp: dark-grey swamp (near trees) or dark-grey marshes (no trees) |
85
|
|
|
|
|
|
|
# plains: light-green grass, bush or bushes near water or forest |
86
|
|
|
|
|
|
|
# forest: green trees (light), green forest, dark-green forest (heavy); |
87
|
|
|
|
|
|
|
# use firs and fir-forest near hills or mountains |
88
|
|
|
|
|
|
|
# hill: light-grey hill, dust hill if sand dunes |
89
|
|
|
|
|
|
|
# mountain: grey mountain, grey mountains (peak) |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
# later, grass land near a settlement might get the colors soil or dark-soil! |
92
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
my %primary = ("water" => ["water"], |
94
|
|
|
|
|
|
|
"swamp" => ["dark-grey swamp"], |
95
|
|
|
|
|
|
|
"desert" => ["dust desert"], |
96
|
|
|
|
|
|
|
"plains" => ["light-green grass"], |
97
|
|
|
|
|
|
|
"forest" => ["green forest", |
98
|
|
|
|
|
|
|
"green forest", |
99
|
|
|
|
|
|
|
"dark-green fir-forest"], |
100
|
|
|
|
|
|
|
"hill" => ["light-grey hill"], |
101
|
|
|
|
|
|
|
"mountain" => ["grey mountain", |
102
|
|
|
|
|
|
|
"grey mountain", |
103
|
|
|
|
|
|
|
"grey mountain", |
104
|
|
|
|
|
|
|
"grey mountain", |
105
|
|
|
|
|
|
|
"grey mountains"]); |
106
|
|
|
|
|
|
|
|
107
|
|
|
|
|
|
|
my %secondary = ("water" => ["light-green grass", |
108
|
|
|
|
|
|
|
"light-green bush", |
109
|
|
|
|
|
|
|
"light-green bushes"], |
110
|
|
|
|
|
|
|
"swamp" => ["light-green grass"], |
111
|
|
|
|
|
|
|
"desert" => ["light-grey hill", |
112
|
|
|
|
|
|
|
"light-grey hill", |
113
|
|
|
|
|
|
|
"dust hill"], |
114
|
|
|
|
|
|
|
"plains" => ["green forest"], |
115
|
|
|
|
|
|
|
"forest" => ["light-green grass", |
116
|
|
|
|
|
|
|
"light-green bush"], |
117
|
|
|
|
|
|
|
"hill" => ["grey mountain"], |
118
|
|
|
|
|
|
|
"mountain" => ["light-grey hill"]); |
119
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
my %tertiary = ("water" => ["green forest", |
121
|
|
|
|
|
|
|
"green trees", |
122
|
|
|
|
|
|
|
"green trees"], |
123
|
|
|
|
|
|
|
"swamp" => ["green forest"], |
124
|
|
|
|
|
|
|
"desert" => ["light-green grass"], |
125
|
|
|
|
|
|
|
"plains" => ["light-grey hill"], |
126
|
|
|
|
|
|
|
"forest" => ["light-grey forest-hill", |
127
|
|
|
|
|
|
|
"light-grey forest-hill", |
128
|
|
|
|
|
|
|
"light-grey hill"], |
129
|
|
|
|
|
|
|
"hill" => ["light-green grass"], |
130
|
|
|
|
|
|
|
"mountain" => ["green fir-forest", |
131
|
|
|
|
|
|
|
"green forest", |
132
|
|
|
|
|
|
|
"green forest-mountains"]); |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
my %wildcard = ("water" => ["dark-grey swamp", |
135
|
|
|
|
|
|
|
"dark-grey marsh", |
136
|
|
|
|
|
|
|
"sand desert", |
137
|
|
|
|
|
|
|
"dust desert", |
138
|
|
|
|
|
|
|
"light-grey hill", |
139
|
|
|
|
|
|
|
"light-grey forest-hill"], |
140
|
|
|
|
|
|
|
"swamp" => ["water"], |
141
|
|
|
|
|
|
|
"desert" => ["water", |
142
|
|
|
|
|
|
|
"grey mountain"], |
143
|
|
|
|
|
|
|
"plains" => ["water", |
144
|
|
|
|
|
|
|
"dark-grey swamp", |
145
|
|
|
|
|
|
|
"dust desert"], |
146
|
|
|
|
|
|
|
"forest" => ["water", |
147
|
|
|
|
|
|
|
"water", |
148
|
|
|
|
|
|
|
"water", |
149
|
|
|
|
|
|
|
"dark-grey swamp", |
150
|
|
|
|
|
|
|
"dark-grey swamp", |
151
|
|
|
|
|
|
|
"dark-grey marsh", |
152
|
|
|
|
|
|
|
"grey mountain", |
153
|
|
|
|
|
|
|
"grey forest-mountain", |
154
|
|
|
|
|
|
|
"grey forest-mountains"], |
155
|
|
|
|
|
|
|
"hill" => ["water", |
156
|
|
|
|
|
|
|
"water", |
157
|
|
|
|
|
|
|
"water", |
158
|
|
|
|
|
|
|
"sand desert", |
159
|
|
|
|
|
|
|
"sand desert", |
160
|
|
|
|
|
|
|
"dust desert", |
161
|
|
|
|
|
|
|
"green forest", |
162
|
|
|
|
|
|
|
"green forest", |
163
|
|
|
|
|
|
|
"green forest-hill"], |
164
|
|
|
|
|
|
|
"mountain" => ["sand desert", |
165
|
|
|
|
|
|
|
"dust desert"]); |
166
|
|
|
|
|
|
|
|
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
my %reverse_lookup = ( |
169
|
|
|
|
|
|
|
# primary |
170
|
|
|
|
|
|
|
"water" => "water", |
171
|
|
|
|
|
|
|
"dark-grey swamp" => "swamp", |
172
|
|
|
|
|
|
|
"dust desert" => "desert", |
173
|
|
|
|
|
|
|
"light-green grass" => "plains", |
174
|
|
|
|
|
|
|
"green forest" => "forest", |
175
|
|
|
|
|
|
|
"dark-green fir-forest" => "forest", |
176
|
|
|
|
|
|
|
"light-grey hill" => "hill", |
177
|
|
|
|
|
|
|
"grey mountain" => "mountain", |
178
|
|
|
|
|
|
|
"grey mountains" => "mountain", |
179
|
|
|
|
|
|
|
# secondary |
180
|
|
|
|
|
|
|
"light-green bush" => "plains", |
181
|
|
|
|
|
|
|
"light-green bushes" => "plains", |
182
|
|
|
|
|
|
|
"dust hill" => "hill", |
183
|
|
|
|
|
|
|
# tertiary |
184
|
|
|
|
|
|
|
"green trees" => "forest", |
185
|
|
|
|
|
|
|
"light-grey forest-hill" => "hill", |
186
|
|
|
|
|
|
|
"green fir-forest" => "forest", |
187
|
|
|
|
|
|
|
"green forest-mountains" => "forest", |
188
|
|
|
|
|
|
|
# wildcard |
189
|
|
|
|
|
|
|
"dark-grey marsh" => "swamp", |
190
|
|
|
|
|
|
|
"sand desert" => "desert", |
191
|
|
|
|
|
|
|
"grey forest-mountain" => "mountain", |
192
|
|
|
|
|
|
|
"grey forest-mountains" => "mountain", |
193
|
|
|
|
|
|
|
"green forest-hill" => "forest", |
194
|
|
|
|
|
|
|
# code |
195
|
|
|
|
|
|
|
"light-soil fields" => "plains", |
196
|
|
|
|
|
|
|
"soil fields" => "plains", |
197
|
|
|
|
|
|
|
); |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
my %encounters = ("settlement" => ["thorp", "thorp", "thorp", "thorp", |
200
|
|
|
|
|
|
|
"village", |
201
|
|
|
|
|
|
|
"town", "town", |
202
|
|
|
|
|
|
|
"large-town", |
203
|
|
|
|
|
|
|
"city"], |
204
|
|
|
|
|
|
|
"fortress" => ["keep", "tower", "castle"], |
205
|
|
|
|
|
|
|
"religious" => ["shrine", "law", "chaos"], |
206
|
|
|
|
|
|
|
"ruin" => [], |
207
|
|
|
|
|
|
|
"monster" => [], |
208
|
|
|
|
|
|
|
"natural" => []); |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
my @needs_fields; |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
sub one { |
213
|
994
|
|
|
994
|
0
|
1467
|
my @arr = @_; |
214
|
994
|
100
|
66
|
|
|
2010
|
@arr = @{$arr[0]} if @arr == 1 and ref $arr[0] eq 'ARRAY'; |
|
412
|
|
|
|
|
730
|
|
215
|
994
|
|
|
|
|
2021
|
return $arr[int(rand(scalar @arr))]; |
216
|
|
|
|
|
|
|
} |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
sub member { |
219
|
0
|
|
|
0
|
0
|
0
|
my $element = shift; |
220
|
0
|
|
|
|
|
0
|
foreach (@_) { |
221
|
0
|
0
|
|
|
|
0
|
return 1 if $element eq $_; |
222
|
|
|
|
|
|
|
} |
223
|
|
|
|
|
|
|
} |
224
|
|
|
|
|
|
|
|
225
|
|
|
|
|
|
|
sub place_major { |
226
|
1
|
|
|
1
|
0
|
4
|
my ($x, $y, $encounter) = @_; |
227
|
1
|
|
|
|
|
5
|
my $thing = one(@{$encounters{$encounter}}); |
|
1
|
|
|
|
|
6
|
|
228
|
1
|
50
|
|
|
|
7
|
return unless $thing; |
229
|
0
|
|
|
|
|
0
|
$log->debug("placing $thing ($encounter) at ($x,$y)"); |
230
|
0
|
|
|
|
|
0
|
my $hex = one(full_hexes($x, $y)); |
231
|
0
|
|
|
|
|
0
|
$x += $hex->[0]; |
232
|
0
|
|
|
|
|
0
|
$y += $hex->[1]; |
233
|
0
|
|
|
|
|
0
|
my $coordinates = Game::TextMapper::Point::coord($x, $y); |
234
|
0
|
|
|
|
|
0
|
my $primary = $reverse_lookup{$world{$coordinates}}; |
235
|
0
|
|
|
|
|
0
|
my ($color, $terrain) = split(' ', $world{$coordinates}, 2); |
236
|
0
|
0
|
|
|
|
0
|
if ($encounter eq 'settlement') { |
237
|
0
|
0
|
|
|
|
0
|
if ($primary eq 'plains') { |
238
|
0
|
|
|
|
|
0
|
$color = one('light-soil', 'soil'); |
239
|
0
|
|
|
|
|
0
|
$log->debug(" " . $world{$coordinates} . " is $primary and was changed to $color"); |
240
|
|
|
|
|
|
|
} |
241
|
0
|
0
|
0
|
|
|
0
|
if ($primary ne 'plains' or member($thing, 'large-town', 'city')) { |
242
|
0
|
|
|
|
|
0
|
push(@needs_fields, [$x, $y]); |
243
|
|
|
|
|
|
|
} |
244
|
|
|
|
|
|
|
} |
245
|
|
|
|
|
|
|
# ignore $terrain for the moment and replace it with $thing |
246
|
0
|
|
|
|
|
0
|
$world{$coordinates} = "$color $thing"; |
247
|
|
|
|
|
|
|
} |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
sub populate_region { |
250
|
15
|
|
|
15
|
0
|
33
|
my ($hex, $primary) = @_; |
251
|
15
|
|
|
|
|
23
|
my $random = rand 100; |
252
|
15
|
100
|
33
|
|
|
228
|
if ($primary eq 'water' and $random < 10 |
|
|
|
33
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
66
|
|
|
|
|
253
|
|
|
|
|
|
|
or $primary eq 'swamp' and $random < 20 |
254
|
|
|
|
|
|
|
or $primary eq 'sand' and $random < 20 |
255
|
|
|
|
|
|
|
or $primary eq 'grass' and $random < 60 |
256
|
|
|
|
|
|
|
or $primary eq 'forest' and $random < 40 |
257
|
|
|
|
|
|
|
or $primary eq 'hill' and $random < 40 |
258
|
|
|
|
|
|
|
or $primary eq 'mountain' and $random < 20) { |
259
|
1
|
|
|
|
|
14
|
place_major($hex->[0], $hex->[1], one(keys %encounters)); |
260
|
|
|
|
|
|
|
} |
261
|
|
|
|
|
|
|
} |
262
|
|
|
|
|
|
|
|
263
|
|
|
|
|
|
|
# Brute forcing by picking random sub hexes until we found an |
264
|
|
|
|
|
|
|
# unassigned one. |
265
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
sub pick_unassigned { |
267
|
225
|
|
|
225
|
0
|
430
|
my ($x, $y, @region) = @_; |
268
|
225
|
|
|
|
|
291
|
my $hex = one(@region); |
269
|
225
|
|
|
|
|
463
|
my $coordinates = Game::TextMapper::Point::coord($x + $hex->[0], $y + $hex->[1]); |
270
|
225
|
|
|
|
|
441
|
while ($world{$coordinates}) { |
271
|
354
|
|
|
|
|
508
|
$hex = one(@region); |
272
|
354
|
|
|
|
|
784
|
$coordinates = Game::TextMapper::Point::coord($x + $hex->[0], $y + $hex->[1]); |
273
|
|
|
|
|
|
|
} |
274
|
225
|
|
|
|
|
438
|
return $coordinates; |
275
|
|
|
|
|
|
|
} |
276
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
sub pick_remaining { |
278
|
30
|
|
|
30
|
0
|
66
|
my ($x, $y, @region) = @_; |
279
|
30
|
|
|
|
|
42
|
my @coordinates = (); |
280
|
30
|
|
|
|
|
46
|
for my $hex (@region) { |
281
|
465
|
|
|
|
|
805
|
my $coordinates = Game::TextMapper::Point::coord($x + $hex->[0], $y + $hex->[1]); |
282
|
465
|
100
|
|
|
|
957
|
push(@coordinates, $coordinates) unless $world{$coordinates}; |
283
|
|
|
|
|
|
|
} |
284
|
30
|
|
|
|
|
136
|
return @coordinates; |
285
|
|
|
|
|
|
|
} |
286
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
# Precomputed for speed |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
sub full_hexes { |
290
|
15
|
|
|
15
|
0
|
34
|
my ($x, $y) = @_; |
291
|
15
|
100
|
|
|
|
41
|
if ($x % 2) { |
292
|
9
|
|
|
|
|
92
|
return ([0, -2], |
293
|
|
|
|
|
|
|
[-2, -1], [-1, -1], [0, -1], [1, -1], [2, -1], |
294
|
|
|
|
|
|
|
[-2, 0], [-1, 0], [0, 0], [1, 0], [2, 0], |
295
|
|
|
|
|
|
|
[-2, 1], [-1, 1], [0, 1], [1, 1], [2, 1], |
296
|
|
|
|
|
|
|
[-1, 2], [0, 2], [1, 2]); |
297
|
|
|
|
|
|
|
} else { |
298
|
6
|
|
|
|
|
51
|
return ([-1, -2], [0, -2], [1, -2], |
299
|
|
|
|
|
|
|
[-2, -1], [-1, -1], [0, -1], [1, -1], [2, -1], |
300
|
|
|
|
|
|
|
[-2, 0], [-1, 0], [0, 0], [1, 0], [2, 0], |
301
|
|
|
|
|
|
|
[-2, 1], [-1, 1], [0, 1], [1, 1], [2, 1], |
302
|
|
|
|
|
|
|
[0, 2]); |
303
|
|
|
|
|
|
|
} |
304
|
|
|
|
|
|
|
} |
305
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
sub half_hexes { |
307
|
15
|
|
|
15
|
0
|
30
|
my ($x, $y) = @_; |
308
|
15
|
100
|
|
|
|
35
|
if ($x % 2) { |
309
|
9
|
|
|
|
|
75
|
return ([-2, -2], [-1, -2], [1, -2], [2, -2], |
310
|
|
|
|
|
|
|
[-3, 0], [3, 0], |
311
|
|
|
|
|
|
|
[-3, 1], [3, 1], |
312
|
|
|
|
|
|
|
[-2, 2], [2, 2], |
313
|
|
|
|
|
|
|
[-1, 3], [1, 3]); |
314
|
|
|
|
|
|
|
} else { |
315
|
6
|
|
|
|
|
47
|
return ([-1, -3], [1, -3], |
316
|
|
|
|
|
|
|
[-2, -2], [2, -2], |
317
|
|
|
|
|
|
|
[-3, -1], [3, -1], |
318
|
|
|
|
|
|
|
[-3, 0], [3, 0], |
319
|
|
|
|
|
|
|
[-2, 2], [-1, 2], [1, 2], [2, 2]); |
320
|
|
|
|
|
|
|
} |
321
|
|
|
|
|
|
|
} |
322
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
sub generate_region { |
324
|
15
|
|
|
15
|
0
|
31
|
my ($x, $y, $primary) = @_; |
325
|
15
|
|
|
|
|
30
|
$world{Game::TextMapper::Point::coord($x, $y)} = one($primary{$primary}); |
326
|
|
|
|
|
|
|
|
327
|
15
|
|
|
|
|
41
|
my @region = full_hexes($x, $y); |
328
|
15
|
|
|
|
|
21
|
my $terrain; |
329
|
|
|
|
|
|
|
|
330
|
15
|
|
|
|
|
30
|
for (1..9) { |
331
|
135
|
|
|
|
|
207
|
my $coordinates = pick_unassigned($x, $y, @region); |
332
|
135
|
|
|
|
|
212
|
$terrain = one($primary{$primary}); |
333
|
135
|
|
|
|
|
383
|
$log->debug(" primary $coordinates => $terrain"); |
334
|
135
|
|
|
|
|
943
|
$world{$coordinates} = $terrain; |
335
|
|
|
|
|
|
|
} |
336
|
|
|
|
|
|
|
|
337
|
15
|
|
|
|
|
30
|
for (1..6) { |
338
|
90
|
|
|
|
|
148
|
my $coordinates = pick_unassigned($x, $y, @region); |
339
|
90
|
|
|
|
|
146
|
$terrain = one($secondary{$primary}); |
340
|
90
|
|
|
|
|
251
|
$log->debug(" secondary $coordinates => $terrain"); |
341
|
90
|
|
|
|
|
576
|
$world{$coordinates} = $terrain; |
342
|
|
|
|
|
|
|
} |
343
|
|
|
|
|
|
|
|
344
|
15
|
|
|
|
|
39
|
for my $coordinates (pick_remaining($x, $y, @region)) { |
345
|
33
|
100
|
|
|
|
67
|
if (rand > 0.1) { |
346
|
30
|
|
|
|
|
45
|
$terrain = one($tertiary{$primary}); |
347
|
30
|
|
|
|
|
87
|
$log->debug(" tertiary $coordinates => $terrain"); |
348
|
|
|
|
|
|
|
} else { |
349
|
3
|
|
|
|
|
10
|
$terrain = one($wildcard{$primary}); |
350
|
3
|
|
|
|
|
14
|
$log->debug(" wildcard $coordinates => $terrain"); |
351
|
|
|
|
|
|
|
} |
352
|
33
|
|
|
|
|
208
|
$world{$coordinates} = $terrain; |
353
|
|
|
|
|
|
|
} |
354
|
|
|
|
|
|
|
|
355
|
15
|
|
|
|
|
43
|
for my $coordinates (pick_remaining($x, $y, half_hexes($x, $y))) { |
356
|
124
|
|
|
|
|
168
|
my $random = rand 6; |
357
|
124
|
100
|
|
|
|
204
|
if ($random < 3) { |
|
|
100
|
|
|
|
|
|
358
|
58
|
|
|
|
|
92
|
$terrain = one($primary{$primary}); |
359
|
58
|
|
|
|
|
149
|
$log->debug(" halfhex primary $coordinates => $terrain"); |
360
|
|
|
|
|
|
|
} elsif ($random < 5) { |
361
|
46
|
|
|
|
|
142
|
$terrain = one($secondary{$primary}); |
362
|
46
|
|
|
|
|
118
|
$log->debug(" halfhex secondary $coordinates => $terrain"); |
363
|
|
|
|
|
|
|
} else { |
364
|
20
|
|
|
|
|
30
|
$terrain = one($tertiary{$primary}); |
365
|
20
|
|
|
|
|
54
|
$log->debug(" halfhex tertiary $coordinates => $terrain"); |
366
|
|
|
|
|
|
|
} |
367
|
124
|
|
|
|
|
767
|
$world{$coordinates} = $terrain; |
368
|
|
|
|
|
|
|
} |
369
|
|
|
|
|
|
|
} |
370
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
sub seed_region { |
372
|
1
|
|
|
1
|
0
|
4
|
my ($seeds, $terrain) = @_; |
373
|
1
|
|
|
|
|
2
|
my $terrain_above; |
374
|
1
|
|
|
|
|
4
|
for my $hex (@$seeds) { |
375
|
15
|
|
|
|
|
75
|
$log->debug("seed_region (" . $hex->[0] . "," . $hex->[1] . ") with $terrain"); |
376
|
15
|
|
|
|
|
102
|
generate_region($hex->[0], $hex->[1], $terrain); |
377
|
15
|
|
|
|
|
67
|
populate_region($hex, $terrain); |
378
|
15
|
|
|
|
|
29
|
my $random = rand 12; |
379
|
|
|
|
|
|
|
# pick next terrain based on the previous one (to the left); or the one |
380
|
|
|
|
|
|
|
# above if in the first column |
381
|
15
|
|
|
|
|
24
|
my $next; |
382
|
15
|
100
|
100
|
|
|
48
|
$terrain = $terrain_above if $hex->[0] == 1 and $terrain_above; |
383
|
15
|
100
|
|
|
|
55
|
if ($random < 6) { |
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
384
|
7
|
|
|
|
|
19
|
$next = one($primary{$terrain}); |
385
|
7
|
|
|
|
|
37
|
$log->debug("picked primary $next"); |
386
|
|
|
|
|
|
|
} elsif ($random < 9) { |
387
|
3
|
|
|
|
|
13
|
$next = one($secondary{$terrain}); |
388
|
3
|
|
|
|
|
16
|
$log->debug("picked secondary $next"); |
389
|
|
|
|
|
|
|
} elsif ($random < 11) { |
390
|
4
|
|
|
|
|
11
|
$next = one($tertiary{$terrain}); |
391
|
4
|
|
|
|
|
20
|
$log->debug("picked tertiary $next"); |
392
|
|
|
|
|
|
|
} else { |
393
|
1
|
|
|
|
|
6
|
$next = one($wildcard{$terrain}); |
394
|
1
|
|
|
|
|
6
|
$log->debug("picked wildcard $next"); |
395
|
|
|
|
|
|
|
} |
396
|
15
|
100
|
|
|
|
106
|
$terrain_above = $terrain if $hex->[0] == 1; |
397
|
15
|
50
|
|
|
|
43
|
die "Terrain lacks reverse_lookup: $next\n" unless $reverse_lookup{$next}; |
398
|
15
|
|
|
|
|
55
|
$terrain = $reverse_lookup{$next}; |
399
|
|
|
|
|
|
|
} |
400
|
|
|
|
|
|
|
} |
401
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
sub agriculture { |
403
|
1
|
|
|
1
|
0
|
6
|
for my $hex (@needs_fields) { |
404
|
0
|
|
|
|
|
0
|
$log->debug("looking to plant fields near " . Game::TextMapper::Point::coord($hex->[0], $hex->[1])); |
405
|
0
|
|
|
|
|
0
|
my $delta = [[[-1, 0], [ 0, -1], [+1, 0], [+1, +1], [ 0, +1], [-1, +1]], # x is even |
406
|
|
|
|
|
|
|
[[-1, -1], [ 0, -1], [+1, -1], [+1, 0], [ 0, +1], [-1, 0]]]; # x is odd |
407
|
0
|
|
|
|
|
0
|
my @plains; |
408
|
0
|
|
|
|
|
0
|
for my $i (0 .. 5) { |
409
|
0
|
|
|
|
|
0
|
my ($x, $y) = ($hex->[0] + $delta->[$hex->[0] % 2]->[$i]->[0], |
410
|
|
|
|
|
|
|
$hex->[1] + $delta->[$hex->[0] % 2]->[$i]->[1]); |
411
|
0
|
|
|
|
|
0
|
my $coordinates = Game::TextMapper::Point::coord($x, $y); |
412
|
0
|
0
|
|
|
|
0
|
if ($world{$coordinates}) { |
413
|
0
|
|
|
|
|
0
|
my ($color, $terrain) = split(' ', $world{$coordinates}, 2); |
414
|
0
|
|
|
|
|
0
|
$log->debug(" $coordinates is " . $world{$coordinates} . " ie. " . $reverse_lookup{$world{$coordinates}}); |
415
|
0
|
0
|
|
|
|
0
|
if ($reverse_lookup{$world{$coordinates}} eq 'plains') { |
416
|
0
|
|
|
|
|
0
|
$log->debug(" $coordinates is a candidate"); |
417
|
0
|
|
|
|
|
0
|
push(@plains, $coordinates); |
418
|
|
|
|
|
|
|
} |
419
|
|
|
|
|
|
|
} |
420
|
|
|
|
|
|
|
} |
421
|
0
|
0
|
|
|
|
0
|
next unless @plains; |
422
|
0
|
|
|
|
|
0
|
my $target = one(@plains); |
423
|
0
|
|
|
|
|
0
|
$world{$target} = one('light-soil fields', 'soil fields'); |
424
|
0
|
|
|
|
|
0
|
$log->debug(" $target planted with " . $world{$target}); |
425
|
|
|
|
|
|
|
} |
426
|
|
|
|
|
|
|
} |
427
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
=head2 generate_map WIDTH, HEIGHT, BW |
429
|
|
|
|
|
|
|
|
430
|
|
|
|
|
|
|
WIDTH and HEIGHT default to 20×10. |
431
|
|
|
|
|
|
|
|
432
|
|
|
|
|
|
|
BW stands for "black & white", i.e. a true value skips background colours. |
433
|
|
|
|
|
|
|
|
434
|
|
|
|
|
|
|
=cut |
435
|
|
|
|
|
|
|
|
436
|
|
|
|
|
|
|
sub generate_map { |
437
|
1
|
|
|
1
|
1
|
12
|
my ($self, $width, $height, $bw) = @_; |
438
|
1
|
0
|
33
|
|
|
7
|
$width = 20 if not defined $width or $width < 1 or $width > 100; |
|
|
|
33
|
|
|
|
|
439
|
1
|
0
|
33
|
|
|
7
|
$height = 10 if not defined $height or $height < 1 or $height > 100; |
|
|
|
33
|
|
|
|
|
440
|
|
|
|
|
|
|
|
441
|
1
|
|
|
|
|
3
|
my $seeds; |
442
|
1
|
|
|
|
|
67
|
for (my $y = 1; $y < $height + 3; $y += 5) { |
443
|
3
|
|
|
|
|
10
|
for (my $x = 1; $x < $width + 3; $x += 5) { |
444
|
|
|
|
|
|
|
# [1,1] [6,3], [11,1], [16,3] |
445
|
15
|
|
|
|
|
26
|
my $y0 = $y + int(($x % 10) / 3); |
446
|
15
|
|
|
|
|
37
|
push(@$seeds, [$x, $y0]); |
447
|
|
|
|
|
|
|
} |
448
|
|
|
|
|
|
|
} |
449
|
|
|
|
|
|
|
|
450
|
1
|
|
|
|
|
3
|
%world = (); # reinitialize! |
451
|
|
|
|
|
|
|
|
452
|
1
|
|
|
|
|
6
|
my @seed_terrain = keys %primary; |
453
|
1
|
|
|
|
|
5
|
seed_region($seeds, one(@seed_terrain)); |
454
|
1
|
|
|
|
|
5
|
agriculture(); |
455
|
|
|
|
|
|
|
|
456
|
|
|
|
|
|
|
# delete extra hexes we generated to fill the gaps |
457
|
1
|
|
|
|
|
72
|
for my $coordinates (keys %world) { |
458
|
397
|
|
|
|
|
733
|
$coordinates =~ /(-?\d\d)(-?\d\d)/; |
459
|
397
|
100
|
100
|
|
|
1114
|
delete $world{$coordinates} if $1 < 1 or $2 < 1; |
460
|
397
|
100
|
100
|
|
|
1038
|
delete $world{$coordinates} if $1 > $width or $2 > $height; |
461
|
|
|
|
|
|
|
} |
462
|
1
|
50
|
|
|
|
62
|
if ($bw) { |
463
|
0
|
|
|
|
|
0
|
for my $coordinates (keys %world) { |
464
|
0
|
|
|
|
|
0
|
my ($color, $rest) = split(' ', $world{$coordinates}, 2); |
465
|
0
|
0
|
|
|
|
0
|
if ($rest) { |
466
|
0
|
|
|
|
|
0
|
$world{$coordinates} = $rest; |
467
|
|
|
|
|
|
|
} else { |
468
|
0
|
|
|
|
|
0
|
delete $world{$coordinates}; |
469
|
|
|
|
|
|
|
} |
470
|
|
|
|
|
|
|
} |
471
|
|
|
|
|
|
|
} |
472
|
|
|
|
|
|
|
|
473
|
1
|
|
|
|
|
71
|
return join("\n", map { $_ . " " . $world{$_} } sort keys %world) . "\n" |
|
200
|
|
|
|
|
396
|
|
474
|
|
|
|
|
|
|
. "include gnomeyland.txt\n"; |
475
|
|
|
|
|
|
|
} |
476
|
|
|
|
|
|
|
|
477
|
|
|
|
|
|
|
=head1 SEE ALSO |
478
|
|
|
|
|
|
|
|
479
|
|
|
|
|
|
|
Erin D. Smale described this algorithm in two famous blog posts: |
480
|
|
|
|
|
|
|
L and |
481
|
|
|
|
|
|
|
L. |
482
|
|
|
|
|
|
|
|
483
|
|
|
|
|
|
|
The map itself uses the I icons by Gregory B. MacKenzie. These are |
484
|
|
|
|
|
|
|
licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License. |
485
|
|
|
|
|
|
|
To view a copy of this license, visit |
486
|
|
|
|
|
|
|
L. |
487
|
|
|
|
|
|
|
|
488
|
|
|
|
|
|
|
=cut |
489
|
|
|
|
|
|
|
|
490
|
|
|
|
|
|
|
1; |