! --------------------------------------------------------------------------
! "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;