File Coverage

blib/lib/Game/PlatformsOfPeril.pm
Criterion Covered Total %
statement 31 397 7.8
branch 0 130 0.0
condition 0 91 0.0
subroutine 11 56 19.6
pod 0 42 0.0
total 42 716 5.8


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # Game::PlatformsOfPeril - this is a terminal-based game, run the
4             # `pperil` command that should be installed with this module to begin
5             #
6             # some details for the unwary, or brave, regarding the code:
7             #
8             # this implementation uses arrays heavily so instead of a more typical
9             # Player object there is an array with various slots that are used for
10             # various purposes. these slots are indexed using constant subs, and
11             # there is some overlap of these slots for animates, items, and terrain.
12             # the @Animates array (where the player, monsters, and items reside) and
13             # $LMap (level map, which has every ROW and COL and then an array (LMC)
14             # for what is in that cell) is where most of the game data resides.
15             # there can be only one terrain (GROUND), one ITEM, and one animate
16             # (ANI) per level map cell; any new interactions will need to support
17             # this. there are also four graphs per level map; these graphs dictate
18             # what moves are possible for animates (double benefit of providing both
19             # legal next moves and for pathfinding across the map). gravity pulls
20             # things down at the beginning of a turn (bottom up), and the player
21             # always moves first in the turn (low id to high), see the game_loop.
22             # level maps are ASCII text, and only one thing can be present in a cell
23             # in the map (with FLOOR being assumed present below any item or
24             # animate). there are some complications around killing things off; dead
25             # things must not interact with anything, but may still be looped to
26             # after their death in the apply_gravity or game_loop UPDATE calls.
27             # hence the BLACK_SPOT
28              
29             package Game::PlatformsOfPeril;
30              
31             our $VERSION = '0.09';
32              
33 1     1   69631 use 5.24.0;
  1         4  
34 1     1   17 use warnings;
  1         3  
  1         33  
35 1     1   516 use File::Spec::Functions qw(catfile);
  1         834  
  1         58  
36 1     1   542 use List::PriorityQueue ();
  1         836  
  1         25  
37 1     1   6 use List::Util qw(first);
  1         2  
  1         110  
38 1     1   510 use List::UtilsBy 0.06 qw(nsort_by rev_nsort_by);
  1         1931  
  1         68  
39 1     1   7 use Scalar::Util qw(weaken);
  1         4  
  1         48  
40 1     1   528 use Term::ReadKey qw(GetTerminalSize ReadKey ReadMode);
  1         2042  
  1         66  
41 1     1   589 use Time::HiRes qw(sleep);
  1         1373  
  1         5  
42 1     1   741 use POSIX qw(STDIN_FILENO TCIFLUSH tcflush);
  1         6388  
  1         5  
