The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# vi:tw=0 syntax=perl:

# package ::_interconnected_map {{{
package Games::RolePlay::MapGen::_disallow_autoviv;

use common::sense;
use Tie::Array;
use parent -norequire => 'Tie::StdArray';
use Carp;

1;

sub TIEARRAY {
    my $class = shift;
    my $this  = bless [], $class;
    my $that  = shift;

    @$this = @$that;

    $this;
}

sub FETCH {
    my $this = shift;
    
    croak "autovivifing new rows and columns is disabled ($_[0]>$#$this)" if $_[0] > $#$this;

    $this->SUPER::FETCH(@_);
}

package Games::RolePlay::MapGen::_interconnected_map;

use common::sense;
use Carp;

1;

# interconnect_map {{{
sub interconnect_map {
    my $map = shift;

    # This interconnected array stuff is _REALLY_ handy, but it needs to be cleaned up, so it gets it's own class

    for($map, @$map) {
        my @a;
        tie @a, "Games::RolePlay::MapGen::_disallow_autoviv", $_;
        $_ = \@a;
    }

    for my $i (0 .. $#$map) {
        my $jend = $#{ $map->[$i] };

        for my $j (0 .. $jend) {
            $map->[$i][$j]->{nb} = {}; # clear it all out
            $map->[$i][$j]->{nb}{s} = $map->[$i+1][$j] unless $i == $#$map;
            $map->[$i][$j]->{nb}{n} = $map->[$i-1][$j] unless $i == 0;
            $map->[$i][$j]->{nb}{e} = $map->[$i][$j+1] unless $j == $jend;
            $map->[$i][$j]->{nb}{w} = $map->[$i][$j-1] unless $j == 0;
        }
    }

    # check
    for my $y (0 .. $#$map) {
        for my $x (0 .. $#{ $map->[$y] }) {
            for my $d (qw(n e s w)) {
                my $o = {n=>"s", s=>"n", e=>"w", w=>"e"}->{$d};

                my $t = $map->[$y][$x];
                my $n = $t->{nb}{$d};

                next unless $n;

                warn "od issues with ($x, $y):$d-$o" unless $t->{od}{$d} == $n->{od}{$o};
                warn "nb issues with ($x, $y):$d-$o" unless $n->{nb}{$o} == $t;
            }
        }
    }
}
# }}}
# disconnect_map {{{
sub disconnect_map {
    my $map = shift;

    local $@;

    eval {
        untie @$_ for grep {tied $_} @$map;
        untie @$map if tied $map;

        for my $i (0 .. $#$map) {
            my $jend = $#{ $map->[$i] };

            for my $j (0 .. $jend) {
                # Destroying the map wouldn't destroy the tiles if they're self
                # referencing like this.  That's not a problem because of the
                # global destructor, *whew*; except that each new map generated,
                # until perl exits, would eat up more memory.  

                delete $map->[$i][$j]{nb}; # So we have to break the self-refs here.
            }
        }
    };

    if( $@ ) {
        # NOTE: The above emits a fatal under global destruction for some
        # reason, probably the bless+tie gets cleaned up in the wrong order or
        # something.  It doesn't really matter since we're already exiting perl
        # anyway.  This assumption may be false under win32, where it may
        # create a memory leak.  Does windows clean up when a process exits?
        # It really aught to, but I have my doubts.

        die $@ unless $@ =~ m/global destruction/;
    }

    # You can test to make sure the tiles are dying when a map goes out of
    # scope by setting the VERBOSE_TILE_DEATH environment variable to a true
    # value.  If they fail to die when they go out of scope, it would say so on
    # the warning line.  If you'd really really like to see that, change the
    # {nb} above to {nb_borked} and you'll see what I mean.

    # Lastly, if you'd like to read a lengthy dissertation on this subject,
    # search for "Two-Phased" in the perlobj man page.
}
# }}}
# new {{{
sub new {
    my $class = shift;
    my $arg   = shift;
    my $map   = bless $arg, $class;

    $map->interconnect_map; # also used by save_map()

    return $map;
}
# }}}
# DESTROY {{{
sub DESTROY {
    my $map = shift;

    $map->disconnect_map; # also used by save_map()
}
# }}}

# }}}
# package ::_group; {{{
package Games::RolePlay::MapGen::_group;

use common::sense;

1;

