ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Games-Sokoban/Sokoban.pm
Revision: 1.2
Committed: Tue May 11 23:20:21 2010 UTC (14 years ago) by root
Branch: MAIN
Changes since 1.1: +2 -2 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 =head1 NAME
2    
3     Games::Sokoban - load/transform/save sokoban levels in various formats
4    
5     =head1 SYNOPSIS
6    
7     use Games::Sokoban;
8    
9     =head1 DESCRIPTION
10    
11     I needed something like this quickly - if you need better docs, you have to ask.
12    
13 root 1.2 Supports xsb (text), rle, sokevo and a small "binpack" format for input
14     and output and can normalise levels as well as calculate unique IDs.
15 root 1.1
16     =over 4
17    
18     =cut
19    
20     package Games::Sokoban;
21    
22     use common::sense;
23    
24     use Carp ();
25     use List::Util ();
26    
27     our $VERSION = '0.01';
28    
29     =item $level = new Games::Sokoban [format => "text|binpack"], [data => "###..."]
30    
31     =cut
32    
33     sub new {
34     my ($class, %arg) = @_;
35    
36     my $self = bless \%arg, $class;
37    
38     $self->data (delete $self->{data}, delete $self->{format})
39     if exists $self->{data};
40    
41     $self
42     }
43    
44     =item $level = new_from_file Games::Sokoban $path[, $format]
45    
46     =cut
47    
48     sub new_from_file {
49     my ($class, $path, $format) = @_;
50    
51     open my $fh, "<:perlio", $path
52     or Carp::croak "$path: $!";
53     local $/;
54    
55     $class->new (data => (scalar <$fh>), format => $format)
56     }
57    
58     sub detect_format($) {
59     my ($data) = @_;
60    
61     return "text" if $data =~ /^[ #\@\*\$\.\+\015\012\-_]+$/;
62    
63     warn $data;#d#
64     return "rle" if $data =~ /^[ #\@\*\$\.\+\015\012\-_|1-9]+$/;
65     exit 5;#d#
66    
67     my ($a, $b) = unpack "ww", $data;
68     return "binpack" if defined $a && defined $b;
69    
70     Carp::croak "unable to autodetect sokoban level format";
71     }
72    
73     =item $level->data ([$new_data, [$new_data_format]]])
74    
75     =cut
76    
77     sub data {
78     my ($self, $data, $format) = @_;
79    
80     $format ||= detect_format $data;
81    
82     if ($format eq "text" or $format eq "rle") {
83     $data =~ y/-_|/ \n/;
84     $data =~ s/(\d)(.)/$2 x $1/ge;
85     my @lines = split /[\015\012]+/, $data;
86     my $w = List::Util::max map length, @lines;
87    
88     $_ .= " " x ($w - length)
89     for @lines;
90    
91     $self->{data} = join "\n", @lines;
92    
93     } elsif ($format eq "binpack") {
94     (my ($w, $s), $data) = unpack "wwB*", $data;
95    
96     my @enc = ('#', '$', '.', ' ', ' ', '###', '*', '# ');
97    
98     $data = join "",
99     map $enc[$_],
100     unpack "C*",
101     pack "(b*)*",
102     unpack "(a3)*", $data;
103    
104     # clip extra chars (max. 2)
105     my $extra = (length $data) % $w;
106     substr $data, -$extra, $extra, "" if $extra;
107    
108     (substr $data, $s, 1) =~ y/ ./@+/;
109    
110     $self->{data} =
111     join "\n",
112     map "#$_#",
113     "#" x $w,
114     (unpack "(a$w)*", $data),
115     "#" x $w;
116    
117     } else {
118     Carp::croak "$format: unsupported sokoban level format requested";
119     }
120    
121     $self->update;
122    
123     ($self->{data})
124     }
125    
126     sub pos2xy {
127     use integer;
128    
129     $_[1] >= 0
130     or Carp::croak "illegal buffer offset";
131    
132     (
133     $_[1] % ($_[0]{w} + 1),
134     $_[1] / ($_[0]{w} + 1),
135     )
136     }
137    
138     sub update {
139     my ($self) = @_;
140    
141     for ($self->{data}) {
142     s/^\n+//;
143     s/\n$//;
144    
145     /^[^\n]+/ or die;
146    
147     $self->{w} = index $_, "\n";
148     $self->{h} = y/\n// + 1;
149     }
150     }
151    
152     =item $text = $level->as_text
153    
154     =cut
155    
156     sub as_text {
157     my ($self) = @_;
158    
159     "$self->{data}\n"
160     }
161    
162     =item $binary = $level->as_binpack
163    
164     =cut
165    
166     sub as_binpack {
167     my ($self) = @_;
168    
169     my $binpack = chr $self->{w} - 2;
170    
171     my $w = $self->{w};
172    
173     my $data = $self->{data};
174    
175     # crop away all four borders
176     $data =~ s/^#+\n//;
177     $data =~ s/#+$//;
178     $data =~ s/#$//mg;
179     $data =~ s/^#//mg;
180    
181     $data =~ y/\n//d;
182    
183     $data =~ /[\@\+]/ or die;
184     my $s = $-[0];
185     (substr $data, $s, 1) =~ y/@+/ ./;
186    
187     $data =~ s/\#\#\#/101/g;
188     $data =~ s/\ \ \ /110/g;
189     $data =~ s/\#\ /111/g;
190    
191     $data =~ s/\#/000/g;
192     $data =~ s/\ /001/g;
193     $data =~ s/\./010/g;
194     $data =~ s/\*/011/g;
195     $data =~ s/\$/100/g;
196    
197     # width, @-offset, data
198    
199     pack "wwB*", $w - 2, $s, $data
200     }
201    
202     =item @lines = $level->as_lines
203    
204     =cut
205    
206     sub as_lines {
207     split /\n/, $_[0]{data}
208     }
209    
210     =item @lines = $level->as_rle
211    
212     http://www.sokobano.de/wiki/index.php?title=Level_format
213    
214     =cut
215    
216     sub as_rle {
217     my $data = $_[0]{data};
218    
219     $data =~ s/ +$//mg;
220     $data =~ y/\n /|-/;
221     $data =~ s/((.)\2{2,8})/(length $1) . $2/ge;
222    
223     $data
224     }
225    
226     =item ($x, $y) = $level->start
227    
228     =cut
229    
230     sub start {
231     my ($self) = @_;
232    
233     $self->{data} =~ /[\@\+]/ or Carp::croak "level has no starting point";
234     $self->pos2xy ($-[0]);
235     }
236    
237     =item $level->hflip
238    
239     =item $level->vflip
240    
241     =item $level->transpose # topleft to bottomright
242    
243     =item $level->rotate_90
244    
245     =item $level->rotate_180
246    
247     =cut
248    
249     sub hflip {
250     $_[0]{data} = join "\n", map { scalar reverse $_ } split /\n/, $_[0]{data};
251     }
252    
253     sub vflip {
254     $_[0]{data} = join "\n", reverse split /\n/, $_[0]{data};
255     }
256    
257     sub transpose {
258     my ($self) = @_;
259    
260     # there must be a more elegant way :/
261     my @c;
262    
263     for (split /\n/, $self->{data}) {
264     my $i;
265    
266     $c[$i++] .= $_ for split //;
267     }
268    
269     $self->{data} = join "\n", @c;
270     ($self->{w}, $self->{h}) = ($self->{h}, $self->{w})
271     }
272    
273     sub rotate_90 {
274     $_[0]->vflip;
275     $_[0]->transpose;
276     }
277    
278     sub rotate_180 {
279     $_[0]{data} = reverse $_[0]{data};
280     }
281    
282     =item $id = $level->simplify
283    
284     Detect playable area, crop to smallest size.
285    
286     =cut
287    
288     sub simplify {
289     my ($self) = @_;
290    
291     # first detect playable area
292     my ($w, $h) = ($self->{w}, $self->{h});
293     my ($x, $y) = $self->start;
294    
295     my @data = split /\n/, $self->{data};
296     my @mask = @data;
297    
298     y/#/\x00/c, y/#/\x7f/ for @mask;
299    
300     my @stack = [$x, $y, 0];
301    
302     while (@stack) {
303     my ($x, $y, $l) = @{ pop @stack };
304     my $line = $mask[$y];
305    
306     for my $x ($x .. $x + $l) {
307     (reverse substr $line, 0, $x + 1) =~ /\x00+/
308     or next;
309    
310     $l = $+[0];
311    
312     $x -= $l - 1;
313     (substr $line, $x) =~ /^\x00+/ or die;
314     $l = $+[0];
315    
316     substr $mask[$y], $x, $l, "\xff" x $l;
317    
318     push @stack, [$x, $y - 1, $l - 1] if $y > 0;
319     push @stack, [$x, $y + 1, $l - 1] if $y < $h - 1;
320     }
321     }
322    
323     my $walls = "#" x $w;
324    
325     for (0 .. $h - 1) {
326     $data[$_] = ($data[$_] & $mask[$_]) | ($walls & ~$mask[$_]);
327     }
328    
329     # reduce borders
330     pop @data while @data > 2 && $data[-2] eq $walls; # bottom
331     shift @data while $data[1] eq $walls; # top
332    
333     for ($self->{data} = join "\n", @data) {
334     s/#$//mg until /[^#]#$/m; # right
335     s/^#//mg until /^#[^#]/m; # left
336     }
337    
338     # phew, done
339     }
340    
341     =item $id = $level->normalise
342    
343     normalises the level map and calculates/returns it's identity code
344    
345     http://www.sourcecode.se/sokoban/level_id.php, assume uppercase and hex.
346    
347     =cut
348    
349     sub normalise {
350     my ($self) = @_;
351    
352     $self->simplify;
353    
354     require Digest::MD5;
355    
356     my ($best_md5, $best_data) = "\xff" x 9;
357    
358     my $chk = sub {
359     my $md5 = substr Digest::MD5::md5 ("$self->{data}\n"), 0, 8;
360     if ($md5 lt $best_md5) {
361     $best_md5 = $md5;
362     $best_data = $self->{data};
363     }
364     };
365    
366     $chk->(); $self->hflip;
367     $chk->(); $self->vflip;
368     $chk->(); $self->hflip;
369     $chk->(); $self->rotate_90;
370     $chk->(); $self->hflip;
371     $chk->(); $self->vflip;
372     $chk->(); $self->hflip;
373     $chk->();
374    
375     $self->data ($best_data, "text");
376    
377     uc unpack "H*", $best_md5
378     }
379    
380     =item $levels = Games::Sokoban::load_sokevo $path
381    
382     Loads a sokevo snapshot/history file and returns all contained levels as
383     Games::Sokoban objects in an arrayref.
384    
385     =cut
386    
387     sub load_sokevo($) {
388     open my $fh, "<", $_[0]
389     or Carp::croak "$_[0]: $!";
390    
391     my @levels;
392    
393     while (<$fh>) {
394     if (/^##+$/) {
395     my $data = $_;
396     while (<$fh>) {
397     $data .= $_;
398     last if /^$/;
399     }
400    
401     push @levels, new Games::Sokoban data => $data;
402     }
403     }
404    
405     \@levels
406     }
407    
408     1;
409    
410     =back
411    
412     =head1 AUTHOR
413    
414     Marc Lehmann <schmorp@schmorp.de>
415     http://home.schmorp.de/
416    
417     =cut
418