43              
44             # ANSI or XTerm control sequences
45 0     0 0 0 sub at { "\e[" . $_[1] . ';' . $_[0] . 'H' }
46 0     0 0 0 sub at_col { "\e[" . $_[0] . 'G' }
47             sub alt_screen () { "\e[?1049h" }
48             sub clear_line () { "\e[2K" }
49             sub clear_right () { "\e[K" }
50             sub clear_screen () { "\e[1;1H\e[2J" }
51             sub hide_cursor () { "\e[?25l" }
52             sub hide_pointer () { "\e[>3p" }
53             sub show_cursor () { "\e[?25h" }
54             sub term_norm () { "\e[m" }
55             sub unalt_screen () { "\e[?1049l" }
56              
57             # WHAT Animates and such can be
58             sub HERO () { 0 }
59             sub MONST () { 1 }
60             sub BOMB () { 2 }
61             sub GEM () { 3 }
62             sub FLOOR () { 4 }
63             sub WALL () { 5 }
64             sub LADDER () { 6 }
65             sub STAIR () { 7 }
66             sub STATUE () { 8 }
67              
68             sub BOMB_COST () { 2 }
69             sub GEM_VALUE () { 1 }
70              
71             # for the Level Map Cell (LMC)
72             sub WHERE () { 0 }
73             sub GROUND () { 1 }
74             sub ITEM () { 2 }
75             sub ANI () { 3 }
76              
77             sub MOVE_FAILED () { 0 }
78             sub MOVE_OK () { 1 }
79             sub MOVE_NEWLVL () { 2 }
80              
81             # for the level map
82             sub COLS () { 23 }
83             sub ROWS () { 23 }
84             sub MAP_DISP_OFF () { 1 }
85              
86             # level map is row, col while points are [ col, row ]
87             sub PROW () { 1 }
88             sub PCOL () { 0 }
89              
90             sub MSG_ROW () { 1 }
91             sub MSG_COL () { 25 }
92             # these also used to determine the minimum size for the terminal
93             sub MSG_MAX () { 24 }
94             sub MSG_COLS_MAX () { 70 }
95              
96             # for Animates (and also some Things for the first few slots)
97             sub WHAT () { 0 }
98             sub DISP () { 1 }
99             # NOTE that GROUND use TYPE to distinguish between different types of
100             # those (FLOOR, STAIR, STATUE) which makes the graph code simpler as
101             # that only needs to look at WHAT for whether motion is possible in that
102             # cell; ANI and ITEM instead use TYPE to tell ANI apart from ITEM
103             sub TYPE () { 2 }
104             sub STASH () { 3 }
105             sub UPDATE () { 4 }
106             sub LMC () { 5 }
107             sub BLACK_SPOT () { 6 }
108              
109             sub GEM_STASH () { 0 }
110             sub BOMB_STASH () { 1 }
111             sub GEM_ODDS () { 1 }
112              
113             sub GEM_ODDS_ADJUST () { 0.05 }
114              
115             sub START_GEMS () { 0 }
116             sub START_BOMBS () { 1 }
117              
118             sub GRAPH_NODE () { 0 }
119             sub GRAPH_WEIGHT () { 1 }
120             sub GRAPH_POINT () { 2 }
121              
122             our %CharMap = (
123             'o' => BOMB,
124             '.' => FLOOR,
125             '*' => GEM,
126             '@' => HERO,
127             '=' => LADDER,
128             'P' => MONST,
129             '%' => STAIR,
130             '&' => STATUE,
131             '#' => WALL,
132             );
133              
134             our (
135             @Animates, @Graphs, $LMap, $Monst_Name, @RedrawA,
136             @RedrawB, $Hero, $TCols, $TRows
137             );
138              
139             our %Examine_Offsets = (
140             'h' => [ -1, +0 ], # left
141             'j' => [ +0, +1 ], # down
142             'k' => [ +0, -1 ], # up
143             'l' => [ +1, +0 ], # right
144             'y' => [ -1, -1 ],
145             'u' => [ +1, -1 ],
146             'b' => [ -1, +1 ],
147             'n' => [ +1, +1 ],
148             );
149              
150             our $Level = 0;
151             our $Level_Path;
152              
153             # plosive practice. these must pluralize properly
154             our @Menagerie = (
155             'Palace Peacock',
156             'Peckish Packrat',
157             'Peevish Penguin',
158             'Piratical Parakeet',
159             'Placid Piranha',
160             'Pleasant Porcupine',
161             'Priggish Python',
162             'Prurient Pachyderm',
163             'Purposeful Plant',
164             # and some not-plosives for reasons lost in the mists of time
165             'Gruesome Goose',
166             'Sinister Swan',
167             'Xenophobic Xarci',
168             );
169             $Monst_Name = $Menagerie[ rand @Menagerie ];
170              
171             our $Redraw_Delay = 0.05;
172             our $Rotate_Delay = 0.20;
173             our $Rotation = 0;
174              
175             our @Scientists = qw(Eigen Maxwell Newton);
176             our $Scientist = $Scientists[ rand @Scientists ];
177              
178             our $Seed;
179              
180             our @Styles =
181             qw(Abstract Art-Deco Brutalist Egyptian Greek Impressionist Post-Modern Roman Romantic);
182             our $Style = $Styles[ rand @Styles ];
183              
184             our %Things = (
185             BOMB, [ BOMB, "\e[31mo\e[0m", ITEM ],
186             FLOOR, [ FLOOR, "\e[33m.\e[0m", FLOOR ],
187             GEM, [ GEM, "\e[32m*\e[0m", ITEM ],
188             LADDER, [ LADDER, "\e[37m=\e[0m", LADDER ],
189             STAIR, [ FLOOR, "\e[37m%\e[0m", STAIR ],
190             STATUE, [ FLOOR, "\e[1;33m&\e[0m", STATUE ],
191             WALL, [ WALL, "\e[35m#\e[0m", WALL ],
192             );
193              
194             our %Descriptions = (
195             BOMB, 'Bomb. Avoid.',
196             FLOOR, 'Empty cell.',
197             GEM, 'A gem. Get these.',
198             HERO, 'The much suffering hero.',
199             LADDER, 'A ladder.',
200             MONST, $Monst_Name . '. Wants to kill you.',
201             STAIR, 'A way out of this mess.',
202             STATUE, 'Empty cell with decorative statue.',
203             WALL, 'A wall.',
204             );
205              
206             $Animates[HERO]->@[ WHAT, DISP, TYPE, STASH, UPDATE ] =
207             (HERO, "\e[1;33m\@\e[0m", ANI, [ START_GEMS, START_BOMBS ], \&update_hero);
208              
209             our %Interact_With = (
210             HERO, # the target of the mover
211             sub {
212             my ($mover, $target) = @_;
213             game_over_monster() if $mover->[WHAT] == MONST;
214             game_over_bomb() if $mover->[WHAT] == BOMB;
215             grab_gem($target, $mover);
216             },
217             MONST,
218             sub {
219             my ($mover, $target) = @_;
220             game_over_monster() if $mover->[WHAT] == HERO;
221             if ($mover->[WHAT] == BOMB) {
222             my @cells = map { kill_animate($_, 1); $_->[LMC][WHERE] } $mover, $target;
223             redraw_ref(\@cells);
224             explode($target);
225             } elsif ($mover->[WHAT] == GEM) {
226             grab_gem($target, $mover);
227             }
228             },
229             BOMB,
230             sub {
231             my ($mover, $target) = @_;
232             game_over_bomb() if $mover->[WHAT] == HERO;
233             if ($mover->[WHAT] == MONST) {
234             my @cells = map { kill_animate($_, 1); $_->[LMC][WHERE] } $mover, $target;
235             redraw_ref(\@cells);
236             explode($mover);
237             }
238             },
239             GEM,
240             sub {
241             my ($mover, $target) = @_;
242             if ($mover->[TYPE] == ANI) {
243             relocate($mover, $target->[LMC][WHERE]);
244             grab_gem($mover, $target);
245             }
246             },
247             );
248              
249             our %Key_Commands = (
250             'h' => move_player(-1, +0), # left
251             'j' => move_player(+0, +1), # down
252             'k' => move_player(+0, -1), # up
253             'l' => move_player(+1, +0), # right
254             '.' => \&move_nop, # rest
255             ' ' => \&move_nop, # also rest
256             'v' => sub { post_message('Version ' . $VERSION); return MOVE_FAILED },
257             'x' => \&move_examine,
258             '<' => sub {
259             post_message($Scientist . q{'s magic wonder left boot, activate!});
260             rotate_left();
261             print draw_level();
262             sleep($Rotate_Delay);
263             return MOVE_OK;
264             },
265             '>' => sub {
266             post_message($Scientist . q{'s magic wonder right boot, activate!});
267             rotate_right();
268             print draw_level();
269             sleep($Rotate_Delay);
270             return MOVE_OK;
271             },
272             '?' => sub {
273             post_help();
274             return MOVE_FAILED;
275             },
276             # for debugging, probably shouldn't be included as it shows exactly
277             # where the monsters are trying to move to which may or may not be
278             # where the player is
279             'T' => sub {
280             local $" = ',';
281             post_message("T $Hero->@* R $Rotation");
282             return MOVE_FAILED;
283             },
284             '@' => sub {
285             local $" = ',';
286             post_message("\@ $Animates[HERO][LMC][WHERE]->@* R $Rotation");
287             return MOVE_FAILED;
288             },
289             '$' => sub {
290             post_message('You have '
291             . $Animates[HERO][STASH][BOMB_STASH]
292             . ' bombs and '
293             . $Animates[HERO][STASH][GEM_STASH]
294             . ' gems.');
295             return MOVE_FAILED;
296             },
297             # by way of history '%' is what rogue (version 3.6) uses for stairs,
298             # except the '>' (or very rarely '<') keys are used to interact with
299             # that symbol
300             '%' => sub {
301             if ($Animates[HERO][LMC][GROUND][TYPE] == STAIR) {
302             load_level();
303             print clear_screen(), draw_level();
304             post_message('Level '
305             . $Level
306             . ' (You have '
307             . $Animates[HERO][STASH][BOMB_STASH]
308             . ' bombs and '
309             . $Animates[HERO][STASH][GEM_STASH]
310             . ' gems.)');
311             return MOVE_NEWLVL;
312             } else {
313             post_message('There are no stairs here?');
314             return MOVE_FAILED;
315             }
316             },
317             'B' => sub {
318             my $lmc = $Animates[HERO][LMC];
319             return MOVE_FAILED, 'You have no bombs (make them from gems).'
320             if $Animates[HERO][STASH][BOMB_STASH] < 1;
321             return MOVE_FAILED, 'There is already an item in this cell.'
322             if defined $lmc->[ITEM];
323             $Animates[HERO][STASH][BOMB_STASH]--;
324             make_item($lmc->[WHERE], BOMB, 0);
325             return MOVE_OK;
326             },
327             'M' => sub {
328             return MOVE_FAILED, 'You need more gems.'
329             if $Animates[HERO][STASH][GEM_STASH] < BOMB_COST;
330             $Animates[HERO][STASH][GEM_STASH] -= BOMB_COST;
331             post_message('You now have ' . ++$Animates[HERO][STASH][BOMB_STASH] . ' bombs');
332             return MOVE_OK;
333             },
334             'q' => sub { game_over('Be seeing you...') },
335             "\003" => sub { #
336             post_message('Enough with these silly interruptions!');
337             return MOVE_FAILED;
338             },
339             "\014" => sub { #
340             redraw_level();
341             return MOVE_FAILED;
342             },
343             "\032" => sub { #
344             post_message('You hear a strange noise in the background.');
345             return MOVE_FAILED;
346             },
347             "\033" => sub {
348             post_message('You cannot escape quite so easily.');
349             return MOVE_FAILED;
350             },
351             );
352              
353             sub apply_gravity {
354 0     0 0 0 for my $ent (rev_nsort_by { $_->[LMC][WHERE][PROW] } @Animates) {
  0     0   0  
355 0 0       0 next if $ent->[BLACK_SPOT];
356 0         0 my $here = $ent->[LMC][WHERE];
357             next
358 0 0 0     0 if $here->[PROW] == ROWS - 1
      0        
      0        
359             or ( $ent->[TYPE] == ANI
360             and $LMap->[ $here->[PROW] ][ $here->[PCOL] ][GROUND][WHAT] == LADDER)
361             or $LMap->[ $here->[PROW] + 1 ][ $here->[PCOL] ][GROUND][WHAT] == WALL;
362 0         0 my $dest = [ $here->[PCOL], $here->[PROW] + 1 ];
363 0 0       0 relocate($ent, $dest) unless interact($ent, $dest);
364 0 0       0 if ($ent->[WHAT] == HERO) {
365 0 0       0 if ($ent->[LMC][GROUND][WHAT] == LADDER) {
366 0         0 post_message('You fall, but grab onto a ladder.');
367             } else {
368 0         0 post_message('You fall!');
369             }
370             }
371             }
372             }
373              
374             sub bad_terminal {
375 0     0 0 0 ($TCols, $TRows) = (GetTerminalSize(*STDOUT))[ 0, 1 ];
376 0   0     0 return (not defined $TCols or $TCols < MSG_COLS_MAX or $TRows < MSG_MAX);
377             }
378              
379             sub bail_out {
380 0     0 0 0 restore_term();
381 0         0 print "\n", at_col(0), clear_line;
382 0 0       0 warn $_[0] if @_;
383 0         0 game_over("Suddenly, the platforms collapse about you.");
384             }
385              
386             sub between {
387 0     0 0 0 my ($min, $max, $value) = @_;
388 0 0       0 if ($value < $min) {
    0          
389 0         0 $value = $min;
390             } elsif ($value > $max) {
391 0         0 $value = $max;
392             }
393 0         0 return $value;
394             }
395              
396             sub draw_level {
397 0     0 0 0 my $s = '';
398 0         0 for my $rownum (0 .. ROWS - 1) {
399 0         0 $s .= at(MAP_DISP_OFF, MAP_DISP_OFF + $rownum);
400 0         0 for my $lmc ($LMap->[$rownum]->@*) {
401 0 0       0 if (defined $lmc->[ANI]) {
    0          
402 0         0 $s .= $lmc->[ANI][DISP];
403             } elsif (defined $lmc->[ITEM]) {
404 0         0 $s .= $lmc->[ITEM][DISP];
405             } else {
406 0         0 $s .= $lmc->[GROUND][DISP];
407             }
408             }
409             }
410 0         0 $s .= at(1, ROWS + 1) . $Things{ WALL, }[DISP] x COLS;
411 0         0 return $s;
412             }
413              
414             sub explode {
415 0     0 0 0 my ($something) = @_;
416 0         0 my $lmc = $something->[LMC];
417 0         0 my $pos = $lmc->[WHERE];
418 0         0 my @colors = ("\e[31m", "\e[33m");
419 0         0 for (1 .. 7) {
420 0         0 print at(map { MAP_DISP_OFF + $_ } $pos->@*), $colors[ rand @colors ], '*',
  0         0  
421             term_norm;
422 0         0 sleep($Redraw_Delay);
423             }
424 0         0 post_message('ka-boom!');
425             # HEROIC DESTRUCTION
426 0 0       0 $lmc->[GROUND] = $Things{ FLOOR, } if $lmc->[GROUND][TYPE] == STATUE;
427 0         0 push @RedrawA, $pos;
428             }
429              
430             # cribbed from some A* article on https://www.redblobgames.com/
431             sub find_hero {
432 0     0 0 0 my ($ent, $mcol, $mrow) = @_;
433              
434 0         0 my $start = $mcol . ',' . $mrow;
435 0         0 my $pcol = $Hero->[PCOL];
436 0         0 my $prow = $Hero->[PROW];
437 0         0 my $end = $pcol . ',' . $prow;
438              
439             # already waiting where the player is going to fall to
440 0 0       0 return if $start eq $end;
441              
442 0         0 my %costs = ($start => 0);
443 0         0 my %seen = ($start => undef);
444 0         0 my $q = List::PriorityQueue->new;
445 0         0 $q->insert($start, 0);
446              
447 0         0 my $linked = 0;
448 0         0 while (my $node = $q->pop) {
449 0 0       0 if ($node eq $end) {
450 0         0 $linked = 1;
451 0         0 last;
452             }
453 0         0 for my $peer ($Graphs[$Rotation]{$node}->@*) {
454 0         0 my $new = $peer->[GRAPH_NODE];
455 0         0 my $cost = $costs{$node} + $peer->[GRAPH_WEIGHT];
456 0 0 0     0 if (not exists $seen{$new} or $cost < $costs{$new}) {
457 0         0 $costs{$new} = $cost;
458             # perhaps they drove taxicabs in Manhattan in a former life?
459 0         0 my $priority =
460             $cost +
461             abs($pcol - $peer->[GRAPH_POINT][PCOL]) +
462             abs($prow - $peer->[GRAPH_POINT][PROW]);
463 0         0 $q->insert($new, $priority);
464 0         0 $seen{$new} = $node;
465             }
466             }
467             }
468 0 0       0 return unless $linked;
469              
470 0         0 my @path;
471 0         0 my $node = $end;
472 0         0 while ($node ne $start) {
473 0         0 unshift @path, $node;
474 0         0 $node = $seen{$node};
475             }
476 0         0 return [ split ',', $path[0] ];
477             }
478              
479             sub game_loop {
480 0 0   0 0 0 game_over('Terminal must be at least ' . MSG_COLS_MAX . 'x' . MSG_MAX)
481             if bad_terminal();
482 0         0 ($Level_Path, $Level, $Seed) = @_;
483 0         0 $SIG{$_} = \&bail_out for qw(INT HUP TERM PIPE QUIT USR1 USR2 __DIE__);
484 0         0 STDOUT->autoflush(1);
485 0         0 load_level();
486 0         0 ReadMode 'raw';
487 0         0 print term_norm, alt_screen, hide_cursor, hide_pointer, clear_screen,
488             draw_level;
489 0         0 post_message('The Platforms of Peril');
490 0         0 post_message('');
491 0         0 post_message('Your constant foes, the ' . properly_plural($Monst_Name));
492 0         0 post_message('seek to destroy your way of life!');
493 0         0 post_help();
494 0         0 post_message('');
495 0         0 post_message('Seed ' . $Seed . ' of version ' . $VERSION);
496 0         0 $SIG{CONT} = \&redraw_level;
497             $SIG{WINCH} = sub {
498 0 0   0   0 post_message('The terminal is too small!') if bad_terminal();
499 0         0 redraw_level();
500 0         0 };
501              
502 0         0 while (1) {
503 0         0 apply_gravity();
504 0         0 @Animates = grep { !$_->[BLACK_SPOT] } @Animates;
  0         0  
505 0 0       0 redraw_movers() if @RedrawA;
506 0 0       0 next if $Animates[HERO][UPDATE]->() == MOVE_NEWLVL;
507 0         0 track_hero();
508 0         0 for my $ent (@Animates[ 1 .. $#Animates ]) {
509 0 0 0     0 $ent->[UPDATE]->($ent) if !$ent->[BLACK_SPOT] and defined $ent->[UPDATE];
510             }
511 0         0 @Animates = grep { !$_->[BLACK_SPOT] } @Animates;
  0         0  
512 0         0 redraw_movers();
513             }
514             }
515              
516             sub game_over {
517 0     0 0 0 my ($msg, $code) = @_;
518 0   0     0 $code //= 1;
519 0         0 restore_term();
520 0         0 print clear_right, $msg, ' (', $Animates[HERO][STASH][GEM_STASH],
521             " gems)\n", clear_right;
522 0         0 exit $code;
523             }
524              
525 0     0 0 0 sub game_over_bomb { game_over('You gone done blowed yourself up.') }
526              
527             sub game_over_monster {
528 0     0 0 0 game_over('The ' . $Monst_Name . ' polished you off.');
529             }
530              
531             sub grab_gem {
532 0     0 0 0 my ($ent, $gem) = @_;
533 0         0 $ent->[STASH][GEM_STASH] += $gem->[STASH];
534 0         0 kill_animate($gem);
535 0 0       0 if ($ent->[WHAT] == MONST) {
536 0         0 post_message('The ' . $Monst_Name . ' grabs a gem.');
537             } else {
538 0         0 post_message('You now have ' . $ent->[STASH][GEM_STASH] . ' gems.');
539             }
540             }
541              
542             sub graph_bilink {
543 0     0 0 0 my ($g, $c1, $r1, $c2, $r2) = @_;
544 0         0 my $from = $c1 . ',' . $r1;
545 0         0 my $to = $c2 . ',' . $r2;
546 0         0 push $g->{$from}->@*, [ $to, 1, [ $c2, $r2 ] ];
547 0         0 push $g->{$to}->@*, [ $from, 1, [ $c1, $r1 ] ];
548             }
549              
550             sub graph_setup {
551 0     0 0 0 my $g = {};
552 0         0 for my $r (0 .. ROWS - 2) {
553 0         0 for my $c (0 .. COLS - 1) {
554 0 0       0 next if $LMap->[$r][$c][GROUND][WHAT] == WALL;
555             # allow left/right, if ladder or wall below permits it
556 0 0 0     0 if ($c != COLS - 1
      0        
      0        
      0        
557             and ( $LMap->[$r][$c][GROUND][WHAT] == LADDER
558             or $LMap->[ $r + 1 ][$c][GROUND][WHAT] == WALL)
559             and (
560             $LMap->[$r][ $c + 1 ][GROUND][WHAT] == LADDER
561             or ( $LMap->[$r][ $c + 1 ][GROUND][WHAT] != WALL
562             and $LMap->[ $r + 1 ][ $c + 1 ][GROUND][WHAT] == WALL)
563             )
564             ) {
565 0         0 graph_bilink($g, $c, $r, $c + 1, $r);
566             }
567 0 0       0 if ($r > 0) {
568             # allow motion up/down ladders
569 0 0 0     0 if ( $LMap->[$r][$c][GROUND][WHAT] == LADDER
    0 0        
      0        
570             and $LMap->[ $r - 1 ][$c][GROUND][WHAT] == LADDER) {
571 0         0 graph_bilink($g, $c, $r, $c, $r - 1);
572             } elsif (
573             $LMap->[$r][$c][GROUND][WHAT] == LADDER
574             or ( $LMap->[$r][$c][GROUND][WHAT] == FLOOR
575             and $LMap->[ $r + 1 ][$c][GROUND][WHAT] == WALL)
576             ) {
577             # can we fall into this cell from above?
578 0         0 graph_shaft($g, $c, $r);
579             }
580             }
581             }
582             }
583 0         0 for my $c (0 .. COLS - 1) {
584 0 0       0 next if $LMap->[ ROWS - 1 ][$c][GROUND][WHAT] == WALL;
585 0 0 0     0 if ( $LMap->[ ROWS - 1 ][$c][GROUND][WHAT] == LADDER
586             and $LMap->[ ROWS - 2 ][$c][GROUND][WHAT] == LADDER) {
587 0         0 graph_bilink($g, $c, ROWS - 1, $c, ROWS - 2);
588             } else {
589 0         0 graph_shaft($g, $c, ROWS - 1);
590             }
591 0 0       0 if ($c != COLS - 1) {
592 0         0 graph_bilink($g, $c, ROWS - 1, $c + 1, ROWS - 1);
593             }
594             }
595 0         0 return $g;
596             }
597              
598             sub graph_shaft {
599 0     0 0 0 my ($g, $c, $r) = @_;
600 0         0 for my $x (reverse 0 .. $r - 1) {
601 0 0       0 last if $LMap->[$x][$c][GROUND][WHAT] == WALL;
602 0         0 my $weight = $r - $x;
603 0 0       0 if ($LMap->[$x][$c][GROUND][WHAT] == LADDER) {
604 0 0       0 if ($weight == 1) {
605 0         0 graph_udlink($g, $c, $x, $c, $r, 1, [ $c, $x ]);
606             } else {
607 0         0 graph_udlink($g, $c, $x, $c, $x + 1, 1, [ $c, $x ]);
608 0         0 graph_udlink($g, $c, $x + 1, $c, $r, $weight - 2, [ $c, $r ]);
609             }
610 0         0 last;
611             }
612             # can fall into this shaft from the left or right?
613 0 0 0     0 if ($c != 0
      0        
614             and (
615             $LMap->[$x][ $c - 1 ][GROUND][WHAT] == LADDER
616             or ( $LMap->[$x][ $c - 1 ][GROUND][WHAT] == FLOOR
617             and $LMap->[ $x + 1 ][ $c - 1 ][GROUND][WHAT] == WALL)
618             )
619             ) {
620 0         0 graph_udlink($g, $c - 1, $x, $c, $x, 1, [ $c, $x ]);
621 0         0 graph_udlink($g, $c, $x, $c, $r, $weight - 1, [ $c, $r ]);
622             }
623 0 0 0     0 if ($c != COLS - 1
      0        
624             and (
625             $LMap->[$x][ $c + 1 ][GROUND][WHAT] == LADDER
626             or ( $LMap->[$x][ $c + 1 ][GROUND][WHAT] == FLOOR
627             and $LMap->[ $x + 1 ][ $c + 1 ][GROUND][WHAT] == WALL)
628             )
629             ) {
630 0         0 graph_udlink($g, $c + 1, $x, $c, $x, $weight, [ $c, $x ]);
631 0         0 graph_udlink($g, $c, $x, $c, $r, $weight - 1, [ $c, $r ]);
632             }
633             }
634             }
635              
636             sub graph_udlink {
637 0     0 0 0 my ($g, $c1, $r1, $c2, $r2, $weight, $point) = @_;
638 0         0 my $from = $c1 . ',' . $r1;
639 0         0 my $to = $c2 . ',' . $r2;
640 0         0 push $g->{$from}->@*, [ $to, $weight, $point ];
641             }
642              
643             sub interact {
644 0     0 0 0 my ($mover, $dest) = @_;
645 0         0 for my $i (ANI, ITEM) {
646 0         0 my $target = $LMap->[ $dest->[PROW] ][ $dest->[PCOL] ][$i];
647 0 0       0 if (defined $target) {
648             # this code is assumed to take care of everything and be the
649             # final say on the interaction
650 0         0 $Interact_With{ $target->[WHAT] }->($mover, $target);
651 0         0 return 1;
652             }
653             }
654 0         0 return 0;
655             }
656              
657             sub kill_animate {
658 0     0 0 0 my ($ent, $no_draw) = @_;
659 0 0       0 push @RedrawA, $ent->[LMC][WHERE] unless defined $no_draw;
660 0         0 $ent->[BLACK_SPOT] = 1;
661             # NOTE this only works for TYPE of ANI or ITEM, may need to rethink
662             # how STATUE and STAIRS are handled if there are GROUND checks on
663             # TYPE as those abuse the TYPE field for other things (see %Things)
664 0         0 undef $ent->[LMC][ $ent->[TYPE] ];
665             }
666              
667             sub load_level {
668 0     0 0 0 my $file = catfile($Level_Path, 'level' . $Level++);
669 0 0       0 game_over('You have completed all the levels.', 0) unless -e $file;
670              
671 0 0       0 open(my $fh, '<', $file) or game_over("Failed to open '$file': $!");
672              
673 0         0 splice @Animates, 1;
674 0         0 undef $Animates[HERO][LMC];
675 0         0 $LMap = [];
676              
677 0         0 my $rownum = 0;
678 0         0 while (my $line = readline $fh) {
679 0         0 chomp $line;
680 0 0       0 game_over("Wrong number of columns at $file:$.") if length $line != COLS;
681 0         0 my $colnum = 0;
682 0         0 for my $v (split //, $line) {
683 0   0     0 my $c = $CharMap{$v} // game_over("Unknown character $v at $file:$.");
684 0         0 my $point = [ $colnum++, $rownum ]; # PCOL, PROW (x, y)
685 0 0       0 if (exists $Things{$c}) {
686 0 0       0 if ($c eq BOMB) {
    0          
687 0         0 push $LMap->[$rownum]->@*, [ $point, $Things{ FLOOR, } ];
688 0         0 make_item($point, BOMB, 0);
689             } elsif ($c eq GEM) {
690 0         0 push $LMap->[$rownum]->@*, [ $point, $Things{ FLOOR, } ];
691 0         0 make_item($point, GEM, GEM_VALUE);
692             } else {
693 0         0 push $LMap->[$rownum]->@*, [ $point, $Things{$c} ];
694             }
695             } else {
696 0 0       0 if ($c eq HERO) {
    0          
697 0 0       0 game_over("Player placed twice in $file")
698             if defined $Animates[HERO][LMC];
699             push $LMap->[$rownum]->@*,
700 0         0 [ $point, $Things{ FLOOR, }, undef, $Animates[HERO] ];
701 0         0 $Animates[HERO][LMC] = $LMap->[$rownum][-1];
702 0         0 $Hero = $point;
703 0         0 weaken($Animates[HERO][LMC]);
704             } elsif ($c eq MONST) {
705 0         0 push $LMap->[$rownum]->@*, [ $point, $Things{ FLOOR, } ];
706 0         0 make_monster($point);
707             } else {
708 0         0 game_over("Unknown object '$v' at $file:$.");
709             }
710             }
711             }
712 0 0       0 last if ++$rownum == ROWS;
713             }
714 0 0       0 game_over("Too few rows in $file") if $rownum < ROWS;
715 0 0       0 game_over("No player in $file") unless defined $Animates[HERO][LMC];
716              
717 0         0 $Rotation = 0;
718 0         0 for my $rot (1 .. 4) {
719 0         0 $Graphs[$Rotation] = graph_setup();
720 0         0 rotate_left();
721             }
722             }
723              
724             sub make_item {
725 0     0 0 0 my ($point, $thingy, $stash, $update) = @_;
726 0         0 my $item;
727             $item->@[ WHAT, DISP, TYPE, STASH, UPDATE, LMC ] = (
728 0         0 $Things{$thingy}->@*,
729             $stash, $update, $LMap->[ $point->[PROW] ][ $point->[PCOL] ]
730             );
731 0         0 push @Animates, $item;
732 0         0 $LMap->[ $point->[PROW] ][ $point->[PCOL] ][ITEM] = $item;
733 0         0 weaken($item->[LMC]);
734             }
735              
736             sub make_monster {
737 0     0 0 0 my ($point) = @_;
738 0         0 my $monst;
739 0         0 my $ch = substr $Monst_Name, 0, 1;
740             # STASH replicates that of the HERO for simpler GEM handling code
741             # though the BOMB_STASH is instead used for GEM_ODDS
742 0         0 $monst->@[ WHAT, DISP, TYPE, STASH, UPDATE, LMC ] = (
743             MONST, "\e[1;33m$ch\e[0m", ANI, [ 0, 0.0 ],
744             \&update_monst, $LMap->[ $point->[PROW] ][ $point->[PCOL] ]
745             );
746 0         0 push @Animates, $monst;
747 0         0 $LMap->[ $point->[PROW] ][ $point->[PCOL] ][ANI] = $monst;
748 0         0 weaken($monst->[LMC]);
749             }
750              
751             sub move_animate {
752 0     0 0 0 my ($ent, $cols, $rows) = @_;
753 0         0 my $lmc = $ent->[LMC];
754              
755 0         0 my $from = $lmc->[WHERE][PCOL] . ',' . $lmc->[WHERE][PROW];
756 0         0 my $to =
757             ($lmc->[WHERE][PCOL] + $cols) . ',' . ($lmc->[WHERE][PROW] + $rows);
758              
759             return MOVE_FAILED
760 0 0   0   0 unless first { $_->[GRAPH_NODE] eq $to } $Graphs[$Rotation]{$from}->@*;
  0         0  
761              
762 0         0 my $dest = [ $lmc->[WHERE][PCOL] + $cols, $lmc->[WHERE][PROW] + $rows ];
763              
764 0 0       0 relocate($ent, $dest) unless interact($ent, $dest);
765 0         0 return MOVE_OK;
766             }
767              
768             # so the player can see if there is a ladder under something; this is an
769             # important consideration on some levels
770             sub move_examine {
771 0     0 0 0 my $key;
772 0         0 my $row = $Animates[HERO][LMC][WHERE][PROW];
773 0         0 my $col = $Animates[HERO][LMC][WHERE][PCOL];
774 0         0 print at(MSG_COL, MSG_ROW + $_), clear_right for 1 .. MSG_MAX;
775 0         0 print at(MSG_COL, MSG_ROW), clear_right,
776             'Move cursor to view a cell. Esc exits', show_cursor;
777 0         0 while (1) {
778 0         0 print at(MSG_COL, MSG_ROW + $_), clear_right for 3 .. 5;
779 0         0 my $disp_row = 2;
780 0         0 for my $i (ANI, ITEM) {
781 0         0 my $x = $LMap->[$row][$col][$i];
782 0 0       0 if (defined $x) {
783             print at(MSG_COL, MSG_ROW + $disp_row++), clear_right, $x->[DISP],
784 0         0 ' - ', $Descriptions{ $x->[WHAT] };
785             }
786             }
787 0         0 my $g = $LMap->[$row][$col][GROUND];
788             print at(MSG_COL, MSG_ROW + $disp_row), clear_right, $g->[DISP],
789 0         0 ' - ', $Descriptions{ $g->[TYPE] },
790             at(MAP_DISP_OFF + $col, MAP_DISP_OFF + $row);
791 0         0 $key = ReadKey(0);
792 0 0       0 last if $key eq "\033";
793 0         0 my $distance = 1;
794 0 0       0 if (ord $key < 97) { # SHIFT moves faster
795 0         0 $key = lc $key;
796 0         0 $distance = 5;
797             }
798 0   0     0 my $dir = $Examine_Offsets{$key} // next;
799 0         0 $row = between(0, ROWS - 1, $row + $dir->[PROW] * $distance);
800 0         0 $col = between(0, COLS - 1, $col + $dir->[PCOL] * $distance);
801             }
802 0         0 print hide_cursor;
803 0         0 show_messages();
804 0         0 return MOVE_FAILED;
805             }
806              
807 0     0 0 0 sub move_nop { return MOVE_OK }
808              
809             sub move_player {
810 4     4 0 8 my ($cols, $rows) = @_;
811 0     0     sub { move_animate($Animates[HERO], $cols, $rows) }
812 4         46 }
813              
814             sub post_help {
815 0     0 0   my $ch = substr $Monst_Name, 0, 1;
816 0           post_message('');
817 0           post_message(
818             ' ' . $Animates[HERO][DISP] . ' - You ' . $ch . ' - a ' . $Monst_Name);
819             post_message(
820 0           ' ' . $Things{ STATUE, }[DISP] . ' - a large granite statue done in the');
821 0           post_message(' ' . $Style . ' style');
822             post_message(' '
823             . $Things{ BOMB, }[DISP]
824             . ' - Bomb '
825 0           . $Things{ GEM, }[DISP]
826             . ' - Gem (get these)');
827 0           post_message('');
828 0           post_message(' h j k l - move');
829 0           post_message(' < > - activate left or right boot');
830 0           post_message(' B - drop a Bomb');
831 0           post_message(' M - make a Bomb (consumes ' . BOMB_COST . ' Gems)');
832             post_message(
833 0           ' % - when on ' . $Things{ STAIR, }[DISP] . ' goes to the next level');
834 0           post_message(' . space - pass a turn (handy when falling)');
835 0           post_message('');
836 0           post_message(' q - quit the game (no save)');
837 0           post_message(' $ - display Bomb and Gem counts');
838 0           post_message(' ? - post these help messages');
839 0           post_message('');
840 0           post_message('You have '
841             . $Animates[HERO][STASH][BOMB_STASH]
842             . ' bombs and '
843             . $Animates[HERO][STASH][GEM_STASH]
844             . ' gems.');
845             }
846              
847             {
848             my @log;
849              
850             sub post_message {
851 0     0 0   my ($msg) = @_;
852 0           while (@log >= MSG_MAX) { shift @log }
  0            
853 0           push @log, $msg;
854 0           show_messages();
855             }
856 0     0 0   sub clear_messages { @log = () }
857              
858             sub show_messages {
859 0     0 0   for my $i (0 .. $#log) {
860 0           print at(MSG_COL, MSG_ROW + $i), clear_right, $log[$i];
861             }
862             }
863             }
864              
865             # fsvo properly... damnit Jim I'm a sysadmin not a linguist
866             sub properly_plural {
867 0     0 0   my ($name) = @_;
868 0 0         $name =~ s/oo/ee/ ? $name : $name . 's';
869             }
870              
871 0     0 0   sub redraw_level { print clear_screen, draw_level; show_messages() }
  0            
872              
873             sub redraw_movers {
874 0     0 0   redraw_ref(\@RedrawA);
875 0           sleep($Redraw_Delay);
876 0           redraw_ref(\@RedrawB);
877 0           @RedrawA = ();
878 0           @RedrawB = ();
879             }
880              
881             sub redraw_ref {
882 0     0 0   CELL: for my $point ($_[0]->@*) {
883 0           for my $i (ANI, ITEM) {
884 0           my $ent = $LMap->[ $point->[PROW] ][ $point->[PCOL] ][$i];
885 0 0 0       if (defined $ent and !$ent->[BLACK_SPOT]) {
886 0           print at(map { MAP_DISP_OFF + $_ } $point->@*), $ent->[DISP];
  0            
887 0           next CELL;
888             }
889             }
890 0           print at(map { MAP_DISP_OFF + $_ } $point->@*),
  0            
891             $LMap->[ $point->[PROW] ][ $point->[PCOL] ][GROUND][DISP];
892             }
893             }
894              
895             sub relocate {
896 0     0 0   my ($ent, $dest) = @_;
897 0           my $src = $ent->[LMC][WHERE];
898 0           push @RedrawA, $src;
899 0           push @RedrawB, $dest;
900 0           my $lmc = $LMap->[ $dest->[PROW] ][ $dest->[PCOL] ];
901 0           $lmc->[ $ent->[TYPE] ] = $ent;
902 0           undef $LMap->[ $src->[PROW] ][ $src->[PCOL] ][ $ent->[TYPE] ];
903 0           $ent->[LMC] = $lmc;
904 0           weaken($ent->[LMC]);
905             }
906              
907             sub restore_term {
908 0     0 0   ReadMode 'restore';
909 0           print term_norm, show_cursor, unalt_screen;
910             }
911              
912             sub rotate_left {
913 0     0 0   my $lm;
914 0           for my $r (0 .. ROWS - 1) {
915 0           for my $c (0 .. COLS - 1) {
916 0           my $newr = COLS - 1 - $c;
917 0           $lm->[$newr][$r] = $LMap->[$r][$c];
918 0           $lm->[$newr][$r][WHERE] = [ $r, $newr ];
919             }
920             }
921 0           $LMap = $lm;
922 0           $Rotation = ($Rotation + 1) % 4;
923             }
924              
925             sub rotate_right {
926 0     0 0   my $lm;
927 0           for my $r (0 .. ROWS - 1) {
928 0           for my $c (0 .. COLS - 1) {
929 0           my $newc = ROWS - 1 - $r;
930 0           $lm->[$c][$newc] = $LMap->[$r][$c];
931 0           $lm->[$c][$newc][WHERE] = [ $newc, $c ];
932             }
933             }
934 0           $LMap = $lm;
935 0           $Rotation = ($Rotation - 1) % 4;
936             }
937              
938             sub track_hero {
939 0     0 0   $Hero = $Animates[HERO][LMC][WHERE];
940              
941             # route monsters to where the player will fall to as otherwise they
942             # tend to freeze or head in the wrong direction
943 0           my $row = $Hero->[PROW];
944 0           my $col = $Hero->[PCOL];
945 0 0 0       return if $row == ROWS - 1 or $LMap->[$row][$col][GROUND][WHAT] == LADDER;
946              
947 0           my $goal = $row;
948 0           for my $r ($row + 1 .. ROWS - 1) {
949 0 0         last if $LMap->[$r][$col][GROUND][WHAT] == WALL;
950 0 0 0       if ($LMap->[$r][$col][GROUND][WHAT] == LADDER
      0        
      0        
      0        
      0        
951             or ( $r < ROWS - 2
952             and $LMap->[$r][$col][GROUND][WHAT] == FLOOR
953             and $LMap->[ $r + 1 ][$col][GROUND][WHAT] == WALL)
954             or ( $r == ROWS - 1
955             and $LMap->[$r][$col][GROUND][WHAT] == FLOOR)
956             ) {
957 0           $goal = $r;
958 0           last;
959             }
960             }
961 0           $Hero = [ $col, $goal ];
962             }
963              
964             sub update_hero {
965 0     0 0   my ($key, $ret);
966 0           tcflush(STDIN_FILENO, TCIFLUSH);
967 0           while (1) {
968 0           while (1) {
969 0           $key = ReadKey(0);
970 0 0         last if exists $Key_Commands{$key};
971             #post_message(sprintf "Illegal command \\%03o", ord $key);
972             }
973 0           $ret = $Key_Commands{$key}->();
974 0 0         last if $ret != MOVE_FAILED;
975             }
976 0           return $ret;
977             }
978              
979             sub update_monst {
980 0     0 0   my ($ent) = @_;
981 0           my $mcol = $ent->[LMC][WHERE][PCOL];
982 0           my $mrow = $ent->[LMC][WHERE][PROW];
983              
984             # prevent monster move where only gravity should apply
985             # NOTE one may have the clever idea that monsters can run across the
986             # heads of other monsters though that would require changes to how
987             # the graph is setup to permit such moves, and additional checks to
988             # see if something to tread upon is available (and then to let the
989             # hero do that (like in Lode Runner) or to prevent them from such
990             # head-running...)
991 0 0 0       if ( $mrow != ROWS - 1
      0        
992             and $ent->[LMC][GROUND][WHAT] == FLOOR
993             and $LMap->[ $mrow + 1 ][$mcol][GROUND][WHAT] != WALL) {
994 0           return;
995             }
996              
997 0           my $dest = find_hero($ent, $mcol, $mrow);
998 0 0         return unless defined $dest;
999              
1000 0 0         relocate($ent, $dest) unless interact($ent, $dest);
1001              
1002 0 0 0       if ($ent->[STASH][GEM_STASH] > 0
1003             and !defined $ent->[LMC][ITEM]) {
1004 0 0         if (rand() < $ent->[STASH][GEM_ODDS]) {
1005 0           post_message('The ' . $Monst_Name . ' drops a gem!');
1006 0           $ent->[STASH][GEM_STASH]--;
1007 0           make_item($ent->[LMC][WHERE], GEM, GEM_VALUE);
1008 0           $ent->[STASH][GEM_ODDS] = 0.0 - GEM_ODDS_ADJUST;
1009             }
1010 0           $ent->[STASH][GEM_ODDS] += GEM_ODDS_ADJUST;
1011             }
1012             }
1013              
1014             1;
1015             __END__