Chase Me!

Copyright © 2023 by Víctor Parada

CHASEME

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.


Description

Collect as many coins as you can while being chased by some evil guardians.


Instructions

CHASEME start Press the button to start the game.
CHASEME move

CHASEME green 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:

CHASEME blue Blue: He is always following you, but you could guide him to paths that send him far away.

CHASEME pink Pink: He moves through the whole maze, taking unexpected paths that could interfere with your plans.

CHASEME orange Orange: Beware! He is as fast as you...

They always move forward, except on paths with a dead-end.

CHASEME clean When you collect all the coins, you gain access to the next maze. There are 60 levels with different maze and guardians variations.
CHASEME touched If one of the guardians catches you, you lose a life and proceed to the next maze if there are lives left.
CHASEME recover You recover a life when you clean a maze. Try to keep them all to face those "impossible" mazes and do your best.
CHASEME gameover If you lose all 3 lives, the game is over. Press the button to start again.
CHASEME done 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?

Development of the game

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

Shiru's original game for NES.

Lynx

Port for Atari Lynx.

7800

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.

CHASEME prototype

Proof of concept in Graphics mode 2.

CHASEME prototype

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.

CHASEME prototype

Bitmapped sprites and tiles.

CHASEME prototype

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.

CHASEME prototype

Using missiles for multicolor sprites.

CHASEME prototype

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.

CHASEME rc

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.


Download and try

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 code

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)