Sunday, February 28, 2016

NUTS! - Atari BASIC 10-Liner Contest 2016 Entry

Update April 3, 2016: 3rd Place!


Climb the trees, jump to gather acorns, but beware the blue jays! Earn points by climbing (press fire) and jumping left and right (move the joystick) to collect acorns, which only fall when you are climbing. The acorns are worth more the higher you climb, but there are more blue jays, too. The game is over when you run into a blue jay. Your high-score is recorded so you can try and beat it the next round. Grab the ATR here and play it on your favorite emulator (I use Altirra).

This is only my second BASIC program in modern times and is my entry to the NOMAM 2016 BASIC 10-Liner Contest. I like the 10-line constraint and find it a fun (for now) challenge to do my own code optimization squeezing it into 10 x120-character lines. For this program, I wrote it up in stages, getting each feature to work. I saved doing the pixel art for near the end. In the middle of development, I tested on the boy. He made me remove a timer:

"Dad, gamers hate timers in a runner, which is what this is." 

So, I removed the countdown timer and added an algorithm to increase difficulty with advancement. (More birds the more you climb.) After I compressed it down with single-character variable names, abbreviated statements, and some hand-optimized coding, I had space left over. I added sound effects in the remaining character count. I was surprised by how much more enjoyable the game play was with sound effects. Read on for the code. Here's a video of the final product. 



Here's the code listing in 10 lines each 120 characters or fewer:
0 DIMS$(76):S$="{...a mess of ATASCII...}":Q=ADR(S$):R=PEEK(106)
1 POKE106,R-8:GR.1:POKE106,R:CLS:POKE54279,R-4:POKE559,46:DP.53256,257:DP.53258,257:POKE53277,3:DP.708,$12C4:P=(R-4)*256
2 W=53248:DP.W,$6868:DP.W+2,$7888:DP.704,$850A:DP.706,$1D85:M.Q+64,DPEEK(560)+7,12:M.57344,(R-8)*256,1024:POKE756,R-8
3 M.Q+56,(R-8)*256+264,8:?#6;"NUTS!","HI:";J:?#6;"SCORE:":F.X=0TO10:?#6;" aaaaaa aaaaaa":N.X:F=1:S=16:H=104:B=1:E=0
4 D=12:G=16:K=0:Z=1:DO:POKE53278,1:A=2*STRIG(0):IFF=0:F=PTRIG(0)-PTRIG(1):END.:IFA=0:-M.P+664,P+666,78:-M.P+792,P+794,78
5 SO.1,0,Z,2:Z=Z=0:K=K+1:S=S-4:IFS<0:S=15:END.:IFB:D=D-2:M.Q+22-B*6+D,P+728-64*B,2:B=B*(D>0):EL.:B=(RND>(1-L))-(RND<L)
6 D=12:L=K/5E3:END.:END.:IFE:G=G-4+A:M.Q+40+G,P+920,4-A:E=G>0:EL.:G=16:E=(2-A)*(RND>.9):END.:-M.P+920,P+924-A,78:IFF=-1
7 IFH=104:F=0:EL.:H=H-8:M.Q,P+589,8:END.:END.:IFF=1:IFH=136:F=0:EL.:H=H+8:M.Q+8,P+589,8:END.:END.:POKEW,H:SO.0,0,0,0
8 X=PEEK(53260):SO.1,0,0,0:IFX&8:SO.0,50,10,15:K=K+K DIV 5:-M.P,P+964,28:ELSE:IFX&6:EXIT:END.:END.:POS.6,1:?#6;K;
9 SO.2,H,10+(F=0),6:PAUSE 0:POKE54277,S:LOOP:IFK>J:J=K:END.:SO.2,0,0,0:G.1

Below is the expanded code with commentary following each group of statements.

DIM S$(76)
S$="...{a bunch of ATASCII}..."
Q=ADR(S$)
Sets up a string full of ATASCII characters containing the pixel graphics for the acorn, squirrel, bluejay, and tree bark. The end contains characters making up part of the display list to enable vertical scrolling. The address of the string is stored in Q, which I use many times copy parts of the string into memory locations.

R=PEEK(106)
1 POKE 106,R-8
GRAPHICS 1
POKE 106,R
CLS 
Sets up the screen and creates blank memory for storing sprites and custom character set. The line #1 is used a jump at the end a game to restart another round.

POKE 54279,R-4: POKE 559,46: DPOKE 53256,257: DPOKE 53258,257: POKE 53277,3: DPOKE 708,$12C4: P=(R-4)*256: W=53248: DPOKE W,$6868: DPOKE W+2,$7888: DPOKE 704,$850A: DPOKE 706,$1D85
Sets up player-missile (sprite) graphics and colors. Turbo BASIC XL's double poke (abbreviated DP.) is great for saving code space when you need to set two adjacent 1-byte registers.

MOVE Q+64,DPEEK(560)+7,12
Modify the display list to make all but the first two rows Graphics 2 mode with vertical scrolling.

MOVE 57344,(R-8)*256,1024: POKE 756,R-8: MOVE Q+56,(R-8)*256+264,8
Copy the default character set into RAM and put a custom character into the "A" location. Point the hardware here.

