| 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
|
|
10
|
use Game::TextMapper::Log; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
25
|
|
|
49
|
1
|
|
|
1
|
|
13
|
use Game::TextMapper::Point; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
6
|
|
|
50
|
1
|
|
|
1
|
|
35
|
use Modern::Perl '2018'; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
6
|
|
|
51
|
1
|
|
|
1
|
|
114
|
use Mojo::Base -base; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
21
|
|
|
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
|
966
|
|
|
966
|
0
|
1380
|
my @arr = @_; |
|
214
|
966
|
100
|
66
|
|
|
2005
|
@arr = @{$arr[0]} if @arr == 1 and ref $arr[0] eq 'ARRAY'; |
|
|
412
|
|
|
|
|
681
|
|
|
215
|
966
|
|
|
|
|
2841
|
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
|
2
|
|
|
2
|
0
|
6
|
my ($x, $y, $encounter) = @_; |
|
227
|
2
|
|
|
|
|
3
|
my $thing = one(@{$encounters{$encounter}}); |
|
|
2
|
|
|
|
|
6
|
|
|
228
|
2
|
100
|
|
|
|
6
|
return unless $thing; |
|
229
|
1
|
|
|
|
|
8
|
$log->debug("placing $thing ($encounter) at ($x,$y)"); |
|
230
|
1
|
|
|
|
|
8
|
my $hex = one(full_hexes($x, $y)); |
|
231
|
1
|
|
|
|
|
5
|
$x += $hex->[0]; |
|
232
|
1
|
|
|
|
|
3
|
$y += $hex->[1]; |
|
233
|
1
|
|
|
|
|
4
|
my $coordinates = Game::TextMapper::Point::coord($x, $y); |
|
234
|
1
|
|
|
|
|
4
|
my $primary = $reverse_lookup{$world{$coordinates}}; |
|
235
|
1
|
|
|
|
|
5
|
my ($color, $terrain) = split(' ', $world{$coordinates}, 2); |
|
236
|
1
|
50
|
|
|
|
4
|
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
|
1
|
|
|
|
|
5
|
$world{$coordinates} = "$color $thing"; |
|
247
|
|
|
|
|
|
|
} |
|
248
|
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
sub populate_region { |
|
250
|
15
|
|
|
15
|
0
|
22
|
my ($hex, $primary) = @_; |
|
251
|
15
|
|
|
|
|
21
|
my $random = rand 100; |
|
252
|
15
|
100
|
66
|
|
|
170
|
if ($primary eq 'water' and $random < 10 |
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
|
|
33
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
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
|
2
|
|
|
|
|
9
|
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
|
391
|
my ($x, $y, @region) = @_; |
|
268
|
225
|
|
|
|
|
322
|
my $hex = one(@region); |
|
269
|
225
|
|
|
|
|
498
|
my $coordinates = Game::TextMapper::Point::coord($x + $hex->[0], $y + $hex->[1]); |
|
270
|
225
|
|
|
|
|
442
|
while ($world{$coordinates}) { |
|
271
|
323
|
|
|
|
|
464
|
$hex = one(@region); |
|
272
|
323
|
|
|
|
|
588
|
$coordinates = Game::TextMapper::Point::coord($x + $hex->[0], $y + $hex->[1]); |
|
273
|
|
|
|
|
|
|
} |
|
274
|
225
|
|
|
|
|
427
|
return $coordinates; |
|
275
|
|
|
|
|
|
|
} |
|
276
|
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
sub pick_remaining { |
|
278
|
30
|
|
|
30
|
0
|
60
|
my ($x, $y, @region) = @_; |
|
279
|
30
|
|
|
|
|
35
|
my @coordinates = (); |
|
280
|
30
|
|
|
|
|
45
|
for my $hex (@region) { |
|
281
|
465
|
|
|
|
|
808
|
my $coordinates = Game::TextMapper::Point::coord($x + $hex->[0], $y + $hex->[1]); |
|
282
|
465
|
100
|
|
|
|
962
|
push(@coordinates, $coordinates) unless $world{$coordinates}; |
|
283
|
|
|
|
|
|
|
} |
|
284
|
30
|
|
|
|
|
95
|
return @coordinates; |
|
285
|
|
|
|
|
|
|
} |
|
286
|
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
# Precomputed for speed |
|
288
|
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
sub full_hexes { |
|
290
|
16
|
|
|
16
|
0
|
24
|
my ($x, $y) = @_; |
|
291
|
16
|
100
|
|
|
|
30
|
if ($x % 2) { |
|
292
|
9
|
|
|
|
|
47
|
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
|
7
|
|
|
|
|
45
|
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
|
24
|
my ($x, $y) = @_; |
|
308
|
15
|
100
|
|
|
|
29
|
if ($x % 2) { |
|
309
|
9
|
|
|
|
|
41
|
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
|
|
|
|
|
26
|
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
|
30
|
my ($x, $y, $primary) = @_; |
|
325
|
15
|
|
|
|
|
23
|
$world{Game::TextMapper::Point::coord($x, $y)} = one($primary{$primary}); |
|
326
|
|
|
|
|
|
|
|
|
327
|
15
|
|
|
|
|
31
|
my @region = full_hexes($x, $y); |
|
328
|
15
|
|
|
|
|
23
|
my $terrain; |
|
329
|
|
|
|
|
|
|
|
|
330
|
15
|
|
|
|
|
23
|
for (1..9) { |
|
331
|
135
|
|
|
|
|
214
|
my $coordinates = pick_unassigned($x, $y, @region); |
|
332
|
135
|
|
|
|
|
203
|
$terrain = one($primary{$primary}); |
|
333
|
135
|
|
|
|
|
366
|
$log->debug(" primary $coordinates => $terrain"); |
|
334
|
135
|
|
|
|
|
862
|
$world{$coordinates} = $terrain; |
|
335
|
|
|
|
|
|
|
} |
|
336
|
|
|
|
|
|
|
|
|
337
|
15
|
|
|
|
|
27
|
for (1..6) { |
|
338
|
90
|
|
|
|
|
148
|
my $coordinates = pick_unassigned($x, $y, @region); |
|
339
|
90
|
|
|
|
|
141
|
$terrain = one($secondary{$primary}); |
|
340
|
90
|
|
|
|
|
241
|
$log->debug(" secondary $coordinates => $terrain"); |
|
341
|
90
|
|
|
|
|
570
|
$world{$coordinates} = $terrain; |
|
342
|
|
|
|
|
|
|
} |
|
343
|
|
|
|
|
|
|
|
|
344
|
15
|
|
|
|
|
32
|
for my $coordinates (pick_remaining($x, $y, @region)) { |
|
345
|
33
|
100
|
|
|
|
60
|
if (rand > 0.1) { |
|
346
|
30
|
|
|
|
|
47
|
$terrain = one($tertiary{$primary}); |
|
347
|
30
|
|
|
|
|
74
|
$log->debug(" tertiary $coordinates => $terrain"); |
|
348
|
|
|
|
|
|
|
} else { |
|
349
|
3
|
|
|
|
|
8
|
$terrain = one($wildcard{$primary}); |
|
350
|
3
|
|
|
|
|
12
|
$log->debug(" wildcard $coordinates => $terrain"); |
|
351
|
|
|
|
|
|
|
} |
|
352
|
33
|
|
|
|
|
193
|
$world{$coordinates} = $terrain; |
|
353
|
|
|
|
|
|
|
} |
|
354
|
|
|
|
|
|
|
|
|
355
|
15
|
|
|
|
|
34
|
for my $coordinates (pick_remaining($x, $y, half_hexes($x, $y))) { |
|
356
|
124
|
|
|
|
|
155
|
my $random = rand 6; |
|
357
|
124
|
100
|
|
|
|
193
|
if ($random < 3) { |
|
|
|
100
|
|
|
|
|
|
|
358
|
69
|
|
|
|
|
93
|
$terrain = one($primary{$primary}); |
|
359
|
69
|
|
|
|
|
159
|
$log->debug(" halfhex primary $coordinates => $terrain"); |
|
360
|
|
|
|
|
|
|
} elsif ($random < 5) { |
|
361
|
36
|
|
|
|
|
52
|
$terrain = one($secondary{$primary}); |
|
362
|
36
|
|
|
|
|
79
|
$log->debug(" halfhex secondary $coordinates => $terrain"); |
|
363
|
|
|
|
|
|
|
} else { |
|
364
|
19
|
|
|
|
|
26
|
$terrain = one($tertiary{$primary}); |
|
365
|
19
|
|
|
|
|
46
|
$log->debug(" halfhex tertiary $coordinates => $terrain"); |
|
366
|
|
|
|
|
|
|
} |
|
367
|
124
|
|
|
|
|
742
|
$world{$coordinates} = $terrain; |
|
368
|
|
|
|
|
|
|
} |
|
369
|
|
|
|
|
|
|
} |
|
370
|
|
|
|
|
|
|
|
|
371
|
|
|
|
|
|
|
sub seed_region { |
|
372
|
1
|
|
|
1
|
0
|
3
|
my ($seeds, $terrain) = @_; |
|
373
|
1
|
|
|
|
|
2
|
my $terrain_above; |
|
374
|
1
|
|
|
|
|
4
|
for my $hex (@$seeds) { |
|
375
|
15
|
|
|
|
|
70
|
$log->debug("seed_region (" . $hex->[0] . "," . $hex->[1] . ") with $terrain"); |
|
376
|
15
|
|
|
|
|
88
|
generate_region($hex->[0], $hex->[1], $terrain); |
|
377
|
15
|
|
|
|
|
64
|
populate_region($hex, $terrain); |
|
378
|
15
|
|
|
|
|
20
|
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
|
|
|
|
|
19
|
my $next; |
|
382
|
15
|
100
|
100
|
|
|
37
|
$terrain = $terrain_above if $hex->[0] == 1 and $terrain_above; |
|
383
|
15
|
100
|
|
|
|
34
|
if ($random < 6) { |
|
|
|
100
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
384
|
9
|
|
|
|
|
19
|
$next = one($primary{$terrain}); |
|
385
|
9
|
|
|
|
|
27
|
$log->debug("picked primary $next"); |
|
386
|
|
|
|
|
|
|
} elsif ($random < 9) { |
|
387
|
2
|
|
|
|
|
6
|
$next = one($secondary{$terrain}); |
|
388
|
2
|
|
|
|
|
10
|
$log->debug("picked secondary $next"); |
|
389
|
|
|
|
|
|
|
} elsif ($random < 11) { |
|
390
|
1
|
|
|
|
|
3
|
$next = one($tertiary{$terrain}); |
|
391
|
1
|
|
|
|
|
5
|
$log->debug("picked tertiary $next"); |
|
392
|
|
|
|
|
|
|
} else { |
|
393
|
3
|
|
|
|
|
6
|
$next = one($wildcard{$terrain}); |
|
394
|
3
|
|
|
|
|
9
|
$log->debug("picked wildcard $next"); |
|
395
|
|
|
|
|
|
|
} |
|
396
|
15
|
100
|
|
|
|
89
|
$terrain_above = $terrain if $hex->[0] == 1; |
|
397
|
15
|
50
|
|
|
|
30
|
die "Terrain lacks reverse_lookup: $next\n" unless $reverse_lookup{$next}; |
|
398
|
15
|
|
|
|
|
26
|
$terrain = $reverse_lookup{$next}; |
|
399
|
|
|
|
|
|
|
} |
|
400
|
|
|
|
|
|
|
} |
|
401
|
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
sub agriculture { |
|
403
|
1
|
|
|
1
|
0
|
5
|
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
|
|
|
4
|
$height = 10 if not defined $height or $height < 1 or $height > 100; |
|
|
|
|
33
|
|
|
|
|
|
440
|
|
|
|
|
|
|
|
|
441
|
1
|
|
|
|
|
11
|
my $seeds; |
|
442
|
1
|
|
|
|
|
6
|
for (my $y = 1; $y < $height + 3; $y += 5) { |
|
443
|
3
|
|
|
|
|
6
|
for (my $x = 1; $x < $width + 3; $x += 5) { |
|
444
|
|
|
|
|
|
|
# [1,1] [6,3], [11,1], [16,3] |
|
445
|
15
|
|
|
|
|
20
|
my $y0 = $y + int(($x % 10) / 3); |
|
446
|
15
|
|
|
|
|
36
|
push(@$seeds, [$x, $y0]); |
|
447
|
|
|
|
|
|
|
} |
|
448
|
|
|
|
|
|
|
} |
|
449
|
|
|
|
|
|
|
|
|
450
|
1
|
|
|
|
|
3
|
%world = (); # reinitialize! |
|
451
|
|
|
|
|
|
|
|
|
452
|
1
|
|
|
|
|
7
|
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
|
|
|
|
|
718
|
$coordinates =~ /(-?\d\d)(-?\d\d)/; |
|
459
|
397
|
100
|
100
|
|
|
1027
|
delete $world{$coordinates} if $1 < 1 or $2 < 1; |
|
460
|
397
|
100
|
100
|
|
|
989
|
delete $world{$coordinates} if $1 > $width or $2 > $height; |
|
461
|
|
|
|
|
|
|
} |
|
462
|
1
|
50
|
|
|
|
33
|
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
|
|
|
|
|
60
|
return join("\n", map { $_ . " " . $world{$_} } sort keys %world) . "\n" |
|
|
200
|
|
|
|
|
385
|
|
|
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; |