mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-25 10:34:14 +00:00
678 lines
14 KiB
Perl
Executable File
678 lines
14 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
## Random map generator for wesnoth
|
|
#
|
|
# Copyright 2003 J.R. Blain
|
|
# Released under the gnu gpl v2 or later.
|
|
#
|
|
# Todo: Insert proper gpl notice here.
|
|
#
|
|
#
|
|
# todo: clean it up some, add some comments, make it produce
|
|
# WML for the map, some form of interface (perl-gtk?) to allow
|
|
# setting map parameters.
|
|
# Reorganize settings variables to make more sense, remove
|
|
# unused crap.
|
|
# Add river/road support.
|
|
# ansi-ize the map
|
|
#
|
|
# Changelog:
|
|
#
|
|
# Nov 18 2003 (Cowboy)
|
|
# - Cleaned up a little after Miyo :)
|
|
# - switched some if ($foo => $bar) to if ($foo >= $bar)
|
|
# so it doesn't think it's a hash assignment
|
|
# and throw a stupid warning.
|
|
#
|
|
# - Updated to clean up some routines,
|
|
# - remove an endless loop (not sure if it was mine or miyos)
|
|
# - added "snow"
|
|
# - updated _scan_ring to handle invalid ranges better. (faster)
|
|
#
|
|
# Nov 18 2003 (Miyo)
|
|
# - added '#!/usr/bin/perl -w'
|
|
# - implemented Getops::Long
|
|
# - usage|help
|
|
# - enable/disable towns
|
|
# - selectable base terrain type
|
|
# - configurable terrain leveling
|
|
#
|
|
# Nov 17 2003
|
|
# - Better comments
|
|
# - Updated proximity scanning routines
|
|
# They now radiate out in squares to judge
|
|
# distance.
|
|
# - little bit of code cleanup in various places.
|
|
# - fixed up castle placing mostly. Still has an
|
|
# issue where it's placing them too close to edges
|
|
# at times.
|
|
#
|
|
# Nov 16 2003
|
|
# - initial release
|
|
|
|
use strict;
|
|
use warnings;
|
|
use List::Util qw( shuffle );
|
|
use POSIX qw( ceil );
|
|
use Getopt::Long;
|
|
|
|
use vars qw( $PARAMS $MAP $NUMBERS $DIRMAP $SQUARES );
|
|
|
|
$NUMBERS = {
|
|
terrain => {
|
|
grassland => {
|
|
percent => 0,
|
|
type => 'g',
|
|
mod => 'round',
|
|
},
|
|
forests => {
|
|
percent => 25,
|
|
type => 'f',
|
|
mod => 'round',
|
|
},
|
|
mountains => {
|
|
percent => 10,
|
|
type => 'm',
|
|
mod => 'round',
|
|
},
|
|
deserts => {
|
|
percent => 10,
|
|
type => 'd',
|
|
mod => 'round',
|
|
},
|
|
shallows => {
|
|
percent => 10,
|
|
type => 'c',
|
|
mod => 'round',
|
|
},
|
|
hills => {
|
|
percent => 10,
|
|
type => 'h',
|
|
mod => 'round',
|
|
},
|
|
snow => {
|
|
percent => 5,
|
|
type => 'S',
|
|
mod => 'round',
|
|
},
|
|
},
|
|
towns => {
|
|
squares_per_town => 6 * 6, # 1 town in this many squares.
|
|
min_town_distance => 2, # at least 3 squares from another town.
|
|
type => 't',
|
|
},
|
|
players => {
|
|
count => 4,
|
|
preferred_distance => 30,
|
|
decrement_unit => 1,
|
|
},
|
|
};
|
|
$PARAMS = {
|
|
map_size => '64x64',
|
|
base_type => 'g', # fill any remaining map spots with this type.
|
|
towns => 1,
|
|
castles => 1,
|
|
usage => 0,
|
|
verbose => 0,
|
|
leveling => 2,
|
|
};
|
|
|
|
|
|
GetOptions (
|
|
'size=s' => \$PARAMS->{map_size},
|
|
'grassland=i' => \$NUMBERS->{terrain}{grassland}{percent},
|
|
'forest=i' => \$NUMBERS->{terrain}{forests}{percent},
|
|
'mountains=i' => \$NUMBERS->{terrain}{mountains}{percent},
|
|
'desert=i' => \$NUMBERS->{terrain}{deserts}{percent},
|
|
'shallow=i' => \$NUMBERS->{terrain}{shallows}{percent},
|
|
'snow=i' => \$NUMBERS->{terrain}{snow}{percent},
|
|
'hills=i' => \$NUMBERS->{terrain}{hills}{percent},
|
|
'players=i' => \$NUMBERS->{players}{count},
|
|
'playerdist=i' => \$NUMBERS->{players}{preferred_distance},
|
|
'towns!' => \$PARAMS->{towns},
|
|
'towndist=i' => \$NUMBERS->{towns}{min_town_distance},
|
|
'townspt=i' => \$NUMBERS->{towns}{squares_per_town},
|
|
'baseterrain=s' => \$PARAMS->{base_type},
|
|
'leveling=i' => \$PARAMS->{leveling},
|
|
'help|usage' => \$PARAMS->{usage},
|
|
'verbose' => \$PARAMS->{verbose},
|
|
) || usage();
|
|
|
|
|
|
|
|
( $PARAMS->{height}, $PARAMS->{width} ) = split /x/,$PARAMS->{map_size};
|
|
|
|
# not used yet, thoughts for the future.
|
|
$SQUARES = {
|
|
m => {
|
|
passable => 0,
|
|
},
|
|
C => {
|
|
passable => 1,
|
|
},
|
|
d => {
|
|
passable => 1,
|
|
},
|
|
c => {
|
|
passable => 1,
|
|
pass_types => {
|
|
R => 'b', # if crossing a river, use a bridge
|
|
}
|
|
},
|
|
s => {
|
|
passable => 0,
|
|
},
|
|
g => {
|
|
passable => 1,
|
|
},
|
|
R => {
|
|
passable => 1,
|
|
pass_types => {
|
|
c => 'b', # water + roads make bridges
|
|
}
|
|
}
|
|
};
|
|
|
|
# $DIRMAP -> used to provide a map of 'general'
|
|
# directions for things like rivers, roads, ect.
|
|
# north could mean north, north-east, or north-west,
|
|
# ect. This allows rivers/roads (and other things that
|
|
# use this approximate direction stuff) to meander a little
|
|
# while staying mostly true to course.
|
|
$DIRMAP = {
|
|
0 => { 7 => 1, 0 => 1, 1 => 1 },
|
|
1 => { 0 => 1, 1 => 1, 2 => 1 },
|
|
2 => { 1 => 1, 2 => 1, 3 => 1 },
|
|
3 => { 2 => 1, 3 => 1, 4 => 1 },
|
|
4 => { 3 => 1, 4 => 1, 5 => 1 },
|
|
5 => { 4 => 1, 5 => 1, 6 => 1 },
|
|
6 => { 5 => 1, 6 => 1, 7 => 1 },
|
|
7 => { 6 => 1, 7 => 1, 0 => 1 },
|
|
};
|
|
|
|
#
|
|
# Terrain types:
|
|
# d => desert
|
|
# R => road
|
|
# g => grass
|
|
# m => mountains
|
|
# f => forests
|
|
# c => coast (or river)
|
|
|
|
# the map is a 2 dimensional grid.
|
|
#
|
|
# 0123456
|
|
# 0 y
|
|
# 1 X yy
|
|
# 2 X
|
|
# 3
|
|
# 4
|
|
# 5
|
|
$MAP = [];
|
|
|
|
usage() if $PARAMS->{usage} == 1;
|
|
|
|
generate_base_map();
|
|
add_terrain();
|
|
fill_map();
|
|
|
|
|
|
if ( $PARAMS->{leveling} >= 1 ) {
|
|
my $t = 1;
|
|
while ($t <= $PARAMS->{leveling}) {
|
|
warn "Leveling...\n" if $PARAMS->{verbose};
|
|
level_terrain();
|
|
++$t;
|
|
}
|
|
}
|
|
|
|
|
|
add_towns() if $PARAMS->{towns};
|
|
|
|
add_castles() if $NUMBERS->{players}->{count} >= 1;
|
|
|
|
print_map();
|
|
|
|
|
|
sub usage {
|
|
|
|
abort_with_error("
|
|
usage: random_map.pl [ OPTIONS ]
|
|
|
|
-help|usage give this help
|
|
-verbose print more information
|
|
|
|
-size NUMxNUM set map size, height x width (default=64x64)
|
|
-leveling NUM set number of terrain levelings to occur (default=2)
|
|
-players NUM set number of player (default=8)
|
|
-playerdist NUM set preferred distance between players (default=30)
|
|
-notowns disable towns
|
|
-towndist set minimun distance between towns (default=2)
|
|
|
|
-baseterrain CHAR set base terrain type (default=g)
|
|
-grassland NUM set grassland % (default=0)
|
|
-forest NUM set forest % (default=10)
|
|
-mountains NUM set mountain % (default=10)
|
|
-desert NUM set desert % (default=10)
|
|
-shallow NUM set shallow water % (default=10)
|
|
-hills NUM set hills % (default=10)
|
|
-snow NUM set snow % (default=5)
|
|
\n");
|
|
}
|
|
|
|
|
|
sub abort_with_error {
|
|
die @_;
|
|
}
|
|
sub add_terrain {
|
|
foreach my $key (keys %{ $NUMBERS->{terrain} }) {
|
|
if ($NUMBERS->{terrain}->{$key}->{mod} eq 'round') {
|
|
my $x = 0;
|
|
my ($counts,$avg_size) = _get_min_max_sizes($key);
|
|
while ($x < $counts) {
|
|
place_round(
|
|
int(rand($PARAMS->{width})),
|
|
int(rand($PARAMS->{height})),
|
|
$avg_size,
|
|
$NUMBERS->{terrain}->{$key}->{type});
|
|
++$x;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
sub add_towns {
|
|
warn "add_towns\n" if $PARAMS->{verbose};
|
|
my $towns = ceil(($PARAMS->{height} * $PARAMS->{width}) / $NUMBERS->{towns}->{squares_per_town});
|
|
my $x = 1;
|
|
while ($x <= $towns) {
|
|
my $y = 1;
|
|
# 200 tries to place a town.
|
|
warn "trying to place town\n" if $PARAMS->{verbose};
|
|
while ($y <= 200) {
|
|
my $plx = int(rand($PARAMS->{height}));
|
|
my $ply = int(rand($PARAMS->{width}));
|
|
if (
|
|
_town_ok_to_place (
|
|
$plx,
|
|
$ply,
|
|
$NUMBERS->{towns}->{type},
|
|
$NUMBERS->{towns}->{min_town_distance}
|
|
)
|
|
) {
|
|
$MAP->[$plx]->[$ply] = $NUMBERS->{towns}->{type};
|
|
warn "placed town at $plx $ply\n" if $PARAMS->{verbose};
|
|
$y = 200; # will exit us from the loop here.
|
|
}
|
|
else {
|
|
warn "town not ok: $y\n" if $PARAMS->{verbose};
|
|
}
|
|
++$y;
|
|
}
|
|
warn "next town\n" if $PARAMS->{verbose};
|
|
++$x;
|
|
}
|
|
}
|
|
sub add_castles {
|
|
warn "add_castles\n" if $PARAMS->{verbose};
|
|
my $castle_count = $NUMBERS->{players}->{count};
|
|
my $x = 1;
|
|
while ($x <= $castle_count) {
|
|
my $cur_distance = $NUMBERS->{players}->{preferred_distance};
|
|
my $done = 0;
|
|
while ($cur_distance > 1 && !$done) {
|
|
my $y = 1;
|
|
# 200 tries to place a castle.
|
|
while ($y <= 200) {
|
|
my $plx = int(rand($PARAMS->{height}));
|
|
my $ply = int(rand($PARAMS->{width}));
|
|
if (
|
|
|
|
_castle_ok_to_place (
|
|
$plx,
|
|
$ply,
|
|
$cur_distance
|
|
)
|
|
) {
|
|
surround(
|
|
$plx,
|
|
$ply,
|
|
'C');
|
|
$MAP->[$plx]->[$ply] = $x;
|
|
$y = 200; # will exit us from the loop here.
|
|
$done = 1;
|
|
}
|
|
++$y;
|
|
}
|
|
$cur_distance -= $NUMBERS->{players}->{decrement_unit};
|
|
}
|
|
if (!$done) {
|
|
die "Something screwed up, could not place castle $x\n";
|
|
}
|
|
++$x;
|
|
}
|
|
}
|
|
|
|
|
|
sub _get_min_max_sizes {
|
|
my ($terrain) = shift;
|
|
my $total_squares = $PARAMS->{width} * $PARAMS->{height};
|
|
my $total_terrain_size = $total_squares * ($NUMBERS->{terrain}->{$terrain}->{percent} / 100);
|
|
my $counts = int(rand(5)) + 3;
|
|
my $avg_size = $total_terrain_size / $counts;
|
|
return ($counts,$avg_size);
|
|
}
|
|
|
|
sub _town_ok_to_place {
|
|
my ($x,$y,$type,$search_dist) = @_;
|
|
warn "checking town\n" if $PARAMS->{verbose};
|
|
# searches circular until it finds a matching terrain type.
|
|
# returns undef if it can't find one. Note, this routine
|
|
# is a bit of a hog.
|
|
|
|
# simple check, are we on top of one?
|
|
if ($MAP->[$x]->[$y] eq $type) {
|
|
return undef;
|
|
}
|
|
warn "$x $y $search_dist $type\n" if $PARAMS->{verbose};
|
|
if (_scan_ring($x,$y,$search_dist,$type)) {
|
|
return undef;
|
|
}
|
|
return 1;
|
|
}
|
|
sub _castle_ok_to_place {
|
|
my ($x,$y,$search_dist) = @_;
|
|
|
|
# searches circular until it finds a matching terrain type.
|
|
# returns undef if it can't find one. Note, this routine
|
|
# is a bit of a hog.
|
|
|
|
# simple check, are we on top of one?
|
|
if (_is_castle($MAP->[$x]->[$y])) {
|
|
return undef;
|
|
}
|
|
if (_scan_border($x,$y,4)) {
|
|
# at least 4 squares from the map edge.
|
|
return undef;
|
|
}
|
|
if (_scan_ring($x,$y,$search_dist,'castle')) {
|
|
return undef;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub _scan_border {
|
|
my ($x,$y,$dist) = @_;
|
|
unless ($MAP->[$x + $dist + 1]->[$y + $dist + 1]) {
|
|
return 1;
|
|
}
|
|
unless ($MAP->[$x - $dist]->[$y - $dist]) {
|
|
return 1;
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
# scans in a ring around location looking
|
|
# for a specific type of terrain.
|
|
# if it finds it, it returns true,
|
|
# otherwise, returns undef.
|
|
# used by place town, place castle
|
|
sub _scan_ring {
|
|
my ($x,$y,$dist,$type) = @_;
|
|
# first we build a list of all the
|
|
# x,y locations that match our ring.
|
|
#
|
|
|
|
|
|
my $sx = $x - $dist; # starting x point
|
|
my $sy = $y - $dist; # starting y point
|
|
my $ex = $x + $dist; # ending x point
|
|
my $ey = $y + $dist; # ending y point
|
|
|
|
|
|
my $cx = $sx;
|
|
while ($cx <= $ex) {
|
|
warn "Looping through $cx\n" if $PARAMS->{verbose};
|
|
if ($cx >= 0 && $cx < $PARAMS->{height}) {
|
|
my $cy = $sy;
|
|
while ($cy <= $ey) {
|
|
if ($cy >= 0 && $cy < $PARAMS->{width}) {
|
|
if ($MAP->[$cx]->[$cy] && $type eq 'castle' && _is_castle($MAP->[$cx]->[$cy])) {
|
|
return 1;
|
|
}
|
|
if ($MAP->[$cx]->[$cy] && $type eq $MAP->[$cx]->[$cy]) {
|
|
return 1;
|
|
}
|
|
}
|
|
++$cy;
|
|
}
|
|
}
|
|
$cx++;
|
|
}
|
|
return undef;
|
|
|
|
}
|
|
|
|
# just a nicely named routine to keep from using
|
|
# regular expressions all over.
|
|
sub _is_castle {
|
|
my ($type) = @_;
|
|
if ($type =~ /^\d$/) {
|
|
return 1;
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
sub surround {
|
|
my ($x,$y,$type) = @_;
|
|
my @dirs = (0,1,2,3,4,5,6,7,8);
|
|
foreach my $dir (@dirs) {
|
|
my ($bx,$by) = _move_direction($x,$y,$dir);
|
|
if (defined($bx) && defined($by)) {
|
|
$MAP->[$bx]->[$by] = $type;
|
|
}
|
|
}
|
|
}
|
|
|
|
#
|
|
# place an object
|
|
# with a general round shape.
|
|
# This includes forests, mountains,
|
|
# ect. This works by choosing a center point
|
|
# and then expanding around it in an almost circular
|
|
# pattern.
|
|
#
|
|
# Takes 4 params:
|
|
# starting X, starting Y, size (hexes), type (f,m,ect)
|
|
#
|
|
|
|
sub place_round {
|
|
my ($x,$y,$size,$type) = @_;
|
|
my $count = 1;
|
|
my $cust = 0;
|
|
while ($count <= $size) {
|
|
if ($cust == 8) {
|
|
$cust = 0;
|
|
}
|
|
if (int(rand(6))) {
|
|
$cust = int(rand(8));
|
|
}
|
|
my %result = _get_next_map_square(x => $x, y => $y, dir => $cust);
|
|
if (defined($result{x}) && defined($result{y})) {
|
|
$MAP->[$result{x}]->[$result{y}] = $type;
|
|
($x,$y) = ($result{x},$result{y});
|
|
}
|
|
else {
|
|
}
|
|
++$count;
|
|
++$cust;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
|
|
# dummy sub for now
|
|
sub _is_placeable {
|
|
return 1;
|
|
}
|
|
|
|
# this routine checks each square on the map,
|
|
# and "levels" it. an individual grassland,
|
|
# surrounded totally by mountains, will become a mountain.
|
|
# any terrain feature with less than 2 of it's own type
|
|
# near it, becomes the predominant feature of the terrain
|
|
# nearby.
|
|
# It's sorta an ugly fix to some of the randomness issues,
|
|
# but thus far has seemed to work out ok.
|
|
sub level_terrain {
|
|
my $height = $PARAMS->{height};
|
|
my $width = $PARAMS->{width};
|
|
$height--;
|
|
$width--;
|
|
my $x = 0;
|
|
while ($x <= $height) {
|
|
my $y = 0;
|
|
while ($y <= $height) {
|
|
_level_terrain($x,$y);
|
|
++$y;
|
|
}
|
|
++$x;
|
|
}
|
|
}
|
|
sub _level_terrain {
|
|
my ($x,$y) = @_;
|
|
my @dirs = (0,1,2,3,4,5,6,7);
|
|
my %counts = ();
|
|
foreach my $dir (@dirs) {
|
|
my ($nx,$ny) = _move_direction($x,$y,$dir);
|
|
if (defined($nx) && defined($ny)) {
|
|
$counts{$MAP->[$nx]->[$ny]}++;
|
|
}
|
|
}
|
|
unless (defined($counts{$MAP->[$x]->[$y]}) && $counts{$MAP->[$x]->[$y]} > 1) {
|
|
my @types = sort { $counts{$b} <=> $counts{$a} } keys %counts;
|
|
$MAP->[$x]->[$y] = $types[0];
|
|
}
|
|
}
|
|
|
|
sub _move_direction {
|
|
my ($x,$y,$dir) = @_;
|
|
if ($dir == 0 && ($x - 1) >= 0 && ($y - 1) >= 0) {
|
|
--$x;
|
|
--$y;
|
|
}
|
|
# move north if possible.
|
|
elsif ($dir == 1 && ($x - 1) >= 0) {
|
|
--$x;
|
|
}
|
|
# move north-east if possible.
|
|
elsif ($dir == 2 && ($x - 1) >= 0 && ($y + 1) < $PARAMS->{width}) {
|
|
++$y;
|
|
--$x;
|
|
}
|
|
# move east if possible
|
|
elsif ($dir == 3 && ($y + 1) < $PARAMS->{width}) {
|
|
++$y;
|
|
}
|
|
# move south-east if possible.
|
|
elsif ($dir == 4 && ($y + 1) < $PARAMS->{width} && ($x + 1) < $PARAMS->{height}) {
|
|
++$y;
|
|
++$x;
|
|
}
|
|
# move south if possible
|
|
elsif ($dir == 5 && ($x + 1) < $PARAMS->{height}) {
|
|
++$x;
|
|
}
|
|
# move south-west if possible
|
|
elsif ($dir == 6 && ($y - 1) >= 0 && ($x + 1) < $PARAMS->{height}) {
|
|
--$y;
|
|
++$x;
|
|
}
|
|
# move west if possible
|
|
elsif ($dir == 7 && ($y - 1) >= 0) {
|
|
--$y;
|
|
}
|
|
else {
|
|
return (undef,undef);
|
|
}
|
|
return ($x,$y);
|
|
}
|
|
|
|
sub _get_next_map_square {
|
|
my %args = @_;
|
|
|
|
unless (defined($args{dir})) {
|
|
$args{dir} = int(rand(8));
|
|
}
|
|
my @try_dirs = keys %{ $DIRMAP->{$args{dir}} };
|
|
|
|
# shuffle them
|
|
@try_dirs = shuffle(@try_dirs);
|
|
|
|
|
|
my $x = $args{x};
|
|
my $y = $args{y};
|
|
foreach my $dir (@try_dirs) {
|
|
my ($bx,$by) = _move_direction($x,$y,$dir);
|
|
if (_is_placeable($bx,$by)) {
|
|
return ( x => $bx, y => $by );
|
|
}
|
|
}
|
|
return (x => undef, y => undef);
|
|
}
|
|
|
|
|
|
#
|
|
# Fills any remaining unused map spaces
|
|
# with a standard type
|
|
#
|
|
|
|
sub fill_map {
|
|
my $x = 0;
|
|
while ($x <= $PARAMS->{height} - 1) {
|
|
my $y = 0;
|
|
while ($y <= $PARAMS->{width} - 1) {
|
|
if ($MAP->[$x]->[$y] eq '@') {
|
|
$MAP->[$x]->[$y] = $PARAMS->{base_type};
|
|
}
|
|
++$y;
|
|
}
|
|
++$x;
|
|
}
|
|
|
|
}
|
|
|
|
#
|
|
# Prints the current map.
|
|
#
|
|
sub print_map {
|
|
my $x = 0;
|
|
while ($x <= $PARAMS->{height} - 1) {
|
|
my $y = 0;
|
|
while ($y <= $PARAMS->{width} - 1) {
|
|
print $MAP->[$x]->[$y];
|
|
++$y;
|
|
}
|
|
++$x;
|
|
print "\n";
|
|
}
|
|
}
|
|
#
|
|
# Generates the base map grids.
|
|
# This essentially fills out all the $MAP->[x]->[y] values
|
|
# with dummy characters.
|
|
#
|
|
sub generate_base_map {
|
|
my $x = 0;
|
|
while ($x <= $PARAMS->{height} - 1) {
|
|
my $y = 0;
|
|
$MAP->[$x] = [];
|
|
while ($y <= $PARAMS->{width} - 1) {
|
|
$MAP->[$x]->[$y] = '@';
|
|
++$y;
|
|
}
|
|
++$x;
|
|
}
|
|
}
|
|
|