ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/Games-Sokoban/Sokoban.pm
Revision: 1.8
Committed: Wed May 12 00:24:37 2010 UTC (14 years ago) by root
Branch: MAIN
Changes since 1.7: +6 -0 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 root 1.3 our $VERSION = '0.02';
28 root 1.1
29 root 1.4 =item $level = new Games::Sokoban [format => "text|rle|binpack"], [data => "###..."]
30 root 1.1
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 root 1.6 Sets the level from the given data.
76    
77 root 1.1 =cut
78    
79     sub data {
80 root 1.6 if (@_ > 1) {
81     my ($self, $data, $format) = @_;
82    
83     $format ||= detect_format $data;
84 root 1.1
85 root 1.6 if ($format eq "text" or $format eq "rle") {
86     $data =~ y/-_|/ \n/;
87     $data =~ s/(\d)(.)/$2 x $1/ge;
88     my @lines = split /[\015\012]+/, $data;
89     my $w = List::Util::max map length, @lines;
90    
91     $_ .= " " x ($w - length)
92     for @lines;
93    
94     $self->{data} = join "\n", @lines;
95    
96     } elsif ($format eq "binpack") {
97     (my ($w, $s), $data) = unpack "wwB*", $data;
98    
99     my @enc = ('#', '$', '.', ' ', ' ', '###', '*', '# ');
100    
101     $data = join "",
102     map $enc[$_],
103     unpack "C*",
104     pack "(b*)*",
105     unpack "(a3)*", $data;
106    
107     # clip extra chars (max. 2)
108     my $extra = (length $data) % $w;
109     substr $data, -$extra, $extra, "" if $extra;
110    
111     (substr $data, $s, 1) =~ y/ ./@+/;
112    
113     $self->{data} =
114     join "\n",
115     map "#$_#",
116     "#" x $w,
117     (unpack "(a$w)*", $data),
118     "#" x $w;
119    
120     } else {
121     Carp::croak "$format: unsupported sokoban level format requested";
122     }
123 root 1.1
124 root 1.6 $self->update;
125 root 1.1 }
126    
127 root 1.6 $_[0]{data}
128 root 1.1 }
129    
130     sub pos2xy {
131     use integer;
132    
133     $_[1] >= 0
134     or Carp::croak "illegal buffer offset";
135    
136     (
137     $_[1] % ($_[0]{w} + 1),
138     $_[1] / ($_[0]{w} + 1),
139     )
140     }
141    
142     sub update {
143     my ($self) = @_;
144    
145     for ($self->{data}) {
146     s/^\n+//;
147     s/\n$//;
148    
149     /^[^\n]+/ or die;
150    
151     $self->{w} = index $_, "\n";
152     $self->{h} = y/\n// + 1;
153     }
154     }
155    
156     =item $text = $level->as_text
157    
158     =cut
159    
160     sub as_text {
161     my ($self) = @_;
162    
163     "$self->{data}\n"
164     }
165    
166     =item $binary = $level->as_binpack
167    
168 root 1.8 Binpack is a very compact binary format (usually 17% of the size of an xsb
169     file), that is still reasonably easy to encode/decode.
170    
171     It only tries to store simplified levels with full fidelity - other levels
172     can be slightly changed outside the playable area.
173    
174 root 1.1 =cut
175    
176     sub as_binpack {
177     my ($self) = @_;
178    
179     my $binpack = chr $self->{w} - 2;
180    
181     my $w = $self->{w};
182    
183     my $data = $self->{data};
184    
185     # crop away all four borders
186     $data =~ s/^#+\n//;
187     $data =~ s/#+$//;
188     $data =~ s/#$//mg;
189     $data =~ s/^#//mg;
190    
191     $data =~ y/\n//d;
192    
193     $data =~ /[\@\+]/ or die;
194     my $s = $-[0];
195     (substr $data, $s, 1) =~ y/@+/ ./;
196    
197     $data =~ s/\#\#\#/101/g;
198     $data =~ s/\ \ \ /110/g;
199     $data =~ s/\#\ /111/g;
200    
201     $data =~ s/\#/000/g;
202     $data =~ s/\ /001/g;
203     $data =~ s/\./010/g;
204     $data =~ s/\*/011/g;
205     $data =~ s/\$/100/g;
206    
207     # width, @-offset, data
208    
209     pack "wwB*", $w - 2, $s, $data
210     }
211    
212     =item @lines = $level->as_lines
213    
214     =cut
215    
216     sub as_lines {
217     split /\n/, $_[0]{data}
218     }
219    
220 root 1.5 =item $line = $level->as_rle
221 root 1.1
222     http://www.sokobano.de/wiki/index.php?title=Level_format
223    
224     =cut
225    
226     sub as_rle {
227     my $data = $_[0]{data};
228    
229     $data =~ s/ +$//mg;
230     $data =~ y/\n /|-/;
231     $data =~ s/((.)\2{2,8})/(length $1) . $2/ge;
232    
233     $data
234     }
235    
236     =item ($x, $y) = $level->start
237    
238 root 1.5 Returns (0-based) starting coordinate.
239    
240 root 1.1 =cut
241    
242     sub start {
243     my ($self) = @_;
244    
245     $self->{data} =~ /[\@\+]/ or Carp::croak "level has no starting point";
246     $self->pos2xy ($-[0]);
247     }
248    
249     =item $level->hflip
250    
251     =item $level->vflip
252    
253     =item $level->transpose # topleft to bottomright
254    
255     =item $level->rotate_90
256    
257     =item $level->rotate_180
258    
259     =cut
260    
261     sub hflip {
262     $_[0]{data} = join "\n", map { scalar reverse $_ } split /\n/, $_[0]{data};
263     }
264    
265     sub vflip {
266     $_[0]{data} = join "\n", reverse split /\n/, $_[0]{data};
267     }
268    
269     sub transpose {
270     my ($self) = @_;
271    
272     # there must be a more elegant way :/
273     my @c;
274    
275     for (split /\n/, $self->{data}) {
276     my $i;
277    
278     $c[$i++] .= $_ for split //;
279     }
280    
281     $self->{data} = join "\n", @c;
282     ($self->{w}, $self->{h}) = ($self->{h}, $self->{w})
283     }
284    
285     sub rotate_90 {
286     $_[0]->vflip;
287     $_[0]->transpose;
288     }
289    
290     sub rotate_180 {
291     $_[0]{data} = reverse $_[0]{data};
292     }
293    
294     =item $id = $level->simplify
295    
296     Detect playable area, crop to smallest size.
297    
298     =cut
299    
300     sub simplify {
301     my ($self) = @_;
302    
303     # first detect playable area
304     my ($w, $h) = ($self->{w}, $self->{h});
305     my ($x, $y) = $self->start;
306    
307     my @data = split /\n/, $self->{data};
308     my @mask = @data;
309    
310     y/#/\x00/c, y/#/\x7f/ for @mask;
311    
312     my @stack = [$x, $y, 0];
313    
314     while (@stack) {
315     my ($x, $y, $l) = @{ pop @stack };
316     my $line = $mask[$y];
317    
318     for my $x ($x .. $x + $l) {
319     (reverse substr $line, 0, $x + 1) =~ /\x00+/
320     or next;
321    
322     $l = $+[0];
323    
324     $x -= $l - 1;
325     (substr $line, $x) =~ /^\x00+/ or die;
326     $l = $+[0];
327    
328     substr $mask[$y], $x, $l, "\xff" x $l;
329    
330     push @stack, [$x, $y - 1, $l - 1] if $y > 0;
331     push @stack, [$x, $y + 1, $l - 1] if $y < $h - 1;
332     }
333     }
334    
335     my $walls = "#" x $w;
336    
337     for (0 .. $h - 1) {
338     $data[$_] = ($data[$_] & $mask[$_]) | ($walls & ~$mask[$_]);
339     }
340    
341     # reduce borders
342     pop @data while @data > 2 && $data[-2] eq $walls; # bottom
343     shift @data while $data[1] eq $walls; # top
344    
345     for ($self->{data} = join "\n", @data) {
346     s/#$//mg until /[^#]#$/m; # right
347     s/^#//mg until /^#[^#]/m; # left
348     }
349    
350     # phew, done
351     }
352    
353     =item $id = $level->normalise
354    
355     normalises the level map and calculates/returns it's identity code
356    
357     http://www.sourcecode.se/sokoban/level_id.php, assume uppercase and hex.
358    
359     =cut
360    
361     sub normalise {
362     my ($self) = @_;
363    
364     $self->simplify;
365    
366     require Digest::MD5;
367    
368     my ($best_md5, $best_data) = "\xff" x 9;
369    
370     my $chk = sub {
371     my $md5 = substr Digest::MD5::md5 ("$self->{data}\n"), 0, 8;
372     if ($md5 lt $best_md5) {
373     $best_md5 = $md5;
374     $best_data = $self->{data};
375     }
376     };
377    
378     $chk->(); $self->hflip;
379     $chk->(); $self->vflip;
380     $chk->(); $self->hflip;
381     $chk->(); $self->rotate_90;
382     $chk->(); $self->hflip;
383     $chk->(); $self->vflip;
384     $chk->(); $self->hflip;
385     $chk->();
386    
387     $self->data ($best_data, "text");
388    
389     uc unpack "H*", $best_md5
390     }
391    
392     =item $levels = Games::Sokoban::load_sokevo $path
393    
394     Loads a sokevo snapshot/history file and returns all contained levels as
395     Games::Sokoban objects in an arrayref.
396    
397     =cut
398    
399     sub load_sokevo($) {
400     open my $fh, "<", $_[0]
401     or Carp::croak "$_[0]: $!";
402    
403     my @levels;
404    
405     while (<$fh>) {
406     if (/^##+$/) {
407     my $data = $_;
408     while (<$fh>) {
409     $data .= $_;
410     last if /^$/;
411     }
412    
413 root 1.7 # sokevo internally locks some cells
414     $data =~ y/^%:,;-=?/ #.$* +#/;
415    
416 root 1.1 push @levels, new Games::Sokoban data => $data;
417     }
418     }
419    
420     \@levels
421     }
422    
423     1;
424    
425     =back
426    
427     =head1 AUTHOR
428    
429     Marc Lehmann <schmorp@schmorp.de>
430     http://home.schmorp.de/
431    
432     =cut
433