Hi. I'm attempting to write a roguelike (think nethack, crawl, angband,
tome, adom, and yes, I suppose even rogue) in Perl and I've ran into a
problem regarding the data structure for the map and handling it easily.
A map is one 2D level, a simple grid. Each tile on the grid will be a
hash of data about the grid. The map object itself has other bits of
data, so that's why I don't just make the object an anonymous array:
$map->{Map}[$y][$x] = { data about this grid tile }
So far, so good. I was using methods to access and change tiles, but it
was extremely awkward. Which looks more convenient?
$map->tile($y, $x, "Type") = "wall";
$map->{Map}[$y][$x]{Type} = "wall";
Well, bad example. But anyway, I seriously despised the first form. The
syntax itself is extremely awkward and when I generate random dungeons,
I'd have to use that awkward syntax a lot. So I messed around with
Tie::Autotie, decided it wasn't up to the task, and cobbled together a
package that I tied both anonymous arrays (The first representing rows,
pointing to the second which represented columns. Y, X is easier to deal
with sometimes.) to. The hash of data describing a tile consists of
different things, but for the most part, I'd by modifying and accessing
one key in the hash. Now I'm at the point where I can do this:
print $map->{Map}[$y][$x]; # $map->{Map}[$y][$x]{Symbol} is printed,
# through FETCH
$map->{Map}[$y][$x] = "wall"; # This basically sets
# $map->{Map}[$y][$x]{Type} to a hashref
# describing a template of that type.
Convenient and easy, so far. Then I run into the first problem. What if
I want to access or change $map->{Map}[$y][$x]{foobar}? The way I tied
the arrays didn't allow for this. So I hacked together a real messy
sort of hack, which is shown somewhere below. So far, I'm coping with
the shortcomings of tie.
But then I realized that when I assign a tile to "wall", other things
should happen. Things regarding the $map object itself. Meaning that I
need a reference to $map, as well as the value of $y. From the 'point of
view' of $x's tied FETCH routine, I cannot access these.
I know Perl, each time it encounters a tied variable that's part of a
multi-level structure, it'll take a temp value and do something with
that. Step by step. It's impossible to tie an entire data structure to
where one FETCH routine could know $map, $y, and $x.
Then I realized something. I was trying to re-implement the Unix
directory structure, complete with a level of indirection that more or
less happened to be '..'
I suppose I could replace the arrays with hashes, and instead do
$map->{Map}{$y}{$x}. In the $y hash and the $x hash (technically
inaccurate, but from the point of view of $y/$x) I could have a "back"
key pointing to the data structure that contains it. This'd solve some
problems and probably create other unforseen ones. Iterating over the
hash instead of the array would be no problem with the foreach loops I
use already.
I'd still like to avoid method calls to change and access tiles. It *is*
slower, as I've experimented and tried it. Maps will get quite big,
sometimes around 300x500 and there are a great number of maps randomly
generated. And the syntax is just easier.
Anyway, the thing that started me on all this was convenience. Accessing
and modifying $map->{Map}[$y][$x] and having all the fancy stuff happen
behind the scenes. But the way I'm tying stuff down right now is
probably a bit messy, as it won't let me _cleanly_ manipulate anything
other than that one key in the hash. Is there an easier way around all
of this mess?
If interested, here's the module I ended up writing. Buggy and messy,
beware.
######################
# Roguelike::Tilemap #
######################
package Roguelike::Tilemap;
use strict;
use warnings;
use Roguelike::Utility;
use Roguelike::Game;
use Roguelike::Tiles;
our $TILE = load Roguelike::Tiles;
our $_GET = "Symbol";
sub TIEARRAY {
my $class = shift;
return bless [ @_ ], $class;
}
sub FETCH {
my $self = shift;
my $index = shift;
return $self->[$index]{$_GET} if ref $self->[$index] eq
"HASH";
return $self->[$index];
}
sub STORE {
elf = shift;
my $index = shift;
my $value = shift;
if ($_GET eq "Symbol") {
if (ref $value eq "ARRAY") {
tie my @data, "Roguelike::Tilemap", @{$value};
$value = \@data;
} else {
$value = $TILE->{$value};
}
}
$self->[$index] = $value;
}
sub FETCHSIZE {
my $self = shift;
return @{$self};
}
sub STORESIZE {
my $self = shift;
my $count = shift;
if ($count > $self->FETCHSIZE) {
foreach ($count - $self->FETCHSIZE() .. $count) {
$self->STORE($_, "");
}
} elsif ($count < $self->FETCHSIZE) {
foreach (0 .. $self->FETCHSIZE() - $count - 2) {
$self->POP();
}
}
}
sub EXTEND {
my $self = shift;
my $count = shift;
$self->STORESIZE($count);
}
sub EXISTS {
my $self = shift;
my $index = shift;
return defined $self->[$index];
}
sub DELETE {
my $self = shift;
my $index = shift;
}
sub CLEAR {
my $self = shift;
return $self = [];
}
sub PUSH {
my $self = shift;
my @list = @_;
my $last = $self->FETCHSIZE();
$self->STORE($last + $_, $list[$_]) foreach 0 .. $#list;
return $self->FETCHSIZE();
}
sub POP {
my $self = shift;
return pop @{$self};
}
sub SHIFT {
my $self = shift;
return shift @{$self};
}
sub UNSHIFT {
my $self = shift;
my @list = @_;
my $size = scalar(@list);
@{$self}[$size .. $#{$self} + $size] = @{$self};
$self->STORE($_, $list[$_]) foreach 0 .. $#list;
}
sub new {
my $class = shift;
tie my @map, "Roguelike::Tilemap";
my $width = shift;
my $height = shift;
my $tile = shift || " ";
foreach my $y (0 .. $height - 1) { # starting at 1 blows up
$map[$y] = [];
foreach my $x (0 .. $width - 1) { # starting at 1 blows up
$map[$y][$x] = $tile;
}
}
my $self = bless { Map => \@map, Data => [] }, $class;
return $self;
}
sub get {
my $self = shift;
my $y = shift;
my $x = shift;
my $attrib = shift;
my $value = shift;
$_GET = $attrib;
$self->{Map}[$y][$x] = $value if defined $value;
my $data = $self->{Map}[$y][$x];
$_GET = "Symbol";
return $data;
}
42;
And a test script, which sort of works.
#!/usr/bin/perl
use strict;
use warnings;
use lib "/home/dabreegster/tilemap";
use Roguelike::Tilemap;
use Roguelike::Utility;
my $map = new Roguelike::Tilemap 2, 5;
dump $map; # doesn't quite work!
print "Y" if $map->{Map}[2][0] eq " ";
print $map->get(2, 0, "Type");
$map->get(3, 3, "foo" => "bar");
print $map->get(3, 3, "foo");
# always give em a width and height. Then go through each one slowly and assign
# it (these comments won't make sense, don't worry)
# if the Tile hash contains a ref to another hash, array, whatever, it doesn't
# work. Ohwell.
Any tips?
~ 'dabreegster' in #perl and #perlcafe on irc.freenode.org