? #6;"NUTS!","HI:";J: ? #6;"SCORE:": FOR X=0 TO 10: ? #6;" aaaaaa      aaaaaa": NEXT X
Draw the playfield onto the screen. The uppercase letters are printed in green. The lowercase "a" points to my special tree bark character with the brown color.

F=1: S=16: H=104: B=1: E=0: D=12: G=16: K=0: Z=1
Initialize a bunch of state variables, e.g. H is the horizontal position of the squirrel.

DO
Begin the main loop!

POKE 53278,1
Clear the collision register.

A=2*STRIG(0)
IF F=0:F=PTRIG(0)-PTRIG(1):ENDIF
Get joystick input. Only read the left/right direction if the squirrel is not jumping (F=0).

IF A=0
Now begins a large set of actions if the user has the fire button pressed:

-MOVE P+664,P+666,78: -MOVE P+792,P+794,78
These two move commands scroll the blue jays down the screen.

SOUND 1,0,Z,2: Z=Z=0
Play the climbing sound and toggle it on and off with flag Z. This takes advantage of the odd numbered distortion values being no sound.

K=K+1
Increase the score by one for climbing.

S=S-4: IF S<0: S=15: ENDIF
Smooth scroll 1/4 of a character to make it look like the squirrel is going up the tree.

IF B: D=D-2: MOVE Q+22-B*6+D,P+728-64*B,2: B=B*(D>0)
If there's a bird being scrolled onto the tree ... then put two lines of the sprite onto the screen at a time. Turn off B when the whole bird makes it.

ELSE: B=(RND>(1-L))-(RND<L): D=12: L=K/5000
Otherwise test to see if there's a new bird. Compute the likelihood based on the score.

ENDIF: ENDIF
End of the bird conditional (IF B). End of the climbing conditional (IF A=0).

IF E: G=G-4+A: MOVE Q+40+G,P+920,4-A: E=G>0
If there's an acorn entering the screen ... introduce either 2 or 4 lines of the acorn at a time depending upon the climbing state.

ELSE: G=16: E=(2-A)*(RND>0.9)
Otherwise figure out if we need another acorn, but only if the squirrel is climbing. This prevents a player from parking out and collecting acorns with no other challenge.

ENDIF : -MOVE P+920,P+924-A,78
Scroll the acorns down the screen.

IF F=-1
  IF H=104
    F=0
  ELSE
    H=H-8: MOVE Q,P+589,8
  ENDIF
ENDIF
IF F=1
  IF H=136
    F=0
  ELSE
    H=H+8: MOVE Q+8,P+589,8
  ENDIF
ENDIF
POKE W,H
This section jumps the squirrel left and right. It moves the player in 8 columns increments and selects which pixel graphic to display (either left or right facing squirrel). This was some of the first loop code I wrote. There may be a more compact way to do this with math and logical expressions, but these two IF-THEN structures do the trick and don't take up too much space. I suspect this is a bit faster because there's no multiplications that would be required in a more compact approach.

SOUND 0,0,0,0: SOUND 1,0,0,0
Part of the sound effect logic - here the jumping and climbing sounds are turned off. 

X=PEEK(53260)
IF X&8
  SOUND 0,50,10,15: K=K+K DIV 5: -MOVE P,P+964,28
ELSE :IF X&6
  EXIT :ENDIF
ENDIF
Check the collision register for either an acorn or blue jay hit. If an acorn, the play a tone, increment the score (by 20%), and erase the acorn. If a blue jay, exit the DO loop.

POSITION 6,1: ? #6;K
Update the score.

SOUND 2,H,10+(F=0),6
Play a sound with pitch based on the horizontal position of the squirrel when it is jumping. These sound effects were squeezed in at the end, which accounts for the inconsistent way they are implemented.

REPEAT :UNTIL PEEK(54283)>93
PAUSE 0
POKE 54277,S
Poor man's vertical blank interrupt (VBI). Since the rules prohibit machine code, just hand around until the VCOUNT register is mostly down the screen and then update the fine scrolling register
Call a PAUSE routine to sync up the code with the vertical blank (PAUSE counts v-blanks to keep time). This prevents flicker on the bottom row and tearing of the squirrel sprite while jumping. 

LOOP
End of the game loop - go back and DO it all again.

IF K>J: J=K: ENDIF
When a squirrel hits a blue jay, the EXIT shifts execution to here. Update the high score.

SOUND 2,0,0,0
Turn off the jumping sound. The other sounds are already turned off.

GOTO 1
Restart the game without resetting the high score.

2 comments:

  1. Very nice game. Indeed, I have some comments:

    - KDIV5 is being interpreted as a constant variable with a value of zero. It should be a space after the K.

    - Instead of that VBI routine, try a "PAUSE 0". I've used it to synchronize many updates to the screen without flickering.

    I didn't know that CLS trick to clear memory, but I prefer to use "POKE M,0:MOVE M,M+1,N-1" to clear an arbitrary zone, or better, a single "GR.8:GR.0" to clear 8K of memory bellow RAMTOP, which gives 6K bellow screen memory for P/M data.

    ReplyDelete