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 Five things about this interpreter shape everything below, and they REM are worth knowing before the first line stops making sense. REM REM 1. The text layer repaints every row of the window opaque black after REM the program's steps have run, so by default anything DRAW or BOX REM put on the screen is wiped before it is ever presented. Sprites are REM drawn after the text layer, so a sprite is the one thing that is REM seen for nothing. Everything drawn here is therefore captured with REM SSHAPE and installed as a sprite with SPRSAV -- that is what REM sprites 1 and 2 are. They are the screen. REM REM (WINDOW can shrink the text area and hand the rest of the window to REM the drawing verbs; it could not be reached from this build when REM this was written. It would not help here anyway: a drawing has to REM be re-issued every frame and fit inside one batch, and a sprite has REM neither constraint.) REM REM 2. The host runs 256 source lines and then presents the frame, and a REM present throws the drawing buffer away. So all the drawing between REM a GRAPHIC 1, 1 and its SSHAPE has to fit inside one of those REM batches or the capture comes back half empty. Waiting for TI# to REM tick lands us on a batch boundary -- see PACE -- and every draw REM routine below is written to stay under about two hundred lines. REM Arithmetic is free; only drawing has a deadline. That is why the REM lettering is turned into a list of strokes when a number changes REM and merely replayed when it is time to draw. REM REM 3. There are eight sprites and no more. Two are the screen, five are REM the artwork, one is the gem. That is the whole budget, and it is REM why the bricks are drawn rather than made of art. 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 5. 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 LX#(60) DIM LY#(60) DIM RS#(6) DIM RC#(6) DIM BRC#(6) DIM BRV#(6) LN# = 0 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. SHN# = 99 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 ===================================================================== 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. 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 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 ===================================================================== LABEL FRAME GOSUB PACE 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 Pacing, and the batch boundary it buys REM REM TI# is refreshed from the host's clock once per batch, so the step on REM which it changes is the first step of a batch. Spinning here until it REM moves is the only way a program in this dialect can find out where a REM frame boundary is, and a capture that starts anywhere else risks being REM cut in half by a present. Two jiffies is thirty frames a second. REM REM **LASTT# is sampled here rather than carried over from the last REM frame**, and that is the whole correctness of it. Carried over, a REM frame whose work ran long would find two jiffies already gone and REM return on whatever step it happened to be on -- somewhere in the REM middle of a batch -- and the capture that followed would come back REM holding half of the frame before it. Sampling here means the loop REM always sees TI# change under it, and a change is only ever seen on REM the first step of a batch. REM ===================================================================== LABEL PACE LASTT# = TI# LABEL PACEEDGE IF TI# - LASTT# < 2 THEN GOTO PACEEDGE RETURN 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. LABEL DRAWJOB IF SHN# < 14 THEN GOTO DRAWJOB2 GOSUB DRAWPROTOS RETURN 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 5 GRAPHIC 1, 1 WIDTH 1 SHN# = 99 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# 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$ SHN# = 6 DPLAY# = 1 DHUD# = 1 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 Six runs of stamps, one per row colour, because picking the stamp per REM brick would cost a line per brick inside the one routine in this REM program that has a deadline. The live list is built row by row, so a REM row is a contiguous run of it: RS# is where the run starts and RC# how REM long it is. Z$ = S0$ : T1# = RS#(0) : T2# = RC#(0) : GOSUB STAMPROW Z$ = S1$ : T1# = RS#(1) : T2# = RC#(1) : GOSUB STAMPROW Z$ = S2$ : T1# = RS#(2) : T2# = RC#(2) : GOSUB STAMPROW Z$ = S3$ : T1# = RS#(3) : T2# = RC#(3) : GOSUB STAMPROW Z$ = S4$ : T1# = RS#(4) : T2# = RC#(4) : GOSUB STAMPROW Z$ = S5$ : T1# = RS#(5) : T2# = RC#(5) : GOSUB STAMPROW REM A skipped BEGIN block holding a FOR used to break the RETURN at the end REM of the routine holding it, so this loop is guarded by a GOTO instead. REM Fixed since; left alone because a GOTO guard costs nothing. 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 SSHAPE Z$, 0, 60, 800, 600 SPRSAV Z$, 2 SPRITE 2, 1, 2 MOVSPR 2, 0, 60 SHN# = SHN# + 1 RETURN REM Stamp T2# bricks starting at T1# with the shape in Z$. The count of REM one is spelled out because a FOR whose bounds are equal does not run REM its body in this dialect, and one brick left in a row is exactly that. LABEL STAMPROW IF T2# < 1 THEN RETURN IF T2# = 1 THEN GSHAPE Z$, LX#(T1#), LY#(T1#) IF T2# < 2 THEN RETURN FOR I# = T1# TO T1# + T2# - 1 GSHAPE Z$, LX#(I#), LY#(I#) NEXT I# 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, 1 WIDTH 1 COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5 COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6 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 SSHAPE Z$, 0, 0, 800, 60 SPRSAV Z$, 1 SPRITE 1, 1, 2 MOVSPR 1, 0, 0 SHN# = SHN# + 1 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 BALLBRICKS 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. A ball can only be over four cells at once, so the REM cells are worked out from its box rather than by walking sixty REM bricks. DO rather than FOR throughout: a FOR whose bounds are equal REM does not run its body in this dialect, and a ball inside one column is REM exactly that case. LABEL BALLBRICKS IF BRN# = 0 THEN RETURN LFT# = BLX%(B#) TOP# = BLY%(B#) RGT# = LFT# + 21 BOT# = TOP# + 21 IF BOT# < BRKY# THEN RETURN IF TOP# > BRKY# + 6 * 24 THEN RETURN CC1# = (LFT# - BRKX#) / 72 CC2# = (RGT# - BRKX#) / 72 RR1# = (TOP# - BRKY#) / 24 RR2# = (BOT# - BRKY#) / 24 IF CC1# < 0 THEN CC1# = 0 IF CC2# > 9 THEN CC2# = 9 IF RR1# < 0 THEN RR1# = 0 IF RR2# > 5 THEN RR2# = 5 IF CC2# < CC1# THEN RETURN IF RR2# < RR1# THEN RETURN HIT# = 0 R# = RR1# DO C# = CC1# DO IF HIT# = 0 THEN GOSUB TESTCELL C# = C# + 1 LOOP UNTIL C# > CC2# OR HIT# = 1 R# = R# + 1 LOOP UNTIL R# > RR2# OR HIT# = 1 RETURN REM Reflect off whichever face the ball has less of itself past: the REM standard box resolution, and the reason a ball clipping the end of a REM row goes sideways instead of straight back down. T1# to T4# are the REM overlapping rectangle; its width against its height is the whole test. LABEL TESTCELL N# = R# * 10 + C# IF BRK#(N#) = 0 THEN RETURN BX1# = BRKX# + C# * 72 BY1# = BRKY# + R# * 24 IF RGT# < BX1# THEN RETURN IF LFT# > BX1# + 67 THEN RETURN IF BOT# < BY1# THEN RETURN IF TOP# > BY1# + 15 THEN RETURN T1# = RGT# : IF BX1# + 67 < T1# THEN T1# = BX1# + 67 T2# = LFT# : IF BX1# > T2# THEN T2# = BX1# T3# = BOT# : IF BY1# + 15 < T3# THEN T3# = BY1# + 15 T4# = TOP# : IF BY1# > T4# THEN T4# = BY1# IF T1# - T2# < T3# - T4# THEN BLVX%(B#) = 0.0 - BLVX%(B#) IF T1# - T2# >= T3# - T4# THEN BLVY%(B#) = 0.0 - BLVY%(B#) BRK#(N#) = 0 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 HIT# = 1 GOSUB BUILDLIVE GOSUB HUDTEXT DPLAY# = 1 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# GOSUB BUILDLIVE 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# GOSUB BUILDLIVE 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# FOR R# = 0 TO 5 IF R# < T3# THEN GOSUB FILLROW NEXT R# GOSUB BUILDLIVE 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. LABEL FILLROW FOR C# = 0 TO 9 BRK#(R# * 10 + C#) = 1 BRN# = BRN# + 1 NEXT C# RETURN LABEL LEVELSPEED SPD% = 5.6 + 0.45 * LEVEL# IF SPD% > 11.0 THEN SPD% = 11.0 RETURN REM Flatten the brick grid into the list DRAWPLAY walks. Doing it here REM rather than in the draw routine is the whole trick: this costs four REM lines a brick and has all the time in the world, the draw costs two REM and has to finish before the host presents. LABEL BUILDLIVE LN# = 0 FOR R# = 0 TO 5 RS#(R#) = LN# RC#(R#) = 0 GOSUB BUILDROW NEXT R# RETURN LABEL BUILDROW FOR C# = 0 TO 9 IF BRK#(R# * 10 + C#) > 0 THEN BEGIN LX#(LN#) = BRKX# + C# * 72 LY#(LN#) = BRKY# + R# * 24 LN# = LN# + 1 RC#(R#) = RC#(R#) + 1 BEND NEXT C# 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