Update README: I forgot the word "file"
[robots.git] / robots.inf
1 ! --------------------------------------------------------------------------
2 !  "ROBOTS":  Another abuse of the Z-machine, Copied Right in 1995-1997
3 !  Torbjörn Andersson, d91tan@Update.UU.SE
4 !
5 !  This program is free. By all means, do fold, spindle and mutilate.
6 !  Be aware, however, that removing my name from it may cause
7 !  irreparable damage to your karma. If you do redistribute it in any
8 !  form, please drop me a note. I'd love to hear about it! If you
9 !  think you can make money from it, you are more optimistic than I
10 !  thought.
11 !
12 !  I got the idea of writing this when seeing Andrew Plotkin's much
13 !  more interesting game "Freefall". I used his code for reference
14 !  about some technical details.
15 !
16 !  I don't know who originally came up with this game idea. I have
17 !  seen it under the name "DALEKS", but that version was a bit
18 !  different. This one uses (almost) both the layout and the key
19 !  configuration of the version which can, at least usually, be found
20 !  in /usr/games on Unix systems.
21 !
22 !  Release 2 makes some slight optimizations (I hope) to the code
23 !  which detects collisions between robots, and makes a few cosmetic
24 !  changes.
25 !
26 !  Release 3 cleans up some of the code a bit, makes some further
27 !  optimizations to the collision-detection and allows the user to
28 !  keep playing even when the maximum number of robots have been
29 !  reached. (It just won't increase the number of robots any further.)
30 !  For this reason, I've lowered the maximum number of robots from 500
31 !  to 300, which should still be more than enough.
32 !
33 !  Release 4 changes @read_char 1 0 0 key; to @read_char 1 key; since
34 !  I have been informed (no pun intended) that the former is
35 !  considered illegal by some intepreters. Of course, I then felt
36 !  obliged to test the limits of portability again by changing it to
37 !  use @@156 for the non-standard character in my name. To make the
38 !  new release a bit more worthwhile, I've cleaned up MoveRobots() a
39 !  bit (I hope), and added a variable to keep track of bonus earned
40 !  while waiting.
41 !
42 !  Release 5; I was told that @beep without argument crashed an
43 !  interpreter (I don't know which one), so I changed it to use
44 !  @sound_effect 1 instead, to comply with the most recent version of
45 !  the Z-machine specification.
46 !
47 !  Release 6; nothing specific, but I have made a few minor updates to
48 !  make it compile under Inform 6. Some day, I should write a nice,
49 !  object-oriented version. Or maybe not...
50 ! --------------------------------------------------------------------------
51
52 #ifv3;
53 Message fatalerror "This program must be compiled as a version 4 (or
54     later) story file.";
55 #endif;
56
57 Switches xv5s;
58
59 Release 6;
60
61 ! Game constants
62
63 Constant PrefLines      24;             ! This is the screen size for which
64 Constant PrefCols       80;             ! the game is designed.
65
66 Constant FieldRows      22;             ! Size of the playing field.
67 Constant FieldColumns   59;
68 Constant FieldSize      FieldRows * FieldColumns;
69
70 Constant RobotScore     10;             ! Points for killing one robot
71 Constant BonusScore     11;             ! Ditto while 'W'aiting.
72 Constant Robot          '+';            ! Symbols used on the game field
73 Constant Player         64;             ! '@'
74 Constant JunkHeap       '*';
75 Constant Empty          0;
76
77 Constant IncRobots      10;             ! Robots added for each level
78 Constant MaxRobots      300;            ! Max number of robots
79
80 ! Global variables
81
82 Global score            = 0;            ! Current score
83 Global high_score       = 0;            ! Highest score this session
84 Global waiting          = 0;            ! Set when 'W'aiting
85 Global wait_bonus       = 0;            ! Bonus while waiting
86 Global beep_flag        = 1;            ! Sound on/off
87 Global player_x         = 0;            ! Player's current position
88 Global player_y         = 0;            !          -  "  -
89 Global num_robots       = IncRobots;    ! Number of robots on level
90 Global active_robots    = IncRobots;    ! Number of live robots on level
91
92 ! The PlayingField contains information about robots and junkheaps (though not
93 ! about the player). It is used for fast lookup when moving the player or a
94 ! robot. An alternative solution would be to keep an array of the junkheaps,
95 ! similar to RobotList, which would save memory but which would also be much
96 ! less efficient.
97
98 Array PlayingField ->  FieldSize;
99
100 ! The RobotList encodes the individual robots' positions in words (two bytes),
101 ! and is used to speed up the operations which work on all robots. It would be
102 ! possible to search PlayingField, but that would be impractical. It is assumed
103 ! that no player will survive long enough for the array to overflow.
104
105 Array RobotList    --> MaxRobots;
106
107 ! --------------------------------------------------------------------------
108 !   MAIN FUNCTION
109 ! --------------------------------------------------------------------------
110
111 [ Main screen_height screen_width i;
112     screen_height = 0->32;
113     screen_width  = 0->33;
114
115     if (screen_height < PrefLines || screen_width < PrefCols) {
116         style bold;
117         print "^^[The interpreter thinks your screen is ", screen_width,
118               (char) 'x', screen_height, ". It is recommended that you \
119               use at least ", PrefCols, (char) 'x', PrefLines, ".]";
120         style roman;
121     }
122
123     print "^^", (Strong) "ROBOTS", " - Another abuse of the ",
124         (Emphasize) "Z-Machine", "^A nostalgic diversion by Torbj@:orn
125         Andersson^Release ", (0-->1) & $03ff, " / Serial number ";
126
127     for (i = 18 : i < 24 : i++)
128         print (char) 0->i;
129
130     print " / Inform v";
131     inversion;
132
133     print "^^~You can't miss it,~ they said. ~A white house in a
134         clearing with a small mailbox outside; just open the kitchen
135         window and the entrance to the Great Underground Empire isn't
136         far away.~^^";
137     print "You found the house and the window all right, and a
138         trapdoor leading down. But as the trapdoor crashed shut behind
139         you, you realized that something was very wrong. Surely the
140         GUE shouldn't look like a large square room with bare walls,
141         and what about those menacing shapes advancing towards
142         you...?^^";
143     print "[Press any key to continue.]^";
144
145     ReadKeyPress();
146
147     while (PlayGame())
148         ;
149
150     ! This magic incantation should restore the screen to something
151     ! more normal (for a text adventure). Actually, I'm not 100% sure
152     ! how much of this is really needed.
153     @set_cursor 1 1;
154     @split_window 0;
155     @erase_window $ffff;
156     @set_window 0;
157
158     print "^^The idea of writing something like this came from seeing
159         Andrew Plotkin's much more interesting game ",
160         (Emphasize) "Freefall", ". It's really quite amusing to see
161         what the Z-Machine can do with a little persuasion.^^";
162     print "[Press any key to exit.]^";
163
164     ReadKeyPress();
165     quit;
166 ];
167
168 ! --------------------------------------------------------------------------
169 !   THE ACTUAL GAME
170 ! --------------------------------------------------------------------------
171
172 ! This function plays a game of "robots"
173
174 [ PlayGame x y n key got_keypress meta old_score;
175     ! Clear the screen, initialize the game board and draw it on screen.
176     y = FieldRows + 2;
177
178     @erase_window $ffff;
179     @split_window y;
180     @set_window   1;
181
182     score         = 0;
183     num_robots    = IncRobots;
184     active_robots = IncRobots;
185
186     InitPlayingField();
187     DrawPlayingField();
188
189     ! "Infinite" loop (there are 'return' statements to terminate it) which
190     ! waits for keypresses and moves the robots. The 'meta' variable is used
191     ! to keep track of whether or not anything game-related really happened.
192     for (::) {
193         meta = 0;
194
195         ! Remember the player's old position.
196         x = player_x;
197         y = player_y;
198
199         ! Wait for a valid keypress. If the player is 'W'aiting, it is the
200         ! same as if he or she is constantly pressing the '.' key, except the
201         ! robots will actually be allowed to walk into the player.
202         for (got_keypress = 0 : ~~got_keypress :) {
203             got_keypress = 1;
204
205             if (~~waiting)
206                 key = ReadKeyPress();
207             else
208                 key = '.';
209
210             if (wait_bonus == -1) {
211                 wait_bonus = 0;
212                 n = FieldColumns + 4;
213                 @set_cursor 24 n;
214                 spaces(10);
215             }
216
217             switch (key) {
218                 '.':
219                 'Y': player_x--; player_y--;
220                 'K': player_y--;
221                 'U': player_x++, player_y--;
222                 'H': player_x--;
223                 'L': player_x++;
224                 'B': player_x--; player_y++;
225                 'J': player_y++;
226                 'N': player_x++; player_y++;
227                 'T':
228                     GetNewPlayerPos();
229                 'W':
230                     old_score  = score;
231                     wait_bonus = 0;
232                     waiting    = 1;
233                 'Q':
234                     return AnotherGame();
235                 'R':
236                     DrawPlayingField();
237                     meta = 1;
238                 'S':
239                     if (~~beep_flag)
240                         beep_flag = 1;
241                     else
242                         beep_flag = 0;
243
244                     meta = 1;
245                 default:
246                     got_keypress = 0;
247                     DoBeep();
248             }
249         }
250
251         ! If the command was a movement command, check if the player is moving
252         ! to a safe spot or not. (Exception: Teleports are inherently risky,
253         ! but will always put you in an empty spot on the game board, so don't
254         ! warn about that.
255         !
256         ! If the player has moved, redraw that part of the game board.
257         !
258         ! If the move is not accepted, make sure the player remains at the
259         ! original location, warn him or her, and make sure the robots don't
260         ! move.
261         if (~~meta) {
262             if (key == 'T' ||
263                 (InsideField(player_x, player_y)  &&
264                 SafeSpot(player_x, player_y))) {
265                     if (x ~= player_x || y ~= player_y) {
266                         DrawObject(x, y, ' ');
267                         DrawObject(player_x, player_y, Player);
268                     }
269             } else {
270                 if (~~waiting) {
271                     player_x = x;
272                     player_y = y;
273                     DoBeep();
274                     meta = 1;
275                 }
276             }
277
278             ! If the player made a valid move, move the robots.
279             if (~~meta)
280                 MoveRobots();
281
282             ! The robots have moved and dead robots have been handled by
283             ! MoveRobots(). Now it's time to see if the player survived, and
284             ! maybe even won the game.
285             if (GetPiece(player_x, player_y) == Empty) {
286                 if (~~active_robots) {
287                     waiting = 0;
288
289                     UpdateScore(0);
290
291                     num_robots = num_robots + IncRobots;
292
293                     if (num_robots > MaxRobots)
294                         num_robots = MaxRobots;
295
296                     InitPlayingField();
297                     DrawPlayingField();
298                 } else
299                     DrawObject(player_x, player_y, 0);
300             } else {
301                 DrawObject(player_x, player_y, 0);
302                 print "AARRrrgghhhh....";
303
304                 if (waiting) {
305                     score = old_score;
306                     waiting = 0;
307                 }
308
309                 UpdateScore(0);
310                 return AnotherGame();
311             }
312         }
313     }
314 ];
315
316 ! This function moves the robots and handles collisions between robots and
317 ! other robots or junkheaps.
318
319 [ MoveRobots i j robot_x robot_y hit;
320     ! Traverse the list of active robots. At this point there should be no
321     ! 'dead' robots in the list.
322     for (i = 0, hit = 0 : i < active_robots : i++) {
323         robot_x = RobotX(i);
324         robot_y = RobotY(i);
325
326         ! Remove the robot from the playing field and the game board (though
327         ! not from the robot list.
328         DrawObject(robot_x, robot_y, ' ');
329         PutPiece(robot_x, robot_y, Empty);
330
331         ! The robot will always try to move towards the player, regardless of
332         ! obstacles.
333         if (robot_x ~= player_x) {
334             if (robot_x < player_x)
335                 robot_x++;
336             else
337                 robot_x--;
338         }
339
340         if (robot_y ~= player_y) {
341             if (robot_y < player_y)
342                 robot_y++;
343             else
344                 robot_y--;
345         }
346
347         ! Any robot moving onto a junk heap is destroyed. Otherwise, the robot
348         ! is inserted on the playing field at its new location.
349         if (GetPiece(robot_x, robot_y) == JunkHeap) {
350             hit = 1;
351             RobotList-->i = -1;
352             UpdateScore(1);
353         } else {
354             ! Draw the robot on screen to reduce the flicker. The final
355             ! drawing is done in the next loop, as some robots may have
356             ! been erased by other moving robots.
357             DrawObject(robot_x, robot_y, Robot);
358             PutRobot(robot_x, robot_y, i);
359         }
360     }
361
362     ! If a robot was removed, clean up the robot list.
363     if (hit)
364         CleanRobotList();
365
366     ! To make sure that no robot is accidentally 'removed' from the board
367     ! (which could happen if a robot onto another robot before the other
368     ! robot moves, since the other robot will 'blank' its old position on
369     ! the board) we draw all the robots again.
370     for (i = 0, hit = 0 : i < active_robots : i++) {
371         robot_x = RobotX(i);
372         robot_y = RobotY(i);
373
374         ! If two robots ended up in the same position, there was a
375         ! collision. I don't know if it's a good idea or not, but I
376         ! don't want to do the robot-removal yet, so just set a flag
377         ! that there are collisions to detect.
378         if (GetPiece(robot_x, robot_y) == Robot)
379             hit = 1;
380
381         DrawObject(robot_x, robot_y, Robot);
382         PutPiece(robot_x, robot_y, Robot);
383     }
384
385     ! If no robots collided, all is done.
386     if (~~hit)
387         rtrue;
388
389     CleanRobotList();
390
391     ! At least one collision occured. It's time to find out which robots
392     ! collided. This code is the game's major cause of slowdown.
393     for (i = 0, hit = 0 : i < active_robots - 1 : i++) {
394         for (j = i + 1 : j < active_robots : j++) {
395             if (RobotList-->i ~= -1 && RobotList-->i == RobotList-->j) {
396                 robot_x = RobotX(i);
397                 robot_y = RobotY(i);
398
399                 PutPiece(robot_x, robot_y, JunkHeap);
400                 DrawObject(robot_x, robot_y, JunkHeap);
401
402                 RobotList-->i = -1;
403                 RobotList-->j = -1;
404
405                 ! Don't give the player any points for robots killing him/her
406                 if (robot_x ~= player_x || robot_y ~= player_y)
407                     UpdateScore(2);
408
409                 ! Since RobotList-->i now is -1, we won't find any other
410                 ! robots on the same position, so terminate the inner loop.
411                 ! I don't know if it'd be better to save the position of
412                 ! robot i, and follow the loop to its very end.
413                 break;
414             }
415         }
416     }
417
418     ! I know at least one collision occured, and therefore I know that robots
419     ! have been removed.
420     CleanRobotList();
421
422     ! And even now we are not done: What if three robots went to the same
423     ! square? In that case, there should be a robot sitting on a junkheap
424     ! now. This can only happen if the previous loop detected a collision
425     ! between two robots.
426     for (i = 0, hit = 0 : i < active_robots : i++) {
427         robot_x = RobotX(i);
428         robot_y = RobotY(i);
429
430         if (GetPiece(robot_x, robot_y) == JunkHeap) {
431             hit = 1;
432             RobotList-->i = -1;
433
434             if (robot_x ~= player_x || robot_y ~= player_y)
435                 UpdateScore(1);
436         }
437     }
438
439     if (hit)
440         CleanRobotList();
441 ];
442
443 ! --------------------------------------------------------------------------
444 !   THE GAME BOARD
445 ! --------------------------------------------------------------------------
446
447 ! These two functions are used for printing the game board. This is done both
448 ! when starting on a level and when using the 'R'edraw command.
449
450 [ DrawPlayingField i x y;
451     @erase_window 1;
452
453     ! Draw the border around the game board.
454     DrawHorizontalLine(1);
455     DrawHorizontalLine(FieldRows + 2);
456
457     x = FieldColumns + 2;
458
459     for (i = 2 : i <= FieldRows + 1 : i++) {
460         @set_cursor i 1;  print (char) '|';
461         @set_cursor i x;  print (char) '|';
462     }
463
464     ! Draw the robots on the game board.
465     for (i = 0 : i < active_robots : i++)
466         DrawObject(RobotX(i), RobotY(i), Robot);
467
468     ! If some robots have died, we have to traverse the entire PlayingField
469     ! looking for junkheaps. Fortunately, this only happens when 'R'edrawing
470     ! the screen, which shouldn't be very often.
471     if (active_robots < num_robots) {
472         for (x = 0 : x < FieldColumns : x++) {
473             for (y = 0 : y < FieldRows : y++) {
474                 if (GetPiece(x, y) == JunkHeap) {
475                     DrawObject(x, y, JunkHeap);
476                 }
477             }
478         }
479     }
480
481     ! Put some help text to the right of the game board.
482     x = FieldColumns + 4;
483
484     @set_cursor 1  x;  print "Directions:";
485
486     @set_cursor 3  x;  print "y k u";
487     @set_cursor 4  x;  print " @@92|/ ";
488     @set_cursor 5  x;  print "h-.-l";
489     @set_cursor 6  x;  print " /|@@92 ";
490     @set_cursor 7  x;  print "b j n";
491
492     @set_cursor 9  x;  print "Commands:";
493
494     @set_cursor 11 x;  print "w:  wait for end";
495     @set_cursor 12 x;  print "t:  teleport";
496     @set_cursor 13 x;  print "q:  quit";
497     @set_cursor 14 x;  print "r:  redraw screen";
498
499     @set_cursor 16 x;  print "Legend:";
500
501     @set_cursor 18 x;  print (char) Robot,    ":  robot";
502     @set_cursor 19 x;  print (char) JunkHeap, ":  junk heap";
503     @set_cursor 20 x;  print (char) Player,   ":  you";
504
505     if (wait_bonus > 0) {
506         @set_cursor 24 x;       print "Bonus: ", wait_bonus;
507         wait_bonus = -1;
508     }
509
510     @set_cursor 22 x;  print "Score: ", score;
511     @set_cursor 23 x;  print "High:  ", high_score;
512
513     ! Finally, draw the player on the game board.
514     DrawObject(player_x, player_y, Player);
515     DrawObject(player_x, player_y, 0);
516 ];
517
518 [ DrawHorizontalLine row i;
519     @set_cursor row 1;
520
521     print (char) '+';
522
523     for (i = 0 : i < FieldColumns : i++)
524         print (char) '-';
525
526     print (char) '+';
527 ];
528
529 ! --------------------------------------------------------------------------
530 !   HELP FUNCTIONS
531 ! --------------------------------------------------------------------------
532
533 [ Strong str;
534     style bold;
535     print (string) str;
536     style roman;
537 ];
538
539 [ Emphasize str;
540     style underline;
541     print (string) str;
542     style roman;
543 ];
544
545 ! Test is a coordinate is safe to move it, ie that
546 !
547 !   a) There is no junkheap on it
548 !   b) There are no robots on any adjacent coordinate
549
550 [ SafeSpot xpos ypos x y;
551     if (GetPiece(xpos, ypos) == JunkHeap)
552         rfalse;
553
554     for (x = xpos - 1 : x <= xpos + 1 : x++) {
555         for (y = ypos - 1 : y <= ypos + 1 : y++) {
556             if (InsideField(x, y) && GetPiece(x, y) == Robot)
557                 rfalse;
558         }
559     }
560
561     rtrue;
562 ];
563
564 ! Update the score after killing 'n' robots. If 'n' is 0 it will simply
565 ! redraw the score. If we are 'W'aiting, the score is not written since it
566 ! is not known whether or not the player will actually get points until he
567 ! or she has survived the entire level.
568
569 [ UpdateScore n x;
570     if (n) {
571         if (waiting) {
572             wait_bonus = wait_bonus + n * (BonusScore - RobotScore);
573             score = score + (n * BonusScore);
574         } else
575             score = score + (n * RobotScore);
576     }
577
578     if (~~waiting) {
579         x = FieldColumns + 11;
580
581         @set_cursor 22 x;  print score;
582
583         if (score > high_score) {
584             high_score = score;
585             @set_cursor 23 x;  print high_score;
586         }
587     }
588 ];
589
590 ! Ask the user if he or she wants to play another game
591
592 [ AnotherGame x;
593     x = FieldColumns + 4;
594     @set_cursor 24 x;
595     print "Another game? ";
596
597     for (::) {
598         switch (ReadKeyPress()) {
599             'Y': rtrue;
600             'N': rfalse;
601         }
602     }
603 ];
604
605 ! Get a new position for the player. This is used both when 'T'eleporting and
606 ! when starting on a new level, and ensures that the player will not land on
607 ! any robot or junkpile. The player may, however, land right next to a robot,
608 ! which is fatal when 'T'eleporting, and uncomfortable when starting on a new
609 ! level.
610                         
611 [ GetNewPlayerPos;
612     for (::) {
613         player_x = random(FieldColumns) - 1;
614         player_y = random(FieldRows) - 1;
615
616         if (GetPiece(player_x, player_y) == Empty)
617             break;
618     }
619 ];
620
621 ! The code which checks for robots colliding is horrendously inefficient, so
622 ! in order to speed it up as the game proceeds, remove 'dead' robots from the
623 ! list and keep a counter of 'active' robots.
624
625 [ CleanRobotList i j;
626     for (i = 0, j = 0 : i < active_robots : i++) {
627         if (RobotList-->i ~= -1) {
628             RobotList-->j = RobotList-->i;
629             j++;
630         }           
631     }
632
633     active_robots = j;
634 ];
635
636 ! --------------------------------------------------------------------------
637 !   INITIALIZATION
638 ! --------------------------------------------------------------------------
639
640 ! Initialize the PlayingField and RobotList
641
642 [ InitPlayingField i x y;
643     active_robots = num_robots;
644
645     for (i = 0 : i < FieldSize : i++)
646         PlayingField->i = Empty;
647
648     for (i = 0 : i < num_robots : i++) {
649         for (::) {
650             x = random(FieldColumns) - 1;
651             y = random(FieldRows) - 1;
652
653             if (GetPiece(x, y) == Empty) {
654                 PutPiece(x, y, Robot);
655                 PutRobot(x, y, i);
656                 break;
657             }
658         }
659     }
660
661     GetNewPlayerPos();
662 ];
663
664 ! --------------------------------------------------------------------------
665 !   PRIMITIVES
666 ! --------------------------------------------------------------------------
667
668 ! Produce an annoying 'beep', if the sound is turned on. The sound is toggled
669 ! with 'S', which, since it isn't properly documented, must surely be a bug
670 ! rather than a feature. :-)
671
672 [ DoBeep;
673     if (beep_flag)
674         @sound_effect 1;
675 ];
676
677 ! Read a single character from stream 1 (the keyboard) and return it. If the
678 ! character is lower-case, it is translated to upper-case first.
679
680 [ ReadKeyPress x;
681     @read_char 1 -> x;
682
683     if (x >= 'a' && x <= 'z')
684         x = x - ('a' - 'A');
685
686     return x;
687 ];
688
689 ! These two primitives are used for reading the PlayingField and inserting new
690 ! values in it respectively.
691
692 [ GetPiece x y;
693     return PlayingField->(y * FieldColumns + x);
694 ];
695
696 [ PutPiece x y type;
697     PlayingField->(y * FieldColumns + x) = type;
698 ];
699
700 ! These three primitives are used for getting and setting the coordinates of
701 ! a robot respectively. A dead robot is marked as -1 in RobotList, and it is
702 ! up to the calling functions to test this if necessary.
703
704 [ RobotX n;
705     return (RobotList-->n) / 256;
706 ];
707
708 [ RobotY n;
709     return (RobotList-->n) % 256;
710 ];
711
712 [ PutRobot x y n;
713     RobotList-->n = x * 256 + y;
714 ];
715
716 ! Print a character on the game board. Note that it is up to the calling
717 ! function to make sure that this bears any resemblance to what is actually
718 ! stored in the PlayingField.
719
720 [ DrawObject x y c;
721     x = x + 2;
722     y = y + 2;
723
724     @set_cursor y x;
725
726     if (c)
727         print (char) c;
728 ];
729
730 ! Primitive for testing if a coordinate is inside the game board.
731
732 [ InsideField x y;
733     if (x >= 0 && y >= 0 && x < FieldColumns && y < FieldRows)
734         rtrue;
735
736     rfalse;
737 ];
738
739 end;