REM ===================================================================== REM BREAKOUT, for the AKGL BASIC interpreter, using sprite artwork. REM REM Run it with the SDL build: build-akgl/basic breakout-sprites.bas REM REM Four things about this interpreter shape everything below, and they are REM worth knowing before the first line stops making sense. REM REM 1. The text layer repaints every row it owns, opaque, after the REM program's steps have run -- and by default it owns the whole window, REM so anything DRAW or BOX put on the screen would be wiped before it REM was ever seen. WINDOW moves it to two rows at the bottom, which is REM the first executable line of this program, and everything below REM then draws into a layer the frame composites and *keeps*. REM REM Nothing here is captured into a sprite. It used to be: sprites 1 REM and 2 held an SSHAPE of the HUD strip and of the whole play field, REM because a sprite was the one thing drawn from state the interpreter REM kept and a drawing lasted exactly one frame. Both slots are free REM now, and the field is drawn once per level rather than rebuilt. REM REM 2. Nothing has a drawing deadline. The host runs 256 source lines and REM then presents, and a present used to throw the drawing buffer away REM -- so a capture spanning that boundary came back half empty, and a REM PACE routine spun on TI# to start each frame's drawing at the top REM of a batch. With nothing captured, a drawing that spans two batches REM simply arrives over two frames and the layer keeps both halves. REM REM What survives from that era is worth keeping anyway: the lettering REM is turned into a list of strokes when a number changes and merely REM replayed when it is drawn. Arithmetic away from the frame is free. REM REM 3. There are eight sprites and no more. Five are the artwork, one is REM the gem, and two are spare. The bricks are still drawn rather than REM made of art -- sixty of them would not fit in eight slots however REM many are free -- but they are *collidable* without being sprites, REM because SOLID registers a rectangle and costs no slot. REM REM 4. There are 128 variables and 64 labels, and this program is close REM enough to both that a constant is spelled out rather than named REM and several routines that would read better as subroutines are REM BEGIN blocks instead. REM REM And five things in this dialect did not do what they looked like they REM did, and each one cost an evening. THREE OF THEM HAVE SINCE BEEN REM FIXED, and are marked below; the shape of this listing is still REM theirs, which is why they are still written down: REM REM * **The left operand decides whether an expression is done in REM integers or in floats.** 0 - V% throws V%'s fraction away and REM 0.0 - V% does not; LEVEL# * 0.45 is zero and 0.45 * LEVEL# is REM not. This is the dangerous one, because nothing fails -- the REM program simply computes something else. It silently quantised REM every bounce in this game to whole pixels and silently cancelled REM the per-level speed increase. Put the float first, and put the REM result somewhere with a % on it. REM * A FOR whose bounds are equal does not run its body at all, so REM every loop over a list has the one-item case written out beside REM it. REM * FIXED. A FOR inside a BEGIN block that was SKIPPED did not REM survive the RETURN of the routine holding it; the interpreter REM lost the GOSUB and reported "RETURN outside the context of REM GOSUB". Loops in this program are guarded by GOTO rather than REM wrapped in a block, and are left that way because it costs REM nothing. TODO.md section 9 item 2. REM * FIXED. SSHAPE and GSHAPE ignored the subscript on a string array, REM so every element resolved to element zero. The six brick stamps REM are six separate scalars for that reason; an array works now. REM TODO.md section 9 item 6. REM * READ walks one cursor through every DATA item in the file in the REM order they were written, no matter which routine is reading. REM REM Artwork: Kenney's Puzzle Pack 1, CC0. See art/PROVENANCE.md. REM Written by Claude Opus (1M) REM ===================================================================== REM --------------------------------------------------------------- state REM 0 title, 1 waiting to serve, 2 playing, 3 ball lost, 4 level cleared, REM 5 game over, 6 paused. STATE# = 0 RUNNING# = 1 SCORE# = 0 LIVES# = 3 LEVEL# = 1 HISCORE# = 0 STIMER# = 0 LASTT# = 0 KEYV# = 0 SNDON# = 1 REM ------------------------------------------------------------- geometry REM WALLL and WALLR are the inside faces of the side walls; BRKX and BRKY REM are the top left corner of the brick field. WALLL# = 12 WALLR# = 788 BRKX# = 42 BRKY# = 108 REM The rest of the layout is written out where it is used rather than REM named. A brick is 68 by 16 on a 72 by 24 pitch, ten columns by six REM rows; the ceiling is at 72 and a ball below 596 is gone. This program REM sits a handful of variables under the interpreter's ceiling of 128 and REM a name costs one whether it holds a constant or not. REM ---------------------------------------------------------------- paddle PDX% = 348 PDY# = 540 PDW# = 104 PDDIR# = 0 PDCO# = 0 PDEXP# = 0 CTON# = 0 CTX% = 348 CTTM# = 0 REM ----------------------------------------------------------------- balls DIM BLON#(3) DIM BLX%(3) DIM BLY%(3) DIM BLVX%(3) DIM BLVY%(3) DIM BLST#(3) BLN# = 0 BLOFF# = 41 SPD% = 6.0 REM What the balls on screen are actually travelling at, which is only the REM same as SPD% between a change and the RESCALE that answers it. RAT% is REM the ratio between the two, and it is a float because an integer one REM would hold the 0.75 of a slowdown as 0 and stop the ball dead. BSPD% = 6.0 RAT% = 1.0 SLOWT# = 0 STKON# = 0 STKT# = 0 REM ------------------------------------------------------------------ gem GMON# = 0 GMTYP# = 0 GMX% = 0 GMY% = 0 REM --------------------------------------------------------------- bricks DIM BRK#(60) DIM BRC#(6) DIM BRV#(6) BRN# = 0 REM ------------------------------------------------------- shapes and dirt REM SHN# counts the SSHAPE slots spent. There are sixteen, nothing gives REM one back, and GRAPHIC 5 gives back all of them at once -- so the brick REM stamps are thrown away and rebuilt every time the pool runs dry. REM One scalar per row colour, and scalars rather than an array because REM SSHAPE and GSHAPE used to read the leaf they were handed without REM applying its subscript -- a string array element always resolved to REM element zero, and every brick came out the same colour. That is fixed REM and an array would work now; six scalars is what this listing has. S0$ = "" S1$ = "" S2$ = "" S3$ = "" S4$ = "" S5$ = "" Z$ = "" REM 99 means the stamps are gone and have to be rebuilt before anything REM can be drawn with them. DHUD# = 1 DPLAY# = 1 BAN$ = "" BANT# = 0 REM ------------------------------------------------------------- the font ALPHA$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 :-.!" DIM FNS#(280) DIM FNI#(41) DIM FNC#(41) REM Strokes waiting to be drawn: H is the HUD strip, P the field. Each REM stroke carries the colour source it wants, so one pass over the list REM draws a line in as many colours as it likes. DIM HX1#(120) DIM HY1#(120) DIM HX2#(120) DIM HY2#(120) DIM HC#(120) HN# = 0 DIM PX1#(110) DIM PY1#(110) DIM PX2#(110) DIM PY2#(110) DIM PC#(110) PN# = 0 BUILDH# = 0 REM ------------------------------------------------- the text being built TX$ = "" TXX# = 0 TXY# = 0 TXS# = 3 TXC# = 1 TLEN# = 0 TXI# = 0 GC# = 0 GN# = 0 GP# = 0 GX# = 0 GK# = 0 P1# = 0 P2# = 0 NUM$ = "" REM ------------------------------------------------------ the paddle bounce REM Eight landing zones across the bar, each with a velocity of very REM nearly the same length, so the ball changes direction without the REM program ever needing a square root -- there is no SQR in this dialect. DIM ZVX%(8) DIM ZVY%(8) REM --------------------------------------------------------------- scratch I# = 0 K# = 0 N# = 0 R# = 0 C# = 0 D# = 0 B# = 0 T1# = 0 T2# = 0 T3# = 0 T4# = 0 BX1# = 0 BY1# = 0 LFT# = 0 TOP# = 0 RGT# = 0 BOT# = 0 CC1# = 0 CC2# = 0 RR1# = 0 RR2# = 0 HIT# = 0 SEED# = 1 RNMAX# = 2 RNVAL# = 0 REM The collision handler answers through these, so they are created out here REM like everything else: a name first seen inside a GOSUB dies at its RETURN. MB# = 0 AX# = 0 DP% = 0.0 SLX# = 0 SLY# = 0 SLI# = 0 REM The two eraser stamps. Declared here for exactly the same reason -- built REM inside DRAWPROTOS and left undeclared, they were SHAPE:6 and SHAPE:7 inside REM it and empty everywhere else. BL$ = "" HBL$ = "" REM ===================================================================== REM Start up REM ===================================================================== REM Tables before font, and the order is not a preference: READ walks one REM cursor through every DATA item in the program in the order they appear REM in the file, so whichever loader runs first gets the DATA that is REM written first. The tables are written first. REM The text layer repaints every row it owns, opaque, so it has to be moved REM out of the way before anything drawn can be seen. Two rows at the bottom is REM enough for the final score, and hands the other thirty-five to the drawing REM verbs. Everything this game draws then simply stays there -- a drawing goes REM into a layer the frame composites, so nothing here is captured into a sprite REM and nothing is redrawn every frame. WINDOW 0, 35, 49, 36 GOSUB LOADTABLES GOSUB LOADFONT SEED# = MOD(1 + TI# * 7919, 2147483648) RNMAX# = 97 GOSUB NEXTRAND SPRSAV "art/paddleBlu.png", 3 SPRSAV "art/paddleRed.png", 4 SPRSAV "art/ballBlue.png", 5 SPRSAV "art/ballGrey.png", 6 SPRSAV "art/ballGrey.png", 7 SPRSAV "art/element_red_polygon_glossy.png", 8 SPRITE 3, 0, 2 SPRITE 4, 0, 2 SPRITE 5, 0, 2 SPRITE 6, 0, 2 SPRITE 7, 0, 2 SPRITE 8, 0, 2 REM SOUND's frequency argument is a SID register value rather than hertz -- REM register * 1022730 / 16777216 is the pitch -- so the numbers below are REM notes worked out once and written down: 17175 is C6, 12861 G5, 8579 C5, REM 6431 G4, 4298 C4, 3609 A3, 2411 D3, 1804 A2. REM REM Three voices. Voice 1 is the ball hitting things, voice 2 is the gem REM and the ball being lost, voice 3 is PLAY's fanfares -- kept apart so a REM brick going does not cut a tune off mid-note. VOL 8 ENVELOPE 0, 0, 6, 0, 4 TEMPO 12 COLLISION 2, BRICKHIT REM The stamps, once. They used to be rebuilt whenever the SSHAPE pool ran dry, REM because every frame's capture spent another slot; nothing captures now, so REM eight slots are spent here and never again. GOSUB DRAWPROTOS GOSUB TITLESCREEN REM ===================================================================== REM The frame loop REM REM PACE first, because it is what puts the drawing at the top of a host REM batch; then at most one capture, because a capture is the only thing REM with a deadline; then the game, which may take as long as it likes. REM ===================================================================== REM No pacing. PACE spun on TI# until a batch boundary so that an SSHAPE REM capture would not be cut in half by a present; nothing is captured now, so REM a drawing that spans two batches simply arrives over two frames and the REM layer keeps both halves. LABEL FRAME GOSUB DRAWJOB GOSUB READKEYS IF STATE# = 0 THEN GOSUB TITLETICK IF STATE# = 1 THEN GOSUB SERVETICK IF STATE# = 2 THEN GOSUB PLAYTICK IF STATE# = 3 THEN GOSUB LOSTTICK IF STATE# = 4 THEN GOSUB CLEARTICK IF RUNNING# = 0 THEN GOTO SHUTDOWN GOTO FRAME LABEL SHUTDOWN FOR I# = 1 TO 8 SPRITE I#, 0 NEXT I# IF SCORE# > HISCORE# THEN HISCORE# = SCORE# PRINT "FINAL SCORE " + SCORE# + " BEST " + HISCORE# END REM ===================================================================== REM One capture per frame, and only one REM REM Rebuilding the brick stamps comes first, because everything else REM draws with them and GRAPHIC 5 has just thrown them away. REM ===================================================================== REM Labels rather than BEGIN blocks here. When this was written it was not REM a style choice -- a loop inside a block that was skipped left the REM interpreter with no GOSUB to return from and stopped the program. That REM is fixed; the shape is kept because it reads the same either way. REM The stamps are built once now rather than counted and rebuilt. Nothing REM captures per frame any more, so the sixteen SSHAPE slots are spent once and REM stay spent -- eight of them -- and GRAPHIC 5 is never called. LABEL DRAWJOB LABEL DRAWJOB2 IF DPLAY# = 0 THEN GOTO DRAWJOB3 GOSUB DRAWPLAY DPLAY# = 0 RETURN LABEL DRAWJOB3 IF DHUD# = 0 THEN RETURN GOSUB DRAWHUD DHUD# = 0 RETURN REM --------------------------------------------------------------------- REM Six brick stamps, one per row colour. Filled two rules at a time so REM the set costs about a hundred and thirty lines rather than the two REM hundred and seventy a rule-at-a-time loop would take. PAINT would be REM one statement instead of sixteen and is not an option: it costs nearly REM four milliseconds a call, which is an eighth of a frame. LABEL DRAWPROTOS GRAPHIC 1, 1 WIDTH 1 FOR R# = 0 TO 5 COLOR 1, BRC#(R#) T1# = R# * 20 T2# = T1# + 8 FOR K# = 0 TO 7 DRAW 1, 0, T1# + K# TO 67, T1# + K# : DRAW 1, 0, T2# + K# TO 67, T2# + K# NEXT K# NEXT R# REM A seventh stamp, in the background colour, is how a broken brick is taken REM off the screen. There is no filled-rectangle verb and PAINT costs four REM milliseconds a call, so erasing is a GSHAPE of something blank -- one line REM instead of sixteen, and it reuses machinery that is already here. COLOR 1, 1 FOR K# = 0 TO 15 DRAW 1, 0, 130 + K# TO 67, 130 + K# NEXT K# REM And an eighth the width of the HUD strip, for the same reason: the strip is REM rewritten whenever a number in it changes, and the old digits have to go REM somewhere first. FOR K# = 0 TO 59 DRAW 1, 0, 160 + K# TO 799, 160 + K# NEXT K# SSHAPE Z$, 0, 0, 68, 16 : S0$ = Z$ SSHAPE Z$, 0, 20, 68, 36 : S1$ = Z$ SSHAPE Z$, 0, 40, 68, 56 : S2$ = Z$ SSHAPE Z$, 0, 60, 68, 76 : S3$ = Z$ SSHAPE Z$, 0, 80, 68, 96 : S4$ = Z$ SSHAPE Z$, 0, 100, 68, 116 : S5$ = Z$ SSHAPE Z$, 0, 130, 68, 146 : BL$ = Z$ SSHAPE Z$, 0, 160, 800, 220 : HBL$ = Z$ GRAPHIC 1, 1 DPLAY# = 1 DHUD# = 1 RETURN REM Take one brick off the screen: stamp the blank over it. Called when a brick REM breaks, so the field is never redrawn as a whole during play -- which is REM what lets the whole live-list machinery go. LABEL ERASEBRICK Z$ = BL$ GSHAPE Z$, BRKX# + C# * 72, BRKY# + R# * 24 RETURN REM --------------------------------------------------------------------- REM The field: walls, whatever bricks are still standing, and whatever REM lettering belongs on the field. Nothing here computes anything it REM could have computed on an earlier frame. LABEL DRAWPLAY GRAPHIC 1, 1 WIDTH 2 COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5 COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6 BOX 5, 2, 62, 797, 597 : BOX 1, 6, 66, 793, 593 REM Every brick still standing, stamped where it belongs. This runs when a REM level is laid out and not again -- a broken brick is erased in place by REM ERASEBRICK, so there is no reason to redraw sixty of them to remove one. REM That is what took away the flattened live list, its four arrays, and the REM run-per-row loop that walked it. FOR R# = 0 TO 5 IF R# = 0 THEN Z$ = S0$ IF R# = 1 THEN Z$ = S1$ IF R# = 2 THEN Z$ = S2$ IF R# = 3 THEN Z$ = S3$ IF R# = 4 THEN Z$ = S4$ IF R# = 5 THEN Z$ = S5$ FOR C# = 0 TO 9 IF BRK#(R# * 10 + C#) > 0 THEN GSHAPE Z$, BRKX# + C# * 72, BRKY# + R# * 24 NEXT C# NEXT R# IF PN# = 1 THEN DRAW PC#(0), PX1#(0), PY1#(0) TO PX2#(0), PY2#(0) IF PN# < 2 THEN GOTO PLDONE FOR I# = 0 TO PN# - 1 DRAW PC#(I#), PX1#(I#), PY1#(I#) TO PX2#(I#), PY2#(I#) NEXT I# LABEL PLDONE RETURN REM --------------------------------------------------------------------- REM The HUD strip. Same shape as DRAWPLAY: one pass over a stroke list REM that was built on some earlier frame, when there was time. LABEL DRAWHUD GRAPHIC 1, 0 WIDTH 1 COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5 COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6 REM The old strip goes first. Nothing here clears the screen -- a drawing REM stays, which is the whole point -- so the digits that were there have to be REM stamped over before the new ones are drawn. Z$ = HBL$ GSHAPE Z$, 0, 0 BOX 5, 0, 56, 799, 57 IF HN# = 1 THEN DRAW HC#(0), HX1#(0), HY1#(0) TO HX2#(0), HY2#(0) IF HN# < 2 THEN GOTO HUDONE FOR I# = 0 TO HN# - 1 DRAW HC#(I#), HX1#(I#), HY1#(I#) TO HX2#(I#), HY2#(I#) NEXT I# LABEL HUDONE RETURN REM ===================================================================== REM Input REM REM GET takes one keystroke per call from a ring the host fills, so the REM loop empties it every frame. A held arrow arrives as about twenty six REM repeats a second, which is nearly one a frame -- but the first repeat REM is a quarter of a second behind the press, and a paddle that stalled REM for a quarter of a second would be unusable. So a press buys seven REM frames of travel (PDCO#) and a held key keeps renewing it. REM ===================================================================== LABEL READKEYS GET KEYV# IF KEYV# = 0 THEN RETURN IF KEYV# = 1073741904 THEN PDDIR# = 0 - 1 : PDCO# = 7 IF KEYV# = 1073741903 THEN PDDIR# = 1 : PDCO# = 7 IF KEYV# = 32 THEN GOSUB PRESSFIRE IF KEYV# = 112 THEN GOSUB PRESSPAUSE IF KEYV# = 115 THEN GOSUB PRESSMUTE IF KEYV# = 113 THEN RUNNING# = 0 IF KEYV# = 27 THEN RUNNING# = 0 GOTO READKEYS LABEL PRESSFIRE IF STATE# = 0 THEN GOSUB STARTGAME : RETURN IF STATE# = 5 THEN GOSUB TITLESCREEN : RETURN IF STATE# = 1 THEN GOSUB LAUNCH : RETURN IF STATE# = 2 THEN GOSUB UNSTICK RETURN REM VOL rather than a test around every SOUND: one line here beats eleven REM scattered through the game, and a silenced voice costs nothing to issue. LABEL PRESSMUTE SNDON# = 1 - SNDON# IF SNDON# = 1 THEN VOL 8 IF SNDON# = 0 THEN VOL 0 RETURN LABEL PRESSPAUSE IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED" : GOSUB SETBANNER : RETURN IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER RETURN REM ===================================================================== REM The states REM ===================================================================== LABEL TITLETICK STIMER# = STIMER# + 1 RETURN LABEL SERVETICK GOSUB MOVEPADDLE BLX%(0) = PDX% + BLOFF# BLY%(0) = PDY# - 23 MOVSPR 5, BLX%(0), BLY%(0) RETURN LABEL PLAYTICK GOSUB MOVEPADDLE GOSUB MOVEBALLS GOSUB MOVEGEM GOSUB TIMERS IF BRN# = 0 THEN GOSUB LEVELDONE IF BLN# = 0 AND STATE# = 2 THEN GOSUB BALLGONE RETURN LABEL LOSTTICK STIMER# = STIMER# - 1 IF STIMER# > 0 THEN RETURN IF LIVES# > 0 THEN GOSUB SERVEAGAIN IF LIVES# < 1 THEN GOSUB GAMEISOVER RETURN LABEL CLEARTICK STIMER# = STIMER# - 1 IF STIMER# > 0 THEN RETURN LEVEL# = LEVEL# + 1 GOSUB SETUPLEVEL RETURN REM ===================================================================== REM Paddle REM ===================================================================== LABEL MOVEPADDLE IF PDCO# > 0 THEN BEGIN PDCO# = PDCO# - 1 PDX% = PDX% + PDDIR# * 9 BEND IF PDCO# < 1 THEN PDDIR# = 0 IF PDX% < WALLL# THEN PDX% = WALLL# IF PDX% > WALLR# - PDW# THEN PDX% = WALLR# - PDW# MOVSPR 3, PDX%, PDY# IF CTON# = 0 THEN RETURN REM The catcher mirrors the player, so it guards the side the paddle left. CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104 MOVSPR 4, CTX%, 470 RETURN REM ===================================================================== REM Balls REM ===================================================================== LABEL MOVEBALLS FOR B# = 0 TO 2 IF BLON#(B#) = 1 THEN GOSUB MOVEONE NEXT B# RETURN LABEL MOVEONE IF BLST#(B#) = 1 THEN BEGIN BLX%(B#) = PDX% + BLOFF# BLY%(B#) = PDY# - 23 BEND IF BLST#(B#) = 0 THEN BEGIN BLX%(B#) = BLX%(B#) + BLVX%(B#) BLY%(B#) = BLY%(B#) + BLVY%(B#) GOSUB BALLWALLS BEND IF BLON#(B#) = 0 THEN RETURN IF BLST#(B#) = 0 THEN BEGIN GOSUB BALLPADDLE BEND MOVSPR 5 + B#, BLX%(B#), BLY%(B#) RETURN LABEL BALLWALLS IF BLX%(B#) < WALLL# THEN BEGIN BLX%(B#) = WALLL# BLVX%(B#) = 0.0 - BLVX%(B#) SOUND 1, 3609, 2 BEND IF BLX%(B#) > WALLR# - 22 THEN BEGIN BLX%(B#) = WALLR# - 22 BLVX%(B#) = 0.0 - BLVX%(B#) SOUND 1, 3609, 2 BEND IF BLY%(B#) < 72 THEN BEGIN BLY%(B#) = 72 BLVY%(B#) = 0.0 - BLVY%(B#) SOUND 1, 3609, 2 BEND IF BLY%(B#) < 596 THEN RETURN BLON#(B#) = 0 BLN# = BLN# - 1 SOUND 2, 2411, 6 SPRITE 5 + B#, 0 RETURN REM --------------------------------------------------------------------- REM Brick collision, all of it. REM REM COLLISION 2 fires between source lines when a ball overlaps one of the REM rectangles SOLID registered, and RCOLLISION says which and which way out. REM That replaced a bounding-box narrowing that turned the ball's box into a REM range of candidate cells, a nested DO walking them, and a cell test that REM computed an overlap rectangle and compared its width to its height to pick REM an axis -- about sixty lines. REM REM The handler cannot know which ball hit, so it asks each live one. Three is REM not a loop worth writing. REM The mask is by *sprite*, and the balls are sprites 5, 6 and 7 -- so their REM bits are 16, 32 and 64, not 1, 2 and 4. Ball B# is sprite 5 + B#, which is REM the same arithmetic MOVSPR and RCOLLISION use for them everywhere else. LABEL BRICKHIT MB# = BUMP(2) IF MB# = 0 THEN RETURN B# = 0 IF (MB# AND 16) <> 0 THEN GOSUB ONEBRICK B# = 1 IF (MB# AND 32) <> 0 THEN GOSUB ONEBRICK B# = 2 IF (MB# AND 64) <> 0 THEN GOSUB ONEBRICK RETURN REM One ball against whatever it hit. B# is which ball; sprite 5 + B# is its REM slot, and RCOLLISION is keyed on the sprite number. LABEL ONEBRICK IF BLON#(B#) = 0 THEN RETURN N# = RCOLLISION(5 + B#, 1) IF N# < 1 THEN RETURN N# = N# - 1 IF BRK#(N#) = 0 THEN RETURN DP% = RCOLLISION(5 + B#, 4) BLX%(B#) = BLX%(B#) + (RCOLLISION(5 + B#, 2) * DP%) BLY%(B#) = BLY%(B#) + (RCOLLISION(5 + B#, 3) * DP%) AX# = RCOLLISION(5 + B#, 7) IF AX# = 1 THEN BLVX%(B#) = 0.0 - BLVX%(B#) IF AX# = 2 THEN BLVY%(B#) = 0.0 - BLVY%(B#) R# = N# / 10 C# = MOD(N#, 10) BRK#(N#) = 0 SOLID N# + 1 BRN# = BRN# - 1 SCORE# = SCORE# + BRV#(R#) REM The top row rings at C6 and each row down drops about a third, so the REM wall plays itself down a scale as it comes apart. SOUND 1, 17175 - R# * 2100, 4 BX1# = BRKX# + C# * 72 BY1# = BRKY# + R# * 24 GOSUB ERASEBRICK GOSUB HUDTEXT IF GMON# = 1 THEN RETURN RNMAX# = 7 GOSUB NEXTRAND IF RNVAL# = 0 THEN GOSUB SPAWNGEM RETURN REM --------------------------------------------------------------------- REM The bars. T1# is the bar's left edge, T2# its top, T3# its width. LABEL BALLPADDLE IF BLVY%(B#) < 0 THEN RETURN HIT# = 0 T1# = PDX% : T2# = PDY# : T3# = PDW# GOSUB HITBAR IF CTON# = 0 THEN RETURN IF HIT# = 1 THEN RETURN T1# = CTX% : T2# = 470 : T3# = 104 GOSUB HITBAR RETURN LABEL HITBAR IF BLY%(B#) + 21 < T2# THEN RETURN IF BLY%(B#) > T2# + 23 THEN RETURN IF BLX%(B#) + 21 < T1# THEN RETURN IF BLX%(B#) > T1# + T3# THEN RETURN T4# = (BLX%(B#) + 11 - T1#) * 8 / T3# IF T4# < 0 THEN T4# = 0 IF T4# > 7 THEN T4# = 7 BLVX%(B#) = ZVX%(T4#) * SPD% BLVY%(B#) = ZVY%(T4#) * SPD% BLY%(B#) = T2# - 23 SOUND 1, 4298, 5 HIT# = 1 IF STKON# = 0 THEN RETURN BLST#(B#) = 1 BLOFF# = BLX%(B#) - PDX% RETURN LABEL UNSTICK FOR B# = 0 TO 2 IF BLST#(B#) = 1 THEN BEGIN BLST#(B#) = 0 BLVX%(B#) = ZVX%(4) * SPD% BLVY%(B#) = ZVY%(4) * SPD% BEND NEXT B# RETURN REM ===================================================================== REM The gem, and what catching one does REM ===================================================================== LABEL SPAWNGEM RNMAX# = 5 GOSUB NEXTRAND GMTYP# = RNVAL# + 1 GMX% = BX1# + 10 GMY% = BY1# IF GMTYP# = 1 THEN SPRSAV "art/element_red_polygon_glossy.png", 8 IF GMTYP# = 2 THEN SPRSAV "art/element_yellow_polygon_glossy.png", 8 IF GMTYP# = 3 THEN SPRSAV "art/element_green_polygon_glossy.png", 8 IF GMTYP# = 4 THEN SPRSAV "art/element_blue_polygon_glossy.png", 8 IF GMTYP# = 5 THEN SPRSAV "art/element_purple_polygon_glossy.png", 8 GMON# = 1 SOUND 2, 10810, 3 SPRITE 8, 1, 2 MOVSPR 8, GMX%, GMY% RETURN LABEL MOVEGEM IF GMON# = 0 THEN RETURN GMY% = GMY% + 3.2 MOVSPR 8, GMX%, GMY% HIT# = 0 IF GMY% > 596 THEN HIT# = 1 IF GMY% + 46 > PDY# AND GMY% < PDY# + 24 THEN GOSUB CATCHGEM IF HIT# = 0 THEN RETURN GMON# = 0 SPRITE 8, 0 RETURN LABEL CATCHGEM IF GMX% + 48 < PDX% THEN RETURN IF GMX% > PDX% + PDW# THEN RETURN GOSUB TAKEGEM HIT# = 1 RETURN REM The x-expand bit is the only scaling a sprite has, and doubling the REM artwork is exactly what EXPAND wants. LABEL TAKEGEM SOUND 2, 8579, 14, 1, 17175, 400 SCORE# = SCORE# + 50 GOSUB HUDTEXT IF GMTYP# = 1 THEN BEGIN PDW# = 208 PDEXP# = 600 SPRITE 3, 1, 2, 0, 1, 0 BAN$ = "EXPAND" BEND IF GMTYP# = 2 THEN BEGIN BAN$ = "MULTI" GOSUB SPLITBALLS BEND IF GMTYP# = 3 THEN BEGIN SLOWT# = 480 SPD% = 4.2 GOSUB RESCALE BAN$ = "SLOW" BEND IF GMTYP# = 4 THEN BEGIN STKON# = 1 STKT# = 540 BAN$ = "STICKY" BEND IF GMTYP# = 5 THEN BEGIN CTON# = 1 CTTM# = 720 SPRITE 4, 1, 2 BAN$ = "CATCH" BEND BANT# = 90 GOSUB SETBANNER RETURN REM Two more balls, thrown off the first one at slightly different angles REM so they do not travel as a stack. LABEL SPLITBALLS IF BLN# > 2 THEN RETURN IF BLON#(0) = 0 THEN RETURN FOR B# = 1 TO 2 IF BLON#(B#) = 0 THEN BEGIN BLON#(B#) = 1 BLST#(B#) = 0 BLX%(B#) = BLX%(0) BLY%(B#) = BLY%(0) BLVX%(B#) = ZVX%(B# * 5 - 4) * SPD% BLVY%(B#) = ZVY%(B# * 5 - 4) * SPD% BLN# = BLN# + 1 SPRITE 5 + B#, 1, 2 BEND NEXT B# RETURN REM Bring every ball in play to the current speed without changing where it REM is heading. REM REM Every velocity this program assigns is one of the eight unit vectors in REM ZVX%/ZVY# times SPD%, and a bounce only ever flips a sign -- so a ball REM is travelling at exactly the SPD% that was in force when it last left a REM bar. BSPD% remembers what that was, which makes the rescale a ratio of REM two speeds rather than a change of magnitude, and no square root is REM needed. There isn't one in this dialect. REM REM Scaling by the vertical component instead, which is what this routine REM did first, is not a slowdown at all: a shallow ball's small vertical REM gets stretched up to the new speed and drags the large horizontal with REM it. SLOW made the ball faster. Sixty degrees of the eight zones are REM shallow enough to do it. LABEL RESCALE IF BSPD% < 0.1 THEN BSPD% = SPD% RAT% = SPD% / BSPD% BSPD% = SPD% FOR B# = 0 TO 2 IF BLON#(B#) = 1 THEN BEGIN BLVX%(B#) = BLVX%(B#) * RAT% BLVY%(B#) = BLVY%(B#) * RAT% BEND NEXT B# RETURN REM ===================================================================== REM Timers REM ===================================================================== LABEL TIMERS IF BANT# > 0 THEN BEGIN BANT# = BANT# - 1 IF BANT# = 0 THEN BEGIN BAN$ = "" GOSUB SETBANNER BEND BEND IF PDEXP# > 0 THEN BEGIN PDEXP# = PDEXP# - 1 IF PDEXP# = 0 THEN BEGIN PDW# = 104 SPRITE 3, 1, 2, 0, 0, 0 BEND BEND IF SLOWT# > 0 THEN BEGIN SLOWT# = SLOWT# - 1 IF SLOWT# = 0 THEN BEGIN GOSUB LEVELSPEED GOSUB RESCALE BEND BEND IF STKT# > 0 THEN BEGIN STKT# = STKT# - 1 IF STKT# = 0 THEN BEGIN STKON# = 0 GOSUB UNSTICK BEND BEND IF CTTM# > 0 THEN BEGIN CTTM# = CTTM# - 1 IF CTTM# = 0 THEN BEGIN CTON# = 0 SPRITE 4, 0 BEND BEND RETURN REM ===================================================================== REM Losing a ball, clearing a level, starting and ending a game REM ===================================================================== LABEL BALLGONE SOUND 2, 7218, 30, 2, 1804, 220 LIVES# = LIVES# - 1 STATE# = 3 STIMER# = 45 GMTYP# = 0 BAN$ = "MISS" GOSUB SETBANNER GOSUB HUDTEXT RETURN LABEL SERVEAGAIN BAN$ = "" GOSUB SETBANNER GOSUB CLEARPOWERS GOSUB RESETBALL STATE# = 1 RETURN LABEL GAMEISOVER PLAY "V3 T1 U8 O4 QG QE QC O3 HG" IF SCORE# > HISCORE# THEN HISCORE# = SCORE# STATE# = 5 GOSUB CLEARPOWERS BRN# = 0 FOR I# = 0 TO 59 BRK#(I#) = 0 NEXT I# SOLID SPRITE 3, 0 SPRITE 5, 0 PN# = 0 TXS# = 9 : TXC# = 4 : TXX# = 148 : TXY# = 230 TX$ = "GAME OVER" BUILDH# = 0 GOSUB BUILDTEXT TXS# = 5 : TXC# = 2 : TXX# = 200 : TXY# = 380 T4# = SCORE# GOSUB FMTNUM TX$ = "SCORE " + NUM$ BUILDH# = 0 GOSUB BUILDTEXT HN# = 0 TXS# = 3 : TXC# = 5 : TXY# = 20 : TXX# = 40 T4# = HISCORE# GOSUB FMTNUM TX$ = "BEST " + NUM$ BUILDH# = 1 GOSUB BUILDTEXT TXC# = 6 : TXX# = 400 TX$ = "SPACE FOR TITLE" BUILDH# = 1 GOSUB BUILDTEXT DPLAY# = 1 DHUD# = 1 RETURN LABEL LEVELDONE PLAY "V3 T2 U8 O4 QC QE QG O5 HC" STATE# = 4 STIMER# = 60 GMTYP# = 0 BAN$ = "CLEAR" GOSUB SETBANNER RETURN LABEL TITLESCREEN STATE# = 0 STIMER# = 0 GOSUB CLEARPOWERS BLN# = 0 BRN# = 0 FOR I# = 0 TO 59 BRK#(I#) = 0 NEXT I# SOLID SPRITE 3, 0 SPRITE 5, 0 SPRITE 6, 0 SPRITE 7, 0 PN# = 0 TXS# = 12 : TXC# = 2 : TXX# = 120 : TXY# = 170 TX$ = "BREAKOUT" BUILDH# = 0 GOSUB BUILDTEXT TXS# = 4 : TXC# = 1 : TXX# = 220 : TXY# = 370 TX$ = "SPACE TO START" BUILDH# = 0 GOSUB BUILDTEXT HN# = 0 TXS# = 3 : TXC# = 6 : TXX# = 40 : TXY# = 20 TX$ = "ARROWS MOVE P PAUSE Q QUIT" BUILDH# = 1 GOSUB BUILDTEXT DPLAY# = 1 DHUD# = 1 RETURN LABEL STARTGAME PLAY "V3 T2 U8 O4 IC IE IG O5 IC" SCORE# = 0 LIVES# = 3 LEVEL# = 1 GOSUB SETUPLEVEL RETURN REM --------------------------------------------------------------------- REM Lay a level out. Every third one is a row shorter, so the field REM changes shape as well as pace. LABEL SETUPLEVEL GOSUB CLEARPOWERS GOSUB LEVELSPEED T3# = 6 IF MOD(LEVEL#, 3) = 2 THEN T3# = 5 IF MOD(LEVEL#, 3) = 0 THEN T3# = 4 BRN# = 0 FOR I# = 0 TO 59 BRK#(I#) = 0 NEXT I# REM Retire the previous level's rectangles *before* laying this one out, not REM after. This is where GOSUB BUILDLIVE used to sit, and it meant something REM different: rebuild the list of what is standing. Left in place, the clear REM ran after FILLROW had registered all sixty and threw them all away again -- REM which cost nothing visible and simply stopped the ball hitting anything. SOLID FOR R# = 0 TO 5 IF R# < T3# THEN GOSUB FILLROW NEXT R# GOSUB RESETBALL GOSUB HUDTEXT GMTYP# = 0 BAN$ = "" GOSUB SETBANNER STATE# = 1 DPLAY# = 1 RETURN REM Again a subroutine rather than a block, for the same reason. REM One row of bricks: the array, and the rectangle the interpreter collides REM against. The id is the array index plus one, so RCOLLISION hands back a REM number that indexes BRK#() with no lookup at all. LABEL FILLROW FOR C# = 0 TO 9 BRK#(R# * 10 + C#) = 1 BRN# = BRN# + 1 SLX# = BRKX# + C# * 72 SLY# = BRKY# + R# * 24 SLI# = (R# * 10 + C#) + 1 SOLID SLI#, SLX#, SLY#, SLX# + 68, SLY# + 16 NEXT C# RETURN LABEL LEVELSPEED SPD% = 5.6 + 0.45 * LEVEL# IF SPD% > 11.0 THEN SPD% = 11.0 RETURN LABEL RESETBALL FOR B# = 0 TO 2 BLON#(B#) = 0 BLST#(B#) = 0 NEXT B# SPRITE 6, 0 SPRITE 7, 0 BLON#(0) = 1 BLN# = 1 BSPD% = SPD% PDX% = 348 PDW# = 104 BLOFF# = 41 SPRITE 3, 1, 2, 0, 0, 0 SPRITE 5, 1, 2 MOVSPR 3, PDX%, PDY# BLX%(0) = PDX% + BLOFF# BLY%(0) = PDY# - 23 MOVSPR 5, BLX%(0), BLY%(0) RETURN LABEL LAUNCH SOUND 1, 6431, 3 STATE# = 2 BLST#(0) = 0 BLVX%(0) = ZVX%(5) * SPD% BLVY%(0) = ZVY%(5) * SPD% RETURN LABEL CLEARPOWERS GOSUB LEVELSPEED PDW# = 104 PDEXP# = 0 SLOWT# = 0 STKON# = 0 STKT# = 0 CTON# = 0 CTTM# = 0 GMON# = 0 BANT# = 0 BLOFF# = 41 SPRITE 4, 0 SPRITE 8, 0 SPRITE 3, 1, 2, 0, 0, 0 RETURN REM ===================================================================== REM Text REM REM The HUD line, rebuilt whenever a number in it changes. Four colours: REM labels in cyan, the score in yellow, the lives in light green and the REM level in light red. Colour is the whole reason the HUD is drawn REM rather than printed -- the interpreter's text layer is white, and it REM has no verb that changes that. REM ===================================================================== LABEL HUDTEXT HN# = 0 BUILDH# = 1 TXS# = 4 TXY# = 14 TXC# = 1 : TXX# = 24 TX$ = "SCORE" GOSUB BUILDTEXT TXC# = 2 : TXX# = 144 T4# = SCORE# GOSUB FMTNUM TX$ = NUM$ GOSUB BUILDTEXT TXC# = 1 : TXX# = 316 TX$ = "LIVES" GOSUB BUILDTEXT TXC# = 6 : TXX# = 436 TX$ = "" + LIVES# GOSUB BUILDTEXT TXC# = 1 : TXX# = 500 TX$ = "LEVEL" GOSUB BUILDTEXT TXC# = 4 : TXX# = 620 TX$ = "" + LEVEL# GOSUB BUILDTEXT DHUD# = 1 RETURN REM The banner over the field: whatever BAN$ says, centred, in the colour REM of the gem that put it there. LABEL SETBANNER PN# = 0 DPLAY# = 1 IF BAN$ = "" THEN RETURN BUILDH# = 0 TXS# = 8 TXY# = 430 TXX# = 400 - LEN(BAN$) * 20 TXC# = 5 IF GMTYP# = 1 THEN TXC# = 4 IF GMTYP# = 2 THEN TXC# = 2 IF GMTYP# = 3 THEN TXC# = 6 IF GMTYP# = 4 THEN TXC# = 1 IF GMTYP# = 5 THEN TXC# = 3 REM Sources 1 to 6 are cyan, yellow, purple, light red, light grey and REM green; the five gems are red, yellow, green, blue and purple, so the REM banner is as close to the gem as seven colour sources allow. TX$ = BAN$ GOSUB BUILDTEXT RETURN REM --------------------------------------------------------------------- REM Turn TX$ into strokes, into the HUD list when BUILDH# is 1 and the REM field list when it is 0. This is the expensive routine in the program, REM about sixteen lines a character, and it is deliberately nowhere near a REM capture: it runs when a value changes, and the drawing that replays REM its output happens on some later frame. LABEL BUILDTEXT TLEN# = LEN(TX$) IF TLEN# = 0 THEN RETURN TXI# = 0 DO GC# = INSTR(ALPHA$, MID(TX$, TXI#, 1)) IF GC# > 0 - 1 THEN GOSUB BUILDGLYPH TXI# = TXI# + 1 LOOP UNTIL TXI# >= TLEN# RETURN LABEL BUILDGLYPH GN# = FNC#(GC#) IF GN# = 0 THEN RETURN GP# = FNI#(GC#) GX# = TXX# + TXI# * TXS# * 5 GK# = 0 DO P1# = FNS#(GP#) : P2# = FNS#(GP# + 1) : GP# = GP# + 2 IF BUILDH# = 1 AND HN# < 120 THEN BEGIN HX1#(HN#) = GX# + (P1# / 10) * TXS# HY1#(HN#) = TXY# + MOD(P1#, 10) * TXS# HX2#(HN#) = GX# + (P2# / 10) * TXS# HY2#(HN#) = TXY# + MOD(P2#, 10) * TXS# HC#(HN#) = TXC# HN# = HN# + 1 BEND IF BUILDH# = 0 AND PN# < 110 THEN BEGIN PX1#(PN#) = GX# + (P1# / 10) * TXS# PY1#(PN#) = TXY# + MOD(P1#, 10) * TXS# PX2#(PN#) = GX# + (P2# / 10) * TXS# PY2#(PN#) = TXY# + MOD(P2#, 10) * TXS# PC#(PN#) = TXC# PN# = PN# + 1 BEND GK# = GK# + 1 LOOP UNTIL GK# >= GN# RETURN REM T4# as six digits with leading zeros, into NUM$. LABEL FMTNUM NUM$ = "" T2# = T4# FOR I# = 1 TO 6 D# = MOD(T2#, 10) NUM$ = MID("0123456789", D#, 1) + NUM$ T2# = T2# / 10 NEXT I# RETURN REM ===================================================================== REM A generator, because this dialect has no RND REM REM The usual constants, kept inside 2^31 so the multiply stays inside a REM 64 bit integer, and the answer taken from the middle bits because the REM low ones of a power-of-two modulus barely move. A GOSUB rather than a REM DEF on purpose: a multi-line DEF that assigns to a global spends a REM value-pool slot on every call and never gives it back, which empties REM the pool after a few thousand throws. REM ===================================================================== LABEL NEXTRAND SEED# = MOD(SEED# * 1103515245 + 12345, 2147483648) RNVAL# = MOD(SHR(SEED#, 16), RNMAX#) RETURN REM ===================================================================== REM Tables REM ===================================================================== LABEL LOADTABLES FOR I# = 0 TO 5 READ BRC#(I#) NEXT I# FOR I# = 0 TO 5 READ BRV#(I#) NEXT I# FOR I# = 0 TO 7 READ ZVX%(I#) NEXT I# FOR I# = 0 TO 7 READ ZVY%(I#) NEXT I# RETURN REM Row colours, top to bottom, and what a brick in that row is worth. DATA 3, 9, 8, 6, 4, 5 DATA 60, 50, 40, 30, 20, 10 REM The eight landing zones. Each pair is very nearly a unit vector, so REM the ball leaves a bar at the speed it arrived and only the angle REM changes. DATA -0.85, -0.62, -0.40, -0.18, 0.18, 0.40, 0.62, 0.85 DATA -0.53, -0.78, -0.92, -0.98, -0.98, -0.92, -0.78, -0.53 REM ===================================================================== REM The stroke font REM REM One glyph per DATA line: how many strokes, then that many pairs of REM points. A point is coded X*10+Y on a grid four wide and seven tall, REM so 0 is the top left corner, 30 the top right and 36 the bottom REM right. INSTR into ALPHA$ is what turns a character into a glyph REM number, which is why the order of these lines matters. REM ===================================================================== LABEL LOADFONT GX# = 0 FOR I# = 0 TO 40 READ N# FNC#(I#) = N# FNI#(I#) = GX# IF N# > 0 THEN GOSUB READGLYPH NEXT I# RETURN REM Its own subroutine rather than a BEGIN block: a FOR nested inside a REM block that is itself inside a FOR does not find its own NEXT. LABEL READGLYPH FOR K# = 1 TO N# * 2 READ D# FNS#(GX#) = D# GX# = GX# + 1 NEXT K# RETURN REM A DATA 4, 0,30, 0,6, 30,36, 3,33 REM B DATA 6, 0,6, 0,30, 3,33, 6,36, 30,33, 33,36 REM C DATA 3, 0,30, 0,6, 6,36 REM D DATA 6, 0,6, 0,20, 20,31, 31,35, 35,26, 26,6 REM E DATA 4, 0,6, 0,30, 3,33, 6,36 REM F DATA 3, 0,6, 0,30, 3,33 REM G DATA 5, 0,30, 0,6, 6,36, 33,36, 13,33 REM H DATA 3, 0,6, 30,36, 3,33 REM I DATA 1, 10,16 REM J DATA 3, 30,36, 6,36, 3,6 REM K DATA 3, 0,6, 30,3, 3,36 REM L DATA 2, 0,6, 6,36 REM M DATA 4, 0,6, 30,36, 0,13, 13,30 REM N DATA 3, 0,6, 30,36, 0,36 REM O DATA 4, 0,30, 6,36, 0,6, 30,36 REM P DATA 4, 0,6, 0,30, 3,33, 30,33 REM Q DATA 5, 0,30, 6,36, 0,6, 30,36, 24,36 REM R DATA 5, 0,6, 0,30, 3,33, 30,33, 13,36 REM S DATA 5, 0,30, 0,3, 3,33, 33,36, 6,36 REM T DATA 2, 0,30, 10,16 REM U DATA 3, 0,6, 30,36, 6,36 REM V DATA 2, 0,16, 16,30 REM W DATA 4, 0,6, 30,36, 6,13, 13,36 REM X DATA 2, 0,36, 30,6 REM Y DATA 3, 0,13, 30,13, 13,16 REM Z DATA 3, 0,30, 30,6, 6,36 REM 0 DATA 5, 0,30, 6,36, 0,6, 30,36, 30,6 REM 1 DATA 2, 10,16, 1,10 REM 2 DATA 5, 0,30, 30,33, 3,33, 3,6, 6,36 REM 3 DATA 4, 0,30, 30,36, 3,33, 6,36 REM 4 DATA 3, 0,3, 3,33, 30,36 REM 5 DATA 5, 0,30, 0,3, 3,33, 33,36, 6,36 REM 6 DATA 5, 0,30, 0,6, 3,33, 33,36, 6,36 REM 7 DATA 2, 0,30, 30,36 REM 8 DATA 5, 0,30, 6,36, 0,6, 30,36, 3,33 REM 9 DATA 5, 0,30, 0,3, 3,33, 30,36, 6,36 REM space DATA 0 REM colon DATA 2, 12,13, 14,15 REM dash DATA 1, 3,33 REM full stop DATA 1, 15,16 REM exclamation mark DATA 2, 10,14, 15,16