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

# Content
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 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
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.02';
28
29 =item $level = new Games::Sokoban [format => "text|rle|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 Sets the level from the given data.
76
77 =cut
78
79 sub data {
80 if (@_ > 1) {
81 my ($self, $data, $format) = @_;
82
83 $format ||= detect_format $data;
84
85 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
124 $self->update;
125 }
126
127 $_[0]{data}
128 }
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 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 =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 =item $line = $level->as_rle
221
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 Returns (0-based) starting coordinate.
239
240 =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 # sokevo internally locks some cells
414 $data =~ y/^%:,;-=?/ #.$* +#/;
415
416 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