Copyright © 2018 by Víctor Parada
This is a little game for the 2018 NOMAM's BASIC 10-liners Contest. This program fits in the "stock" PUR-80 category, and it was written using Atari BASIC for the 8-bits ATARI XL/XE. Development started on 2018-02-11, and it took 1+4 days. The final version's date is 2018-02-14.
UPDATE: It obtained the 6th place of 24 entries in the category.
Drive your parachute and land on the bottom of the canyon before you get too many lethal shots.
You are a paratrooper that is falling inside a canyon. | |
Move to the left or to the right to avoid obstacles. | |
The canyon gets narrower as you go down. | |
Each time you hit an obstacle or wall, the hurt indicator bar increases. | |
As you go down, it gets darker. | |
You win the game if you reach the ground before the bar is full. | |
You loose if you got too many hits during the descend. |
Games programmed in Atari BASIC are rather slow for many reasons:
PEEK
and POKE
, which can address and manipulate only one byte at a time.To speed up things, fortunately it was possible to call a machine language routine to perform complex tasks like to copy some blocks of memory from one addres to another or to put a P/M sprite on screen and move it around.
Other BASIC dialects for the Atari 8-bit computers that were based on this interpreter solved some of the speed problems and included many new extensions to perform that tasks directly from BASIC statements.
But the new "stock" PUR-80 category at the NOMAM tenliners contest does not allow neither other interpreters than Atari BASIC, nor USR
extensions. This limits the number of things that could be done.
The main problem was the memory management. Without a MOVE
statement, the only other way to move a chunck of memory is using string variables, but it is not possible to access every byte of the computer memory as it can be done with POKE
, only the memory reserved for string variables using DIM
statement can be initialized and data be copied between the variables or inside one of them as substrings.
Knowing how the memory pointers are managed by Atari BASIC, I tried to hack them and make BASIC think that the whole memory is inside a string variable, so I could use substrings assignments to move a block of memory from one buffer to another area to create animations, but it had some problems that made this method useless.
Then it came another idea: define a big string variable and change O.S. vectors to point inside of the string. For example, assign a block of a string for the screen memory, so screen elements should be assigned to a substring. Of course there are some restriction, like page boundaries for some HW objects like PMBASE. To align them, a simple filler could be used.
One method to align data was to compute a base index to skip as many bytes as required from the beginning of a string to reach a starting point in a page or 1K boundary, and then use that index as a base in all the substring assignments. This had two problems: it was required a floating point addition operation on every substring referenced slowing down the program, and it requires at least 2 or 4 extra chars in th listing for every reference.
Another method was to define a dummy string variable of a size that make the following variables in the DIM
statement be aligned with memory pages. This require more bytes to declare the variables, but many other are saved later in the code as the substrings indexes are fixed.
Using the first method, I did a simple proof of concept that scrolled screen memory from the bottom to the top of it like in the game Descend by Kevin Savetz (NOMAM 2017) using only substring assignments. That was faster than using screen O.S. routines and data conversion... Wow! The problem was that texts need to be assigned as internal code, not as ATASCII code, but it is not a real problem, as it is the same when MOVE
was used in other BASIC dialects.
Scrolling prototype
The test required 6 full lines of code. That gave me 4 lines to include a player and I had to decide if it would be another char or a P/M graphics. I opted for a player, because it would be smoother than blocks and P/M collisons could be used to check for that state.
In the test I used randomly pre-generated patterns of ASCII symbols displayed in ANTIC 4 graphics mode, so they seemed to be like foliage and that gave me the idea of a forest. Instead of a spaceship, a paratrooper seemed to be appropiate and I designed a very simple one using a single player. I named the game as "Deep Forest".
The first line of the screen was initially reserved as a text line to display the score, lifes and the "game over" message, but it turned into a health bar, and the final status of the game was also displayed there.
Paratrooper in the "Deep Forest"
I had to use FOR
-NEXT
with STEP 0
loops to manage the game and span blocks of statemente between many lines and not to cut lines in the middle. No GOTO
statemente were used until then because the destination statemens were not the first one in a line.
When the game was almost ready, I needed some extra space to add sound, and I had to reorganize the code. While doing that, I noticed that the base index of the string was used so many times in the code, and I could save them if I change the method of the added index for the string buffer to the dummy string variable. A side effect was that the statements were realigned in a way that the first statement of the game loop was at the beginning of a line, so I could remove the FOR
-NEXT
for that loop and replace it with ON
-GOTO
.
I had to choose what to do with the remaining space in the listing. There was not enough space to redefine the charset, but I could increase a bit the difficulty to the game, by adding some extra obstacles in the blank string that was used to create the path and randomize the substring position of it when copying the bytes.
Inserting extra objects to add difficulty
To accomodate the main loop as a destination of the last GOTO
statement, I had to distribute some of the initialization statements arround that point. I mean, as some of the instructions to initialize the game had to be put inside the main loop because there was no room en the previous lines. To execute the same code in every loop didn't harm the program.
Still some free bytes... Hum... Let's change the color of the background as the paratrooper goes down: from a light blue sky to a dark blue. Nice, but as another playfield element was also blue, I had to change it, and I did it to a red/brown one. The forest turned into a canyon.
New game title: "Deep Canyon"
I noticed that most of the times I played, the path went to the left. The number of all possible values of a long uniform random sequence should be the same, and the path should end in the center of the screen. So why it was going to the left? Because the width of the path was narrowed from time to time by removing the last byte from the right!!! A way to compensate that distortion was to ponder the expression and increase the possibilities to move the path to the right insead of to the left. The first prototype has a 25% of possibilities to choose the left, 25% for the right and 50% to stay in the same position than before, while in the last version of the game, there is 31% to choose the left, 31% to stay and 38% to move to the right, which is compensated with the removal of the rightmost byte of the path every some lines.
The last few bytes in the initialization lines were used to enlarge the player and to add extra random objects in the path. After all those changes, the scroll was not as fast as the initial proof of concept, but it results in a playable game.
Just before sending this game to the contest, I made some changes in the code to make it even harder, because I always won during the tests ;-) I just shortened the hurts/health bar by 30%, but I had to find an extra byte to change the initial value of the variable. This change also gave me more space to print the end of game messages.
Get the DEEP.ATR file and set it as drive 1 in a real Atari (or emulator). Turn on the computer and the game should start after loading.
The abbreviated BASIC code is the following:
The full and expanded BASIC listing is:
0 |
LINE 0: Global initialization |
graphics 28 |
Sets screen to graphics mode 12 (ANTIC 4) without text window. Size is 40x24. |
poke 87,0 |
Makes BASIC believe that it is still in graphics mode 0 (required by INPUT statement) |
dim d$(40),a$(80),b$(80),n$(80) |
Reserves memory for game elements data: - D$ has the data used in the game. - A$ has the path data with some obstacles. - B$ has the canyon pattern. - N$ is used to build the next playfield line in the first half and contains the ground pattern in its second half. |
poke 53277,2 |
GRACTL=2: Players only, no missiles. |
poke 53259,0 |
P3 normal width. |
poke 623,1 |
GPRIOR=1, player over playfield. |
1 |
LINE 1: More global initialization... |
poke 559,58 |
SDMCTL=2+32+8+16: One line resolution players only (no missiles) in standard playfield |
t=adr(n$)+80 m=int(t/2048)*8+8 |
Calculates the needed filler to match the playfield a 2K memory boundary. |
poke 54279,m+8 |
Sets PMBASE to 8 pages of memory after that, matching the second half of string P$. |
dim f$(m*256-t),p$(4096) |
Reserves more memory: - F$ for the filler. - P$ for the playfield area (first 2K) and the P/M data (remaining 2K). |
2 |
LINE 2: Even more global initialization!!! |
d$="{binary data}" |
Defines the players shape and end of games messages. |
for t=1 to 80 b$(t)=chr$(131+rnd(0)*3) n$(t)="\9D" next t |
Sets a random pattern for the walls and a fixed texture for the ground. |
3 |
LINE 3: Main loop |
p$=d$ p$(4096)=p$ p$(2)=p$ |
Cleans the playfield and P/M data. |
a$=p$ a$(9)="\07" a$(34)="\0E" a$(47)=a$ |
Initializes the obstacles in the path. |
c=1 |
Initializes the global counter. |
x=128 |
Sets starting horizontal position of the player. |
y=155 |
Sets the initial height of the player. |
4 |
LINE 4: More game initialization |
z=3889 |
Sets initial offset of the player in the PM buffer. |
l=14 |
Sets current health to paint the bar. |
h=2 w=37 |
Sets the initial horizontal position and the width of the path. |
poke 39811,66 |
Changes the first line of the display list to graphics mode 0 (ANTIC 2). |
poke 39812,0 poke 39813,m |
Changes the address pointer for the playfield in the display list to our reserved area. |
poke 707,30 |
Sets the player color. |
setcolor 1,15,9 |
Changes playfield color. |
5 |
LINE 5: Game loop... Moves the player horizontally |
s=not s |
Height counters decreases every 2 steps. This flags when to decrease them. |
j=stick(0) x=x+((x<196)*(j=7)-(x>52)*(j=11))*2 poke 53251,x |
Moves the player horizontally, verifying the screen limits. |
i=rnd(0)*39+1 |
Selects a random index (no INT() so it is rounded). |
6 |
LINE 6: Computes the next position of the path |
n$(1,40)=b$(i) |
Selects a random pattern for the next step. |
h=h+sgn(int(rnd(0)*3.2-1)) h=h+(h<2)-(h+w>39) n$(h,h+w)=a$(i) |
Moves the path and inserts it in the patten using the same random value, so some obstacles might appear in the path. |
7 |
LINE 7: Prepares the next step |
p$(z,z+15)=d$ |
Moves the player vertically. |
poke 53278,0 |
Clears all P/M collision registers. |
y=y-s |
Decreases the height counter. |
poke 712,128+y/14 |
Sets the background color, from light blue to dark blue. |
g=y=0 |
Check if the paratruper landed. |
8 |
LINE 8: Scrolls the playfield and updates the health bar |
p$(961,1000)=n$(1+40*g) |
Places the new bottom line just bellow the playfield, which could be the ground if the player landed. |
p$(41,961)=p$(81) |
Scrolls up the playfield, removing the first line and including the new last line in one single step. |
q=peek(53255)>0 |
Checks for collision between the player and the playfield. |
l=l+q p$(l,l)="\80" |
Increases the health/damage counter and update the health bar. |
e=g+(l>39) |
Checks if the game has finished. |
w=w-(w>3)*(c=0) |
Decreases the size of the path every 8 new rows. |
9 |
LINE 9: Plays the sounds and checks for the end of the game |
sound 0,202,4,8*q*(e=0) |
Plays a sound if a collision was detected. |
c=(c+1)*(c<7) |
Increases the global counter (0 to 7 and again). |
z=z+s |
Decreases the players height every 2 steps. |
on e+1 goto 5 |
If the game has not finished, jumps to the game loop. |
p$(5,9)=d$(16+5*g) |
Prints the corresponding end of game message. |
input f$ |
Waits for RETURN key to start again. |
goto 3 |
Jumps to the main loop to start a new game. |
Return to my 10-liners page.
© 2018 by Víctor Parada - 2018-03-04 (Update: 2020-08-15)