File Coverage

blib/lib/Games/2048/Board.pm
Criterion Covered Total %
statement 53 186 28.4
branch 7 122 5.7
condition 1 35 2.8
subroutine 14 36 38.8
pod 0 26 0.0
total 75 405 18.5


line stmt bran cond sub pod time code
1             package Games::2048::Board;
2 4     4   64 use 5.012;
  4         8  
  4         131  
3 4     4   17 use Moo;
  4         4  
  4         24  
4              
5 4     4   1314274 use Text::Wrap;
  4         10473  
  4         264  
6 4     4   2587 use Term::ANSIColor;
  4         25801  
  4         405  
7 4     4   44 use POSIX qw/floor ceil/;
  4         6  
  4         33  
8 4     4   323 use List::Util qw/max min/;
  4         8  
  4         12886  
9              
10             extends 'Games::2048::Grid';
11              
12             has needs_redraw => is => 'rw', default => 1;
13             has score => is => 'rw', default => 0;
14             has win => is => 'rw', default => 0;
15             has lose => is => 'rw', default => 0;
16              
17             has best_score => is => 'rw', default => 0;
18             has no_animations => is => 'rw', default => 0;
19             has zoom => is => 'rw', default => 2, trigger => 1, coerce => \&_coerce_zoom;
20             has colors => is => 'rw', builder => 1, coerce => \&_coerce_colors;
21              
22             has appearing => is => 'rw';
23             has moving => is => 'rw';
24             has moving_vec => is => 'rw';
25              
26             has border_width => is => 'rw', default => 2;
27             has border_height => is => 'rw', default => 1;
28             has cell_width => is => 'rw', default => 7;
29             has cell_height => is => 'rw', default => 3;
30             has score_width => is => 'rw', default => 7;
31             has score_height => is => 'rw', default => 1;
32             has options_height => is => 'rw', default => 5;
33              
34             my @zooms = (
35             [ 3, 1 ],
36             [ 5, 2 ],
37             [ 7, 3 ],
38             [ 9, 4 ],
39             [ 11, 5 ],
40             );
41              
42             sub insert_tile {
43 474     474 0 4279 my ($self, $tile) = @_;
44              
45 474         924 $self->needs_redraw(1);
46 474 50       11340 return if $self->no_animations;
47              
48 474         875 $tile->appearing(1);
49 474         11832 $self->appearing(Games::2048::Animation->new(
50             duration => 0.3,
51             first_value => -1 / max($self->cell_width, $self->cell_height),
52             last_value => 1,
53             ));
54              
55 474         11817 $tile;
56             }
57              
58             sub move_tiles {
59 30     30 0 504 my ($self, $vec) = @_;
60              
61 30         100 $self->needs_redraw(1);
62 30 50       825 return if $self->no_animations;
63              
64 30         150 $self->reset_animations;
65              
66 30         129 $self->moving_vec($vec);
67 30         841 $self->moving(Games::2048::Animation->new(
68             duration => 0.2,
69             first_value => 0,
70             last_value => $self->size - 1,
71             ));
72             }
73              
74             sub reset_appearing {
75 30     30 0 66 my $self = shift;
76 30         91 $_->appearing(0) for $self->each_tile;
77 30         669 $self->appearing(undef);
78             }
79              
80             sub reset_moving {
81 30     30 0 48 my $self = shift;
82 30         97 for ($self->each_tile) {
83 252         569 $_->moving_from(undef);
84 252         493 $_->merging_tiles(undef);
85             }
86 30         309 $self->moving(undef);
87             }
88              
89             sub reset_animations {
90 30     30 0 50 my $self = shift;
91 30         89 $self->reset_moving;
92 30         98 $self->reset_appearing;
93             }
94              
95             sub draw {
96 0     0 0 0 my ($self, $redraw) = @_;
97              
98 0 0 0     0 return if $redraw and !$self->needs_redraw;
99              
100 0         0 $self->hide_cursor;
101 0 0       0 $self->restore_cursor if $redraw;
102 0         0 $self->needs_redraw(0);
103              
104 0 0       0 say "" if !$redraw;
105              
106 0         0 $self->draw_hud;
107 0         0 $self->draw_border_horizontal;
108              
109             # set if anything is *actually* moving or appearing
110 0         0 my $moving;
111             my $appearing;
112              
113 0         0 for my $y (0..$self->size-1) {
114 0         0 for my $line (0..$self->cell_height-1) {
115 0         0 $self->draw_border_vertical;
116              
117 0         0 for my $x (0..$self->size-1) {
118 0         0 my $tile = $self->tile([$x, $y]);
119              
120 0         0 my $string;
121 0 0       0 my $value = $tile ? $tile->value : undef;
122 0         0 my $color = $self->tile_color($value);
123 0         0 my $bgcolor = $self->tile_color(undef);
124              
125 0 0 0     0 if (defined $value and length($value) > $self->cell_width * $self->cell_height) {
126 0         0 $value = int($value/1000) . "k";
127             }
128              
129 0   0     0 my $lines = min(ceil(length($value // '') / $self->cell_width), $self->cell_height);
130 0         0 my $first_line = floor(($self->cell_height - $lines) / 2);
131 0         0 my $this_line = $line - $first_line;
132              
133 0 0 0     0 if ($this_line >= 0 and $this_line < $lines) {
134 0         0 my $cols = min(ceil(length($value) / $lines), $self->cell_width);
135 0         0 my $string_offset = $this_line * $cols;
136 0         0 my $string_length = min($cols, length($value) - $string_offset, $self->cell_width);
137 0         0 my $cell_offset = floor(($self->cell_width - $string_length) / 2);
138              
139 0         0 $string = " " x $cell_offset;
140              
141 0         0 $string .= substr($value, $string_offset, $string_length);
142              
143 0         0 $string .= " " x ($self->cell_width - $cell_offset - $string_length);
144             }
145             else {
146 0         0 $string = " " x $self->cell_width;
147             }
148              
149 0 0 0     0 if ($tile and $tile->appearing and $self->appearing) {
      0        
150             # if any animation is going we need to keep redrawing
151 0         0 $self->needs_redraw(1);
152              
153 0         0 my $value = $self->appearing->value;
154 0         0 $appearing = 1;
155              
156 0         0 my $x_center = ($self->cell_width - 1) / 2;
157 0         0 my $y_center = ($self->cell_height - 1) / 2;
158              
159 0         0 my $on = 0;
160 0         0 my $extra = 0;
161 0         0 for my $col (0..$self->cell_width-1) {
162 0         0 my $x_distance = $col / $x_center - 1;
163 0         0 my $y_distance = $line / $y_center - 1;
164 0         0 my $distance = $x_distance**2 + $y_distance**2;
165              
166 0         0 my $within = $distance <= 2 * $value**2;
167              
168 0 0 0     0 if ($within xor $on) {
169 0         0 $on = $within;
170              
171 0 0       0 my $insert = $on
172             ? $color
173             : $bgcolor;
174              
175 0         0 substr($string, $col + $extra, 0) = $insert;
176 0         0 $extra += length($insert);
177             }
178             }
179 0 0       0 if ($on) {
180 0         0 $string .= $bgcolor;
181             }
182             }
183             else {
184 0         0 $string = $color . $string . $bgcolor;
185             }
186              
187 0         0 print $string;
188             }
189              
190 0         0 $self->draw_border_vertical;
191 0         0 say color("reset");
192             }
193             }
194              
195             # update animations
196 0 0 0     0 $self->reset_appearing if $appearing and !$self->appearing->update;
197 0 0 0     0 $self->reset_moving if $self->moving and !$moving || !$self->moving->update;
      0        
198              
199 0         0 $self->draw_border_horizontal;
200 0 0       0 $self->show_cursor if !$self->needs_redraw;
201             }
202              
203             sub draw_win {
204 0     0 0 0 my $self = shift;
205 0 0 0     0 return if !$self->win and !$self->lose;
206 0 0       0 my $message =
207             $self->win ? "You win!"
208             : "Game over!";
209 0         0 my $offset = ceil(($self->board_width - length($message)) / 2);
210              
211 0         0 say " " x $offset, colored(uc $message, "bold"), "\n";
212             }
213              
214             sub draw_win_question {
215 0     0 0 0 my $self = shift;
216 0 0       0 print $self->win ? "Keep going?" : "Try again?", " (Y/n) ";
217 0         0 STDOUT->flush;
218             }
219              
220             sub draw_win_answer {
221 0     0 0 0 my ($self, $yes) = @_;
222 0 0       0 say $yes ? "y" : "n";
223             }
224              
225             sub draw_hud {
226 0     0 0 0 my $self = shift;
227              
228 0         0 $self->draw_options;
229 0         0 $self->draw_score;
230             }
231              
232             sub draw_options {
233 0     0 0 0 my $self = shift;
234              
235 0         0 $self->draw_option("( Q ) Quit " . "( R ) New Game");
236 0         0 $self->draw_option("( A ) Animations " . bold_if("On", !$self->no_animations)."/".bold_if("Off", $self->no_animations));
237 0         0 $self->draw_option("( C ) Colors " . bold_if("16", $self->colors == 0)."/".bold_if("256", $self->colors == 1)."/".bold_if("24-bit", $self->colors == 2));
238 0         0 $self->draw_option("(+/-) Zoom " . colored(floor(($self->cell_height + 1) / 4 * 100)."%", "bold"));
239              
240 0         0 say "";
241             }
242              
243             sub bold_if {
244 0     0 0 0 my ($string, $condition) = @_;
245 0 0       0 $condition ? colored($string, "bold") : $string;
246             }
247              
248             sub draw_option {
249 0     0 0 0 my ($self, $line) = @_;
250 0         0 $line =~ s/(\(.*?\))/colored($1, "bold")/ge;
  0         0  
251 0         0 say $line;
252             }
253              
254             sub draw_score {
255 0     0 0 0 my $self = shift;
256              
257 0         0 my $score = "Score:";
258 0         0 my $best_score = "Best:";
259              
260 0         0 my $blank_width = $self->board_width - length($score) - length($best_score);
261 0         0 my $score_width = min(floor(($blank_width - 1) / 2), $self->score_width);
262 0         0 my $inner_padding = $blank_width - $score_width * 2;
263              
264 0         0 $self->draw_sub_score($score, $score_width, $self->score);
265              
266 0         0 print " " x $inner_padding;
267              
268 0         0 $self->draw_sub_score($best_score, $score_width, $self->best_score);
269              
270 0         0 say "";
271             }
272              
273             sub draw_sub_score {
274 0     0 0 0 my ($self, $string, $score_width, $score) = @_;
275 0         0 printf "%s%*d", colored($string, "bold"), $score_width, $score;
276             }
277              
278             sub tile_color {
279 0     0 0 0 my ($self, $value) = @_;
280 0 0       0 if ($self->colors == 2) {
281             return
282 0 0       0 !defined $value ? color("reset") . "\e[38;2;187;173;160m" . "\e[48;2;204;192;179m"
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
283             : $value < 4 ? color("reset") . "\e[38;2;119;110;101m" . "\e[48;2;238;228;218m"
284             : $value < 8 ? color("reset") . "\e[38;2;119;110;101m" . "\e[48;2;237;224;200m"
285             : $value < 16 ? color("reset") . "\e[38;2;249;246;242m" . "\e[48;2;242;177;121m"
286             : $value < 32 ? color("reset") . "\e[38;2;249;246;242m" . "\e[48;2;245;149;99m"
287             : $value < 64 ? color("reset") . "\e[38;2;249;246;242m" . "\e[48;2;246;124;95m"
288             : $value < 128 ? color("reset") . "\e[38;2;249;246;242m" . "\e[48;2;246;94;59m"
289             : $value < 256 ? color("bold") . "\e[38;2;249;246;242m" . "\e[48;2;237;207;114m"
290             : $value < 512 ? color("bold") . "\e[38;2;249;246;242m" . "\e[48;2;237;204;97m"
291             : $value < 1024 ? color("bold") . "\e[38;2;249;246;242m" . "\e[48;2;237;200;80m"
292             : $value < 2048 ? color("bold") . "\e[38;2;249;246;242m" . "\e[48;2;237;197;63m"
293             : $value < 4096 ? color("bold") . "\e[38;2;249;246;242m" . "\e[48;2;237;194;46m"
294             : color("bold") . "\e[38;2;249;246;242m" . "\e[48;2;60;58;50m";
295             }
296 0 0       0 if ($self->colors == 1) {
297             return
298 0 0       0 !defined $value ? color("reset") . "\e[38;5;249m" . "\e[48;5;251m"
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
299             : $value < 4 ? color("reset") . "\e[38;5;243m" . "\e[48;5;231m"
300             : $value < 8 ? color("reset") . "\e[38;5;243m" . "\e[48;5;230m"
301             : $value < 16 ? color("reset") . "\e[38;5;231m" . "\e[48;5;215m"
302             : $value < 32 ? color("reset") . "\e[38;5;231m" . "\e[48;5;209m"
303             : $value < 64 ? color("reset") . "\e[38;5;231m" . "\e[48;5;203m"
304             : $value < 128 ? color("reset") . "\e[38;5;231m" . "\e[48;5;196m"
305             : $value < 256 ? color("bold") . "\e[38;5;231m" . "\e[48;5;227m"
306             : $value < 512 ? color("bold") . "\e[38;5;231m" . "\e[48;5;227m"
307             : $value < 1024 ? color("bold") . "\e[38;5;231m" . "\e[48;5;226m"
308             : $value < 2048 ? color("bold") . "\e[38;5;231m" . "\e[48;5;226m"
309             : $value < 4096 ? color("bold") . "\e[38;5;231m" . "\e[48;5;220m"
310             : color("bold") . "\e[38;5;231m" . "\e[48;5;237m";
311             }
312 0 0       0 my $bright = $^O eq "MSWin32" ? "underline " : "bright_";
313 0 0       0 my $bold = $^O eq "MSWin32" ? "underline" : "bold";
314 0 0       0 return color (
    0          
    0          
    0          
    0          
    0          
    0          
    0          
315             !defined $value ? "reset"
316             : $value < 4 ? "reset reverse cyan"
317             : $value < 8 ? "reset reverse ${bright}blue"
318             : $value < 16 ? "reset reverse blue"
319             : $value < 32 ? "reset reverse green"
320             : $value < 64 ? "reset reverse magenta"
321             : $value < 128 ? "reset reverse red"
322             : $value < 4096 ? "reset reverse yellow"
323             : "reset reverse $bold"
324             );
325             }
326              
327             sub border_color {
328 0     0 0 0 my $self = shift;
329 0         0 $self->tile_color(undef) . color("reverse");
330             }
331              
332             sub board_width {
333 0     0 0 0 my $self = shift;
334 0         0 return $self->size * $self->cell_width + $self->border_width * 2;
335             }
336              
337             sub board_height {
338 0     0 0 0 my $self = shift;
339 0         0 return $self->size * $self->cell_height + $self->border_height * 2 + $self->hud_height;
340             }
341              
342             sub hud_height {
343 0     0 0 0 my $self = shift;
344 0         0 return $self->score_height + $self->options_height;
345             }
346              
347             sub draw_border_horizontal {
348 0     0 0 0 my $self = shift;
349 0         0 say $self->border_color, " " x $self->board_width, color("reset") for 1..$self->border_height;
350             }
351             sub draw_border_vertical {
352 0     0 0 0 my $self = shift;
353 0         0 print $self->border_color, " " x $self->border_width, $self->tile_color(undef);
354             }
355              
356             sub restore_cursor {
357 0     0 0 0 my $self = shift;
358 0         0 printf "\e[%dA", $self->board_height;
359             }
360              
361             sub draw_welcome {
362 0     0 0 0 my $logo = colored(<<'LOGO', "bold");
363             __ _ _
364             _)/ \|_|(_)
365             /__\_/ |(_)
366             LOGO
367              
368 0         0 my $message = <<'MESSAGE';
369              
370             Join the numbers and get to the 2048 tile!
371              
372             HOW TO PLAY: Use your arrow keys to move the tiles. When two tiles with the same number touch, they merge into one!
373             MESSAGE
374              
375 0         0 local $Text::Wrap::columns = Games::2048::Util::window_size;
376 0         0 $message = wrap "", "", $message;
377 0         0 $message =~ s/(2048\s+tile!|HOW\s+TO\s+PLAY:|arrow\s+keys|merge\s+into\s+one!)/colored $1, "bold"/ge;
  0         0  
378              
379 0         0 print $logo, $message;
380             }
381              
382             sub hide_cursor {
383 0     0 0 0 my $self = shift;
384 0         0 state $once = eval 'END { $self->show_cursor }';
385 0         0 print "\e[?25l";
386             }
387             sub show_cursor {
388 0     0 0 0 my $self = shift;
389 0         0 print "\e[?25h";
390             }
391              
392             around no_animations => sub {
393             my $orig = shift;
394             my $self = shift;
395              
396             my $no_anim = $self->$orig(@_);
397              
398             if (@_) {
399             $self->reset_animations if $self->no_animations;
400             $self->needs_redraw(1);
401             }
402             else {
403             $no_anim = 1 if $self->cell_height <= 1 or $self->cell_width <= 1;
404             }
405              
406             $no_anim;
407             };
408              
409             sub _coerce_zoom {
410 55     55   3057 my ($zoom) = @_;
411 55 50       301 $zoom = $#zooms if $zoom > $#zooms;
412 55 50       154 $zoom = 0 if $zoom < 0;
413 55         1284 $zoom;
414             }
415              
416             sub _trigger_zoom {
417 0     0   0 my ($self, $zoom, $old) = @_;
418 0         0 $self->zoom($zoom, undef); # hack because we have no old value FUUUUUU
419             }
420              
421             around zoom => sub {
422             my $orig = shift;
423             my $self = shift;
424              
425             return $self->$orig if !@_;
426              
427             my $old = $self->$orig;
428             my $zoom = @_ == 1 ? $self->$orig(@_) : $old;
429              
430             $self->cell_width($zooms[$zoom][0]);
431             $self->cell_height($zooms[$zoom][1]);
432             $self->draw if defined $old and $zoom != $old;
433              
434             $zoom;
435             };
436              
437             sub _build_colors {
438 55 50   55   37988 return 2 if $ENV{KONSOLE_DBUS_SERVICE};
439 55         101 return 1 if 0;
440 55         152 return 0;
441             }
442              
443             sub _coerce_colors {
444 55     55   124 my ($colors) = @_;
445 55   50     193 $colors //= 0;
446 55 50       329 $colors = 0 if $colors > 2;
447 55 50       202 $colors = 2 if $colors < 0;
448 55         1278 $colors;
449             }
450              
451             around colors => sub {
452             my $orig = shift;
453             my $self = shift;
454              
455             my $colors = $self->$orig(@_);
456              
457             if (@_) {
458             $self->needs_redraw(1);
459             }
460              
461             $colors;
462             };
463              
464             1;