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