Copyright © 2021 by Víctor Parada
This is a little game for the 2021 NOMAM's BASIC 10-liners Contest. This program fits in the EXTREM-256 category, and it was written using FastBasic 4.5.2 for the 8-bits ATARI XL/XE. Development started on 2021-01-26, and it took 3+10+1 days. The final version's date is 2021-03-08.
UPDATE: It obtained the 1st place of 25 entries in the category.
Pedro has been commanded to rescue the child, but he realized that there are many of them!!!
Press the button when the title scren appears. | |
Find a way to reach your target. Yellow rocks can be pushed to clean your path. | |
You can move up and down through ladders, or you can securely fall from any height, but you cannot jump upwards. | |
To get a target, you must approach to them only by the side. You can remove dirt to find a path, but be careful: if there are objects above, they might fall and block your path. | |
If you think that you fell in a trap, press the button to restart the zone. There is no backtrace to undo your latest movements. | |
Some zones have many targets, and you must get all of them to clear the zone. | |
The game is over when all the zones were cleared. If you failed in some zones, a message will appear with the number of times you had to repeat a zone. If there is no message, congratulations, you did an excelent work! Press the button to start all again. |
From time to time, some game genres became popular, and ports or new development appears for some console or computer, and sometimes I think that it could be simplified to be programmed as a tenliner. During the first half of January, I had to test some games for the ZPH's Annual Homebrew Awards 2020, and some of the games were "Pitkat" for the Atari 2600 and "Millie and Molly" for the Atari 7800 consoles. Both were based on games that evolved from the game "Pitman" or "Catrap", a game written in BASIC for the Sharp MZ-700 computer in 1985. I didn't know this fact when I decided to try a simplified version for the tenliners contest of the ones I tested.
Pitkat
Millie & Molly
I started from scratch in FastBasic, because I wanted to use some graphical tricks I previously tried to get a bigger color palette for Atari's graphics mode 2 (ANTIC 7 mode). After some hours I had a working engine for a 20x10 playfield. I had to build a tool to pack the puzzle data in order to be included in the source listing and displayed on screen. The compresion method I used was a simple RLE algorithm (Run-Length Encoding), where each byte of data represented an element of the screen (3 bits to map up to 7 different elements) and a repetition count (5 bits for 32 values), where zero times meant end-of-data. The first two puzzles were designed to test all the game logic in the engine, and I had to add a lot of delays in different portions of the code in order to make the animations be fluent.
Prototype with a demo puzzle.
Puzzle to test the engine at all the transitions.
I decided to change the screen resolution to 16x10, because the puzzles won't be that big and also to save data bytes in order to include as many puzzles as I could in the 10 lines of code.
The next step was to replace the characters I was using with user-frendly bitmaps, and I had to design them. I used a spreadsheet with formulas to generate the required source code to be copied and pasted into the BASIC program. I tried many 8x8 sprites in the search for a theme for the game, like to kill monsters, to collect objects or to save people.
Bitmaps!
First proto.
The first prototype with images felt "heavy", so I decided to use tiles of 7x7 pixels, leaving 1 pixel free between rows and columns, and that gave a nice touch to the puzzle. When it was time to define the colours to be used, I used the DLI feature to build multicolor or shaded sprites. As the main character looked like the Mandalorian, I tested how it would look like. After some interations and sprites adjustments, I got a theme for the game.
By that time, more sounds were added and the game timming adjusted, and also a small animation of the target's arms. I had to modify my developing tools in order to be able to include the levels in the FastBasic source code every time I add or change them for testing.
Got a theme!
Using DLI to colorize
During the following days, I created some puzzles. As it was a simplified version of the previous games, all the puzzles should be created to be solved by one character, and there was only one type of target: the one that falls by gravity. As I defined a list of objects and their attributes, the cost to add a floating object was just to add an object to the list and assign a bitmap to it. All the game logic was based on the attributes and it worked so good. New type of puzzles could be added. I managed to define the tile/sprite using the same asigned colors from the other target.
When I fill all the available coding space with puzzles, I sent a copy of the game to DMSC (FastBasic's author), and he suggested me to use a variant of LZSS compression algorithm instead of the RLE method I was using, because I could save enough space to add more puzzles. This algorithm used data bytes to store a literal (a single 3 bit value) or an offset/amount pair of 4 bits values to copy previously decompressed data. Doing a comparison of the compression ratio between algorithms, puzzle by puzzle, it was clear that some of the puzzles had a better compression ratio using one method than the other, and it was not always the same algorith. I tried to combine both compression methods into a mixed one and, after a brainstorm, I got an algrithm that behaved pretty well, better than both methods by themselves, and I got even more space for puzzles.
The new method for compressing puzzles splits data bytes in three portions: 3 bits for the A component, 4 bits for the B component, and 1 bit to signal the meaning of those components, which it is:
- Bit=1: compressed as RLE, stores A value repeated B+1 times (1 to 16)
- Bit=0: compressed as LZSS, copies A+2 bytes (2 to 9) from an offset of B+2 (2 to 17) to the back.
The bits were sorted in a way that the decompressing algorithm to be the shortest one, and that was B+signal+A. I also had to reassign the order of the objects in order to avoid the $9B value in the data, which it cannot be typed inside a string as it is the new-line character.
I could finally pack 17 levels (zones) in only 3 lines of FastBasic code, and used some extra space from the remaining 7 lines of code to include an aditional animation for the player: he walks.
I wanted some feedback about the game and the levels, so I made a special build and shared it with some friends. After some days, it was clear which levels were the most difficult and which ones required some adjustments.
In the meanwhile, DMSC poosted in AtariAge that a new version of FastBasic would be released soon and requested for beta testing. The main new feature was the ability to pass parameters to procedures, and as one of the procedures of this game required to assign the parameter to a global variable, I changed all the invocations to that procedure, which it also saved some more coding space. After some other suggestions about abbreviations I sent to DMSC that were included in the latest beta of FastBasic 4.5, I could save another 50 bytes of coding space, enough for another level.
With the feedback and the extra space, I modified some levels, changed the order of them, and added new levels: the game reached 20 zones in total.
DISCLAIMER: Neither George, Walt, nor their people have supervised this development.
Get the CHILDREN.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. The ATR includes "NTSC.XEX" to be played on NTSC computers. I will run a bit faster than normal, but you will get the right colors.
The abbreviated BASIC code is the following:
The full and expanded BASIC listing is:
|
THE CHILDREN (c) 2021 Víctor Parada G. |
move adr("{binary data}"),$820D,239 move adr("{binary data}"),$811F,239 move adr("{binary data}"),$8030,240 move adr("{binary data}"),$7FFF,50 |
Maps data There are the following blocks - Number of targets for every level: 1 byte per level. - Starting position of the player: 1 byte per level. - Pointers to start of level data: 2 bytes perl level, plus 2 bytes for a final end-of-data vector. - Compressed map data using a custom combination of LRE and LZSS algorithms. Each byte of data has 3 components: 4 bits for B, 1 flag bit and 3 bits for A, interpreted like this > if flag bit is 1, it was RLE compression, with the A value repeated B+1 times (1 to 16). > if flag bit is 0, it was LZSS compression, with an offset of B-2 (-2 to -17) to copy A+2 bytes (2 to 9). Chuncks of data are stored in memory in reverse order to overwrite the extra leading byte from FB strings. |
t=adr("{binary data}")+1 |
General data - Tiles and their colors (8 bytes) - Properties (8 bytes) - Player shapes (4 bytes) - Playfield colors (708-712) (4 bytes + 1 reused byte) - Bitmaps (72 bytes for 9 tiles) $07->7 Player (to the left) $08->0 Player (back) $09->1 Ladder $0A->2 Rock $0C->3 Sand $0B->4 Wall $0D->5 Target $0E->6 Flotating target $0F->7 Player (to the right) - Title screen (12 bytes) - Bitmap for the target's arms in wide position (2 bytes) - Bitmaps for legs of the player (8 bytes) - Game over text (10 bytes) |
e=t+8 |
Property bits 0-1: free 1-2: walkable 2-4: falls 3-8: pushable 4-16: target 5-32: climbable |
o=e+8 |
Player types 0: space 1: ladder 2: rock 3: brick 4: sand 5: target 6: floating target 7: player |
move $E000,$7000,512 |
Builds a new charset in $7000 |
move o+8,$7038,72 |
Modifies chars 39 to 47 ($27 to $2F) with the tiles bitmaps |
graphics 18 |
Sets up graphics mode 2 (ANTIC 7) without text window |
poke 559,33 |
Sets the screen width to 16 instead of 20 chars |
move o+4,708,5 |
Sets the color palette |
poke 756,$70 |
Enables the new charset |
h=16 |
Constant (to save bytes) |
r=dpeek(88)+32 |
Playfield area |
mset dpeek(560)+6,10,$87 |
Enables DLI for playfield area |
dli set d = $0C wsync into $D016, $AA wsync into $D017, $36 wsync wsync wsync into $D019, $E8 wsync wsync into $D017, $8A wsync into $D016, $38 into $D019, $E4 wsync into $D017, $36 wsync wsync into $D019, $78 wsync into $D016, $34 wsync into $D019 |
Defines DLI code |
dli d |
Activates DLI |
move o+80,r+50,12 |
Title screen |
do |
MAIN LOOP |
l=0 |
Sets starting zone (minus 1) |
i=0 |
Crears the number of retries |
while strig(0) wend |
Waits for the next game |
|
GAME LOOP |
while l<20 |
Repeat for every zone: |
q=1 |
Clears the player retry flag (inverted) |
position 5,0 |
Points to top for the zone number |
exec w |
Draws the zone |
print #6,"ZONE ";l+1 |
Prints the zone number on top |
x=r+peek($8015+l) |
Gets the initial player position in current zone |
n=peek($8000+l) |
Gets the number of targets in the zone |
z=0 |
Player does not start in a ladder |
k=0 |
Sets the player shape as looking to the right |
v=0 |
Resets the bad movement counter |
|
ZONE LOOP |
while n * q |
Repeat the loop until there are no more targets or the player quits it (pressing the trigger) |
j=15-stick(0) |
Reads the joystick |
u=(j=8)-(j=4) |
Detects horizontal movement |
if u |
Moving horizontally? |
k=u<0 |
Detects the direction to update the player shape |
y=x+u |
Finds the position where the player wants to go |
a=peek(y)&7 |
Gets the tile in the destination |
if peek(e+a)&2 |
Walkable? |
poke x,peek(t+z) |
Removes the player from the old position, drawing a ladder if the player previously entered into it |
z=a |
Saves the content type at the new position |
if z<>1 |
Not entering a ladder? |
if z>=5 |
Target found? |
dec n endif |
Decrements the target's count. |
z=0 endif |
Destination was not a ladder |
exec s |
Plays a sound based on the destination |
m=x-h |
Gravity check over last position |
x=y exec p |
Move to the new position |
exec f x |
Gravity check at new position g=x |
exec f m |
Execute gravity at saved location g=m |
elif peek(e+a)&8 and peek(y+u)=0 exec p |
Pushable? Is it free on the other side? |
exec s |
Plays a sound based on the destination |
poke y,0 poke y+u,peek(t+a) |
Moves the object |
exec f y+u |
Gravity check bellow the object in its new position g=y+u |
exec f y-h |
Gravity check where the object was g=y-h |
else inc v |
Count bad moves |
exec p endif |
Updates player side, in case he is looking to the other side |
elif j=2 |
Moving down? |
y=x+h |
Finds the position where the player wants to go |
a=peek(y)&7 |
Gets the tile in the destination |
if a<2 |
Is there a ladder or an empty space? |
poke x,peek(t+z) |
Removes the player from the old position, drawing a ladder if the player previously entered into it |
g=x-h |
Gravity check over last position |
x=y |
Saves the new position |
z=a |
Saves the content type at the new position |
exec s |
Plays a sound based on the destination |
exec p |
Moves to the new position |
exec f g |
Execute gravity at saved location |
exec f x |
g=x |
else inc v endif |
Count bad moves |
elif j=1 |
Moving up? |
y=x-h |
Finds the position where the player wants to go |
a=peek(y)&7 |
Gets the tile in the destination |
if a<2 and z |
Is there a ladder or an empry space while already in a ladder? |
poke x,peek(t+z) |
Removes the player from the old position, drawing a ladder if the player previously entered into it |
x=y |
Saves the new position |
z=a |
Saves the content type at the new position |
exec s |
Plays a sound based on the destination |
exec p |
Moves to the new position |
else inc v endif endif |
Count bad moves |
if v>2 |
Too many bad moves? |
v=0 |
Resets counter |
sound 0,10,12,4 pause 1 sound endif pause 5 |
Whistle! |
dpoke $706C, dpeek(o+60+peek(20)&32) |
Animate the Child's arms |
q=strig(0) |
Player wants to reset the zone and try again? |
if j poke 77,0 endif |
Clear attract mode if the joystick was moved |
wend |
End of ZONE LOOP |
if q |
Does the player want to repeat the uncleared zone? |
for a=0 to 64 sound 0,99-(a mod 10)*3,10,8-a/8 pause next a |
Whistles |
inc l |
Moves to next zone |
else |
Player wants to retry zone |
sound 0,201,12,4 pause 9 |
Buzzer |
inc i endif |
Increments retries counter |
sound |
Shuts up any playing sound |
wend |
End of GAME LOOP |
exec w |
Last level is just a game over screen |
move o+102,r+99,10 |
Displays a "completed" message |
if i |
The player had to repeat a zone? |
position 19,0 print #6,i;" defeats" endif loop |
Displays the number of repetitions |
proc f g |
Subroutine to move in sequence some falling objects |
if g=x and z exit endif |
Do not apply gravity to the player if he is holding a ladder |
b=g |
Saves the starting position to check |
do |
Repeats for every falling object over the one to check |
c=0 |
Flag to check if an object dropped at least one time |
a=peek(g)&7 |
Gets the object type at the checked position |
if peek(e+a)&4 |
Is it a non-fixed object? |
while peek(g+h)=0 pause 1 |
Is there a space below it? |
c=peek(g) poke g,0 |
Removes the object from its current location... |
g=g+h poke g,c |
And puts it again in the new location. |
c=8 wend |
Flags that the object was moved at least once |
sound 0,255,6,c |
Plays a sound when the falling object touches another |
if a=7 x=g endif |
If the object was the player, sets its new position as the current. |
b=b-h g=b pause 1 |
Now, check the object that it was over the one that fell |
sound |
Shuts up the crash sound (if it was enabled) |
else exit endif |
It was a fixed object, no more checks are needed. |
loop endproc |
Next object... |
proc s |
Sound subroutine. Plays a sound based on the content of the player's new position before he steps in |
b=peek(y)&7 |
Gets the content of the destination area |
if b=0 for b=0 to 3 sound 0,y&1*4+40,10,3-b next b |
Steps in open area |
elif b=1 sound 0,(x-r)/h*2+80,10,2 pause 1 sound |
Climbing (up or down) |
elif b=2 for b=24 to 0 step -1 sound 0,95,12,b/3 next b |
Pushing a rock |
elif b=3 sound 0,8,8,3 pause 1 sound |
Digging in sand |
elif b>=5 for b=h to 0 step -1 sound 0,b*3,10,b/2 next b endif endproc |
Target was rescued |
proc p |
Puts the player in its current location, looking to the current side (or backwards if he is over a ladder) |
poke x,peek(o+k+z*2) |
Draws the selected tile |
b=x&1*2+o+94 |
Selects the leg shape |
dpoke $703E,dpeek(b) dpoke $707E,dpeek(b+4) endproc |
Modifies the tiles with the selected shape |
proc w |
Subroutine to display current zone |
for c=9 to -2 step -1 mset r+c*h,h,0 |
Clears the screen row by row, from the bottom to the top |
pause q next c |
The refresh speed is faster if the player wanted to repeat the zone |
a=$802A+l*2 |
Gets the address of vector pointing to the zone data |
c=r |
Sets the start of the playfield |
for n=dpeek(a) to dpeek(a+2)-1 |
Parses the zone map data |
a=peek(n)&7 b=peek(n)/h+1 |
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 peek(n)&8 |
Not similar to previous data? |
mset c,b,peek(t+a) |
Gets new data from source |
c=c+b |
Updates screen pointer to next position to display |
else |
Similar |
move c-b-1,c,a+2 |
Copies data from other just loaded |
c=c+a+2 endif next n endproc |
Updates screen pointer to next position to display |
Return to my 10-liners page.
© 2021 by Víctor Parada - 2021-02-14 (updated: 2021-04-10)