File Coverage

blib/lib/Game/TextPatterns.pm
Criterion Covered Total %
statement 292 296 98.6
branch 105 128 82.0
condition 40 63 63.4
subroutine 37 37 100.0
pod 25 26 96.1
total 499 550 90.7


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # generate patterns of text. run perldoc(1) on this file for documentation
4              
5             package Game::TextPatterns;
6              
7 2     2   368559 use 5.24.0;
  2         12  
8 2     2   12 use warnings;
  2         5  
  2         70  
9 2     2   12 use Carp qw(croak);
  2         4  
  2         96  
10 2     2   10 use List::Util qw(min);
  2         4  
  2         104  
11 2     2   1069 use Moo;
  2         22263  
  2         10  
12 2     2   3691 use namespace::clean;
  2         22500  
  2         22  
13 2     2   626 use Scalar::Util qw(looks_like_number);
  2         5  
  2         8462  
14              
15             our $VERSION = '1.24';
16              
17             with 'MooX::Rebuild'; # for ->rebuild (which differs from ->clone)
18              
19             has pattern => (
20             is => 'rw',
21             coerce => sub {
22             my $type = ref $_[0];
23             if ( $type eq "" ) {
24             my @pat = split $/, $_[0];
25             my $len = length $pat[0];
26             for my $i ( 1 .. $#pat ) {
27             die "columns must be of equal length" if length $pat[$i] != $len;
28             }
29             return \@pat;
30             } elsif ( $type eq 'ARRAY' ) {
31             my $len = length $_[0]->[0];
32             for my $i ( 1 .. $_[0]->$#* ) {
33             die "columns must be of equal length" if length $_[0]->[$i] != $len;
34             }
35             return [ $_[0]->@* ];
36             } elsif ( $_[0]->can("pattern") ) {
37             return [ $_[0]->pattern->@* ];
38             } else {
39             die "unknown pattern type '$type'";
40             }
41             },
42             );
43              
44             sub BUILD {
45 93     93 0 4125 my ( $self, $param ) = @_;
46 93 100       594 croak "a pattern must be supplied" unless exists $param->{pattern};
47             }
48              
49             ########################################################################
50             #
51             # METHODS
52              
53             sub append_cols {
54 22     22 1 53 my ( $self, $fill, $pattern ) = @_;
55 22 50       52 croak "need append_cols(fill, pattern)" if !defined $pattern;
56 22         45 my ( $fill_cur, $fill_new );
57 22 100       52 if ( ref $fill eq 'ARRAY' ) {
58 2         5 ( $fill_cur, $fill_new ) = $fill->@*;
59             } else {
60 20         33 $fill_cur = $fill_new = $fill;
61             }
62 22         344 my $pat = $self->pattern;
63 22         422 my @cur_dim = ( length $_[0]->pattern->[0], scalar $_[0]->pattern->@* );
64 22         201 my @new_dim = $pattern->dimensions;
65 22 100       214 if ( $cur_dim[1] > $new_dim[1] ) {
    100          
66 1         5 for my $i ( 1 .. $cur_dim[1] - $new_dim[1] ) {
67 2         9 $pat->[ -$i ] .= $fill_new x $new_dim[0];
68             }
69             } elsif ( $cur_dim[1] < $new_dim[1] ) {
70 2         7 for my $i ( 1 .. $new_dim[1] - $cur_dim[1] ) {
71 4         12 push $pat->@*, $fill_cur x $cur_dim[0];
72             }
73             }
74 22         325 my $new = $pattern->pattern;
75 22         123 for my $i ( 0 .. $new_dim[1] - 1 ) {
76 53         94 $pat->[$i] .= $new->[$i];
77             }
78 22         48 return $self;
79             }
80              
81             sub append_rows {
82 12     12 1 35 my ( $self, $fill, $pattern ) = @_;
83 12 50       30 croak "need append_rows(fill, pattern)" if !defined $pattern;
84 12         23 my ( $fill_cur, $fill_new );
85 12 100       28 if ( ref $fill eq 'ARRAY' ) {
86 2         6 ( $fill_cur, $fill_new ) = $fill->@*;
87             } else {
88 10         15 $fill_cur = $fill_new = $fill;
89             }
90 12         191 my $pat = $self->pattern;
91 12         218 my @cur_dim = ( length $_[0]->pattern->[0], scalar $_[0]->pattern->@* );
92 12         110 my @new_dim = $pattern->dimensions;
93 12         256 push $pat->@*, $pattern->pattern->@*;
94 12 100       94 if ( $cur_dim[0] > $new_dim[0] ) {
    100          
95 1         5 for my $i ( 0 .. $new_dim[1] - 1 ) {
96 2         8 $pat->[ $cur_dim[1] + $i ] .= $fill_new x ( $cur_dim[0] - $new_dim[0] );
97             }
98             } elsif ( $cur_dim[0] < $new_dim[0] ) {
99 1         4 for my $i ( 0 .. $cur_dim[1] - 1 ) {
100 2         8 $pat->[$i] .= $fill_cur x ( $new_dim[0] - $cur_dim[0] );
101             }
102             }
103 12         28 return $self;
104             }
105              
106             sub as_array {
107 3     3 1 13 my ($self) = @_;
108 3         61 my $pat = $self->pattern;
109 3         18 my @array;
110 3         8 for my $row ( $pat->@* ) {
111 8         42 push @array, [ split //, $row ];
112             }
113 3         14 return \@array;
114             }
115              
116             sub border {
117 2     2 1 567 my ( $self, $width, $char ) = @_;
118 2 100       9 if ( defined $width ) {
119 1 50 33     9 die "width must be a positive integer"
120             if !looks_like_number($width)
121             or $width < 1;
122 1         2 $width = int $width;
123             } else {
124 1         3 $width = 1;
125             }
126 2 100 66     10 if ( defined $char and length $char ) {
127 1         3 $char = substr $char, 0, 1;
128             } else {
129 1         3 $char = '#';
130             }
131 2         73 my $pat = $self->pattern;
132 2         18 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
133 2         7 my ( $newcols, $newrows ) = map { $_ + ( $width << 1 ) } $cols, $rows;
  4         12  
134 2         10 my @np = ( $char x $newcols ) x $width;
135 2         6 for my $row ( $pat->@* ) {
136 2         7 push @np, ( $char x $width ) . $row . ( $char x $width );
137             }
138 2         9 push @np, ( $char x $newcols ) x $width;
139 2         39 $self->pattern( \@np );
140 2         19 return $self;
141             }
142              
143 55     55 1 11990 sub clone { __PACKAGE__->new( pattern => $_[0]->pattern ) }
144              
145 1     1 1 611 sub cols { length $_[0]->pattern->[0] }
146 42     42 1 1258 sub dimensions { length $_[0]->pattern->[0], scalar $_[0]->pattern->@* }
147 1     1 1 1205 sub rows { scalar $_[0]->pattern->@* }
148              
149             sub _normalize_rectangle {
150 23     23   54 my ( $self, $p1, $p2, $cols, $rows ) = @_;
151 23         49 for my $p ( $p1, $p2 ) {
152 46 100       109 $p->[0] += $cols if $p->[0] < 0;
153 46 100       84 $p->[1] += $rows if $p->[1] < 0;
154 46 50 66     265 if ( $p->[0] < 0 or $p->[0] >= $cols or $p->[1] < 0 or $p->[1] >= $rows ) {
      66        
      66        
155 1         13 local $" = ',';
156 1         10 return undef, "crop point @$p out of bounds";
157             }
158             }
159 22 100       54 ( $p1->[0], $p2->[0] ) = ( $p2->[0], $p1->[0] ) if $p1->[0] > $p2->[0];
160 22 50       50 ( $p1->[1], $p2->[1] ) = ( $p2->[1], $p1->[1] ) if $p1->[1] > $p2->[1];
161 22         57 return $p1, $p2;
162             }
163              
164             sub crop {
165 17     17 1 1779 my ( $self, $p1, $p2 ) = @_;
166 17         296 my $pat = $self->pattern;
167 17         106 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
168 17 50       40 if ( !$p2 ) {
169 0         0 $p2 = $p1;
170 0         0 $p1 = [ 0, 0 ];
171             }
172 17         52 ( $p1, $p2 ) = $self->_normalize_rectangle( $p1, $p2, $cols, $rows );
173 17 100       61 croak $p2 unless defined $p1;
174 16         27 my @new;
175 16 100 66     53 unless ( $p2->[0] == 0 or $p2->[1] == 0 ) {
176 15         37 for my $rnum ( $p1->[1] .. $p2->[1] ) {
177 21         67 push @new, substr $pat->[$rnum], $p1->[0], $p2->[0] - $p1->[0] + 1;
178             }
179             }
180 16         283 $self->pattern( \@new );
181 16         131 return $self;
182             }
183              
184             sub draw_in {
185 6     6 1 15 my ( $self, $p1, $p2, $pattern ) = @_;
186 6         99 my $pat = $self->pattern;
187 6         37 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
188 6 100       21 if ( !defined $pattern ) {
189 5         8 $pattern = $p2;
190 5 50       12 croak "need pattern to draw into the object" if !defined $pattern;
191 5         14 $p2 = [ $cols - 1, $rows - 1 ];
192             }
193 6         18 ( $p1, $p2 ) = $self->_normalize_rectangle( $p1, $p2, $cols, $rows );
194 6         100 my $draw = $pattern->pattern;
195 6         36 my ( $draw_cols, $draw_rows ) = ( length $draw->[0], scalar $draw->@* );
196 6         23 my $ccount = min( $draw_cols, $p2->[0] - $p1->[0] + 1 );
197 6         16 my $rcount = min( $draw_rows, $p2->[1] - $p1->[1] + 1 );
198 6         14 for my $rnum ( 0 .. $rcount - 1 ) {
199 9         28 substr( $pat->[ $p1->[1] + $rnum ], $p1->[0], $ccount ) =
200             substr( $draw->[$rnum], 0, $ccount );
201             }
202 6         61 return $self;
203             }
204              
205             sub _adj_4way {
206 5     5   11 my ( $p, $max_col, $max_row ) = @_;
207 5         7 my @adj;
208 5 100       14 push @adj, [ $p->[0] - 1, $p->[1] ] unless $p->[0] == 0;
209 5 100       16 push @adj, [ $p->[0] + 1, $p->[1] ] unless $p->[0] == $max_col;
210 5 100       12 push @adj, [ $p->[0], $p->[1] - 1 ] unless $p->[1] == 0;
211 5 100       13 push @adj, [ $p->[0], $p->[1] + 1 ] unless $p->[1] == $max_row;
212 5         15 return @adj;
213             }
214              
215             sub _adj_8way {
216 5     5   10 my ( $p, $max_col, $max_row ) = @_;
217 5         7 my @adj;
218 5 100       15 push @adj, [ $p->[0] - 1, $p->[1] ] unless $p->[0] == 0;
219 5 100       14 push @adj, [ $p->[0] + 1, $p->[1] ] unless $p->[0] == $max_col;
220 5 100       11 push @adj, [ $p->[0], $p->[1] - 1 ] unless $p->[1] == 0;
221 5 100       13 push @adj, [ $p->[0], $p->[1] + 1 ] unless $p->[1] == $max_row;
222 5 100 100     18 push @adj, [ $p->[0] - 1, $p->[1] - 1 ] unless $p->[0] == 0 or $p->[1] == 0;
223 5 100 100     21 push @adj, [ $p->[0] - 1, $p->[1] + 1 ]
224             unless $p->[0] == 0
225             or $p->[1] == $max_row;
226 5 100 100     31 push @adj, [ $p->[0] + 1, $p->[1] - 1 ]
227             unless $p->[0] == $max_col
228             or $p->[1] == 0;
229 5 100 100     18 push @adj, [ $p->[0] + 1, $p->[1] + 1 ]
230             unless $p->[0] == $max_col
231             or $p->[1] == $max_row;
232 5         16 return @adj;
233             }
234              
235             sub _fill {
236 2     2   6 my ( $self, $p, $char, $adjfn ) = @_;
237 2         26 my $ref = $self->as_array;
238 2         5 my $max_col = $ref->[0]->$#*;
239 2         5 my $max_row = $ref->$#*;
240 2 50 33     29 if ( $p->[0] < 0
      33        
      33        
241             or $p->[0] > $max_col
242             or $p->[1] < 0
243             or $p->[1] > $max_row ) {
244 0         0 croak "point @$p out of bounds";
245             }
246 2         7 my @queue = $p;
247 2         6 my $replace = $ref->[ $p->[1] ][ $p->[0] ];
248 2         6 while ( my $p = pop @queue ) {
249 38 100       131 if ( $ref->[ $p->[1] ][ $p->[0] ] eq $replace ) {
250 10         17 $ref->[ $p->[1] ][ $p->[0] ] = $char;
251 10         16 push @queue, $adjfn->( $p, $max_col, $max_row );
252             }
253             }
254 2         9 $self->from_array($ref);
255 2         38 return $self;
256             }
257              
258 1     1 1 48 sub fill_4way { push @_, \&_adj_4way; return &_fill }
  1         6  
259 1     1 1 10 sub fill_8way { push @_, \&_adj_8way; return &_fill }
  1         4  
260              
261             # "mirrors are abominable" (Jorge L. Borges. "Tlön, Uqbar, Orbis Tertuis")
262             # so the term flip is here used instead
263             sub flip_both {
264 6     6 1 18 my ($self) = @_;
265 6         100 my $pat = $self->pattern;
266 6         39 for my $row ( $pat->@* ) {
267 14         27 $row = reverse $row;
268             }
269 6 50       26 $pat->@* = reverse $pat->@* if $pat->@* > 1;
270 6         20 return $self;
271             }
272              
273             sub flip_cols {
274 6     6 1 17 my ($self) = @_;
275 6         96 for my $row ( $self->pattern->@* ) {
276 13         53 $row = reverse $row;
277             }
278 6         13 return $self;
279             }
280              
281             sub flip_four {
282 5     5 1 13 my ( $self, $reduce_col, $reduce_row ) = @_;
283 5   100     22 $reduce_row //= $reduce_col;
284 5         11 my $q1 = $self->clone;
285 5         13 my $q2 = $q1->clone->flip_cols;
286 5 100       14 if ($reduce_col) {
287 2         8 $q2->crop( [ 0, 0 ], [ -2, -1 ] );
288             }
289 5         13 my $q3 = $q2->clone->flip_rows;
290 5         13 my $q4 = $q1->clone->flip_rows;
291 5 100       12 if ($reduce_row) {
292 2         8 $q3->crop( [ 0, 1 ], [ -1, -1 ] );
293 2         7 $q4->crop( [ 0, 1 ], [ -1, -1 ] );
294             }
295 5         18 $q2->append_cols( '?', $q1 );
296 5         15 $q3->append_cols( '?', $q4 );
297 5         16 $q2->append_rows( '?', $q3 );
298 5         97 return $q2;
299             }
300              
301             sub flip_rows {
302 11     11 1 27 my ($self) = @_;
303 11         172 my $pat = $self->pattern;
304 11 50       89 $pat->@* = reverse $pat->@* if $pat->@* > 1;
305 11         23 return $self;
306             }
307              
308             sub four_up {
309 4     4 1 13 my ( $self, $fill, $do_crop, $reduce ) = @_;
310 4 50       11 if ( defined $fill ) {
311 4 50       11 croak "fill to four_up must not be a ref" if ref $fill;
312             } else {
313 0         0 $fill = '?';
314             }
315 4         12 my @quads = $self->clone;
316 4         67 my $pat = $quads[0]->pattern;
317 4         26 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
318 4 100       12 if ($do_crop) {
319 2         4 my $rownum = $rows - 1;
320 2 100       9 if ( $cols > $rows ) { # wide
    50          
321 1         6 $quads[0]->crop( [ 0, 0 ], [ $rownum, $rownum ] );
322             } elsif ( $cols < $rows ) { # tall
323 1         3 my $colnum = $cols - 1;
324 1         6 $quads[0]->crop( [ 0, $rownum - $colnum ], [ $colnum, $rownum ] );
325             }
326             } else {
327 2 100       11 if ( $cols > $rows ) { # wide
    50          
328 1         3 my $add = $cols - $rows;
329 1         18 my $pad = __PACKAGE__->new( pattern => $fill )->multiply( $cols, $add )
330             ->append_rows( $fill, $quads[0] );
331 1         4 $quads[0] = $pad;
332             } elsif ( $cols < $rows ) { # tall
333 1         3 my $add = $rows - $cols;
334 1         19 my $pad = __PACKAGE__->new( pattern => $fill )->multiply( $add, $rows );
335 1         5 $quads[0]->append_cols( $fill, $pad );
336             }
337             }
338 4         11 for my $r ( 1 .. 3 ) {
339 12         30 push @quads, $quads[0]->clone->rotate($r);
340             }
341 4         15 $quads[1]->append_cols( $fill, $quads[0] );
342 4         13 $quads[2]->append_cols( $fill, $quads[3] );
343 4         14 $quads[1]->append_rows( $fill, $quads[2] );
344 4         81 return $quads[1];
345             }
346              
347             sub from_array {
348 3     3 1 8 my ( $self, $array ) = @_;
349 3         7 my @pat;
350 3         8 for my $row ( $array->@* ) {
351 9         24 push @pat, join( '', $row->@* );
352             }
353 3         78 $self->pattern( \@pat );
354 3         44 return $self;
355             }
356              
357             sub mask {
358 4     4 1 15 my ( $self, $mask, $pattern ) = @_;
359 4         65 my $pat = $self->pattern;
360 4         26 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
361 4         60 my $rep = $pattern->pattern;
362 4         25 for my $r ( 0 .. $rows - 1 ) {
363 9 50       87 $pat->[$r] =~ s{([$mask]+)}{substr($rep->[$r], $-[0], $+[0] - $-[0]) || $1}eg;
  4         53  
364             }
365 4         11 return $self;
366             }
367              
368             sub multiply {
369 6     6 1 1266 my ( $self, $cols, $rows ) = @_;
370 6 50 33     67 die "cols must be a positive integer"
      33        
371             if !defined $cols
372             or !looks_like_number($cols)
373             or $cols < 1;
374 6         17 $cols = int $cols;
375 6 100       17 if ( defined $rows ) {
376 4 50 33     22 die "rows must be a positive integer"
377             if !looks_like_number($rows)
378             or $rows < 1;
379 4         8 $rows = int $rows;
380             } else {
381 2         4 $rows = $cols;
382             }
383 6 100       18 if ( $cols > 1 ) {
384 4         79 for my $row ( $self->pattern->@* ) {
385 4         33 $row = $row x $cols;
386             }
387             }
388 6 100       18 if ( $rows > 1 ) {
389 4         75 $self->pattern( [ ( $self->pattern->@* ) x $rows ] );
390             }
391 6         44 return $self;
392             }
393              
394             sub overlay {
395 4     4 1 857 my ( $self, $p, $overlay, $mask ) = @_;
396 4         10 my ( $cols, $rows ) = $self->dimensions;
397 4 50       50 $p->[0] += $cols - 1 if $p->[0] < 0;
398 4 50       10 $p->[1] += $rows - 1 if $p->[1] < 0;
399 4 50 66     32 if ( $p->[0] < 0 or $p->[0] >= $cols or $p->[1] < 0 or $p->[1] >= $rows ) {
      66        
      66        
400 1         3 local $" = ',';
401 1         25 croak "point @$p out of bounds";
402             }
403 3         7 my ( $colnum, $rownum ) = map { $_ - 1 } $overlay->dimensions;
  6         35  
404 3         8 my $subpat =
405             $self->clone->crop( $p,
406             [ min( $p->[0] + $colnum, $cols - 1 ), min( $p->[1] + $rownum, $rows - 1 ) ] );
407 3         10 my $to_draw = $overlay->clone->mask( $mask, $subpat );
408 3         13 $self->draw_in( $p, $to_draw );
409 3         12 return $self;
410             }
411              
412             sub rotate {
413 19     19 1 1294 my ( $self, $rotate_by ) = @_;
414 19         34 $rotate_by %= 4;
415 19 100       56 if ( $rotate_by == 0 ) { # zero degrees
    100          
416 2         6 return $self;
417             } elsif ( $rotate_by == 2 ) { # 180 degrees
418 5         17 return $self->flip_both;
419             }
420 12         200 my $pat = $self->pattern;
421 12         77 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
422 12         19 my @new;
423 12 100       52 if ( $rotate_by == 1 ) { # 90 degrees
    50          
424 6         26 for my $char ( split //, $pat->[0] ) {
425 26         51 unshift @new, $char;
426             }
427 6 50       20 if ( $rows > 1 ) {
428 6         17 for my $rnum ( 1 .. $rows - 1 ) {
429 9         19 for my $cnum ( 0 .. $cols - 1 ) {
430 44         82 $new[ $cols - $cnum - 1 ] .= substr $pat->[$rnum], $cnum, 1;
431             }
432             }
433             }
434             } elsif ( $rotate_by == 3 ) { # 270 degrees
435 6         24 for my $char ( split //, $pat->[-1] ) {
436 26         43 push @new, $char;
437             }
438 6 50       33 if ( $rows > 1 ) {
439 6         53 for my $rnum ( reverse 0 .. $rows - 2 ) {
440 9         20 for my $cnum ( 0 .. $cols - 1 ) {
441 44         73 $new[$cnum] .= substr $pat->[$rnum], $cnum, 1;
442             }
443             }
444             }
445             }
446 12         216 $self->pattern( \@new );
447 12         138 return $self;
448             }
449              
450             sub string {
451 20     20 1 1435 my ( $self, $sep ) = @_;
452 20   66     109 $sep //= $/;
453 20         360 return join( $sep, $self->pattern->@* ) . $sep;
454             }
455              
456             sub trim {
457 1     1 1 8 my ( $self, $amount ) = @_;
458             # -1 is the last index, so need at least one more than that
459 1         4 my $neg = -( $amount + 1 );
460 1         6 return $self->crop( [ $amount, $amount ], [ $neg, $neg ] );
461             }
462              
463             sub white_noise {
464 3     3 1 1877 my ( $self, $char, $percent ) = @_;
465 3         65 my $pat = $self->pattern;
466 3         22 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
467 3         7 my $total = $cols * $rows;
468 3         9 my $to_fill = int( $total * $percent );
469 3 100       8 if ( $to_fill > 0 ) {
470 2         6 for my $row ( $pat->@* ) {
471 4         9 for my $i ( 0 .. $cols - 1 ) {
472 20 100       53 if ( rand() < $to_fill / $total ) {
473 15         22 substr( $row, $i, 1 ) = $char;
474 15         19 $to_fill--;
475             }
476 20         35 $total--;
477             }
478             }
479             }
480 3         9 return $self;
481             }
482              
483             1;
484             __END__