# new {{{
sub new {
    my $class = shift;
    my $this  = bless {name=>"?", type=>"?", loc=>[], size=>[], loc_size=>"n/a"}, $class;

    if( @_ ) {
        my $h = {@_};
        $this->{name} = $h->{name} if exists $h->{name};
        $this->{type} = $h->{type} if exists $h->{type};
    }

    $this
}
# }}}
# name {{{
sub name {
    my $this = shift;
       $this->{name} = $_[0] if @_;

    $this->{name};
}
# }}}
# type {{{
sub type {
    my $this = shift;
       $this->{type} = $_[0] if @_;

    $this->{type};
}
# }}}
# desc {{{
sub desc {
    my $this = shift;

    $this->{loc_size};
}
# }}}
# add_rectangle {{{
sub add_rectangle {
    my $this = shift;
    my $loc  = shift;
    my $size = shift;
    my $mapo = shift;

    if( $loc and $size ) {
        push @{$this->{loc}},  $loc;
        push @{$this->{size}}, $size;
    }

    my @i = map  { $_->[0] }
            sort { $b->[1]<=>$a->[2] }
            map  { my $t = $this->{size}[$_]; [$_, $t->[0]*$t->[1]] }
            0 .. $#{$this->{loc}};

    my @to_kill; # remove these, they don't say anything
    my %points;  # don't count the same tiles over and over
    my $sloc    = [0,0];
    my $mloc    = [@{$this->{loc}[0]}];
    my $Mloc    = [@{$this->{loc}[0]}];
    my $nloc    = 0;
    for my $i (@i) {
        my $l = $this->{loc}[$i];
        my $s = $this->{size}[$i];

        my $x = $l->[0];
        my $y = $l->[1];

        my $i_count = 0;

        for my $xi (0 .. $s->[0]-1) {
        for my $yi (0 .. $s->[1]-1) {
            my $xc = $x + $xi;
            my $yc = $y + $yi;

            unless( $points{$xc}{$yc} ) {
                $points{$xc}{$yc} = 1;
                $i_count ++;

                $sloc->[0] += $xc;
                $sloc->[1] += $yc;
                $nloc ++;

                $mloc->[0] = $xc if $xc < $mloc->[0];
                $mloc->[1] = $yc if $yc < $mloc->[1];
                $Mloc->[0] = $xc if $xc > $Mloc->[0];
                $Mloc->[1] = $yc if $yc > $Mloc->[1];

                $mapo->[ $yc ][ $xc ]{group} = $this if $mapo;
            }

        }}

        push @to_kill, $i unless $i_count>0;
    }

    for my $kill (sort {$b<=>$a} @to_kill) {
        splice @{$this->{loc}},  $kill, 0;
        splice @{$this->{size}}, $kill, 0;
    }

    my $cloc = [0,0]; 
       $cloc = [ int($sloc->[0]/$nloc),  int($sloc->[1]/$nloc) ] if $nloc > 0;

    my $extent = [ $Mloc->[0]-$mloc->[0]+1, $Mloc->[1]-$mloc->[1]+1 ];

    $this->{loc_size} = "($cloc->[0], $cloc->[1]) $extent->[0]x$extent->[1]";
    $this->{extents}  = [ @$mloc, @$Mloc ];
}
# }}}
# enumerate_tiles {{{
sub enumerate_tiles {
    my $this = shift;

    my @i = map  { $_->[0] }
            sort { $b->[1]<=>$a->[2] }
            map  { my $t = $this->{size}[$_]; [$_, $t->[0]*$t->[1]] }
            0 .. $#{$this->{loc}};

    my @ret;
    my %points;  # don't count the same tiles over and over
    for my $i (@i) {
        my $l = $this->{loc}[$i];
        my $s = $this->{size}[$i];

        my $x = $l->[0];
        my $y = $l->[1];

        for my $xi (0 .. $s->[0]-1) {
        for my $yi (0 .. $s->[1]-1) {
            my $xc = $x + $xi;
            my $yc = $y + $yi;

            unless( $points{$xc}{$yc} ) {
                $points{$xc}{$yc} = 1;

                push @ret, [$xc,$yc];
            }

        }}
    }

    @ret;
}
# }}}
# enumerate_extents {{{
sub enumerate_extents {
    my $this = shift;

    @{ $this->{extents} };
}
# }}}

