Copyright © 2023 by Víctor Parada
This is a little game for the 2024 NOMAM's BASIC 10-liners Contest. This program fits in the EXTREM-256 category, and it was written using FastBasic 4.6 for the 8-bits ATARI XL/XE computers line. Development started on 2023-05-05, and it took 3+10 days. The final version's date is 2023-06-14.
Collect as many coins as you can while being chased by some evil guardians.
Press the button to start the game. | |
You are the green guy. Use the joystick to move around. There are lots of coins you have to collect, but one or more guardians are there to chase and catch you. There are three guardians taking care of the coins, each with his own personality:Blue: He is always following you, but you could guide him to paths that send him far away. Pink: He moves through the whole maze, taking unexpected paths that could interfere with your plans. Orange: Beware! He is as fast as you... They always move forward, except on paths with a dead-end. |
|
When you collect all the coins, you gain access to the next maze. There are 60 levels with different maze and guardians variations. | |
If one of the guardians catches you, you lose a life and proceed to the next maze if there are lives left. | |
You recover a life when you clean a maze. Try to keep them all to face those "impossible" mazes and do your best. | |
If you lose all 3 lives, the game is over. Press the button to start again. | |
You complete the game when you have gone through all the mazes, and the score depends on the number of coins collected. There is an additional bonus for the remaining lives available.
The perfect score is $3557! Will you be able to reach it? |
A ZPH show presented some homebrew for Atari Lynx console, and one of them was Chase by Oceo Team (2022), a port of Chase by Shiru (2004) for NES/Famicom console, which was also ported by Jonas Carlsson for Atari 7800 (2020). I immediately though that it was a good candidate for a tenliner, and I said that during the show.
Shiru's original game for NES.
Port for Atari Lynx.
Port for Atari 7800.
Some days later, I started a prototype in FastBasic using text mode 3 (ANTIC mode 7) with a narrow playfield (16x12 instead of 20x12). Using double line resolution player/missile graphics, I could manage 4 sprites in a very simple way, one for the player and three for the enemies. I decided to move the player every 2 pixels and the enemies move every single pixel. Maze's size would have a maximum of 16x11 tiles, and the top line would keep the score and stats.
Later, it was time to add some AI to make the enemies move. In order to save coding space, I had to figure out how to manage both horizontal and vertical movements in a single procedure. The solution was simple: a single routine to manage current direction as "forward" and give the current parameters to let it recognize the perpendicular movements to check for free paths. Then, the routine could be called with one set of parameters if the enemy was moving horizontally, and transpose the parameters if it was moving vertically. After tunning it a few, the enemies could move through all the maze. To make them more interesting, I gave them different "personalities": one of them always try to chase the player, and other runs as fast as the player. At that moment, there was no collision detection, but the player could "eat" the dots while moving around.
Proof of concept in Graphics mode 2.
Prototype of AI for enemies.
The next step was to add pixel art to the tiles and sprites. While testing a fresh version of Atari FontMaker (by Matosimi and RetroCoder), I created some candidates for this game and selected those that worked the best. The idea was to make 4 tiles to use all 4 available colors in this text mode to make it as colorful as the original, and I tried different variations, but the problem was that only two colors could be used per tile, and one of them must be the background. I moved the score line to the bottom because that way I could change tile colors for each level and set a fixed palette for the score using a DLI. But I felt that the result was not going in the right direction.
As a test, I created other tiles in GFX mode of FontMaker for ANTIC mode 5. This mode allows 4 colors at the same time for each tile, but requires 2 chars to diplay it, so the screen size became 32x12. As the maze map for every check was the screen, I had to move it to another memory buffer, and use mapping to diplay the new tiles. I liked the result and kept this branch of the code as the oficial to continue with the developing.
Bitmapped sprites and tiles.
Changed to ANTIC 5 mode.
There were other things that disturbed me: the dots to be collected were too big, and the players were of a single color, and the dots could be seen through the empty area of the bitmaps. I tried to use the missiles from the P/M graphics in quad width and configured as a fifth player to get another single color and to place behind the players to get a solid background. The idea almost work, except that it was too slow in FastBasic to perform binary operations to do the maths on overlapping pixels when two enemies were at the same height in the maze. The performance of the game fell down and the game was not playable. I only shrinked the dots and it was harder to see them through the players.
It was time to add more mazes to the game. I decided to use the same (de)compression algorithm I wrote for The Children. But there was a feature I could use to save data bytes: there is symmetry in the mazes. I only had to store half (or a bit more in some cases) of the data and complete them using that data from first half. Later, I used the same algorithm to store and then to display the title and other info screens. I had to insert a dummy (unused) value in the list of valid tiles, because with the current table it could be possible to generate a EOL (byte $9B=155) in the compressed data string. It also happened in The Children, but that time I changed the order of the tiles to a list where it was impossible to generate an EOL.
Using missiles for multicolor sprites.
Storing the messages as a maze.
Instead of to follow the rules form the original Chase game, where you must clear a level to get into the next one, I decided that you should play a level once, and go to the next level by clearing the area or by being touched, earning or loosing a life respectively. I introduced a scoring system and that is the reason why I changed the name to "Chase me!".
The game used too much coding space for the logics (more than in The Children) and there was not enough room for a lot of mazes, but I realized that a single maze could be used many times using different combination of enemies. In total, the game could have 40 levels using 17 different mazes.
I sent the game to some friends to get feedback. I was told that it was too difficult to escape from the fastest enemy because it was hard to turn in a junction or corner. DMSC told me that he misses the use of diagonals like in Pacman and he proposed a partial solution. As the result was better than what I had, I took the idea, but implemented it completely. The new version provides a smooth solution to turn in corners and junctions. A single diagonal position of the joystick allows you to zig-zag through the maze without getting stuck, except on end corners, obviously.
Release candidate.
I did some tweaking to the tiles bitmaps, color palettes (for both PAL and NTSC versions), sound FX, score line, start position of the sprites in each maze, and the game seemed to be complete. With the extra space I got, I could add 20 more levels using other combinations of enemies for the same 17 mazes.
Get the CHASEME.ATR file and set it as drive 1 in a real Atari (or emulator). Turn the computer on and the game should start after the loading completes. A joystick in port 1 is required.
NOTE: This game is for PAL computers. For NTSC computers, the ATR contains a file called CHASEMEN.XEX with minor changes in color palette and timers.
The abbreviated BASIC code is the following:
The full and expanded BASIC listing is:
|
Chase me! (c) 2023 Víctor Parada |
move adr("{binary data}"),$8103,240 move adr("{binary data}"),$8016,238 move adr("{binary data}"),$7F37,224 |
General data: - 60 bytes: 1 byte per level: enemies available (3 bits) and corresponding maze number (5 bits) - 9 bytes: Color palette - 4 bytes: Random direction support array (1,-1) - 6(+4) bytes: Mapping for tiles (floor,pill,wall,dummy,outside) - 48(+4) bytes: Bitmaps for sprites - 16 bytes: Initialization data for horizontal and vertical speed arrays - 9 bytes: "completed" message - 48 bytes: Bitmaps for tiles and heart Maps initialization data: - 17 bytes: Number of coins per maze - 68 bytes: Starting location of each sprite in every maze (4 bytes per maze) - 44 bytes: Pointers to packed maze data - 370 bytes: Packed mazes data (17 levels and 4 screens) The strings are stored backwards because FastBasic includes the string length as the first byte, and doing in this way, it overwrites the prefix from the previous MOVE. |
graphics 29 |
Set up graphics mode 14 (ANTIC 5) 40x12 without text window |
poke 559,33 |
Set the screen width to 32 instead of 40 chars |
pmgraphics 2 |
Enable double line resolution P/M graphics |
dpoke dpeek(560)+15,$0785 |
Set bottom line to text mode 2 (ANTIC 7) with DLI bit enabled in the previous line |
move $7F74,704,9 |
Set P/M and playfield colors |
q=dpeek(88) |
Pointer to the playfield in the screen |
s=q+352 |
Pointer to the score line |
n=16 |
Constant to save listing bytes |
dli set _d=$24 into $D018, $0F into $D016,$AA into $D017 dli _d |
Enable DLI to set score line colors |
dim x(3) byte,y(3) byte,w(3),v(3),r(1), m(4),p(175) byte |
Sprite control arrays: 0=Player, 1-3=Enemies - X: Horizontal position - Y: Vertical position - V: Horizontal speed - W: Vertical speed Support arrays - R: New random direction (1,-1) - M: Mapping from maze internal buffer data to displayed tiles (floor,coin,wall,dummy,outside) - P: Maze internal buffer |
move $E000,$7000,512 move $7FD0,$7028,48 poke 756,$70 |
Copy the charset to RAM and replace some chars with tile bitmaps |
move $7F7D,adr(r),14 |
Initialize support arrays |
exec _w 17 |
Display the title screen |
do |
MAIN LOOP |
while strig(0) wend |
Waiting for the fire button to start |
l=0 |
Start level |
j=3 |
Lives |
o=0 |
Score |
repeat |
GAME LOOP |
poke 77,0 |
Disable attract mode |
exec _w 20 print #6,"{binary data}" |
Clear the screen |
mset s,j,$8A |
Harts (lives) |
position 38,8 print #6,"l";l+1 exec _s |
Print the level number and score |
exec _c |
Select random colors for next maze |
g=0 |
Disable coin sound |
h=peek($7F38+l) u=h&$1F |
Get the map number used in current level |
exec _w u |
Display the map for the level |
b=h!n |
Set initial sprite positions if enabled: 3 top bits (5, 6 and 7) from level config for enemies and a constant bit (4 is forced to be present) for the player |
c=n |
The first sprite to be checked is the player |
for i=0 to 3 |
Iterate all the sprites |
if b&c d=peek($8011+u*4+i) |
Is the sprite present in the level? |
if i=0 p(d)=0 dpoke q+d+d,m(0) endif |
Remove the coin at player's start location. The empty cell couldn't be included in packed data because the coin is required by the symmetry algorithm |
x(i)=d&$F*4 y(i)=d/n*4 |
Coordinates are 4x maze's size, X is LO nibble and Y is HI nibble |
exec _m i |
Display the sprite |
else x(i)=0 endif |
Force the enemy to be ignored in this level |
c=c+c next i |
Shift check bit to the left to test next sprite |
t=peek($8000+u) |
Number of coins in the level. It does not include the removed coin at start position |
move $7FB7,adr(w),n |
Set initial speed for all the sprites |
poke $D01E,0 k=0 |
Reset P/M collisions and "killed" flag |
pause 70 |
Small delay to start |
repeat |
LEVEL LOOP |
i=0 repeat |
Iterates between the player and all the enemies |
z=x(i)/4+y(i)/4*n pause |
Absolute position of the sprite in the maze |
if peek($D00C) |
Touched? |
k=1 |
Yes! Set "killed" flag |
exit endif |
Do not update other sprites |
if x(i) |
Is current sprite active in this level? |
exec _m i |
Display the sprite in updated position |
if x(i)&3=y(i)&3 |
Is the sprite centered in a cell? When moving, one of the axis is not in the cell (non zero) and the other must be zero |
if i=0 |
Moving the player |
if p(z) |
Player got a coin? |
p(z)=0 dpoke q+z+z,m(0) |
Remove the coin |
dec t |
Update counter of remaining coins |
inc o exec _s |
Increase the score |
g=7 endif |
Enable coin FX |
a=stick(0) |
Check for a new direction |
b=v(0) |
Save current horizontal speed |
v(0)=((a&4 or p(z-1)>1)-(a&8 or p(z+1)>1))*2 w(0)=((a&1 or p(z-n)>1)-(a&2 or p(z+n)>1))*2 |
Verify that there is not a wall in the selected direction |
if v(0) and w(0) |
Moved diagonally and both paths were free? |
if b v(0)=0 else w(0)=0 endif endif |
Take the perpendicular path |
else |
Moving an enemy. Get a new destination to follow |
if i=1 a=x(0) b=y(0) |
Enemy 1 (Blue) follows the player |
else a=rand(61) b=rand(41) endif |
Select a random destination for enemies 2 (Pink) and 3 (Orange) |
if v(i) exec _r sgn(a-x(i)), sgn(b-y(i)),1,n,sgn(v(i)) v(i)=a w(i)=b |
If currently moving horizontally |
else exec _r sgn(b-y(i)), sgn(a-x(i)),n,1,sgn(w(i)) v(i)=b w(i)=a endif |
For vertical movement, transpose the parameters and the return values |
if i=3 v(3)=v(3)*2 w(3)=w(3)*2 endif endif endif |
Enemy 3 (Orange) has twice the speed |
x(i)=x(i)+v(i) y(i)=y(i)+w(i) endif |
New position for the sprite |
if g dec g sound 0,20+g,10,g endif |
Coin sound FX. It updates for many cycles and turns off automatically in the last iteration |
inc i until i>3 |
Repeat for the next sprite |
until t=0 or k |
Level ends when there are no more coins or an enemy touched the player (killed) |
for a=0 to 162 sound 0,220-a*(k=0),10-k-k,9-a/18 next a |
End of level sound FX. It depends on the killed variable for a crash or a success tone |
if k |
Touched? |
dec j |
Lose a life |
else |
Not touched... |
j=j+(j<3) endif |
Recover a life |
inc l |
Jump to next level (dead or alive) |
until j=0 or l=60 |
Go to next maze unless there are no more lives or no more levels |
exec _w 20 |
Clear the maze area |
exec _c |
Select color for next message |
if j |
Still alive? |
move $7FC7,s,9 |
Yes... The game was completed |
exec _w 18 |
Display the happy face |
o=o+25*j exec _s |
Update the score with a bonus per remaining life |
else |
No... End of the game |
poke s,0 |
Remove the last heart |
exec _w 19 endif loop |
Display the game over message |
proc _w f |
Unpack requested maze and displays it |
a=$8055+f+f |
Get the address of vector pointing to the zone data |
c=adr(p) |
Set the start of the playfield |
for d=dpeek(a) to dpeek(a+2)-1 |
Parse the zone map data |
e=peek(d) b=e/n+1 a=e&7 |
Bits 0-2 (A): Object type || Count of objects in sequence Bit 3: 1=RLE || 0=LZ Bits 4-7 (B): Repetitions || Pointer to recent similar objects |
if e&8 |
Not similar to previous data? |
mset c,b,a |
Get new data from source |
c=c+b |
Update screen pointer to next position to display |
else |
It's similar |
move c-b-1,c,a+2 |
Copy data from other just loaded |
c=c+a+2 endif next d |
Update screen pointer to next position to display |
a=c-adr(p) b=176-a while b dec b p(a)=p(b) inc a wend |
If the maze is not complete, mirror from the top half of the maze, diagonally symmetric |
mset pmadr(0),512,0 |
Clear all sprite bitmaps in P/M area |
for a=0 to 87 dpoke q+a*2,m(p(a)) dpoke s-2-a*2,m(p(175-a)) next a endproc |
Display the maze (vertical animation), mapping tiles from internal data |
proc _c |
Select maze's color randomly |
a=rand(14)+1 |
First color |
repeat b=rand(14)+1 until abs(a-b)>3 |
Second color must not be similar to the first one |
poke 708,a*n+2 |
Use the first color for the floor |
poke 709,b*n+8 |
Use the second color for the blocks |
poke 710,(b+r(rand(2)))*n+4 endproc |
Shadows of blocks are darker, but have a slightly different color |
proc _s position 3,9 print #6,"{binary data}";color(160) o endproc |
Display current score |
proc _m a pmhpos a,x(a)*2+64 move $7F87+a*12,pmadr(a)+y(a)*2+12,n endproc |
Display selected sprite in its current position. Bitmaps include some blank bytes at the top and at the bottom to clean old data when scrolling vertically |
proc _r e f c d a |
Enemy IA - Select next direction for an enemy Parameters: - E: Speed vector to destination in the moving direction - F: Speed vector to destination in perpendicular direction - C: Tile delta in the moving direction - D: Tile delta in perpendicular direction - A: Speed vector in the current moving direction Returns new A and B vectors to be assigned to the enemy in the corresponding directions |
b=0 |
B: Current perpendicular speed vector is always 0 |
c=p(z+a*c)>1 |
Is there a wall in front? |
if rand(2) and p(z+f*d)<2 and f |
Is the perpendicular towards the destination free? |
a=0 b=f |
Randomly choose the free perpendicular (50%) |
elif c or a<>e |
Blocked by a wall or is the player behind? |
if f=0 then f=r(rand(2)) |
Need to move to either side to turn around if the destination is aligned |
if p(z+f*d)<2 a=0 b=f |
Check if the side closest to the destination is free |
elif p(z-f*d)<2 a=0 b=-f |
If not, check the other side |
elif c a=-a |
If neither side is empty and there was a wall in front, turn backwards (dead end) |
endif endif endproc |
If neither side is empty and there is no wall in front, continue straight. |
Return to my 10-liners page.
© 2023 by Víctor Parada - 2023-05-22 (updated: 2023-06-19)