! --------------------------------------------------------------------------
! "ROBOTS": Another abuse of the Z-machine, Copied Right in 1995-1997
! Torbjorn Andersson,
[email protected]
!
! This program is free. By all means, do fold, spindle and mutilate.
! Be aware, however, that removing my name from it may cause
! irreparable damage to your karma. If you do redistribute it in any
! form, please drop me a note. I'd love to hear about it! If you
! think you can make money from it, you are more optimistic than I
! thought.
!
! 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.
!
! 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 worthwhile, 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.
!
! Release 6; nothing specific, but I have made a few minor updates to
! make it compile under Inform 6. Some day, I should write a nice,
! object-oriented version. Or maybe not...
! --------------------------------------------------------------------------
#ifv3;
Message fatalerror "This program must be compiled as a version 4 (or
later) story file.";
#endif;
Switches xv5s;
Release 6;
! 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 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 64; ! '@'
Constant JunkHeap '*';
Constant Empty 0;
Constant IncRobots 10; ! Robots added for each level
Constant MaxRobots 300; ! Max number of robots
! Global variables
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.
Array 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.
Array RobotList --> MaxRobots;
! --------------------------------------------------------------------------
! MAIN FUNCTION
! --------------------------------------------------------------------------
[ Main screen_height screen_width i;
screen_height = 0->32;
screen_width = 0->33;
if (screen_height < PrefLines || screen_width < PrefCols) {
style bold;
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, ".]";
style roman;
}
print "^^", (Strong) "ROBOTS", " - Another abuse of the ",
(Emphasize) "Z-Machine", "^A nostalgic diversion by Torbj@:orn
Andersson^Release ", (0-->1) & $03ff, " / Serial number ";
for (i = 18 : i < 24 : i++)
print (char) 0->i;
print " / Inform v";
inversion;
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())
;
! This 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 ",
(Emphasize) "Freefall", ". It's really quite amusing to see
what the Z-Machine can do with a little persuasion.^^";
print "[Press any key to exit.]^";
ReadKeyPress();
quit;
];
! --------------------------------------------------------------------------
! 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 :) {
got_keypress = 1;
if (~~waiting)
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)
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) {
if (key == 'T' ||
(InsideField(player_x, player_y) &&
SafeSpot(player_x, player_y))) {
if (x ~= player_x || y ~= player_y) {
DrawObject(x, y, ' ');
DrawObject(player_x, player_y, Player);
}
} else {
if (~~waiting) {
player_x = x;
player_y = y;
DoBeep();
meta = 1;
}
}
! If the player made a valid move, move the robots.
if (~~meta)
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) {
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) {
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)
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)
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)
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
! --------------------------------------------------------------------------
[ Strong str;
style bold;
print (string) str;
style roman;
];
[ Emphasize str;
style underline;
print (string) str;
style roman;
];
! 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) && 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) {
if (waiting) {
wait_bonus = wait_bonus + n * (BonusScore - RobotScore);
score = score + (n * BonusScore);
} else
score = score + (n * RobotScore);
}
if (~~waiting) {
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)
@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)
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;