# }}}
# package ::_tile; {{{
package Games::RolePlay::MapGen::_tile;

use common::sense;

1;

sub dup {
    my $that  = shift;
    my $class = $that->{__c};
    my $this  = bless {od=>{n=>1, s=>1, e=>1, w=>1}}, $class;

    $this->{$_}     = $that->{$_}     for grep {not ref $that->{$_}} keys %$that;
    $this->{od}{$_} = $that->{od}{$_} for keys %{ $that->{od} };
    $this->{group}  = $that->{group};
    $this->{_dup} = 1;

    return $this;
}

sub new { my $class = shift; bless { @_, __c=>$class, v=>0, od=>{n=>0, s=>0, e=>0, w=>0} }, $class }
sub DESTROY { warn "tile verbosely dying" if $ENV{VERBOSE_TILE_DEATH} }  # search for VERBOSE above...
# }}}
# package ::_door; {{{
package Games::RolePlay::MapGen::_door;

use common::sense;

1;

sub new {
    my $class = shift; 
    my $this  = bless { @_ }, $class; 

    $this->{locked}   = 0 unless $this->{locked};
    $this->{stuck}    = 0 unless $this->{stuck};
    $this->{secret}   = 0 unless $this->{secret};
    $this->{open_dir} = { major=>undef, minor=>undef } unless ref($this->{open_dir});
    $this->{'open'}   = 0 unless $this->{'open'};

    return $this;
}

# }}}

package Games::RolePlay::MapGen::Tools;

use common::sense;
use Carp;
use parent q(Exporter);

our @EXPORT_OK = qw(choice roll random irange range str_eval _group _tile _door);

1;

# helper functions
# choice {{{
sub choice {
    return $_[&random(int @_)] || "";
}
# }}}
# roll {{{
sub roll {
    my ($num, $sides) = @_;
    my $roll = 0; 
    
    $roll += int rand $sides for 1 .. $num;
    $roll += $num;

    return $roll;
}
# }}}
# random {{{
sub random {
    return int rand shift;
}
# }}}
# range {{{
sub range {
    my $lhs = shift;
    my $rhs = shift;
    my $correlation = shift;

    ($lhs, $rhs) = ($rhs, $lhs) if $rhs < $lhs;

    my $rand;
    if( $correlation ) {
        croak "correlated range without previous value!!" unless defined $global::last_rand;

        if( $correlation == 1 ) {
            $rand = $global::last_rand;

        } elsif( $correlation == -1 ) {
            $rand = 1000000.0 - $global::last_rand;

        } else {
            croak "unsupported correlation value";
        }

    } else {
        $rand = rand 1000000.0;
        $global::last_rand = $rand;

    }

    $rand /= 1000000.0;

    my $diff = $rhs  - $lhs;
       $rand = $rand * $diff;

    return $lhs + $rand;
}
# }}}
# irange {{{
sub irange {
    my $il = shift;
    my $ir = shift;

    $il -= 0.4999999999;
    $ir += 0.4999999999;

    my $s = sprintf('%0.0f', range($il, $ir, @_));

    $s = 0 if $s eq "-0";

    return $s;
}
# }}}
# str_eval {{{
sub str_eval {
    my $str = shift;

    return int $str if $str =~ m/^\d+$/;

    $str =~ s/^\s*(\d+)d(\d+)\s*$/&roll($1, $2)/eg;
    $str =~ s/^\s*(\d+)d(\d+)\s*([\+\-])\s*(\d+)$/&roll($1, $2) + ($3 eq "+" ? $4 : 0-$4)/eg;

    return undef if $str =~ m/\D/;
    return int $str;
}
# }}}

sub _group { return new Games::RolePlay::MapGen::_group(@_) }
sub _tile  { return new Games::RolePlay::MapGen::_tile(@_) }
sub _door  { return new Games::RolePlay::MapGen::_door(@_) }

__END__
# Below is stub documentation for your module. You better edit it!

=head1 NAME

Games::RolePlay::MapGen::Tools - Some support tools and objects for the mapgen suite

