File Coverage

blib/lib/Game/TextPatterns.pm
Criterion Covered Total %
statement 292 296 98.6
branch 84 108 77.7
condition 31 54 57.4
subroutine 37 37 100.0
pod 26 27 96.3
total 470 522 90.0


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