Files
akbasic/examples/breakout/sprites/breakout.bas
Tachikoma ff1730e424 Teach the artwork tutorial the converted listing
Chapter 18 still taught the machinery the listing lost when it moved onto
SOLID and the persistent drawing layer: the frame-boundary PACE routine, the
flattened live list, the SSHAPE pool accounting, and the sixty lines of
BALLBRICKS/TESTCELL that computed a minimum translation axis by hand.

Step 8 now registers each brick with SOLID, arms COLLISION 2 and reads the
contact back with RCOLLISION, including the point that costs an evening if it
is missed: the BUMP mask is by sprite, and the balls are sprites 5, 6 and 7,
so their bits are 16, 32 and 64. The FOR-with-equal-bounds rule that every
one-item case in the chapter depends on is now stated where it is first used
rather than referred to from a step that no longer exists.

The listing loses six declarations orphaned by the conversion and the comment
blocks that still described the captures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 13:13:51 -04:00

1344 lines
37 KiB
QBasic

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 Eight of the sixteen SSHAPE slots, spent once in DRAWPROTOS and never
REM again: six brick stamps and the two erasers. Nothing captures per frame
REM any more, so the pool never runs dry and GRAPHIC 5 is never called.
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$ = ""
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
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 At most one drawing job, then the game, which may take as long as it
REM likes. Nothing here has a deadline: a drawing that spans two host
REM batches simply arrives over two frames and the layer keeps both halves.
REM =====================================================================
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 drawing job per frame, and only one
REM
REM The field is the expensive one -- sixty stamps and a border -- so it
REM never shares a frame with the HUD. Both are flagged rather than drawn
REM where the change happens, so a routine that changes three things costs
REM one drawing.
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
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