! --------------------------------------------------------------------------
!  "ROBOTS":  Another abuse of the Z-machine, Copied Right in 1995
!
!  I got the idea of writing this when seeing Andrew Plotkin's much more
!  interesting game "Freefall". I used his code for reference about some
!  technical details.
!
!  I don't know who originally came up with this game idea. I have seen it
!  under the name "DALEKS", but that version was a bit different. This one
!  uses (almost) both the layout and the key configuration of the version
!  which can, at least usually, be found in /usr/games on Unix systems.
!
!  To compile this program, you need to use Inform 5.5 or later.
!
!  This program was written by Torbj|rn Andersson, [email protected]
!
!  Feel free to do whatever you want with this code, but if you find any
!  bug, please try to find some way of telling me. If you like it, don't
!  forget to smile. If you think you can make money from it, you are more
!  optimistic than I thought.
!
!  Release 2 makes some slight optimizations (I hope) to the code which
!  detects collisions between robots, and makes a few cosmetic changes.
!
!  Release 3 cleans up some of the code a bit, makes some further
!  optimizations to the collision-detection and allows the user to keep
!  playing even when the maximum number of robots have been reached. (It
!  just won't increase the number of robots any further.) For this reason,
!  I've lowered the maximum number of robots from 500 to 300, which should
!  still be more than enough.
!
!  Release 4 changes @read_char 1 0 0 key; to @read_char 1 key; since I
!  have been informed (no pun intended) that the former is considered
!  illegal by some intepreters. Of course, I then felt obliged to test the
!  limits of portability again by changing it to use @@156 for the non-
!  standard character in my name. To make the new release a bit more worth-
!  while, I've cleaned up MoveRobots() a bit (I hope), and added a variable
!  to keep track of bonus earned while waiting.
!
!  Release 5; I was told that @beep without argument crashed an
!  interpreter (I don't know which one), so I changed it to use
!  @sound_effect 1 instead, to comply with the most recent version of the
!  Z-machine specification.
! --------------------------------------------------------------------------

Switches xv5s;

Release 5;

! Game constants

Constant PrefLines      24;             ! This is the screen size for which
Constant PrefCols       80;             ! the game is designed.

Constant FieldRows      22;             ! Size of the playing field.
Constant FieldColumns   59;
Constant FieldSize      1298;           ! FieldRows * FieldColumns

Constant RobotScore     10;             ! Points for killing one robot
Constant BonusScore     11;             ! Ditto while 'W'aiting.
Constant Robot          '+';            ! Symbols used on the game field
Constant Player         '@';
Constant JunkHeap       '*';
Constant Empty          0;

Constant IncRobots      10;             ! Robots added for each level
Constant MaxRobots      300;            ! Max number of robots

! Global variables

Global sw__var          = 0;            ! Needed for switch() and such

Global score            = 0;            ! Current score
Global high_score       = 0;            ! Highest score this session
Global waiting          = 0;            ! Set when 'W'aiting
Global wait_bonus       = 0;            ! Bonus while waiting
Global beep_flag        = 1;            ! Sound on/off
Global player_x         = 0;            ! Player's current position
Global player_y         = 0;            !          -  "  -
Global num_robots       = IncRobots;    ! Number of robots on level
Global active_robots    = IncRobots;    ! Number of live robots on level

! The PlayingField contains information about robots and junkheaps (though not
! about the player). It is used for fast lookup when moving the player or a
! robot. An alternative solution would be to keep an array of the junkheaps,
! similar to RobotList, which would save memory but which would also be much
! less efficient.

Global PlayingField ->  FieldSize;

! The RobotList encodes the individual robots' positions in words (two bytes),
! and is used to speed up the operations which work on all robots. It would be
! possible to search PlayingField, but that would be impractical. It is assumed
! that no player will survive long enough for the array to overflow.

Global RobotList    --> MaxRobots;

! --------------------------------------------------------------------------
!   MAIN FUNCTION
!
!   The earliest-defined routine is not allowed to have local variables, so
!   I have put all that needs local variables in separate functions.
! --------------------------------------------------------------------------

[ Main;
   TestScreenSize();

   print "^^";

   Banner();

   print "^~You can't miss it,~ they said. ~A white house in a clearing \
          with a small mailbox outside; just open the kitchen window and \
          the entrance to the Great Underground Empire isn't far away.~^^";
   print "You found the house and the window all right, and a trapdoor \
          leading down. But as the trapdoor crashed shut behind you, you \
          realized that something was very wrong. Surely the GUE shouldn't \
          look like a large square room with bare walls, and what about \
          those menacing shapes advancing towards you ...?^^";
   print "[Press any key to continue.]^";

   ReadKeyPress();

   while (PlayGame() ~= 0);

   ! These magic incantation should restore the screen to something more
   ! normal (for a text adventure). Actually, I'm not 100% sure how much of
   ! this is really needed.
   @set_cursor 1 1;
   @split_window 0;
   @erase_window $ffff;
   @set_window 0;

   print "^^The idea of writing something like this came from seeing Andrew \
          Plotkin's much more interesting game 'Freefall'. It's really \
          quite amusing to see what the Z-machine can do with a little \
          persuasion.^^";
   print "Torbj@@156rn Andersson, 1995^^";
   print "[Press any key to exit.]^";

   ReadKeyPress();
   quit;
];

[ Banner i;
   style bold; print "ROBOTS"; style roman;
   print " - Another abuse of the Z-Machine^";
   print "A nostalgic diversion by Torbj@@156rn Andersson^";
   print "Release ", (0-->1) & $03ff, " / Serial number ";

   for (i = 18 : i < 24 : i++)
       print (char) 0->i;

   print " / Inform v";
   inversion;

   new_line;
];

! --------------------------------------------------------------------------
!   THE ACTUAL GAME
! --------------------------------------------------------------------------

! This function plays a game of "robots"

[ PlayGame x y n key got_keypress meta old_score;
   ! Clear the screen, initialize the game board and draw it on screen.
   y = FieldRows + 2;

   @erase_window $ffff;
   @split_window y;
   @set_window   1;

   score         = 0;
   num_robots    = IncRobots;
   active_robots = IncRobots;

   InitPlayingField();
   DrawPlayingField();

   ! "Infinite" loop (there are 'return' statements to terminate it) which
   ! waits for keypresses and moves the robots. The 'meta' variable is used
   ! to keep track of whether or not anything game-related really happened.
   for (::) {
       meta = 0;

       ! Remember the player's old position.
       x = player_x;
       y = player_y;

       ! Wait for a valid keypress. If the player is 'W'aiting, it is the
       ! same as if he or she is constantly pressing the '.' key, except the
       ! robots will actually be allowed to walk into the player.
       for (got_keypress = 0 : got_keypress == 0:) {
           got_keypress = 1;

           if (waiting == 0)
               key = ReadKeyPress();
           else
               key = '.';

           if (wait_bonus == -1) {
               wait_bonus = 0;
               n = FieldColumns + 4;
               @set_cursor 24 n;
               spaces(10);
           }

           switch (key) {
               '.':
               'Y': player_x--; player_y--;
               'K': player_y--;
               'U': player_x++, player_y--;
               'H': player_x--;
               'L': player_x++;
               'B': player_x--; player_y++;
               'J': player_y++;
               'N': player_x++; player_y++;
               'T':
                   GetNewPlayerPos();
               'W':
                   old_score  = score;
                   wait_bonus = 0;
                   waiting    = 1;
               'Q':
                   return AnotherGame();
               'R':
                   DrawPlayingField();
                   meta = 1;
               'S':
                   if (beep_flag == 0)
                       beep_flag = 1;
                   else
                       beep_flag = 0;

                   meta = 1;
               default:
                   got_keypress = 0;
                   DoBeep();
           }
       }

       ! If the command was a movement command, check if the player is moving
       ! to a safe spot or not. (Exception: Teleports are inherently risky,
       ! but will always put you in an empty spot on the game board, so don't
       ! warn about that.
       !
       ! If the player has moved, redraw that part of the game board.
       !
       ! If the move is not accepted, make sure the player remains at the
       ! original location, warn him or her, and make sure the robots don't
       ! move.
       if (meta == 0) {
           if (key == 'T' ||
               (InsideField(player_x, player_y) ~= 0 &&
               SafeSpot(player_x, player_y) ~= 0)) {
                   if (x ~= player_x || y ~= player_y) {
                       DrawObject(x, y, ' ');
                       DrawObject(player_x, player_y, Player);
                   }
           } else {
               if (waiting == 0) {
                   player_x = x;
                   player_y = y;
                   DoBeep();
                   meta = 1;
               }
           }

           ! If the player made a valid move, move the robots.
           if (meta == 0)
               MoveRobots();

           ! The robots have moved and dead robots have been handled by
           ! MoveRobots(). Now it's time to see if the player survived, and
           ! maybe even won the game.
           if (GetPiece(player_x, player_y) == Empty) {
               if (active_robots == 0) {
                   waiting = 0;

                   UpdateScore(0);

                   num_robots = num_robots + IncRobots;

                   if (num_robots > MaxRobots)
                       num_robots = MaxRobots;

                   InitPlayingField();
                   DrawPlayingField();
               } else
                   DrawObject(player_x, player_y, 0);
           } else {
               DrawObject(player_x, player_y, 0);
               print "AARRrrgghhhh....";

               if (waiting ~= 0) {
                   score = old_score;
                   waiting = 0;
               }

               UpdateScore(0);
               return AnotherGame();
           }
       }
   }
];

! This function moves the robots and handles collisions between robots and
! other robots or junkheaps.

[ MoveRobots i j robot_x robot_y hit;
   ! Traverse the list of active robots. At this point there should be no
   ! 'dead' robots in the list.
   for (i = 0, hit = 0 : i < active_robots : i++) {
       robot_x = RobotX(i);
       robot_y = RobotY(i);

       ! Remove the robot from the playing field and the game board (though
       ! not from the robot list.
       DrawObject(robot_x, robot_y, ' ');
       PutPiece(robot_x, robot_y, Empty);

       ! The robot will always try to move towards the player, regardless of
       ! obstacles.
       if (robot_x ~= player_x) {
           if (robot_x < player_x)
               robot_x++;
           else
               robot_x--;
       }

       if (robot_y ~= player_y) {
           if (robot_y < player_y)
               robot_y++;
           else
               robot_y--;
       }

       ! Any robot moving onto a junk heap is destroyed. Otherwise, the robot
       ! is inserted on the playing field at its new location.
       if (GetPiece(robot_x, robot_y) == JunkHeap) {
           hit = 1;
           RobotList-->i = -1;
           UpdateScore(1);
       } else {
           ! Draw the robot on screen to reduce the flicker. The final
           ! drawing is done in the next loop, as some robots may have
           ! been erased by other moving robots.
           DrawObject(robot_x, robot_y, Robot);
           PutRobot(robot_x, robot_y, i);
       }
   }

   ! If a robot was removed, clean up the robot list.
   if (hit ~= 0)
       CleanRobotList();

   ! To make sure that no robot is accidentally 'removed' from the board
   ! (which could happen if a robot onto another robot before the other
   ! robot moves, since the other robot will 'blank' its old position on
   ! the board) we draw all the robots again.
   for (i = 0, hit = 0 : i < active_robots : i++) {
       robot_x = RobotX(i);
       robot_y = RobotY(i);

       ! If two robots ended up in the same position, there was a
       ! collision. I don't know if it's a good idea or not, but I
       ! don't want to do the robot-removal yet, so just set a flag
       ! that there are collisions to detect.
       if (GetPiece(robot_x, robot_y) == Robot)
           hit = 1;

       DrawObject(robot_x, robot_y, Robot);
       PutPiece(robot_x, robot_y, Robot);
   }

   ! If no robots collided, all is done.
   if (hit == 0)
       rtrue;

   CleanRobotList();

   ! At least one collision occured. It's time to find out which robots
   ! collided. This code is the game's major cause of slowdown.
   for (i = 0, hit = 0 : i < active_robots - 1 : i++) {
       for (j = i + 1 : j < active_robots : j++) {
           if (RobotList-->i ~= -1 && RobotList-->i == RobotList-->j) {
               robot_x = RobotX(i);
               robot_y = RobotY(i);

               PutPiece(robot_x, robot_y, JunkHeap);
               DrawObject(robot_x, robot_y, JunkHeap);

               RobotList-->i = -1;
               RobotList-->j = -1;

               ! Don't give the player any points for robots killing him/her
               if (robot_x ~= player_x || robot_y ~= player_y)
                   UpdateScore(2);

               ! Since RobotList-->i now is -1, we won't find any other
               ! robots on the same position, so terminate the inner loop.
               ! I don't know if it'd be better to save the position of
               ! robot i, and follow the loop to its very end.
               break;
           }
       }
   }

   ! I know at least one collision occured, and therefore I know that robots
   ! have been removed.
   CleanRobotList();

   ! And even now we are not done: What if three robots went to the same
   ! square? In that case, there should be a robot sitting on a junkheap
   ! now. This can only happen if the previous loop detected a collision
   ! between two robots.
   for (i = 0, hit = 0 : i < active_robots : i++) {
       robot_x = RobotX(i);
       robot_y = RobotY(i);

       if (GetPiece(robot_x, robot_y) == JunkHeap) {
           hit = 1;
           RobotList-->i = -1;

           if (robot_x ~= player_x || robot_y ~= player_y)
               UpdateScore(1);
       }
   }

   if (hit ~= 0)
       CleanRobotList();
];

! --------------------------------------------------------------------------
!   THE GAME BOARD
! --------------------------------------------------------------------------

! These two functions are used for printing the game board. This is done both
! when starting on a level and when using the 'R'edraw command.

[ DrawPlayingField i x y;
   @erase_window 1;

   ! Draw the border around the game board.
   DrawHorizontalLine(1);
   DrawHorizontalLine(FieldRows + 2);

   x = FieldColumns + 2;

   for (i = 2 : i <= FieldRows + 1 : i++) {
       @set_cursor i 1;  print (char) '|';
       @set_cursor i x;  print (char) '|';
   }

   ! Draw the robots on the game board.
   for (i = 0 : i < active_robots : i++)
       DrawObject(RobotX(i), RobotY(i), Robot);

   ! If some robots have died, we have to traverse the entire PlayingField
   ! looking for junkheaps. Fortunately, this only happens when 'R'edrawing
   ! the screen, which shouldn't be very often.
   if (active_robots < num_robots) {
       for (x = 0 : x < FieldColumns : x++) {
           for (y = 0 : y < FieldRows : y++) {
               if (GetPiece(x, y) == JunkHeap) {
                   DrawObject(x, y, JunkHeap);
               }
           }
       }
   }

   ! Put some help text to the right of the game board.
   x = FieldColumns + 4;

   @set_cursor 1  x;  print "Directions:";

   @set_cursor 3  x;  print "y k u";
   @set_cursor 4  x;  print " @@92|/ ";
   @set_cursor 5  x;  print "h-.-l";
   @set_cursor 6  x;  print " /|@@92 ";
   @set_cursor 7  x;  print "b j n";

   @set_cursor 9  x;  print "Commands:";

   @set_cursor 11 x;  print "w:  wait for end";
   @set_cursor 12 x;  print "t:  teleport";
   @set_cursor 13 x;  print "q:  quit";
   @set_cursor 14 x;  print "r:  redraw screen";

   @set_cursor 16 x;  print "Legend:";

   @set_cursor 18 x;  print (char) Robot,    ":  robot";
   @set_cursor 19 x;  print (char) JunkHeap, ":  junk heap";
   @set_cursor 20 x;  print (char) Player,   ":  you";

   if (wait_bonus > 0) {
       @set_cursor 24 x;       print "Bonus: ", wait_bonus;
       wait_bonus = -1;
   }

   @set_cursor 22 x;  print "Score: ", score;
   @set_cursor 23 x;  print "High:  ", high_score;

   ! Finally, draw the player on the game board.
   DrawObject(player_x, player_y, Player);
   DrawObject(player_x, player_y, 0);
];

[ DrawHorizontalLine row i;
   @set_cursor row 1;

   print (char) '+';

   for (i = 0 : i < FieldColumns : i++)
       print (char) '-';

   print (char) '+';
];

! --------------------------------------------------------------------------
!   HELP FUNCTIONS
! --------------------------------------------------------------------------

! Test the screen size. The game will look very odd, and maybe not run at all,
! if the screen is too small.

[ TestScreenSize screen_height screen_width;
   screen_height = 0->32;
   screen_width  = 0->33;

   if (screen_height < PrefLines || screen_width < PrefCols)
       print "^^[The interpreter thinks your screen is ", screen_width,
             (char) 'x', screen_height, ". It is recommended that you \
             use at least ", PrefCols, (char) 'x', PrefLines, ".]";
];

! Test is a coordinate is safe to move it, ie that
!
!   a) There is no junkheap on it
!   b) There are no robots on any adjacent coordinate

[ SafeSpot xpos ypos x y;
   if (GetPiece(xpos, ypos) == JunkHeap)
       rfalse;

   for (x = xpos - 1 : x <= xpos + 1 : x++) {
       for (y = ypos - 1 : y <= ypos + 1 : y++) {
           if (InsideField(x, y) ~= 0 && GetPiece(x, y) == Robot)
               rfalse;
       }
   }

   rtrue;
];

! Update the score after killing 'n' robots. If 'n' is 0 it will simply
! redraw the score. If we are 'W'aiting, the score is not written since it
! is not known whether or not the player will actually get points until he
! or she has survived the entire level.

[ UpdateScore n x;
   if (n ~= 0) {
       if (waiting ~= 0) {
           wait_bonus = wait_bonus + n * (BonusScore - RobotScore);
           score = score + (n * BonusScore);
       } else
           score = score + (n * RobotScore);
   }

   if (waiting == 0) {
       x = FieldColumns + 11;

       @set_cursor 22 x;  print score;

       if (score > high_score) {
           high_score = score;
           @set_cursor 23 x;  print high_score;
       }
   }
];

! Ask the user if he or she wants to play another game

[ AnotherGame x;
   x = FieldColumns + 4;
   @set_cursor 24 x;
   print "Another game? ";

   for (::) {
       switch (ReadKeyPress()) {
           'Y': rtrue;
           'N': rfalse;
       }
   }
];

! Get a new position for the player. This is used both when 'T'eleporting and
! when starting on a new level, and ensures that the player will not land on
! any robot or junkpile. The player may, however, land right next to a robot,
! which is fatal when 'T'eleporting, and uncomfortable when starting on a new
! level.

[ GetNewPlayerPos;
   for (::) {
       player_x = random(FieldColumns) - 1;
       player_y = random(FieldRows) - 1;

       if (GetPiece(player_x, player_y) == Empty)
           break;
   }
];

! The code which checks for robots colliding is horrendously inefficient, so
! in order to speed it up as the game proceeds, remove 'dead' robots from the
! list and keep a counter of 'active' robots.

[ CleanRobotList i j;
   for (i = 0, j = 0 : i < active_robots : i++) {
       if (RobotList-->i ~= -1) {
           RobotList-->j = RobotList-->i;
           j++;
       }
   }

   active_robots = j;
];

! --------------------------------------------------------------------------
!   INITIALIZATION
! --------------------------------------------------------------------------

! Initialize the PlayingField and RobotList

[ InitPlayingField i x y;
   active_robots = num_robots;

   for (i = 0 : i < FieldSize : i++)
       PlayingField->i = Empty;

   for (i = 0 : i < num_robots : i++) {
       for (::) {
           x = random(FieldColumns) - 1;
           y = random(FieldRows) - 1;

           if (GetPiece(x, y) == Empty) {
               PutPiece(x, y, Robot);
               PutRobot(x, y, i);
               break;
           }
       }
   }

   GetNewPlayerPos();
];

! --------------------------------------------------------------------------
!   PRIMITIVES
! --------------------------------------------------------------------------

! Produce an annoying 'beep', if the sound is turned on. The sound is toggled
! with 'S', which, since it isn't properly documented, must surely be a bug
! rather than a feature. :-)

[ DoBeep;
   if (beep_flag ~= 0)
       @sound_effect 1;
];

! Read a single character from stream 1 (the keyboard) and return it. If the
! character is lower-case, it is translated to upper-case first.

[ ReadKeyPress x;
   @read_char 1 x;

   if (x >= 'a' && x <= 'z')
       x = x - ('a' - 'A');

   return x;
];

! These two primitives are used for reading the PlayingField and inserting new
! values in it respectively.

[ GetPiece x y;
   return PlayingField->(y * FieldColumns + x);
];

[ PutPiece x y type;
   PlayingField->(y * FieldColumns + x) = type;
];

! These three primitives are used for getting and setting the coordinates of
! a robot respectively. A dead robot is marked as -1 in RobotList, and it is
! up to the calling functions to test this if necessary.

[ RobotX n;
   return (RobotList-->n) / 256;
];

[ RobotY n;
   return (RobotList-->n) % 256;
];

[ PutRobot x y n;
   RobotList-->n = x * 256 + y;
];

! Print a character on the game board. Note that it is up to the calling
! function to make sure that this bears any resemblance to what is actually
! stored in the PlayingField.

[ DrawObject x y c;
   x = x + 2;
   y = y + 2;

   @set_cursor y x;

   if (c ~= 0)
       print (char) c;
];

! Primitive for testing if a coordinate is inside the game board.

[ InsideField x y;
   if (x >= 0 && y >= 0 && x < FieldColumns && y < FieldRows)
       rtrue;

   rfalse;
];

end;