| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
# Copyright (C) 2024 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::Solo - generate a map generated step by step |
|
21
|
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
=head1 SYNOPSIS |
|
23
|
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
use Modern::Perl; |
|
25
|
|
|
|
|
|
|
use Game::TextMapper::Solo; |
|
26
|
|
|
|
|
|
|
my $map = Game::TextMapper::Solo->new->generate_map(); |
|
27
|
|
|
|
|
|
|
print $map; |
|
28
|
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
=head1 DESCRIPTION |
|
30
|
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
This starts the map and generates all the details directly, for each step, |
|
32
|
|
|
|
|
|
|
without knowledge of the rest of the map. The tricky part is to generate |
|
33
|
|
|
|
|
|
|
features such that no terrible geographical problems arise. |
|
34
|
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
=cut |
|
36
|
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
package Game::TextMapper::Solo; |
|
38
|
11
|
|
|
11
|
|
95
|
use Game::TextMapper::Log; |
|
|
11
|
|
|
|
|
26
|
|
|
|
11
|
|
|
|
|
523
|
|
|
39
|
11
|
|
|
11
|
|
65
|
use Modern::Perl '2018'; |
|
|
11
|
|
|
|
|
28
|
|
|
|
11
|
|
|
|
|
108
|
|
|
40
|
11
|
|
|
11
|
|
4522
|
use List::Util qw(shuffle all any none); |
|
|
11
|
|
|
|
|
33
|
|
|
|
11
|
|
|
|
|
1348
|
|
|
41
|
11
|
|
|
11
|
|
77
|
use Mojo::Base -base; |
|
|
11
|
|
|
|
|
26
|
|
|
|
11
|
|
|
|
|
106
|
|
|
42
|
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
my $log = Game::TextMapper::Log->get; |
|
44
|
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
=head1 ATTRIBUTES |
|
46
|
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
=head2 rows |
|
48
|
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
The height of the map, defaults to 15. |
|
50
|
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
use Modern::Perl; |
|
52
|
|
|
|
|
|
|
use Game::TextMapper::Solo; |
|
53
|
|
|
|
|
|
|
my $map = Game::TextMapper::Solo->new(rows => 20) |
|
54
|
|
|
|
|
|
|
->generate_map; |
|
55
|
|
|
|
|
|
|
print $map; |
|
56
|
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
=head2 cols |
|
58
|
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
The width of the map, defaults to 30. |
|
60
|
|
|
|
|
|
|
|
|
61
|
|
|
|
|
|
|
use Modern::Perl; |
|
62
|
|
|
|
|
|
|
use Game::TextMapper::Solo; |
|
63
|
|
|
|
|
|
|
my $map = Game::TextMapper::Solo->new(cols => 40) |
|
64
|
|
|
|
|
|
|
->generate_map; |
|
65
|
|
|
|
|
|
|
print $map; |
|
66
|
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
=cut |
|
68
|
|
|
|
|
|
|
|
|
69
|
|
|
|
|
|
|
has 'rows' => 15; |
|
70
|
|
|
|
|
|
|
has 'cols' => 30; |
|
71
|
|
|
|
|
|
|
has 'altitudes' => sub{[]}; # these are the altitudes of each hex, a number between 0 (deep ocean) and 10 (ice) |
|
72
|
|
|
|
|
|
|
has 'tiles' => sub{[]}; # these are the tiles on the map, an array of arrays of strings |
|
73
|
|
|
|
|
|
|
has 'flows' => sub{[]}; # these are the water flow directions on the map, an array of coordinates |
|
74
|
|
|
|
|
|
|
has 'rivers' => sub{[]}; # for rendering, the flows are turned into rivers, an array of arrays of coordinates |
|
75
|
|
|
|
|
|
|
has 'trails' => sub{[]}; |
|
76
|
|
|
|
|
|
|
has 'loglevel'; |
|
77
|
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
my @tiles = qw(plain rough swamp desert forest hills green-hills forest-hill mountains mountain volcano ice water coastal ocean); |
|
79
|
|
|
|
|
|
|
my @no_sources = qw(desert volcano water coastal ocean); |
|
80
|
|
|
|
|
|
|
my @settlements = qw(house ruin tower ruined-tower castle ruined-castle); |
|
81
|
|
|
|
|
|
|
my @ruins = qw(ruin ruined-tower ruined-castle); |
|
82
|
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
=head1 METHODS |
|
84
|
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
=head2 generate_map |
|
86
|
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
This method takes no arguments. Set the properties of the map using the |
|
88
|
|
|
|
|
|
|
attributes. |
|
89
|
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
=cut |
|
91
|
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
sub generate_map { |
|
93
|
0
|
|
|
0
|
1
|
|
my ($self) = @_; |
|
94
|
0
|
0
|
|
|
|
|
$log->level($self->loglevel) if $self->loglevel; |
|
95
|
0
|
|
|
|
|
|
$self->random_walk(); |
|
96
|
|
|
|
|
|
|
# my $walks = $self->random_walk(); |
|
97
|
|
|
|
|
|
|
# debug random walks |
|
98
|
|
|
|
|
|
|
# my @walks = @$walks; |
|
99
|
|
|
|
|
|
|
# @walks = @walks[0 .. 10]; |
|
100
|
|
|
|
|
|
|
# $self->trails(\@walks); |
|
101
|
0
|
|
|
|
|
|
$self->add_rivers(); |
|
102
|
0
|
|
|
|
|
|
return $self->to_text(); |
|
103
|
|
|
|
|
|
|
} |
|
104
|
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
sub random_walk { |
|
106
|
0
|
|
|
0
|
0
|
|
my ($self) = @_; |
|
107
|
0
|
|
|
|
|
|
my %seen; |
|
108
|
0
|
|
|
|
|
|
my $tile_count = 0; |
|
109
|
0
|
|
|
|
|
|
my $path_length = 1; |
|
110
|
0
|
|
|
|
|
|
my $max_tiles = $self->rows * $self->cols; |
|
111
|
0
|
|
|
|
|
|
my $start = int($self->rows / 2) * $self->cols + int($self->cols / 2); |
|
112
|
0
|
|
|
|
|
|
$self->altitudes->[$start] = 5; |
|
113
|
0
|
|
|
|
|
|
my @neighbours = $self->neighbours($start); |
|
114
|
|
|
|
|
|
|
# initial river setup: roll a d6 four destination |
|
115
|
0
|
|
|
|
|
|
$self->flows->[$start] = $neighbours[int(rand(6))]; |
|
116
|
|
|
|
|
|
|
# roll a d6 for source, skip if same as destination |
|
117
|
0
|
|
|
|
|
|
my $source = $neighbours[int(rand(6))]; |
|
118
|
0
|
0
|
|
|
|
|
$self->flows->[$source] = $start unless $source == $self->flows->[$start]; |
|
119
|
|
|
|
|
|
|
# initial setup: roll for starting region with a village |
|
120
|
0
|
|
|
|
|
|
$seen{$start} = 1; |
|
121
|
0
|
|
|
|
|
|
$self->random_tile($start, $start, 'house'); |
|
122
|
0
|
0
|
|
|
|
|
push(@{$self->tiles->[$start]}, qq("$tile_count/$start")) if $log->level eq 'debug'; |
|
|
0
|
|
|
|
|
|
|
|
123
|
0
|
|
|
|
|
|
$tile_count++; |
|
124
|
|
|
|
|
|
|
# roll for the immediate neighbours |
|
125
|
0
|
|
|
|
|
|
for my $to (@neighbours) { |
|
126
|
0
|
|
|
|
|
|
$seen{$to} = 1; |
|
127
|
0
|
|
|
|
|
|
$self->random_tile($start, $to); |
|
128
|
0
|
0
|
|
|
|
|
push(@{$self->tiles->[$to]}, qq("$tile_count/$to")) if $log->level eq 'debug'; |
|
|
0
|
|
|
|
|
|
|
|
129
|
0
|
|
|
|
|
|
$tile_count++; |
|
130
|
|
|
|
|
|
|
} |
|
131
|
|
|
|
|
|
|
# remember those walks for debugging (assign to trails, for example) |
|
132
|
0
|
|
|
|
|
|
my $walks = []; |
|
133
|
|
|
|
|
|
|
# while there are still undiscovered hexes |
|
134
|
0
|
|
|
|
|
|
while ($tile_count < $max_tiles) { |
|
135
|
|
|
|
|
|
|
# create an expedition of length l |
|
136
|
0
|
|
|
|
|
|
my $from = $start; |
|
137
|
0
|
|
|
|
|
|
my $to = $start; |
|
138
|
0
|
|
|
|
|
|
my $walk = []; |
|
139
|
0
|
|
|
|
|
|
for (my $i = 0; $i < $path_length; $i++) { |
|
140
|
0
|
|
|
|
|
|
push(@$walk, $to); |
|
141
|
0
|
0
|
|
|
|
|
if (not $seen{$to}) { |
|
142
|
0
|
|
|
|
|
|
$seen{$to} = 1; |
|
143
|
0
|
|
|
|
|
|
$self->random_tile($from, $to); |
|
144
|
0
|
0
|
|
|
|
|
push(@{$self->tiles->[$to]}, qq("$tile_count/$to")) if $log->level eq 'debug'; |
|
|
0
|
|
|
|
|
|
|
|
145
|
0
|
|
|
|
|
|
$tile_count++; |
|
146
|
|
|
|
|
|
|
} |
|
147
|
0
|
|
|
|
|
|
$from = $to; |
|
148
|
0
|
|
|
|
|
|
$to = $self->neighbour($from, \%seen); |
|
149
|
|
|
|
|
|
|
} |
|
150
|
0
|
|
|
|
|
|
$path_length++; |
|
151
|
0
|
|
|
|
|
|
push(@$walks, $walk); |
|
152
|
|
|
|
|
|
|
# last if @$walks > 10; |
|
153
|
|
|
|
|
|
|
} |
|
154
|
0
|
|
|
|
|
|
return $walks; |
|
155
|
|
|
|
|
|
|
} |
|
156
|
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
sub random_tile { |
|
158
|
0
|
|
|
0
|
0
|
|
my ($self, $from, $to, $settlement) = @_; |
|
159
|
0
|
|
|
|
|
|
my $roll = roll_2d6(); |
|
160
|
0
|
|
|
|
|
|
my $altitude = $self->adjust_altitude($roll, $from, $to); |
|
161
|
|
|
|
|
|
|
# coastal water always has flow |
|
162
|
0
|
|
0
|
|
|
|
$self->add_flow($to, ($roll >= 5 and $roll <= 8 or $altitude == 1)); |
|
163
|
0
|
|
|
|
|
|
my $wet = defined $self->flows->[$to]; |
|
164
|
0
|
|
|
|
|
|
my $tile; |
|
165
|
0
|
0
|
|
|
|
|
if ($altitude == 0) { $tile = 'ocean' } |
|
|
0
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
166
|
0
|
|
|
|
|
|
elsif ($altitude == 1) { $tile = 'coastal' } |
|
167
|
0
|
0
|
|
|
|
|
elsif ($altitude == 2) { $tile = $wet ? 'swamp' : 'desert' } |
|
168
|
0
|
0
|
|
|
|
|
elsif ($altitude == 3) { $tile = $wet ? 'swamp' : 'plain' } |
|
169
|
0
|
0
|
|
|
|
|
elsif ($altitude == 4) { $tile = $wet ? 'forest' : 'plain' } |
|
170
|
0
|
0
|
|
|
|
|
elsif ($altitude == 5) { $tile = $wet ? 'forest' : 'plain' } |
|
171
|
0
|
0
|
|
|
|
|
elsif ($altitude == 6) { $tile = $wet ? 'forest-hill' : 'rough' } |
|
172
|
0
|
0
|
|
|
|
|
elsif ($altitude == 7) { $tile = $wet ? 'green-hills' : 'hills' } |
|
173
|
0
|
|
|
|
|
|
elsif ($altitude == 8) { $tile = 'mountains' } |
|
174
|
0
|
0
|
|
|
|
|
elsif ($altitude == 9) { $tile = special() ? 'volcano' : 'mountain' } |
|
175
|
0
|
|
|
|
|
|
else { $tile = 'ice' } |
|
176
|
0
|
|
|
|
|
|
push(@{$self->tiles->[$to]}, $tile); |
|
|
0
|
|
|
|
|
|
|
|
177
|
0
|
0
|
|
|
|
|
if ($settlement) { |
|
|
|
0
|
|
|
|
|
|
|
178
|
0
|
|
|
|
|
|
push(@{$self->tiles->[$to]}, $settlement); |
|
|
0
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
} elsif ($roll == 7) { |
|
180
|
0
|
0
|
0
|
|
|
|
if ($tile eq 'forest' or $tile eq 'forest-hill') { |
|
|
|
0
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
181
|
0
|
|
|
|
|
|
push(@{$self->tiles->[$to]}, $settlements[int(rand($#settlements + 1))]); |
|
|
0
|
|
|
|
|
|
|
|
182
|
|
|
|
|
|
|
} elsif ($tile eq 'desert' or $tile eq 'swamp' or $tile eq 'green-hills') { |
|
183
|
0
|
|
|
|
|
|
push(@{$self->tiles->[$to]}, $ruins[int(rand($#ruins + 1))]); |
|
|
0
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
} |
|
185
|
|
|
|
|
|
|
} |
|
186
|
0
|
0
|
|
|
|
|
push(@{$self->tiles->[$to]}, qq("+$altitude")) if $log->level eq 'debug'; |
|
|
0
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
} |
|
188
|
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
sub adjust_altitude { |
|
190
|
0
|
|
|
0
|
0
|
|
my ($self, $roll, $from, $to) = @_; |
|
191
|
0
|
|
|
|
|
|
my @neighbours = $self->neighbours($to); |
|
192
|
|
|
|
|
|
|
# ocean stays ocean |
|
193
|
0
|
0
|
|
0
|
|
|
if (all { defined $self->altitudes->[$_] and $self->altitudes->[$_] <= 1 } @neighbours) { |
|
|
0
|
0
|
|
|
|
|
|
|
194
|
0
|
|
|
|
|
|
return $self->altitudes->[$to] = 0; |
|
195
|
|
|
|
|
|
|
} |
|
196
|
0
|
|
|
|
|
|
my $altitude = $self->altitudes->[$from]; |
|
197
|
0
|
|
|
|
|
|
my $max = 10; |
|
198
|
|
|
|
|
|
|
# if we're following a river, the altitude rarely goes up; neighbouring hexes |
|
199
|
|
|
|
|
|
|
# also limit the heigh changes |
|
200
|
0
|
|
|
|
|
|
for (@neighbours) { |
|
201
|
0
|
0
|
0
|
|
|
|
if (defined $self->flows->[$_] |
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
202
|
|
|
|
|
|
|
and $self->flows->[$_] == $to |
|
203
|
|
|
|
|
|
|
and defined $self->altitudes->[$_] |
|
204
|
|
|
|
|
|
|
and $self->altitudes->[$_] < $max) { |
|
205
|
0
|
|
|
|
|
|
$max = $self->altitudes->[$_]; |
|
206
|
|
|
|
|
|
|
} |
|
207
|
|
|
|
|
|
|
} |
|
208
|
0
|
|
|
|
|
|
my $delta = 0; |
|
209
|
0
|
0
|
|
|
|
|
if ($roll == 2) { $delta = -2 } |
|
|
0
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
210
|
0
|
|
|
|
|
|
elsif ($roll == 3) { $delta = -1 } |
|
211
|
0
|
|
|
|
|
|
elsif ($roll == 10) { $delta = +1 } |
|
212
|
0
|
|
|
|
|
|
elsif ($roll == 11) { $delta = +1 } |
|
213
|
0
|
|
|
|
|
|
elsif ($roll == 12) { $delta = +2 } |
|
214
|
0
|
|
|
|
|
|
$altitude += $delta; |
|
215
|
0
|
0
|
|
|
|
|
$altitude = $max if $altitude > $max; |
|
216
|
0
|
0
|
|
|
|
|
$altitude = 0 if $altitude < 0; |
|
217
|
0
|
0
|
0
|
0
|
|
|
$altitude = 1 if $altitude == 0 and any { defined $self->altitudes->[$_] and $self->altitudes->[$_] > 1 } @neighbours; |
|
|
0
|
0
|
|
|
|
|
|
|
218
|
0
|
|
|
|
|
|
return $self->altitudes->[$to] = $altitude; |
|
219
|
|
|
|
|
|
|
} |
|
220
|
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
sub add_flow { |
|
222
|
0
|
|
|
0
|
0
|
|
my ($self, $to, $source) = @_; |
|
223
|
0
|
|
|
|
|
|
my @neighbours = $self->neighbours($to); |
|
224
|
|
|
|
|
|
|
# don't do anything if there's already water flow |
|
225
|
0
|
0
|
|
|
|
|
return if defined $self->flows->[$to]; |
|
226
|
|
|
|
|
|
|
# don't do anything if this is ocean |
|
227
|
0
|
0
|
0
|
|
|
|
return if defined $self->altitudes->[$to] and $self->altitudes->[$to] == 0; |
|
228
|
|
|
|
|
|
|
# if this hex can be a source or water from a neighbour flows into it |
|
229
|
0
|
0
|
0
|
|
|
|
if ($source and not $self->tiles->[$to] and $self->altitudes->[$to] >= 1 and $self->altitudes->[$to] <= 8 |
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
230
|
0
|
0
|
|
0
|
|
|
or any { defined $self->flows->[$_] and $self->flows->[$_] == $to } @neighbours) { |
|
231
|
|
|
|
|
|
|
# prefer a lower neighbour (or an undefined one), but "lower" works only for |
|
232
|
|
|
|
|
|
|
# known hexes so there must already be water flow, there, and that water |
|
233
|
|
|
|
|
|
|
# flow must not be circular |
|
234
|
|
|
|
|
|
|
my @candidates = grep { |
|
235
|
0
|
0
|
0
|
|
|
|
not defined $self->altitudes->[$_] |
|
|
0
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
or $self->altitudes->[$_] < $self->altitudes->[$to] |
|
237
|
|
|
|
|
|
|
and $self->flowable($to, $_) |
|
238
|
|
|
|
|
|
|
} @neighbours; |
|
239
|
0
|
0
|
|
|
|
|
if (@candidates) { |
|
240
|
0
|
|
|
|
|
|
$self->flows->[$to] = $candidates[0]; |
|
241
|
0
|
|
|
|
|
|
return; |
|
242
|
|
|
|
|
|
|
} |
|
243
|
|
|
|
|
|
|
# or if this hex is at the edge, prefer flowing off the edge of the map |
|
244
|
0
|
0
|
|
|
|
|
if (@neighbours < 6) { |
|
245
|
0
|
|
|
|
|
|
$self->flows->[$to] = -1; |
|
246
|
0
|
|
|
|
|
|
return; |
|
247
|
|
|
|
|
|
|
} |
|
248
|
|
|
|
|
|
|
# or prefer of equal altitude but again this works only for known hexes so |
|
249
|
|
|
|
|
|
|
# there must already be water flow, there, and that water flow must not be |
|
250
|
|
|
|
|
|
|
# circular |
|
251
|
|
|
|
|
|
|
@candidates = grep { |
|
252
|
0
|
0
|
|
|
|
|
$self->altitudes->[$_] == $self->altitudes->[$to] |
|
|
0
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
and $self->flowable($to, $_) |
|
254
|
|
|
|
|
|
|
} @neighbours; |
|
255
|
0
|
0
|
|
|
|
|
if (@candidates) { |
|
256
|
0
|
|
|
|
|
|
$self->flows->[$to] = $candidates[0]; |
|
257
|
0
|
|
|
|
|
|
return; |
|
258
|
|
|
|
|
|
|
} |
|
259
|
|
|
|
|
|
|
# or it's magic!! |
|
260
|
0
|
|
|
|
|
|
@candidates = grep { $self->flowable($to, $_) } @neighbours; |
|
|
0
|
|
|
|
|
|
|
|
261
|
0
|
0
|
|
|
|
|
if (@candidates) { |
|
262
|
0
|
|
|
|
|
|
$log->info("Awkward transition at " . $self->xy($to)); |
|
263
|
0
|
|
|
|
|
|
$self->flows->[$to] = $candidates[0]; |
|
264
|
0
|
|
|
|
|
|
return; |
|
265
|
|
|
|
|
|
|
} |
|
266
|
|
|
|
|
|
|
# Or it's a dead end… and entrance into the underworld, obviously |
|
267
|
0
|
0
|
|
|
|
|
if ($self->altitudes->[$to] > 1) { |
|
268
|
0
|
|
|
|
|
|
push(@{$self->tiles->[$to]}, 'cave'); |
|
|
0
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
} |
|
270
|
|
|
|
|
|
|
} |
|
271
|
|
|
|
|
|
|
} |
|
272
|
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
# A river can from A to B if B is undefined or if B has flow that doesn't return |
|
274
|
|
|
|
|
|
|
# to A. |
|
275
|
|
|
|
|
|
|
sub flowable { |
|
276
|
0
|
|
|
0
|
0
|
|
my ($self, $from, $to) = @_; |
|
277
|
0
|
|
|
|
|
|
my $flow = 0; |
|
278
|
0
|
|
0
|
|
|
|
while (defined $self->flows->[$to] and $self->flows->[$to] >= 0) { |
|
279
|
0
|
|
|
|
|
|
$to = $self->flows->[$to]; |
|
280
|
0
|
0
|
|
|
|
|
return 0 if $to == $from; |
|
281
|
0
|
|
|
|
|
|
$flow = 1; |
|
282
|
|
|
|
|
|
|
} |
|
283
|
0
|
|
|
|
|
|
return $flow; |
|
284
|
|
|
|
|
|
|
} |
|
285
|
|
|
|
|
|
|
|
|
286
|
|
|
|
|
|
|
sub add_rivers { |
|
287
|
0
|
|
|
0
|
0
|
|
my ($self) = @_; |
|
288
|
0
|
|
|
|
|
|
my %seen; |
|
289
|
0
|
|
|
|
|
|
for my $coordinate (0 .. $self->rows * $self->cols - 1) { |
|
290
|
0
|
0
|
|
|
|
|
next unless defined $self->flows->[$coordinate]; |
|
291
|
0
|
0
|
|
|
|
|
next if $self->altitudes->[$coordinate] <= 1; # do not show rivers starting here |
|
292
|
0
|
0
|
|
|
|
|
next if $seen{$coordinate}; |
|
293
|
0
|
|
|
|
|
|
$seen{$coordinate} = 1; |
|
294
|
0
|
0
|
|
|
|
|
if (none { |
|
295
|
0
|
0
|
|
0
|
|
|
defined $self->flows->[$_] |
|
296
|
|
|
|
|
|
|
and $self->flows->[$_] == $coordinate |
|
297
|
|
|
|
|
|
|
} $self->neighbours($coordinate)) { |
|
298
|
0
|
|
|
|
|
|
my $river = []; |
|
299
|
0
|
|
|
|
|
|
while (defined $coordinate) { |
|
300
|
0
|
|
|
|
|
|
push(@$river, $coordinate); |
|
301
|
0
|
0
|
|
|
|
|
last if $coordinate == -1; |
|
302
|
0
|
|
|
|
|
|
$seen{$coordinate} = 1; |
|
303
|
0
|
|
|
|
|
|
$coordinate = $self->flows->[$coordinate]; |
|
304
|
|
|
|
|
|
|
} |
|
305
|
0
|
|
|
|
|
|
push(@{$self->rivers}, $river); |
|
|
0
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
} |
|
307
|
|
|
|
|
|
|
} |
|
308
|
|
|
|
|
|
|
} |
|
309
|
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
sub special { |
|
311
|
0
|
|
|
0
|
0
|
|
return rand() < 1/6; |
|
312
|
|
|
|
|
|
|
} |
|
313
|
|
|
|
|
|
|
|
|
314
|
|
|
|
|
|
|
sub roll_2d6 { |
|
315
|
0
|
|
|
0
|
0
|
|
return 2 + int(rand(6)) + int(rand(6)); |
|
316
|
|
|
|
|
|
|
} |
|
317
|
|
|
|
|
|
|
|
|
318
|
|
|
|
|
|
|
sub neighbour { |
|
319
|
0
|
|
|
0
|
0
|
|
my ($self, $coordinate, $seen) = @_; |
|
320
|
0
|
|
|
|
|
|
my @neighbours = $self->neighbours($coordinate); |
|
321
|
|
|
|
|
|
|
# If a seen hash reference is provided, prefer new hexes |
|
322
|
0
|
0
|
|
|
|
|
if ($seen) { |
|
323
|
0
|
|
|
|
|
|
my @candidates = grep {!($seen->{$_})} @neighbours; |
|
|
0
|
|
|
|
|
|
|
|
324
|
0
|
0
|
|
|
|
|
return $candidates[0] if @candidates; |
|
325
|
|
|
|
|
|
|
} |
|
326
|
0
|
|
|
|
|
|
return $neighbours[0]; |
|
327
|
|
|
|
|
|
|
} |
|
328
|
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
# Returns the coordinates of neighbour regions, in random order, but only if on |
|
330
|
|
|
|
|
|
|
# the map. |
|
331
|
|
|
|
|
|
|
sub neighbours { |
|
332
|
0
|
|
|
0
|
0
|
|
my ($self, $coordinate) = @_; |
|
333
|
0
|
|
|
|
|
|
my @offsets; |
|
334
|
0
|
0
|
|
|
|
|
if ($coordinate % 2) { |
|
335
|
0
|
|
|
|
|
|
@offsets = (-1, +1, $self->cols, -$self->cols, $self->cols -1, $self->cols +1); |
|
336
|
0
|
0
|
|
|
|
|
$offsets[3] = undef if $coordinate < $self->cols; # top edge |
|
337
|
0
|
0
|
|
|
|
|
$offsets[2] = $offsets[4] = $offsets[5] = undef if $coordinate >= ($self->rows - 1) * $self->cols; # bottom edge |
|
338
|
0
|
0
|
|
|
|
|
$offsets[0] = $offsets[4] = undef if $coordinate % $self->cols == 0; # left edge |
|
339
|
0
|
0
|
|
|
|
|
$offsets[1] = $offsets[5] = undef if $coordinate % $self->cols == $self->cols - 1; # right edge |
|
340
|
|
|
|
|
|
|
} else { |
|
341
|
0
|
|
|
|
|
|
@offsets = (-1, +1, $self->cols, -$self->cols, -$self->cols -1, -$self->cols +1); |
|
342
|
0
|
0
|
|
|
|
|
$offsets[3] = $offsets[4] = $offsets[5] = undef if $coordinate < $self->cols; # top edge |
|
343
|
0
|
0
|
|
|
|
|
$offsets[2] = undef if $coordinate >= ($self->rows - 1) * $self->cols; # bottom edge |
|
344
|
0
|
0
|
|
|
|
|
$offsets[0] = $offsets[4] = undef if $coordinate % $self->cols == 0; # left edge |
|
345
|
0
|
0
|
|
|
|
|
$offsets[1] = $offsets[5] = undef if $coordinate % $self->cols == $self->cols - 1; # right edge |
|
346
|
|
|
|
|
|
|
} |
|
347
|
0
|
|
|
|
|
|
return map { $coordinate + $_ } shuffle grep {$_} @offsets; |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
348
|
|
|
|
|
|
|
} |
|
349
|
|
|
|
|
|
|
|
|
350
|
|
|
|
|
|
|
# Return the direction of a neighbour given its coordinates. 0 is up (north), 1 |
|
351
|
|
|
|
|
|
|
# is north-east, 2 is south-east, 3 is south, 4 is south-west, 5 is north-west. |
|
352
|
|
|
|
|
|
|
sub direction { |
|
353
|
0
|
|
|
0
|
0
|
|
my ($self, $from, $to) = @_; |
|
354
|
0
|
|
|
|
|
|
my @offsets; |
|
355
|
0
|
0
|
|
|
|
|
if ($from % 2) { |
|
356
|
0
|
|
|
|
|
|
@offsets = (-$self->cols, +1, $self->cols +1, $self->cols, $self->cols -1, -1); |
|
357
|
|
|
|
|
|
|
} else { |
|
358
|
0
|
|
|
|
|
|
@offsets = (-$self->cols, -$self->cols +1, +1, $self->cols, -1, -$self->cols -1); |
|
359
|
|
|
|
|
|
|
} |
|
360
|
0
|
|
|
|
|
|
for (my $i = 0; $i < 6; $i++) { |
|
361
|
0
|
0
|
|
|
|
|
return $i if $from + $offsets[$i] == $to; |
|
362
|
|
|
|
|
|
|
} |
|
363
|
|
|
|
|
|
|
} |
|
364
|
|
|
|
|
|
|
|
|
365
|
|
|
|
|
|
|
sub to_text { |
|
366
|
0
|
|
|
0
|
0
|
|
my ($self) = @_; |
|
367
|
0
|
|
|
|
|
|
my $text = ""; |
|
368
|
0
|
|
|
|
|
|
for my $i (0 .. $self->rows * $self->cols - 1) { |
|
369
|
0
|
0
|
|
|
|
|
next unless $self->tiles->[$i]; |
|
370
|
0
|
|
|
|
|
|
my @tiles = @{$self->tiles->[$i]}; |
|
|
0
|
|
|
|
|
|
|
|
371
|
0
|
0
|
0
|
|
|
|
push(@tiles, "arrow" . $self->direction($i, $self->flows->[$i])) if defined $self->flows->[$i] and $log->level eq 'debug'; |
|
372
|
0
|
|
|
|
|
|
$text .= $self->xy($i) . " @tiles\n"; |
|
373
|
|
|
|
|
|
|
} |
|
374
|
0
|
|
|
|
|
|
for my $river (@{$self->rivers}) { |
|
|
0
|
|
|
|
|
|
|
|
375
|
0
|
0
|
0
|
|
|
|
$text .= $self->xy(@$river) . " river\n" if ref($river) and @$river > 1; |
|
376
|
|
|
|
|
|
|
} |
|
377
|
0
|
|
|
|
|
|
for my $trail (@{$self->trails}) { |
|
|
0
|
|
|
|
|
|
|
|
378
|
0
|
0
|
0
|
|
|
|
$text .= $self->xy(@$trail) . " trails\n" if ref($trail) and @$trail > 1; |
|
379
|
|
|
|
|
|
|
# More emphasis |
|
380
|
|
|
|
|
|
|
# $text .= $self->xy(@$trail) . " border\n" if ref($trail) and @$trail > 1; |
|
381
|
|
|
|
|
|
|
} |
|
382
|
|
|
|
|
|
|
# add arrows for the flow |
|
383
|
|
|
|
|
|
|
$text .= join("\n", |
|
384
|
|
|
|
|
|
|
qq{}, |
|
385
|
|
|
|
|
|
|
map { |
|
386
|
0
|
|
|
|
|
|
my $angle = 60 * $_; |
|
|
0
|
|
|
|
|
|
|
|
387
|
0
|
|
|
|
|
|
qq{}; |
|
388
|
|
|
|
|
|
|
} (0 .. 5)); |
|
389
|
0
|
|
|
|
|
|
$text .= "\ninclude bright.txt\n"; |
|
390
|
0
|
|
|
|
|
|
return $text; |
|
391
|
|
|
|
|
|
|
} |
|
392
|
|
|
|
|
|
|
|
|
393
|
|
|
|
|
|
|
sub xy { |
|
394
|
0
|
|
|
0
|
0
|
|
my ($self, @coordinates) = @_; |
|
395
|
0
|
|
|
|
|
|
for (my $i = 0; $i < @coordinates; $i++) { |
|
396
|
0
|
0
|
|
|
|
|
if ($coordinates[$i] == -1) { |
|
397
|
0
|
|
|
|
|
|
$coordinates[$i] = $self->edge($coordinates[$i - 1]); |
|
398
|
|
|
|
|
|
|
} else { |
|
399
|
0
|
|
|
|
|
|
$coordinates[$i] = sprintf("%02d%02d", $coordinates[$i] % $self->cols + 1, int($coordinates[$i] / $self->cols) + 1); |
|
400
|
|
|
|
|
|
|
} |
|
401
|
|
|
|
|
|
|
} |
|
402
|
0
|
|
|
|
|
|
return join("-", @coordinates); |
|
403
|
|
|
|
|
|
|
} |
|
404
|
|
|
|
|
|
|
|
|
405
|
|
|
|
|
|
|
sub edge { |
|
406
|
0
|
|
|
0
|
0
|
|
my ($self, $coordinate) = @_; |
|
407
|
0
|
|
|
|
|
|
my ($x, $y) = $coordinate =~ /(..)(..)/; |
|
408
|
0
|
0
|
|
|
|
|
if ($x == 1) { |
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
409
|
0
|
|
|
|
|
|
return "00" . $y; |
|
410
|
|
|
|
|
|
|
} elsif ($x == $self->cols) { |
|
411
|
0
|
|
|
|
|
|
return sprintf("%02d", $self->cols+1) . $y; |
|
412
|
|
|
|
|
|
|
} elsif ($y == 1) { |
|
413
|
0
|
|
|
|
|
|
return $x . "00"; |
|
414
|
|
|
|
|
|
|
} elsif ($y == $self->rows) { |
|
415
|
0
|
|
|
|
|
|
return $x . sprintf("%02d", $self->rows+1); |
|
416
|
|
|
|
|
|
|
} |
|
417
|
|
|
|
|
|
|
|
|
418
|
|
|
|
|
|
|
} |
|
419
|
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
1; |