File Coverage

blib/lib/Game/TextPatterns.pm
Criterion Covered Total %
statement 257 260 98.8
branch 78 100 78.0
condition 25 42 59.5
subroutine 32 32 100.0
pod 23 24 95.8
total 415 458 90.6


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   233955 use 5.24.0;
  2         19  
8 2     2   10 use warnings;
  2         4  
  2         56  
9 2     2   9 use Carp qw(croak);
  2         3  
  2         100  
10 2     2   11 use List::Util qw(min);
  2         4  
  2         122  
11 2     2   977 use Moo;
  2         21070  
  2         10  
12 2     2   3559 use namespace::clean;
  2         21330  
  2         13  
13 2     2   578 use Scalar::Util qw(looks_like_number);
  2         4  
  2         6915  
14              
15             our $VERSION = '0.73';
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 91     91 0 4043 my ( $self, $param ) = @_;
46 91 100       603 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 58 my ( $self, $fill, $pattern ) = @_;
55 22 50       50 croak "need append_cols(fill, pattern)" if !defined $pattern;
56 22         40 my ( $fill_cur, $fill_new );
57 22 100       79 if ( ref $fill eq 'ARRAY' ) {
58 2         5 ( $fill_cur, $fill_new ) = $fill->@*;
59             } else {
60 20         38 $fill_cur = $fill_new = $fill;
61             }
62 22         381 my $pat = $self->pattern;
63 22         414 my @cur_dim = ( length $_[0]->pattern->[0], scalar $_[0]->pattern->@* );
64 22         203 my @new_dim = $pattern->dimensions;
65 22 100       215 if ( $cur_dim[1] > $new_dim[1] ) {
    100          
66 1         4 for my $i ( 1 .. $cur_dim[1] - $new_dim[1] ) {
67 2         7 $pat->[ -$i ] .= $fill_new x $new_dim[0];
68             }
69             } elsif ( $cur_dim[1] < $new_dim[1] ) {
70 2         8 for my $i ( 1 .. $new_dim[1] - $cur_dim[1] ) {
71 4         11 push $pat->@*, $fill_cur x $cur_dim[0];
72             }
73             }
74 22         350 my $new = $pattern->pattern;
75 22         160 for my $i ( 0 .. $new_dim[1] - 1 ) {
76 53         99 $pat->[$i] .= $new->[$i];
77             }
78 22         52 return $self;
79             }
80              
81             sub append_rows {
82 12     12 1 35 my ( $self, $fill, $pattern ) = @_;
83 12 50       31 croak "need append_rows(fill, pattern)" if !defined $pattern;
84 12         21 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         202 my $pat = $self->pattern;
91 12         230 my @cur_dim = ( length $_[0]->pattern->[0], scalar $_[0]->pattern->@* );
92 12         107 my @new_dim = $pattern->dimensions;
93 12         269 push $pat->@*, $pattern->pattern->@*;
94 12 100       91 if ( $cur_dim[0] > $new_dim[0] ) {
    100          
95 1         4 for my $i ( 0 .. $new_dim[1] - 1 ) {
96 2         7 $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         7 $pat->[$i] .= $fill_cur x ( $new_dim[0] - $cur_dim[0] );
101             }
102             }
103 12         29 return $self;
104             }
105              
106             sub as_array {
107 1     1 1 7 my ($self) = @_;
108 1         19 my $pat = $self->pattern;
109 1         7 my @array;
110 1         3 for my $row ( $pat->@* ) {
111 2         8 push @array, [ split //, $row ];
112             }
113 1         8 return \@array;
114             }
115              
116             sub border {
117 2     2 1 565 my ( $self, $width, $char ) = @_;
118 2 100       7 if ( defined $width ) {
119 1 50 33     10 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         4 $width = 1;
125             }
126 2 100 66     9 if ( defined $char and length $char ) {
127 1         4 $char = substr $char, 0, 1;
128             } else {
129 1         2 $char = '#';
130             }
131 2         46 my $pat = $self->pattern;
132 2         16 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
133 2         6 my ( $newcols, $newrows ) = map { $_ + ( $width << 1 ) } $cols, $rows;
  4         11  
134 2         9 my @np = ( $char x $newcols ) x $width;
135 2         5 for my $row ( $pat->@* ) {
136 2         9 push @np, ( $char x $width ) . $row . ( $char x $width );
137             }
138 2         7 push @np, ( $char x $newcols ) x $width;
139 2         39 $self->pattern( \@np );
140 2         18 return $self;
141             }
142              
143 55     55 1 11718 sub clone { __PACKAGE__->new( pattern => $_[0]->pattern ) }
144              
145 1     1 1 619 sub cols { length $_[0]->pattern->[0] }
146 42     42 1 1223 sub dimensions { length $_[0]->pattern->[0], scalar $_[0]->pattern->@* }
147 1     1 1 1145 sub rows { scalar $_[0]->pattern->@* }
148              
149             sub _normalize_rectangle {
150 23     23   51 my ( $self, $p1, $p2, $cols, $rows ) = @_;
151 23         49 for my $p ( $p1, $p2 ) {
152 46 100       99 $p->[0] += $cols if $p->[0] < 0;
153 46 100       89 $p->[1] += $rows if $p->[1] < 0;
154 46 50 66     268 if ( $p->[0] < 0 or $p->[0] >= $cols or $p->[1] < 0 or $p->[1] >= $rows ) {
      66        
      66        
155 1         3 local $" = ',';
156 1         6 return undef, "crop point @$p out of bounds";
157             }
158             }
159 22 100       78 ( $p1->[0], $p2->[0] ) = ( $p2->[0], $p1->[0] ) if $p1->[0] > $p2->[0];
160 22 50       52 ( $p1->[1], $p2->[1] ) = ( $p2->[1], $p1->[1] ) if $p1->[1] > $p2->[1];
161 22         64 return $p1, $p2;
162             }
163              
164             sub crop {
165 17     17 1 1668 my ( $self, $p1, $p2 ) = @_;
166 17         313 my $pat = $self->pattern;
167 17         126 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
168 17 50       73 if ( !$p2 ) {
169 0         0 $p2 = $p1;
170 0         0 $p1 = [ 0, 0 ];
171             }
172 17         47 ( $p1, $p2 ) = $self->_normalize_rectangle( $p1, $p2, $cols, $rows );
173 17 100       70 croak $p2 unless defined $p1;
174 16         27 my @new;
175 16 100 66     57 unless ( $p2->[0] == 0 or $p2->[1] == 0 ) {
176 15         36 for my $rnum ( $p1->[1] .. $p2->[1] ) {
177 21         65 push @new, substr $pat->[$rnum], $p1->[0], $p2->[0] - $p1->[0] + 1;
178             }
179             }
180 16         315 $self->pattern( \@new );
181 16         128 return $self;
182             }
183              
184             sub draw_in {
185 6     6 1 18 my ( $self, $p1, $p2, $pattern ) = @_;
186 6         106 my $pat = $self->pattern;
187 6         39 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
188 6 100       17 if ( !defined $pattern ) {
189 5         9 $pattern = $p2;
190 5 50       14 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         108 my $draw = $pattern->pattern;
195 6         37 my ( $draw_cols, $draw_rows ) = ( length $draw->[0], scalar $draw->@* );
196 6         24 my $ccount = min( $draw_cols, $p2->[0] - $p1->[0] + 1 );
197 6         14 my $rcount = min( $draw_rows, $p2->[1] - $p1->[1] + 1 );
198 6         15 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         64 return $self;
203             }
204              
205             # "mirrors are abominable" (Jorge L. Borges. "Tlön, Uqbar, Orbis Tertuis")
206             # so the term flip is here used instead
207             sub flip_both {
208 6     6 1 16 my ($self) = @_;
209 6         107 my $pat = $self->pattern;
210 6         39 for my $row ( $pat->@* ) {
211 14         29 $row = reverse $row;
212             }
213 6 50       30 $pat->@* = reverse $pat->@* if $pat->@* > 1;
214 6         18 return $self;
215             }
216              
217             sub flip_cols {
218 6     6 1 23 my ($self) = @_;
219 6         154 for my $row ( $self->pattern->@* ) {
220 13         55 $row = reverse $row;
221             }
222 6         30 return $self;
223             }
224              
225             sub flip_four {
226 5     5 1 12 my ( $self, $reduce_col, $reduce_row ) = @_;
227 5   100     21 $reduce_row //= $reduce_col;
228 5         11 my $q1 = $self->clone;
229 5         13 my $q2 = $q1->clone->flip_cols;
230 5 100       14 if ($reduce_col) {
231 2         7 $q2->crop( [ 0, 0 ], [ -2, -1 ] );
232             }
233 5         29 my $q3 = $q2->clone->flip_rows;
234 5         12 my $q4 = $q1->clone->flip_rows;
235 5 100       13 if ($reduce_row) {
236 2         9 $q3->crop( [ 0, 1 ], [ -1, -1 ] );
237 2         7 $q4->crop( [ 0, 1 ], [ -1, -1 ] );
238             }
239 5         18 $q2->append_cols( '?', $q1 );
240 5         15 $q3->append_cols( '?', $q4 );
241 5         16 $q2->append_rows( '?', $q3 );
242 5         114 return $q2;
243             }
244              
245             sub flip_rows {
246 11     11 1 27 my ($self) = @_;
247 11         180 my $pat = $self->pattern;
248 11 50       86 $pat->@* = reverse $pat->@* if $pat->@* > 1;
249 11         24 return $self;
250             }
251              
252             sub four_up {
253 4     4 1 14 my ( $self, $fill, $do_crop, $reduce ) = @_;
254 4 50       11 if ( defined $fill ) {
255 4 50       12 croak "fill to four_up must not be a ref" if ref $fill;
256             } else {
257 0         0 $fill = '?';
258             }
259 4         9 my @quads = $self->clone;
260 4         72 my $pat = $quads[0]->pattern;
261 4         27 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
262 4 100       11 if ($do_crop) {
263 2         15 my $rownum = $rows - 1;
264 2 100       11 if ( $cols > $rows ) { # wide
    50          
265 1         7 $quads[0]->crop( [ 0, 0 ], [ $rownum, $rownum ] );
266             } elsif ( $cols < $rows ) { # tall
267 1         3 my $colnum = $cols - 1;
268 1         5 $quads[0]->crop( [ 0, $rownum - $colnum ], [ $colnum, $rownum ] );
269             }
270             } else {
271 2 100       10 if ( $cols > $rows ) { # wide
    50          
272 1         3 my $add = $cols - $rows;
273 1         19 my $pad = __PACKAGE__->new( pattern => $fill )->multiply( $cols, $add )
274             ->append_rows( $fill, $quads[0] );
275 1         4 $quads[0] = $pad;
276             } elsif ( $cols < $rows ) { # tall
277 1         4 my $add = $rows - $cols;
278 1         19 my $pad = __PACKAGE__->new( pattern => $fill )->multiply( $add, $rows );
279 1         4 $quads[0]->append_cols( $fill, $pad );
280             }
281             }
282 4         12 for my $r ( 1 .. 3 ) {
283 12         30 push @quads, $quads[0]->clone->rotate($r);
284             }
285 4         15 $quads[1]->append_cols( $fill, $quads[0] );
286 4         12 $quads[2]->append_cols( $fill, $quads[3] );
287 4         13 $quads[1]->append_rows( $fill, $quads[2] );
288 4         115 return $quads[1];
289             }
290              
291             sub from_array {
292 1     1 1 5 my ( $self, $array ) = @_;
293 1         2 my @pat;
294 1         3 for my $row ( $array->@* ) {
295 3         11 push @pat, join( '', $row->@* );
296             }
297 1         28 $self->pattern( \@pat );
298 1         25 return $self;
299             }
300              
301             sub mask {
302 4     4 1 15 my ( $self, $mask, $pattern ) = @_;
303 4         68 my $pat = $self->pattern;
304 4         25 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
305 4         66 my $rep = $pattern->pattern;
306 4         25 for my $r ( 0 .. $rows - 1 ) {
307 9 50       76 $pat->[$r] =~ s{([$mask]+)}{substr($rep->[$r], $-[0], $+[0] - $-[0]) || $1}eg;
  4         36  
308             }
309 4         10 return $self;
310             }
311              
312             sub multiply {
313 6     6 1 1171 my ( $self, $cols, $rows ) = @_;
314 6 50 33     62 die "cols must be a positive integer"
      33        
315             if !defined $cols
316             or !looks_like_number($cols)
317             or $cols < 1;
318 6         16 $cols = int $cols;
319 6 100       15 if ( defined $rows ) {
320 4 50 33     22 die "rows must be a positive integer"
321             if !looks_like_number($rows)
322             or $rows < 1;
323 4         7 $rows = int $rows;
324             } else {
325 2         4 $rows = $cols;
326             }
327 6 100       19 if ( $cols > 1 ) {
328 4         85 for my $row ( $self->pattern->@* ) {
329 4         33 $row = $row x $cols;
330             }
331             }
332 6 100       18 if ( $rows > 1 ) {
333 4         80 $self->pattern( [ ( $self->pattern->@* ) x $rows ] );
334             }
335 6         42 return $self;
336             }
337              
338             sub overlay {
339 4     4 1 859 my ( $self, $p, $overlay, $mask ) = @_;
340 4         12 my ( $cols, $rows ) = $self->dimensions;
341 4 50       47 $p->[0] += $cols - 1 if $p->[0] < 0;
342 4 50       11 $p->[1] += $rows - 1 if $p->[1] < 0;
343 4 50 66     32 if ( $p->[0] < 0 or $p->[0] >= $cols or $p->[1] < 0 or $p->[1] >= $rows ) {
      66        
      66        
344 1         2 local $" = ',';
345 1         25 croak "point @$p out of bounds";
346             }
347 3         9 my ( $colnum, $rownum ) = map { $_ - 1 } $overlay->dimensions;
  6         51  
348 3         8 my $subpat =
349             $self->clone->crop( $p,
350             [ min( $p->[0] + $colnum, $cols - 1 ), min( $p->[1] + $rownum, $rows - 1 ) ] );
351 3         11 my $to_draw = $overlay->clone->mask( $mask, $subpat );
352 3         12 $self->draw_in( $p, $to_draw );
353 3         13 return $self;
354             }
355              
356             sub rotate {
357 19     19 1 1272 my ( $self, $rotate_by ) = @_;
358 19         36 $rotate_by %= 4;
359 19 100       50 if ( $rotate_by == 0 ) { # zero degrees
    100          
360 2         6 return $self;
361             } elsif ( $rotate_by == 2 ) { # 180 degrees
362 5         17 return $self->flip_both;
363             }
364 12         213 my $pat = $self->pattern;
365 12         79 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
366 12         22 my @new;
367 12 100       37 if ( $rotate_by == 1 ) { # 90 degrees
    50          
368 6         22 for my $char ( split //, $pat->[0] ) {
369 26         54 unshift @new, $char;
370             }
371 6 50       18 if ( $rows > 1 ) {
372 6         17 for my $rnum ( 1 .. $rows - 1 ) {
373 9         18 for my $cnum ( 0 .. $cols - 1 ) {
374 44         87 $new[ $cols - $cnum - 1 ] .= substr $pat->[$rnum], $cnum, 1;
375             }
376             }
377             }
378             } elsif ( $rotate_by == 3 ) { # 270 degrees
379 6         24 for my $char ( split //, $pat->[-1] ) {
380 26         44 push @new, $char;
381             }
382 6 50       32 if ( $rows > 1 ) {
383 6         20 for my $rnum ( reverse 0 .. $rows - 2 ) {
384 9         19 for my $cnum ( 0 .. $cols - 1 ) {
385 44         72 $new[$cnum] .= substr $pat->[$rnum], $cnum, 1;
386             }
387             }
388             }
389             }
390 12         223 $self->pattern( \@new );
391 12         110 return $self;
392             }
393              
394             sub string {
395 20     20 1 1363 my ( $self, $sep ) = @_;
396 20   66     105 $sep //= $/;
397 20         373 return join( $sep, $self->pattern->@* ) . $sep;
398             }
399              
400             sub trim {
401 1     1 1 8 my ( $self, $amount ) = @_;
402             # -1 is the last index, so need at least one more than that
403 1         3 my $neg = -( $amount + 1 );
404 1         5 return $self->crop( [ $amount, $amount ], [ $neg, $neg ] );
405             }
406              
407             sub white_noise {
408 3     3 1 1859 my ( $self, $char, $percent ) = @_;
409 3         68 my $pat = $self->pattern;
410 3         22 my ( $cols, $rows ) = ( length $pat->[0], scalar $pat->@* );
411 3         8 my $total = $cols * $rows;
412 3         8 my $to_fill = int( $total * $percent );
413 3 100       8 if ( $to_fill > 0 ) {
414 2         6 for my $row ( $pat->@* ) {
415 4         21 for my $i ( 0 .. $cols - 1 ) {
416 20 100       38 if ( rand() < $to_fill / $total ) {
417 15         25 substr( $row, $i, 1 ) = $char;
418 15         18 $to_fill--;
419             }
420 20         30 $total--;
421             }
422             }
423             }
424 3         9 return $self;
425             }
426              
427             1;
428             __END__