=head1 SYNOPSIS

    use Games::RolePlay::MapGen::Tools qw( choice roll random range str_eval );

    my $r1 = roll(1, 20);                   # 1d20, 1-20
    my $r2 = random(20);                    # 0-19
    my $r3 = range(50, 100);                # some number between 50 and 100 (not an integer!)
    my $r4 = range(9000, 10000, 1);         # 100% positively correlated with the last range (ie, not random at all)
    my $r5 = range(7, 12, -1);              # 100% negatively correlated with the last range (ie, not random at all)
    my $ri = irange(0, 7);                  # An integer between 0 and 7
    my $e  = choice(qw(test this please));  # picks one of test, this, and please at random
    my $v  = str_eval("1d8");               # returns int(roll(1,8)) -- returns undef on parse error

    # This package also exports _group and _tile, which are shortcut functions for new
    # Games::RolePlay::MapGen::_tile and ::_group objects.

=head1 Games::RolePlay::MapGen::_group

At this time, the ::_group object is just a blessed hash that contains some
variables that need to be set by the ::Generator objects.

   ## 1.0.3 ## $group->{name}     = "Room #$rn";
   ## 1.0.3 ## $group->{loc_size} = "$size[0]x$size[1] ($spot[0], $spot[1])";
   ## 1.0.3 ## $group->{type}     = "room";
   ## 1.0.3 ## $group->{size}     = [@size];
   ## 1.0.3 ## $group->{loc}      = [@spot];

   # Starting with 1.1.0, rooms can have irregular shapes made up of
   # rectangles.  The rectangles start at @start = @{$group->{loc}[$i]} and
   # have @size = @{$group->{size}[$i]}.

   $group->{name} = "Room #$rn";
   $group->{type} = "room";
   $group->{loc}  = [\@spot, \@another_spot, ...];
   $group->{size} = [\@size, \@another_size, ...];

   # The loc_size description became problematic in 1.1.x.  It will now
   # indicate the maximum extent (x-diff x y-diff) and the "center of mass"
   # (average location).

   $group->{loc_size} = "($center[0], $center[1]) $extent[0]x$extent[1]";

   Happily, there is a new method to add a rectangle that does all the work:

   $group->add_rectangle(\@loc, \@size);

   If you pass the map object, it will mark the appropriate tiles its $self

   $group->add_rectangle(\@loc, \@size, $the_map);
    # marks $mapo->[ $y ][ $x ]{group} = $group;
    # (can be called without arguments to recalculate {loc_size})

   # There is also a new $group->{extents}, which describes the min and max
   # locations.  They are really only useful for truely rectangular rooms.

   # These new methods can be used to access the group's tiles and extents:
   my @tiles   = $group->enumerate_tiles;
   my @extents = $group->enumerate_extents;

   # lastly,
   my $desc = $group->desc; # returns the {loc_size} calculated by add_rectangle

=head1 Games::RolePlay::MapGen::_tile

At this time, the ::_tile object is just a blessed hash that the
::Generators instantiate at every map location.  There are no required
variables at this time.

    v=>0, 
    od=>{n=>0, s=>0, e=>0, w=>0}

Though, for convenience, visited is set to 0 and "open directions" is set to
all zeros.

=head1 Games::RolePlay::MapGen::_interconnected_map

This object interconnects all the tiles in a map array, so 
$tile = $map->[$y][$x] and $tile->{nb} is an array of neighboring tiles.
Example: $east_neighbor = $map->[$y][$x]->{nb}{e};

(It also cleans up self-referencing loops at DESTROY time.)

=head1 Games::RolePlay::MapGen::_door

A simple object that stores information about a door.  Example:

    my $door = &_door(
        stuck    => 0,
        locked   => 0,
        secret   => 0,
        open_dir => {
            major => "n", # the initial direction of the opening door
            minor => "w", # the final direction of the opening door (think 90 degree swing)
        },
    );

    print "The door is locked.\n" if $door->{locked};

=head1 AUTHOR

Jettero Heller <japh@voltar-confed.org>

Jet is using this software in his own projects...
If you find bugs, please please please let him know. :)

Actually, let him know if you find it handy at all.
Half the fun of releasing this stuff is knowing 
that people use it.

=head1 COPYRIGHT

Copyright (c) 2008 Paul Miller -- LGPL [Software::License::LGPL_2_1]

    perl -MSoftware::License::LGPL_2_1 -e '$l = Software::License::LGPL_2_1->new({holder=>"Paul Miller"});
          print $l->fulltext' | less

=head1 SEE ALSO

perl(1)

=cut