#!/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;
	}
}

