diff --git a/MAINTENANCE.md b/MAINTENANCE.md index ec69c36..abac7d1 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -611,6 +611,7 @@ a missing decision rather than a free pass. | `text` | **Never executed.** A block diagram, or a stack trace captured from a real run | | `basic screenshot=NAME` | Also the source of `docs/images/NAME.png`. The harness checks the file exists; `tools/docs_screenshots.sh` is what makes it | | `basic size=WxH` | That figure's surface, when 320x200 is not the size the point needs | +| `basic text=1` | That figure is drawn **with** the text layer, for a program whose output is characters | Attributes combine: `basic requires=akgl setup=ship` is a real tag in `docs/08-sprites.md`. @@ -627,8 +628,18 @@ $ cmake --build build-akgl --target docs_screenshots `tools/screenshot.c` is a second, much smaller SDL host — the dummy video driver, a software renderer, run the program to completion, read the target back, write a PNG. It -draws no text layer, deliberately: a `READY` in the corner is noise in a figure about -`BOX`, and skipping it means no font has to be found. +draws no text layer by default, deliberately: a `READY` in the corner is noise in a figure +about `BOX`, and skipping it means no font has to be found. + +**`text=1` turns the text layer on**, for the case that default gets wrong: a figure of a +program whose output *is* characters. `docs/17-tutorial-breakout.md` builds a game whose +wall, HUD and messages are all in the grid, and without the layer its figure is two sprites +on a black field. The tag makes `docs_screenshots.sh` hand the tool +`assets/fonts/C64_Pro_Mono-STYLE.ttf` at `AKBASIC_FRONTEND_FONT_SIZE`, which is what the +standalone frontend opens, so the figure's cell size is the reader's cell size. The sink is +then the akgl one *alone* rather than a tee, so the program's output goes into the picture +instead of onto stdout — which the caller reads to decide a figure failed. A raised error +is still caught, by `docs_examples` running the same block. Three rules around it: diff --git a/TODO.md b/TODO.md index 325d473..b6b3b14 100644 --- a/TODO.md +++ b/TODO.md @@ -2100,6 +2100,120 @@ in either corpus runs for minutes, which is why the first of these had never bee `AKBASIC_ERR_BOUNDS` from the pool is documented as "bounded and diagnosable, which is the point" (`include/akbasic/value.h:34`). With a `TRAP` armed it is currently neither. +### Found while rewriting the game tutorials + +Chapters 17 and 18 were rewritten to teach a beginner rather than to explain a finished +listing, which meant writing every rule down as a rule instead of as a comment beside the +code that dodges it. Three of them turned out to have no entry anywhere. The first two are +the same shape as item 12 above — a parser function that consumes one operator where it +should loop — and the third is why both games are built out of `LABEL` and `GOTO`. + +35. **An expression mixing `-` and `+` at the same level is evaluated right to left.** + `a - b + c` computes `a - (b + c)`. A C128, and every BASIC this descends from, folds a + run of same-precedence additive operators left to right. + + ```basic + PRINT 10 - 3 + 2 + PRINT 1 - 2 - 3 + ``` + + ```output + 5 + -4 + ``` + + The second line is right and the first is not; 9 is the answer. **Nothing fails** — this + is item 4's failure mode over again, a program that quietly computes something else — and + the two tutorials work around it by parenthesising every mixed `+` and `-` as a rule + rather than where it happens to matter. + + **The cause is the grammar's shape, not a missing loop.** `subtraction()` + (`src/parser.c:425`) sits *above* `addition()` (`:447`) as its own precedence level, and + both loop correctly — but `subtraction()`'s right operand is parsed by `addition()`, which + then consumes the `+` that should have been the *next* term of the subtraction. `1 - 2 - 3` + is right because the outer loop sees both minuses; `10 - 3 + 2` is wrong because the inner + one eats the plus first. + + The fix is one additive level rather than two: a single function matching `PLUS` and + `MINUS` in one `while`, building left-associatively, with `multiplication()` beneath it. + That is what item 12 left half-done — it made each of the two levels fold left without + noticing that there should only have been one. Touches `src/parser.c` and wants cases in + `tests/parser_expressions.c` beside item 12's, asserting `10 - 3 + 2` is 9 and + `10 + 3 - 2` is 11 (which already passes, and is the asymmetry that gives it away). + +36. **Only one unparenthesised `AND` or `OR` is matched per expression.** A second one is left + in the token stream, and in the place a program actually writes them — a condition — the + `THEN` check then finds the wrong token and the line is refused: + + ```basic + X# = 1 + Y# = 2 + IF X# = 1 AND Y# = 2 THEN PRINT "TWO IS FINE" + IF X# = 1 AND Y# = 2 AND X# = 1 THEN PRINT "THREE IS NOT" + ``` + + ```output + TWO IS FINE + ? 4 : PARSE ERROR Incomplete IF statement + ``` + + Parenthesising is the workaround and it works — `IF (A AND B) AND C THEN` parses — which + is what the tutorials tell a reader to do. It is a refusal rather than a wrong answer, so + it costs a puzzled minute rather than an evening; item 42 in §5 fixed the case of *one* + `AND` and stopped there. + + `logicalandor()` (`src/parser.c:329`) is `if ( akbasic_parser_match(obj, OPS, 2) )` at `:339` + where `subtraction()` at `:425` is `while`. **This is item 12's defect exactly**, on the + one operator pair item 12 did not reach, and the reference has the same shape at + `basicparser.go`. The fix is the same fix: `while`, folding left, reassigning `left` from + the leaf built on the previous pass. `tests/parser_expressions.c` for the expression form + and `tests/parser_commands.c` for the `IF` that made it visible. + + Worth doing together with item 35: they are one afternoon, one file, and between them they + are the two rules a tutorial currently has to spend a paragraph on. + +37. **A `GOTO` that leaves a `FOR` or a `DO` leaks the loop's scope**, so a program that uses + the pattern as its main loop stops after the thirty-second time round: + + ```basic + N# = 0 + LABEL TOP + DO + N# = N# + 1 + IF N# < 100 THEN GOTO TOP + LOOP UNTIL N# > 99 + PRINT "SURVIVED " + N# + ``` + + ```output + ? 3 : PARSE ERROR Environment pool exhausted at line 3 (32 in use) + ``` + + Thirty-two is `AKBASIC_MAX_ENVIRONMENTS` (`include/akbasic/types.h:31`), and the branch + that reports is the `DO` being entered for the thirty-third time rather than the `GOTO` + that did the damage — the same misattribution item 2 had. The `FOR` form behaves + identically. + + **This is a real pattern rather than a contrivance.** A game's main loop is a loop the + program leaves from the middle of: losing a life, clearing a level and quitting are all + "stop what you are doing and go somewhere else". Both Breakout listings in `examples/` + are built out of `LABEL`/`GOTO` for exactly this reason, which is the shape a reader of + chapters 17 and 18 is now told to copy, and it is a workaround rather than a preference — + a `DO ... LOOP` around the frame would be the natural way to write either one. + + The mechanism is the one item 2 documents from the other end: a loop's environment is + pushed when the line is parsed and popped by its `NEXT` or `LOOP`, and nothing else pops + it. `GOTO` sets the next line and returns (`akbasic_cmd_goto()`, `src/runtime_commands.c`); + it has no idea it has just left a scope. + + A fix has to know which scopes the branch target is *outside* of, which the environment + stack can answer if a loop's environment records the source line that pushed it: on a + `GOTO`, pop every loop scope whose line is not on the path to the target. That is more + machinery than items 35 and 36 want between them, and it is worth pricing against simply + documenting it — chapters 13 and 17 both now say "loop with `GOTO`, never jump out of a + `DO`", which is a rule a reader can follow. Wants `tests/for_next.c` either way, because + the reduction above is four lines and currently nothing asserts it in either direction. + ## 7. Filing gaps against `libakgl` When phase 7 or phase 8 needs something `libakgl` does not have, **stop and file it** in diff --git a/docs/03-the-language.md b/docs/03-the-language.md index 100b9a4..c579a5c 100644 --- a/docs/03-the-language.md +++ b/docs/03-the-language.md @@ -21,6 +21,11 @@ a character that says what it holds: There is no such thing as a variable with no suffix. A bare name is a **label** — see Chapter 4 — so `GOTO DONE` and `LABEL DONE` are how the two meet. +**A name is letters and digits and nothing else.** There is no underscore: `HIGH_SCORE#` +does not scan, and what it reports is `UNKNOWN TOKEN _` rather than anything about naming, +so it is worth knowing before you meet it. Run words together — `HIGHSCORE#` — or shorten +them, which is what the listings in this guide do. + `@` is the odd one out: it says "a structure" without saying *which*, so a structure variable has to be declared with `DIM E@ AS ENEMY` before it can be used. That is **[Chapter 16](16-structures.md)**, along with records, pointers and how a host shares its diff --git a/docs/06-graphics.md b/docs/06-graphics.md index 723b299..11034bc 100644 --- a/docs/06-graphics.md +++ b/docs/06-graphics.md @@ -250,5 +250,5 @@ lines and then presents, and presenting throws the drawing buffer away — so a drawing verbs longer than one batch is torn rather than merely transient, and an `SSHAPE` at the end of it captures only the part issued since the present. Watching `TI#` change is how a program finds the boundary; Chapter 13 has the measured numbers and -[Chapter 18](18-tutorial-breakout-artwork.md#step-4-find-the-frame-boundary) has a +[Chapter 18](18-tutorial-breakout-artwork.md#step-5-find-the-frame-boundary) has a routine that uses them. diff --git a/docs/13-differences.md b/docs/13-differences.md index 139a409..852a0f1 100644 --- a/docs/13-differences.md +++ b/docs/13-differences.md @@ -153,7 +153,7 @@ interpreter's error code, which bears no relation to a Commodore error number. P 256 lines a batch: after synchronising to a jiffy edge, 220 lines of drawing survive a capture and 250 do not. The only way a program can see the boundary is to watch `TI#`, which is refreshed once per batch — so the step on which it changes is the first step - of one. [Chapter 18](18-tutorial-breakout-artwork.md#step-4-find-the-frame-boundary) + of one. [Chapter 18](18-tutorial-breakout-artwork.md#step-5-find-the-frame-boundary) builds a pacing routine out of exactly that. - **`CHAR` ignores its colour argument** and needs a text device with a cursor. diff --git a/docs/14-architecture.md b/docs/14-architecture.md index c64cf97..bc44eb4 100644 --- a/docs/14-architecture.md +++ b/docs/14-architecture.md @@ -181,7 +181,7 @@ A script cannot read the budget, but it can *see* it: `akbasic_runtime_settime() called once per frame by the host, so `TI#` changes on the first step of a batch and nowhere else. Spinning until it changes is the only frame synchronisation this dialect has, and it is what -[Chapter 18](18-tutorial-breakout-artwork.md#step-4-find-the-frame-boundary) is built on. +[Chapter 18](18-tutorial-breakout-artwork.md#step-5-find-the-frame-boundary) is built on. A host that gives the interpreter a larger budget makes more drawing fit; one that never calls `settime()` takes the synchronisation away entirely. diff --git a/docs/17-tutorial-breakout.md b/docs/17-tutorial-breakout.md index 6259af4..46e911f 100644 --- a/docs/17-tutorial-breakout.md +++ b/docs/17-tutorial-breakout.md @@ -1,16 +1,20 @@ # 17. Tutorial: Breakout -This chapter builds a complete game — three lives, six rows of bricks, three level -layouts, a high score and an attract mode that plays itself — out of nothing but the -verbs in the earlier chapters. The finished listing is -[`examples/breakout/characters/breakout.bas`](../examples/breakout/characters/breakout.bas), -and it is worth having open beside this. +This chapter builds a complete game from an empty file: three lives, six rows of bricks, +three level layouts, a score, a high score, sound, and a title screen that plays itself. +Nothing is loaded from disk — the ball and the paddle are drawn from numbers in the +listing, and the wall, the HUD and the messages are characters written into the text grid. -**The ball and the paddle are sprites; the bricks, the HUD and the messages are -characters in the text grid.** That split is the first decision and everything else -follows from it, so it is Step 1. +This is what you are building: -Run the finished game with the SDL build: +![The finished game: a HUD line across the top, six rows of bricks, the ball resting on the paddle](images/breakout-game.png) + +The finished listing is +[`examples/breakout/characters/breakout.bas`](../examples/breakout/characters/breakout.bas). +You do not need it to follow along, but it is the same program assembled, and it is worth +opening once you have your own running. + +Run it with the SDL build: ```sh norun $ ./build-akgl/basic examples/breakout/characters/breakout.bas @@ -23,35 +27,120 @@ $ ./build-akgl/basic examples/breakout/characters/breakout.bas | P | pause | | Q or escape | quit | -Chapter 18 builds the same game again out of sprite artwork, and takes a completely -different shape. Read this one first: it is the smaller of the two and it is where the -rules that constrain both are explained. +## What you will do + +- **[Step 1](#step-1-get-a-program-onto-the-screen)** — get a program onto the screen, and + find out which build you need +- **[Step 2](#step-2-measure-the-screen)** — measure the screen and work the geometry out + from what you measured +- **[Step 3](#step-3-declare-every-name-first)** — declare every name the program will use, + before anything uses it +- **[Step 4](#step-4-make-the-ball-and-the-paddle)** — make the ball and the paddle out of + sprite data +- **[Step 5](#step-5-build-the-brick-wall)** — build the brick wall out of characters, and + keep a grid saying which bricks are still there +- **[Step 6](#step-6-write-the-main-loop)** — write the main loop and the frame pacing +- **[Step 7](#step-7-read-the-keyboard)** — read the keyboard without blocking +- **[Step 8](#step-8-move-the-paddle)** — move the paddle and keep it on the screen +- **[Step 9](#step-9-move-the-ball)** — move the ball and bounce it off the walls +- **[Step 10](#step-10-break-bricks)** — work out which brick the ball is touching, and + break it +- **[Step 11](#step-11-bounce-off-the-paddle)** — bounce off the paddle at an angle the + player chooses +- **[Step 12](#step-12-draw-the-hud-and-the-messages)** — draw the HUD and the messages +- **[Step 13](#step-13-lives-levels-and-game-over)** — handle lives, levels and game over +- **[Step 14](#step-14-add-sound)** — add sound, on a machine that may not have any +- **[Step 15](#step-15-add-an-attract-mode)** — add a title screen that plays itself +- **[Step 16](#step-16-put-the-program-together)** — put the pieces in one file, in the + right order + +Each step is a piece you can type in and run. Work through them in order, and Step 16 is +where they become one program. --- -## Step 1: Decide what is a sprite and what is a character +## Step 1: Get a program onto the screen -Two layers of this interpreter are redrawn from state it keeps for you, every frame, -without your program doing anything: +**Goal: a program that runs, and a build that can draw.** -- the **text grid**, repainted row by row by the akgl sink, and -- the **sprites**, walked slot by slot after it. +A BASIC program here is a text file. Line numbers are optional, so a file is just +statements, one per line: -The drawing verbs are not one of them. `DRAW`, `BOX`, `CIRCLE` and `PAINT` go straight -into the renderer's back buffer, and **the text layer owns every row of the window by -default** — so it paints over anything they drew on its way past. `WINDOW` shrinks the -text area and gives the rest of the window to the drawing verbs, but a drawing still has -to be re-issued every frame and has to fit in one batch to survive the capture; see -[Chapter 13](13-differences.md#graphics). +```basic +PRINT "HELLO" +END +``` -So this game uses no drawing verb at all. Anything that has to move smoothly is a sprite; -anything that can live on a 16-pixel grid is text. Ball and paddle move; bricks, score -and messages do not. +```output +HELLO +``` -**If you want artwork on the screen, that rule flips** and the whole architecture changes -with it. That is Chapter 18. +Save that as `first.bas` and run it: -## Step 2: Measure the screen once, at the top +```sh norun +$ ./build/basic first.bas +``` + +There are two builds and the difference matters for this whole chapter. `./build/basic` is +the plain one: it reads and writes text and has **no graphics device, no sprites and no +sound**. `./build-akgl/basic` opens a window and has all three. A game needs the second. + +Ask for something that needs a device and you get a refusal naming what is missing rather +than a crash: + +```basic requires=noakgl +PRINT "BEFORE" +SPRITE 1, 1, 2 +PRINT "AFTER" +``` + +```output +BEFORE +? 2 : RUNTIME ERROR SPRITE needs a sprite device and this runtime has none + +``` + +That is worth seeing once, because it is how the finished game behaves if you start it +with the wrong build: it stops at the first graphics verb and tells you why. + +For the rest of this chapter, run everything with `./build-akgl/basic`. + +Two more things to know before writing anything longer. + +**Every variable carries a type suffix.** `A#` is an integer, `A%` is a float, `A$` is a +string. There is no such thing as a plain `A` — a bare word is a label. This is different +from Commodore BASIC, where `A%` is the integer. + +**`+` joins a string to a number.** `"SCORE " + 40` is `"SCORE 40"`, and `"" + N#` is how +you turn a number into a string. There is no `STR$`. + +```basic +N# = 40 +PRINT "SCORE " + N# +END +``` + +```output +SCORE 40 +``` + +## Step 2: Measure the screen + +**Goal: every position in the game derived from the window's real size, so the game fits +whatever window and font the player has.** + +Do not guess the size of anything. Four functions tell you what you have: + +| Call | Gives | +|---|---| +| `RGR(1)` | the window's width in pixels | +| `RGR(2)` | the window's height in pixels | +| `RGR(3)` | the width of one character cell, in pixels | +| `RGR(4)` | the height of one character cell | +| `RWINDOW(0)` | the text grid's height, in rows | +| `RWINDOW(1)` | the text grid's width, in columns | + +Put them at the very top of the program: ```basic norun SCW# = RGR(1) @@ -62,23 +151,12 @@ COLS# = RWINDOW(1) ROWS# = RWINDOW(0) ``` -Six numbers, none of them assumed. `RGR(1)` and `RGR(2)` are the window in pixels, -`RGR(3)` and `RGR(4)` are one character cell, and `RWINDOW` gives the text grid in -columns and rows — see [Chapter 6](06-graphics.md#rgr). Everything below is derived from -these, including the ball's speed relative to the wall, so the game fits whatever window -the host opened and whatever font it loaded. +`RGR(1)` is first on purpose. It is the first statement in the program that needs a +graphics device, so on the wrong build this is the line that refuses, and it refuses +before anything else has happened. -**The first version of this file had `CW# = 16` written out**, measured by hand against -the bundled C64_Pro_Mono at 16 points, because there was no way to ask — and it was the -one thing in the listing that would break on a different font. `RGR(3)` and `RWINDOW` -were added to the interpreter because of it; `TODO.md` §6 item 31 is the history. - -The order matters only for the error you get without a graphics device: `RGR(1)` refuses -first, naming the device that is missing, which is what should stop a game that cannot -draw. - -Lay the rest of the geometry out in the same block — the wall in cells, the play area in -pixels: +Now derive the layout from those numbers rather than writing pixel positions down. The +wall is measured in **character cells**, because it is made of characters: ```basic norun BRW# = 4 @@ -86,71 +164,104 @@ BCOLS# = 10 BROWS# = 6 BTOP# = 4 BLEFT# = (COLS# - (BRW# * BCOLS#)) / 2 +``` + +A brick is four cells wide and there are ten of them, so the wall is forty cells across. +`BLEFT#` centres those forty in however many columns there turned out to be. `BTOP#` is +the row the wall starts on. Integer division truncates, which is exactly what a column +index wants. + +The ball and the paddle are sprites, and **sprites are positioned in pixels**, so the play +area is measured in pixels: + +```basic norun TOPY# = 2 * CH# MAXX# = SCW# - 8 PW# = 144 PY# = 528 +LOSEY# = PY# + 32 +PSPD# = 10 +MSGROW# = 35 +TITROW# = 20 ``` -A brick is four cells wide, so ten of them are forty cells, and `BLEFT#` centres that in -whatever `COLS#` turned out to be. Integer division truncates here, which is exactly what -a cell index wants. +`TOPY#` is the ceiling the ball bounces off, two rows down from the top so it clears the +HUD line. `MAXX#` is as far right as the ball can go — the window less the ball's own +eight pixels. `PW#` is the paddle's width and `PY#` the row of pixels it sits on. +`MSGROW#` and `TITROW#` are text rows, counted from the top, for messages. -## Step 3: Declare the names a subroutine has to answer through +**Cells and pixels are two different coordinate systems and only one routine in the whole +game converts between them.** That routine is in Step 10. Everywhere else, bricks are in +cells and everything that moves is in pixels. -A `GOSUB` gets its own scope. Assignment walks up the chain and finds an outer name, but a -name **first seen** inside a subroutine is created in that subroutine and dies at -`RETURN` — so a routine cannot answer its caller through a name it invented itself. +## Step 3: Declare every name first -That is why the game declares this block before anything calls anything: +**Goal: a subroutine that can hand an answer back to its caller.** -```basic norun -DIM BR#(60) -DIM LAY$(6) -DIM BSG$(6) -SCORE# = 0 -LIVES# = 3 -BX# = 0 -BY# = 0 -HIT# = 0 -BI# = 0 -``` +A `GOSUB` gets its own scope. That has one consequence you must design around: -`HIT#` and `BI#` are how `HITTEST` (Step 8) answers the routine that called it. Declared -out here they are one variable the whole program shares; left to `HITTEST`, they would be -two that vanish at `RETURN` and the caller would read zeros for ever, with no diagnostic -of any kind. [Chapter 4](04-control-flow.md#goto-and-gosub) has the scope rules. +- Assigning to a name that already exists **outside** the subroutine walks up and finds it. + The change is visible to the caller. +- Assigning to a name that has **never been seen before** creates it inside the + subroutine. It disappears at `RETURN`, and the caller reads zero — with no error and no + warning of any kind. -**Creating a scalar inside a scope is free**, so nothing else has to be declared up front: +So a subroutine cannot answer through a name it invented itself. Declare the names first, +at the top of the program, and then any routine can write to them: ```basic X# = 0 -FOR T# = 1 TO 20000 - GOSUB SUBA -NEXT T# -PRINT "OK " + X# +GOSUB SUBA +PRINT "THE CALLER SEES " + X# END LABEL SUBA -LOC# = 1 -X# = X# + LOC# +X# = 99 RETURN ``` ```output -OK 20000 +THE CALLER SEES 99 ``` -Twenty thousand calls, each creating a local. **This did not use to work.** A scalar drew -storage from the same 4096-slot pool an array does, scope exit gave back the variable but -not its storage, and a program creating one name per tick died in about half a minute — -naming whichever line was unlucky rather than the line that was wrong. It is what killed -the first version of this game after twenty-five seconds. `TODO.md` §6 item 30 has the -history; `tests/value_pool.c` is what keeps it fixed. +Take `X# = 0` off the top of that program and it prints `0`. -**An array is different, and still costs slots for good.** `DIM` inside a subroutine takes -pool storage that never comes back, because a pointer into a record can outlive the scope -that declared it — see [Chapter 16](16-structures.md#limits): +This is why the game opens with a block that names everything before the first `GOSUB`. +Arrays go in the same block. Here is the start of it — the names the later steps refer to +most; [Step 16](#step-16-put-the-program-together) has the finished block, all 66 of them, +and you can paste that in now if you would rather not come back to it: + +```basic norun +DIM BR#(60) +DIM SB#(63) +DIM SP#(63) +DIM LAY$(6) +DIM BSG$(6) + +SCORE# = 0 +HIGH# = 0 +LIVES# = 3 +LEVEL# = 1 +LEFTN# = 0 +BSPD# = 4 +STATE# = 0 +HELD# = 0 +PX# = 0 +BX# = 0 +BY# = 0 +BVX# = 0 +BVY# = 0 +HIT# = 0 +BI# = 0 +BR2# = 0 +``` + +`HIT#`, `BI#` and `BR2#` are the three the rule is really about: they are how the +collision test in Step 10 answers the routine that called it. + +**`DIM` belongs at the top and nowhere else.** An array declared inside a `GOSUB` or a +loop takes storage from a pool of 4096 elements that is never given back, so a `DIM` that +runs repeatedly will eventually end the program: ```basic X# = 0 @@ -172,12 +283,173 @@ RETURN ``` -So the rule that survives is narrow: **`DIM` at the top, not in a loop.** Which is where -you would have put it anyway. +A plain number costs nothing, though, so you can create as many of those inside a routine +as you like: -## Step 4: Build the wall, and write a row whole +```basic +X# = 0 +FOR T# = 1 TO 20000 + GOSUB SUBA +NEXT T# +PRINT "OK " + X# +END -Levels are `DATA`, one string per row, `1` for a brick and `0` for a hole: +LABEL SUBA +LOC# = 1 +X# = X# + LOC# +RETURN +``` + +```output +OK 20000 +``` + +Twenty thousand calls, each creating a local, and the program is fine. The rule that +survives is the narrow one: **`DIM` at the top, never inside anything.** + +### Two rules about writing expressions + +Both apply everywhere in this chapter, so they are worth fixing in your fingers now. + +**Parenthesise any expression that mixes `+` and `-`.** Write `(A# - B#) + C#`, not +`A# - B# + C#`: + +```basic +A# = 10 +B# = 3 +C# = 2 +PRINT (A# - B#) + C# +END +``` + +```output +9 +``` + +**Parenthesise a condition that uses more than one `AND` or `OR`.** Write +`IF (A# = 1 AND B# = 2) AND C# = 3 THEN`: + +```basic +A# = 1 +B# = 2 +C# = 3 +IF (A# = 1 AND B# = 2) AND C# = 3 THEN PRINT "ALL THREE" +END +``` + +```output +ALL THREE +``` + +Both habits are free, and both are being made unnecessary: the parser's additive +precedence is [`TODO.md` §6 item 35](../TODO.md) and the second `AND` is item 36 beside +it. Once those land, the plain forms will mean what they look like they mean, and +parentheses you wrote in the meantime will still be correct. + +## Step 4: Make the ball and the paddle + +**Goal: two sprites on the screen, in the right places.** + +A sprite is a 24-by-21 pattern of pixels. `SPRSAV` loads one from an **integer array of 63 +numbers**: three bytes per row, twenty-one rows, most significant bit on the left. A bit +that is 1 draws; a bit that is 0 is transparent. + +```text + byte 0 byte 1 byte 2 + 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 + ^ column 0 ^ column 23 +``` + +The ball is an 8-by-8 disc drawn in the **top-left corner** of the pattern. Putting it +there means the sprite's position and the ball's pixel rectangle are the same thing, so no +part of the game needs to add an offset to work out where the ball actually is: + +```text + . . # # # # . . 60 + . # # # # # # . 126 + # # # # # # # # 255 +``` + +The paddle is a solid bar, 24 wide and 6 deep — the full width of a sprite. `SPRITE`'s +x-expand flag doubles it to 48 pixels, and three of those laid end to end make one +144-pixel paddle with no seam. + +```basic requires=akgl screenshot=breakout-paddle size=240x120 +DIM SB#(63) +DIM SP#(63) +I# = 0 +D# = 0 +FOR I# = 0 TO 62 + SB#(I#) = 0 + SP#(I#) = 0 +NEXT I# +FOR I# = 0 TO 7 + READ D# + SB#(I# * 3) = D# +NEXT I# +FOR I# = 0 TO 17 + SP#(I#) = 255 +NEXT I# +SPRSAV SB#, 1 +SPRSAV SP#, 2 +SPRSAV SP#, 3 +SPRSAV SP#, 4 +SPRITE 1, 1, 2 +SPRITE 2, 1, 15, 0, 1, 0 +SPRITE 3, 1, 15, 0, 1, 0 +SPRITE 4, 1, 15, 0, 1, 0 +MOVSPR 1, 108, 40 +MOVSPR 2, 48, 80 +MOVSPR 3, 96, 80 +MOVSPR 4, 144, 80 + +DATA 60, 126, 255, 255, 255, 255, 126, 60 +``` + +![The ball above three paddle segments meeting with no seam](images/breakout-paddle.png) + +Read that from the middle. `SPRSAV SB#, 1` installs the ball's pattern into slot 1. +`SPRITE 1, 1, 2` turns slot 1 on in colour 2. `SPRITE 2, 1, 15, 0, 1, 0` turns slot 2 on +in colour 15 with the fourth argument after the colour — the x-expand flag — set to 1. +`MOVSPR` puts a sprite at a pixel position, measured from its top-left corner. + +The finished game writes the same 63 numbers out as `DATA`, one row per line, with the +bit pattern drawn in a comment beside each, and loads them in a routine called once at +startup. [Step 16](#step-16-put-the-program-together) has that routine and both patterns +in full; the loops above are the same two patterns spelled shorter so the figure fits on a +page. + +Moving the four sprites is one routine, and every coordinate is worked out into a variable +before it is passed: + +```basic norun +LABEL SHOWSPR +MOVSPR 1, BX#, BY# +MOVSPR 2, PX#, PY# +X2# = PX# + 48 +MOVSPR 3, X2#, PY# +X3# = PX# + 96 +MOVSPR 4, X3#, PY# +RETURN +``` + +**Do not write `MOVSPR 3, PX# + 48, PY#`.** `MOVSPR` reads a leading sign as *move by this +much* rather than *move to here*, and an expression beginning with a sign is the relative +form. Computing into `X2#` first says exactly what you mean. See +[Chapter 8](08-sprites.md#showing-and-moving). + +## Step 5: Build the brick wall + +**Goal: a wall on the screen, and a grid in memory saying which bricks are still standing.** + +Two things are needed and they are separate: the *state* (which of the sixty bricks are +alive) and the *picture* (what the player sees). Keep them in step by always redrawing the +row you just changed. + +### The layouts + +A level is six strings of ten characters, `1` for a brick and `0` for a hole, stored as +`DATA`: ```basic norun LABEL LAY1 @@ -195,11 +467,19 @@ DATA "1111111111" DATA "1111111111" DATA "0111111110" DATA "0011111100" + +LABEL LAY3 +DATA "1010101010" +DATA "0101010101" +DATA "1111001111" +DATA "1100110011" +DATA "1011111101" +DATA "0110110110" ``` -`RESTORE` takes a label — it wants a line number and a label evaluates to one — so a -layout is just a named place in the `DATA` stream, and adding a fourth level is a block -and one `IF`: +`READ` takes the next `DATA` item, and `RESTORE` moves that cursor to a given place. +`RESTORE` accepts a label, so each layout is simply a named position in the `DATA` stream +and choosing one is three `IF`s: ```basic norun LABEL LOADLAY @@ -213,14 +493,50 @@ NEXT R# RETURN ``` -Now draw it — and here is the second rule that shapes this listing: +Adding a fourth level is a `DATA` block and one more `IF`. -**`CHAR` terminates the row where it stops.** A row of the grid is a C string, so writing -at column N erases everything from N+1 on. Writing *past* the end of a short row is fine -— the gap fills with spaces — but there is still no way to poke one character into the -middle of a row and leave the rest of it alone. +**One `READ` cursor walks every `DATA` item in the file, in the order they were written**, +no matter which routine is doing the reading. If you have two things to load — a font and +a table, say — the one whose `DATA` comes first in the file must be loaded first, or use +`RESTORE` to say where to start. -So a row is rebuilt whole and written from column 0, in one `CHAR`: +### The grid + +Turn those strings into a flat array of sixty numbers, and count how many bricks there +are while you are at it: + +```basic norun +LABEL BUILDW +LEFTN# = 0 +FOR R# = 0 TO 5 + S$ = LAY$(R#) + GOSUB BUILDR +NEXT R# +RETURN + +LABEL BUILDR +FOR C# = 0 TO 9 + T$ = MID(S$, C#, 1) + I# = (R# * BCOLS#) + C# + BR#(I#) = 0 + IF T$ = "1" THEN BR#(I#) = 1 + IF T$ = "1" THEN LEFTN# = LEFTN# + 1 +NEXT C# +RETURN +``` + +`MID` counts from **zero** here, unlike Commodore BASIC. `BR#(row * 10 + col)` is the +brick at that cell; `LEFTN#` is how many are left, so "is the level finished" is a +comparison rather than a scan of sixty cells. + +### Drawing a row + +`CHAR` writes a string into the text grid at a row and column. It has one property that +decides how the rest of this section is written: **`CHAR` terminates the row where it +stops.** Writing at column 12 erases everything from column 13 to the end of the line. + +So do not write bricks one at a time. Build the whole row as a string and write it once, +from column 0: ```basic DIM LAY$(6) @@ -266,105 +582,50 @@ RETURN [--][--] [--][--] [--][--][--] ``` -That block runs anywhere, with or without a graphics device, because `PRINT` and `CHAR` -are building the same string. In the game the last line is `CHAR 1, 0, R2#, S$` instead, -and the leading spaces are `" " * BLEFT#`. +A missing brick contributes four spaces, so every row is the same length and the columns +line up. That block uses `PRINT`, so it runs on either build — which makes it a good way +to check your layouts before you have a window. -**The rows are told apart by shape, not by colour.** `CHAR` parses a colour argument and -ignores it; the sink draws in one colour. `[##]`, `[==]` and `[--]` are what makes a -60-point row look different from a 10-point one. - -Killing a brick clears its cell and redraws that one row: +In the game, replace the `PRINT S$` with a `CHAR`, and use the real left margin: ```basic norun -LABEL KILLBR -BR#(BI#) = 0 -LEFTN# = LEFTN# - 1 -PTS# = (BROWS# - BR2#) * 10 -SCORE# = SCORE# + PTS# -GOSUB DRAWBR -GOSUB DRAWHUD -IF LEFTN# < 1 THEN STATE# = 2 +LABEL DRAWBR +S$ = "" +IF BLEFT# > 0 THEN S$ = " " * BLEFT# +BSEG$ = BSG$(BR2#) +FOR CC# = 0 TO 9 + I# = (BR2# * BCOLS#) + CC# + IF BR#(I#) = 1 THEN S$ = S$ + BSEG$ + IF BR#(I#) = 0 THEN S$ = S$ + " " +NEXT CC# +R2# = BTOP# + BR2# +CHAR 1, 0, R2#, S$ RETURN ``` -`LEFTN#` is counted up when the wall is built and down when a brick goes, so "is the -level clear" is a comparison rather than a scan of sixty cells. +`" " * BLEFT#` repeats a string, which is how you get a margin of any width. -## Step 5: Make the ball and the paddle +**Tell the rows apart by shape, not by colour.** `CHAR` accepts a colour argument and +ignores it — the text layer draws in one colour — so `[##]`, `[==]` and `[--]` are what +makes a 60-point row look different from a 10-point one. -`SPRSAV` takes an **integer array** of 63 bytes: three per row, twenty-one rows, most -significant bit leftmost. A clear bit is transparent — see -[Chapter 8](08-sprites.md#from-data). - -Two patterns are all this game needs. The ball is an 8 by 8 disc in the *top-left corner* -of the pattern, so the sprite's position and its pixel rectangle are the same thing and -no offset arithmetic is needed anywhere. The paddle is a solid 24 by 6 bar with `SPRITE`'s -x-expand bit set, which makes it 48 wide; three of them laid end to end are one 144-pixel -paddle. - -```basic requires=akgl screenshot=breakout-paddle size=240x120 -DIM SB#(63) -DIM SP#(63) -I# = 0 -D# = 0 -FOR I# = 0 TO 62 - SB#(I#) = 0 - SP#(I#) = 0 -NEXT I# -FOR I# = 0 TO 7 - READ D# - SB#(I# * 3) = D# -NEXT I# -FOR I# = 0 TO 17 - SP#(I#) = 255 -NEXT I# -SPRSAV SB#, 1 -SPRSAV SP#, 2 -SPRSAV SP#, 3 -SPRSAV SP#, 4 -SPRITE 1, 1, 2 -SPRITE 2, 1, 15, 0, 1, 0 -SPRITE 3, 1, 15, 0, 1, 0 -SPRITE 4, 1, 15, 0, 1, 0 -MOVSPR 1, 108, 40 -MOVSPR 2, 48, 80 -MOVSPR 3, 96, 80 -MOVSPR 4, 144, 80 - -DATA 60, 126, 255, 255, 255, 255, 126, 60 -``` - -![](images/breakout-paddle.png) - -The three segments meet with no seam, which is the whole point of the arrangement. The -game writes the same 63 bytes out as `DATA` a row at a time, with the bit pattern drawn -in a comment beside it; the loops above are the same two patterns spelled shorter so the -figure fits on a page. - -Moving them is four `MOVSPR`s, and each coordinate is worked out into a variable first: +Drawing the whole wall is then six calls: ```basic norun -LABEL SHOWSPR -MOVSPR 1, BX#, BY# -MOVSPR 2, PX#, PY# -X2# = PX# + 48 -MOVSPR 3, X2#, PY# -X3# = PX# + 96 -MOVSPR 4, X3#, PY# +LABEL DRAWW +FOR R# = 0 TO 5 + BR2# = R# + GOSUB DRAWBR +NEXT R# RETURN ``` -**Do not write `MOVSPR 3, PX# + 48, PY#`.** `MOVSPR` reads a leading sign as *move by this -much* rather than *move to here* — see [Chapter 8](08-sprites.md#showing-and-moving) — and -an expression that starts with one is the relative form. Computing into `X2#` first is -unambiguous. +## Step 6: Write the main loop -Sprites are positioned by their top-left corner in **device pixels**, so ball, paddle and -walls are all plain pixel arithmetic. Only the bricks are in cells, and exactly one -routine converts between the two (Step 8). +**Goal: a loop that runs one frame's worth of work and comes back, for as long as the game +lasts.** -## Step 6: Write the main loop as a `GOTO` loop +Write the main loop with `LABEL` and `GOTO`: ```basic norun LABEL TICK @@ -388,28 +649,65 @@ STATE# = 0 GOTO TICK ``` -**`DO ... LOOP` would be the natural way to write this and it is the wrong one.** Every -loop and every `GOSUB` pushes one of 32 scopes, and a state change that jumped out of a -`DO` — losing a life, clearing a level — would leak its scope every single time. Thirty-two -lives later the program stops. A `GOTO` loop pushes nothing at all, so the deepest chain -in play is the six `GOSUB`s of a brick hit. +**A label is one bare word: letters and digits, no underscore.** `TICKEND` is a label; +`TICK_END` is a parse error, and the message you get is `UNKNOWN TOKEN _` rather than +anything about naming, so it is worth not writing in the first place. The same goes for +variable names. -Nothing here `GOSUB`s into the state changes either. They are branch targets that run and -then `GOTO TICK`, so the stack is empty again by the time the next frame starts. +`STATE#` is a plain number that the rest of the game writes when something has happened: +0 is playing, 1 is "the ball was lost", 2 is "the level is clear", 3 is "quit", 5 is +"start a new game". Nothing acts on it where it is set — the loop notices on the next pass +and branches. That keeps every state change in one place and out of the middle of the +physics. -`SLEEP 0.02` is the frame pacing, and **it does not block**: it records a deadline and -the step loop declines to advance the program until the clock reaches it, so the host -goes on drawing frames and pumping the keyboard the whole time. Fifty ticks a second is -what that asks for. Chapter 18 needs a much sharper instrument than this one and builds -it out of `TI#`. +The branch targets end in `GOTO TICK` rather than `RETURN`. Nothing `GOSUB`s into a state +change, so the subroutine stack is empty again by the time the next frame begins. -## Step 7: Read the keyboard through a countdown +**`SLEEP 0.02` is the frame pacing, and it does not block the host.** It records a +deadline and the interpreter declines to advance your program until the clock reaches it, +so the window keeps redrawing and the keyboard keeps being read the whole time. 0.02 +seconds asks for fifty frames a second. -`GET` is the only non-blocking read there is. There is **no key-up event and no held-key -state** — what you get instead is the host's own key repeat arriving as ordinary key-down -events, about one a tick while a key is held down. +### Why `GOTO` rather than `DO ... LOOP` -So drain the queue every tick, and let a keystroke refill a countdown: +A `DO ... LOOP` around the frame would read better, and it is not usable here: **a `GOTO` +that jumps out of a `FOR` or a `DO` does not release the loop's scope.** There are 32 +scopes, so a game that leaves its main loop once per lost life stops on the +thirty-second one: + +```basic +N# = 0 +LABEL TOP +DO + N# = N# + 1 + IF N# < 100 THEN GOTO TOP +LOOP UNTIL N# > 99 +PRINT "SURVIVED " + N# +``` + +```output +? 3 : PARSE ERROR Environment pool exhausted at line 3 (32 in use) + +``` + +A `LABEL`/`GOTO` loop pushes no scope at all, so it can run for as long as the machine is +on. Use `FOR` and `DO` freely for work that finishes inside a frame — the wall builder in +Step 5 is a `FOR` inside a `FOR` — and use `GOTO` for anything you will branch out of. +This is [`TODO.md` §6 item 37](../TODO.md); when it is fixed, `DO ... LOOP` will be +available for a main loop too, and a `GOTO` loop will still be correct. + +## Step 7: Read the keyboard + +**Goal: a paddle that moves smoothly while a key is held, using the only input verb there +is.** + +`GET` takes one keystroke from a queue the host fills, and returns 0 if there is nothing +waiting. It never blocks. There is **no key-up event and no way to ask whether a key is +currently down**. + +What you get instead is the operating system's own key repeat, arriving as ordinary +keypresses about once a frame while a key is held. So: drain the queue every frame, and +let each keystroke refill a countdown. ```basic norun LABEL READKEY @@ -431,6 +729,28 @@ IF K# = 27 THEN STATE# = 3 RETURN ``` +Eight `GET`s a frame is enough to keep the queue from backing up. `PDIR#` is which way to +move and `PDEC#` is how many more frames to keep moving; a keypress sets both. + +The key codes are the host's. 1073741903 and 1073741904 are the right and left arrows, 32 +is space, 112 is `P`, 113 is `Q` and 27 is escape. [Chapter 12](12-function-reference.md) +says where they come from. A quick way to find any other key is to print what you get: + +```basic norun +LABEL SHOWKEY +GET K# +IF K# <> 0 THEN PRINT "KEY " + K# +GOTO SHOWKEY +``` + +Note `0 - 1` rather than `-1` in `PDIR# = 0 - 1`. Both work; writing the subtraction out +is the habit that goes with the parenthesising rule from Step 3, and it is what the rest +of this chapter does. + +## Step 8: Move the paddle + +**Goal: the paddle moves while the countdown lasts, and stops at both edges.** + ```basic norun LABEL MOVEPAD IF PDEC# < 1 THEN GOTO PADCLMP @@ -443,18 +763,32 @@ IF PX# > M# THEN PX# = M# RETURN ``` -The paddle coasts to a stop over twelve ticks when the repeats stop, so **held reads as -held and tapped reads as a nudge** — with no held-key state anywhere. 1073741903 and -1073741904 are the right and left arrows; the codes are the host's, and Chapter 12's -`GET` entry says where they come from. +The countdown is the whole trick. A single tap sets `PDEC#` to 12 and the paddle coasts +for twelve frames; holding the key down refills it faster than it drains, so the paddle +keeps going. **Held reads as held and tapped reads as a nudge, with no held-key state +anywhere.** -Note `0 - 1` rather than `-1`. Unary minus on a literal is fine, but the habit of writing -the subtraction out is worth forming here: this parser reads `a - b + c` as `a - (b + c)`, -so every mixed `+` and `-` in the game is parenthesised as a rule rather than where it -happens to matter. It also matches only **one** unparenthesised `AND` or `OR` per -expression. Both are in [Chapter 13](13-differences.md). +The clamp is written as a jump to `PADCLMP` rather than wrapped around the movement, +because the paddle has to be pushed back inside the window whether it moved this frame or +not. -## Step 8: Move the ball one axis at a time +`M# = SCW# - PW#` is computed into a variable rather than written inline. Any expression +you are about to compare or pass is clearer that way, and it keeps lines short — **a line +here holds at most 32 tokens**, so long conditions want breaking up into named pieces +anyway. + +Going over 32 currently stops the interpreter rather than raising a BASIC error you could +`TRAP`, which makes a too-long line harder to diagnose than it should be. That is recorded +in [`TODO.md` §8](../TODO.md), and the fix is to report it as the parse error it is; the +limit itself stays either way, so short lines are the habit regardless. + +## Step 9: Move the ball + +**Goal: a ball that moves, bounces off three walls, and is lost off the bottom.** + +The ball has a position (`BX#`, `BY#`) and a velocity (`BVX#`, `BVY#`) in pixels per +frame. Moving it is adding one to the other — but do the two axes **separately**, and test +each move on its own: ```basic norun LABEL MOVEBAL @@ -472,12 +806,47 @@ IF BY# > LOSEY# THEN STATE# = 1 RETURN ``` -X first and then Y, **each move tested on its own**, so a ball arriving at the corner of -a brick reverses one axis rather than both. `OX#` and `OY#` remember where it came from, -which is what a collision backs it out to. +Doing X first and then Y means a ball arriving at the corner of a brick reverses one axis +rather than both, which is what looks right. `OX#` and `OY#` remember where the ball came +from, so a collision has somewhere to put it back to. -The brick test converts a pixel to a cell, and it is the only place in the program that -does: +A wall bounce puts the ball on the wall and flips the sign of that axis: + +```basic norun +LABEL WALLL +BX# = 0 +BVX# = 0 - BVX# +RETURN + +LABEL WALLR +BX# = MAXX# +BVX# = 0 - BVX# +RETURN + +LABEL WALLT +BY# = TOPY# +BVY# = 0 - BVY# +RETURN +``` + +There is no wall at the bottom. Below `LOSEY#` the ball is gone, and the loop's dispatch +in Step 6 picks that up. + +Draw a visible ceiling once, at startup, so the ball turns against something the player +can see: + +```basic norun +FRAME$ = "=" * COLS# +CHAR 1, 0, 1, FRAME$ +``` + +## Step 10: Break bricks + +**Goal: work out which brick a point is inside, and take it out of the wall.** + +This is the one routine that converts pixels into cells. Give it a point in `TX#` and +`TY#` and it sets `HIT#` to 1 or 0, and when it is 1 it also sets `BI#` — the index into +the sixty-element grid — and `BR2#`, the row: ```basic norun LABEL HITTEST @@ -496,10 +865,16 @@ IF BR#(BI#) = 1 THEN HIT# = 1 RETURN ``` -Callers hand it a point in `TX#`/`TY#` and read `HIT#`, `BI#` and `BR2#` back out — which -works only because Step 3 created all three outside the routine. +Read it as four questions in a row, each of which can answer "no": is the point below the +top of the wall, above the bottom of it, right of the left edge, left of the right edge. +Only if all four pass is there a cell to look at. Dividing a pixel by `CH#` gives a text +row; dividing by `CW#` gives a text column; dividing the column by `BRW#` gives a brick +column, because a brick is four cells wide. -**Test the ball's leading edge, not its centre:** +`HIT#`, `BI#` and `BR2#` work as answers only because Step 3 created all three outside +this routine. + +**Test the ball's leading edge rather than its centre:** ```basic norun LABEL XBRICK @@ -512,14 +887,48 @@ BX# = OX# BVX# = 0 - BVX# GOSUB KILLBR RETURN + +LABEL YBRICK +TX# = BX# + 4 +TY# = BY# +IF BVY# > 0 THEN TY# = BY# + 7 +GOSUB HITTEST +IF HIT# = 0 THEN RETURN +BY# = OY# +BVY# = 0 - BVY# +GOSUB KILLBR +RETURN ``` -A ball tested by its centre sinks four pixels into a brick before it turns, and it looks -like the brick was hit late. The cost of the leading edge is that a brick clipped at the -very corner can be missed by up to seven pixels' worth of ball; that is the better trade -of the two, and it is worth knowing which one you took. +Moving right, the leading edge is `BX# + 7`; moving left it is `BX#`. A ball tested by its +centre sinks four pixels into a brick before it turns, and it looks like the brick was hit +late. The cost of testing the edge is that a brick clipped at the very corner can be +missed by up to seven pixels' worth of ball. That is the better of the two trades, and it +is worth knowing which one you took. -## Step 9: Make the paddle bounce mean something +Breaking a brick clears its cell, scores it, and redraws **just that row**: + +```basic norun +LABEL KILLBR +BR#(BI#) = 0 +LEFTN# = LEFTN# - 1 +PTS# = (BROWS# - BR2#) * 10 +SCORE# = SCORE# + PTS# +GOSUB DRAWBR +GOSUB DRAWHUD +IF LEFTN# < 1 THEN STATE# = 2 +RETURN +``` + +The top row is worth 60 and the bottom row 10. `LEFTN#` reaching zero sets the state that +Step 6's dispatch turns into a level change. + +## Step 11: Bounce off the paddle + +**Goal: a bounce whose angle is decided by where on the paddle the ball landed.** + +This is the part that makes it a game rather than a demonstration. Divide the paddle into +five zones and let the zone choose the horizontal speed: ```basic norun LABEL PADHIT @@ -541,23 +950,42 @@ IF BVX# <> 0 THEN GOTO PADAIM BVX# = 2 IF PVX# < 0 THEN BVX# = 0 - 2 LABEL PADAIM -GOSUB BEEPP RETURN ``` -Five zones across the face and the zone decides the angle: `Z#` is 0 to 4, so the new -horizontal speed is -6, -3, 0, +3, +6. Where you catch it decides where it goes, which is -the entire game. +The first five lines are the overlap test, each written so it can bail out. `Z#` comes out +0 to 4, so `(Z# - 2) * 3` gives horizontal speeds of -6, -3, 0, +3 and +6. Catch the ball +on the left of the paddle and it goes left; catch it in the middle and it goes straight +up. -The dead-centre case needs its own handling. A ball with **no** sideways speed rises and -falls down one column for ever, which is how attract mode used to hang itself — so the -middle zone keeps the direction the ball came in on and gives it a shallow angle instead -of zero. +**Straight up is a problem, so the middle zone does not give it.** A ball with no sideways +speed rises and falls down the same column for ever and never reaches a brick it has not +already broken. The last three lines before `PADAIM` catch that case and give the ball a +shallow angle in the direction it arrived from instead. -There is a watchdog for the same failure in a subtler form. Ten seconds without touching -a brick means the ball has found an orbit that misses the wall, so it gets a new angle — -**at the next paddle bounce, never in mid-air.** Changing it in flight looks exactly like -the ball hitting something that is not there: +That handles the obvious case. The subtle one is a ball that has found some other orbit +missing the wall, and the cure for that is a watchdog. Count frames since the last brick, +and after ten seconds' worth mark the ball for a new angle. **Add these two lines to the +end of `MOVEBAL`** from Step 9, just before its `RETURN`: + +```basic norun +STALL# = STALL# + 1 +IF STALL# > 500 THEN NUDGE# = 1 +``` + +and **add one line to `KILLBR`** from Step 10, so that hitting a brick resets the count: + +```basic norun +STALL# = 0 +``` + +Then apply the nudge at the **next paddle bounce**, never in mid-air — a ball that changes +direction in open space looks exactly like it hit something invisible. **Add this line to +`PADHIT` above**, immediately before `LABEL PADAIM`: + +```basic norun +IF NUDGE# = 1 THEN GOSUB UNSTICK +``` ```basic norun LABEL UNSTICK @@ -570,24 +998,95 @@ IF BVX# = 0 THEN BVX# = 3 RETURN ``` -`RANDOM` is a linear congruential generator, because **this dialect has no `RND`** — and -no `INT`, `SQR`, `ASC` or `TIMER` either. `TI#` is jiffies off the host's clock and is -uptime rather than zero-based, which makes it a fine seed: +### You have to write your own random numbers + +**There is no `RND` in this dialect**, and no `INT`, `SQR`, `ASC` or `TIMER` either. A +linear congruential generator is nine tokens and does the job. Put the number of possible +answers in `RMAX#` and read the result from `RND#`: + +```basic +SEED# = 12345 +RMAX# = 6 +RND# = 0 +I# = 0 +FOR I# = 1 TO 5 + GOSUB RANDOM + PRINT "ROLL " + (RND# + 1) +NEXT I# +END -```basic norun LABEL RANDOM SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648) RND# = MOD((SEED# / 65536), RMAX#) RETURN ``` -The multiply stays inside a 64-bit integer for any seed under 2^31, and the answer comes -from the middle bits because the low bits of a power-of-two modulus barely move. -Integer division truncates for free, which is the `INT` you do not have. +```output +ROLL 1 +ROLL 5 +ROLL 2 +ROLL 1 +ROLL 2 +``` -## Step 10: The HUD and the messages +The multiplication stays inside a 64-bit integer for any seed below 2147483648, which is +why the modulus is that number. The answer is taken from the middle bits — `SEED# / 65536` +— because the low bits of a power-of-two modulus barely change from one call to the next. +Integer division truncating for free is the `INT` you do not have. -Same rule as the wall: build the whole line, write it once. +Seed it from the clock at startup. `TI#` is the host's uptime in sixtieths of a second, +which is different every time the game is run: + +```basic norun +SEED# = TI# +``` + +Use `RANDOM` for the serve, too, so the ball does not always leave in the same direction: + +```basic norun +LABEL SERVE +PX# = (SCW# - PW#) / 2 +HELD# = 1 +BX# = PX# + ((PW# / 2) - 4) +BY# = PY# - 10 +RMAX# = 2 +GOSUB RANDOM +BVX# = BSPD# +IF RND# = 0 THEN BVX# = 0 - BSPD# +BVY# = 0 - BSPD# +PDEC# = 0 +GOSUB SHOWSPR +RETURN +``` + +`HELD#` is the flag Step 6's loop tests: while it is 1 the ball sits on the paddle, and +`HOLDBAL` keeps it there: + +```basic norun +LABEL HOLDBAL +BX# = PX# + ((PW# / 2) - 4) +BY# = PY# - 10 +RETURN +``` + +Space clears `HELD#` and the ball launches. That is the `KEYSPC` that Step 7's `HANDKEY` +calls: + +```basic norun +LABEL KEYSPC +IF HELD# = 0 THEN RETURN +HELD# = 0 +GOSUB CLRMSG +RETURN +``` + +Step 15 adds two lines to the top of it. + +## Step 12: Draw the HUD and the messages + +**Goal: a status line and centred messages, both drawn the way `CHAR` wants.** + +Same rule as the brick rows: build the whole line as one string, then write it once. ```basic V# = 0 @@ -620,12 +1119,17 @@ RETURN SCORE 001250 LIVES 3 LEVEL 2 HIGH 009900 ``` -`"" + V#` is how a number becomes a string: `+` concatenates a string with a number, and -that is the conversion — see [Chapter 5](05-strings-and-formatting.md). Every `+` after -the first is parenthesised, for the reason in Step 7. +In the game that becomes a routine called `DRAWHUD`, whose last line is +`CHAR 1, 0, 0, H$` instead of `PRINT H$`; everything else is the same, which means you can +develop the HUD on either build. [Step 16](#step-16-put-the-program-together) has it +written out. -A centred message is the same idea, and needs no padding on the right because `CHAR` -terminating the row is what erases the rest of it: +`"" + V#` turns a number into a string, and `PAD6` pads it to six digits so the line does +not change width as the score grows. Every `+` after the first is parenthesised, for the +reason in Step 3. + +A centred message needs no padding on the right, because `CHAR` terminating the row is +what erases whatever was there before: ```basic norun LABEL SHOWAT @@ -641,13 +1145,120 @@ CHAR 1, 0, MROW#, " " RETURN ``` -Clearing a message is a single space written at column 0 — one character, and the -terminator behind it takes the rest of the row with it. +Set `MSG$` and `MROW#`, then `GOSUB SHOWAT`. Clearing a message is a single space written +at column 0 — one character, and the terminator behind it takes the rest of the row with +it. -## Step 11: Ask for sound once, then believe the answer +Two small wrappers save repeating the row number: + +```basic norun +LABEL SHOWMSG +MROW# = MSGROW# +GOSUB SHOWAT +RETURN + +LABEL CLRMSG +MROW# = MSGROW# +GOSUB CLRAT +RETURN +``` + +## Step 13: Lives, levels and game over + +**Goal: the states from Step 6, written out.** + +Each of these is a branch target, not a subroutine. It does its work and jumps back to +`TICK`. + +Losing a ball: + +```basic norun +LABEL LOSTLIF +LIVES# = LIVES# - 1 +GOSUB DRAWHUD +IF LIVES# < 1 THEN GOTO GAMEOVR +GOSUB SERVE +MSG$ = "BALL LOST -- PRESS SPACE" +GOSUB SHOWMSG +STATE# = 0 +GOTO TICK +``` + +Clearing a level. The ball speeds up each time, to a ceiling: + +```basic norun +LABEL LEVELUP +MSG$ = "LEVEL CLEARED" +GOSUB SHOWMSG +SLEEP 1.5 +LEVEL# = LEVEL# + 1 +IF BSPD# < 7 THEN BSPD# = BSPD# + 1 +GOSUB NEWLEV +STATE# = 0 +GOTO TICK + +LABEL NEWLEV +GOSUB LOADLAY +GOSUB BUILDW +GOSUB DRAWW +GOSUB DRAWHUD +GOSUB SERVE +MSG$ = "PRESS SPACE TO LAUNCH" +GOSUB SHOWMSG +RETURN +``` + +Game over waits for a key in a small loop of its own: + +```basic norun +LABEL GAMEOVR +MSG$ = "GAME OVER -- SPACE PLAYS AGAIN, Q QUITS" +GOSUB SHOWMSG +LABEL GOWAIT +GET K# +IF K# = 32 THEN GOTO NEWGAME +IF K# = 113 THEN GOTO BYE +IF K# = 27 THEN GOTO BYE +SLEEP 0.05 +GOTO GOWAIT +``` + +And quitting turns the sprites off, so they are not left on the screen behind the final +message: + +```basic norun +LABEL BYE +SPRITE 1, 0 +SPRITE 2, 0 +SPRITE 3, 0 +SPRITE 4, 0 +SCNCLR +PRINT "BREAKOUT" +PRINT "FINAL SCORE " + SCORE# +PRINT "HIGH SCORE " + HIGH# +SLEEP 2 +QUIT +``` + +The high score is one comparison, called whenever the score changes: + +```basic norun +LABEL HISCORE +IF SCORE# > HIGH# THEN HIGH# = SCORE# +RETURN +``` + +**There is nowhere to save it.** This interpreter has no disk — see +[Chapter 9](09-files-and-disk.md) — so the high score lasts as long as the process does. + +## Step 14: Add sound + +**Goal: sound where there is a sound device, silence where there is not, and no crash +either way.** `SOUND` refuses on a machine with no audio device, and an untrapped refusal ends the -game. Ask once at startup, record the answer, and never ask again: +program. So ask once at startup, remember the answer, and never ask again. Here is the +idea on its own, as a program you can run to see which kind of machine you are on: ```basic requires=noakgl SND# = 1 @@ -667,28 +1278,119 @@ RESUME NEXT NO AUDIO DEVICE, PLAYING SILENTLY ``` -That output is the stdio build, which has no devices at all. On the SDL build the same -program prints the other line. `TRAP` with no argument disarms the handler again — leave -it armed and the next refusal anywhere in the game is silently swallowed. See +That output is from the plain build, which has no devices at all; on the SDL build the +same program prints the other line. + +`TRAP NOAUDIO` arms a handler. `RESUME NEXT` returns to the statement after the one that +failed. **`TRAP` with no argument disarms it again** — leave it armed and the next +refusal anywhere in your game is silently swallowed. See [Chapter 4](04-control-flow.md#trapping-errors). -After that every sound is guarded by the flag it recorded: +In the game that becomes a routine and its handler, called once from the setup block +before anything else makes a noise: + +```basic norun +LABEL SNDPROBE +SND# = 1 +TRAP NOAUDIO +SOUND 1, 2000, 1 +TRAP +RETURN + +LABEL NOAUDIO +SND# = 0 +RESUME NEXT +``` + +`NOAUDIO` has no `RETURN` of its own, and must not have one: `RESUME NEXT` is what leaves a +handler, and it goes back into `SNDPROBE` at the `TRAP` after the `SOUND` — so the disarm +still happens and the `RETURN` after it is the one that runs. + +After that, every sound is one line guarded by the flag: ```basic norun LABEL BEEPB IF SND# = 1 THEN SOUND 1, 12000, 2 RETURN + +LABEL BEEPP +IF SND# = 1 THEN SOUND 2, 6000, 3 +RETURN + +LABEL BEEPW +IF SND# = 1 THEN SOUND 3, 9000, 1 +RETURN + +LABEL BEEPL +IF SND# = 1 THEN SOUND 1, 1500, 20 +RETURN ``` -**`SOUND`'s frequency argument is a SID register value, not hertz** — the pitch is -`register * 1022730 / 16777216`, and [Chapter 7](07-sound.md#sound) has the arithmetic. -Work the notes out once and write them down in a comment beside the numbers. +Then call them. Each is one `GOSUB` added to a routine you already have: -## Step 12: Give it an attract mode, and test with it +| Add | To | From | +|---|---|---| +| `GOSUB BEEPB` | `KILLBR`, before its `RETURN` | [Step 10](#step-10-break-bricks) | +| `GOSUB BEEPP` | `PADHIT`, at `LABEL PADAIM` | [Step 11](#step-11-bounce-off-the-paddle) | +| `GOSUB BEEPW` | `WALLL`, `WALLR` and `WALLT`, in all three | [Step 9](#step-9-move-the-ball) | +| `GOSUB BEEPL` | `LOSTLIF`, as its first line | [Step 13](#step-13-lives-levels-and-game-over) | -The title screen plays itself, and this is not decoration — it is how the game gets -tested without a hand on the keyboard. `DEMO#` is the only difference between the demo -and a real game: +So `WALLL` from Step 9 becomes: + +```basic norun +LABEL WALLL +BX# = 0 +BVX# = 0 - BVX# +GOSUB BEEPW +RETURN +``` + +and `WALLR` and `WALLT` take the same line in the same place. + +**`SOUND`'s frequency argument is a SID register value, not hertz.** The pitch you get is +`register * 1022730 / 16777216`, so 12000 is about 732 Hz. Work the notes you want out +once and write the numbers down in a comment beside them; +[Chapter 7](07-sound.md#sound) has the arithmetic. + +## Step 15: Add an attract mode + +**Goal: a title screen that plays the game by itself.** + +This is not decoration. A demo that plays for two minutes exercises the ball, the bricks, +the level change and the speed-up *together*, for thousands of frames, which is the only +way some defects show up at all. + +One flag is the whole difference between the demo and a real game: + +```basic norun +LABEL TITLE +DEMO# = 1 +SCORE# = 0 +LIVES# = 3 +LEVEL# = 1 +BSPD# = 4 +GOSUB LOADLAY +GOSUB BUILDW +GOSUB DRAWW +GOSUB DRAWHUD +GOSUB TITTEXT +GOSUB SERVE +STATE# = 0 +GOTO TICK +``` + +Everything else is four small edits to routines you already have. Each one says which. + +**In `MOVEPAD`** from Step 8, add these two lines at the top, immediately after the +`LABEL`, so that a demo paddle is driven by the machine instead of by `PDEC#`: + +```basic norun +IF DEMO# = 1 THEN GOSUB DEMOPAD +IF DEMO# = 1 THEN GOTO PADCLMP +``` + +The demo paddle tracks the ball — but **aims off-centre by a random amount**, chosen +afresh at every bounce: ```basic norun LABEL DEMOPAD @@ -708,54 +1410,444 @@ DOFF# = RND# - 40 RETURN ``` -The demo paddle tracks the ball but **aims off-centre by a random amount**, refreshed at -every bounce, so it plays like something with a hand on it and occasionally misses. A -demo that tracks perfectly never loses a ball and therefore never tests losing one. +`DOFF#` is between -40 and +40 pixels of deliberate error, so the machine plays like +something with a hand on the paddle and occasionally misses. **A demo that tracks +perfectly never loses a ball and therefore never tests losing one.** -Leave it running for two minutes and watch the score climb: that exercises the ball, the -bricks, the level change and the speed-up — and it is the only thing that exercises them -*together*, for long enough. Every defect this game found took thousands of ticks to show -itself, and a green test suite proved nothing about any of them. - -The machine does not get on the scoreboard. Attract mode keeps score so the wall behaves, -but only a player's score is ever the high one: +`DEMOAIM` has to be called from somewhere, and the somewhere is the bounce. **In `PADHIT`** +from Step 11, beside the `NUDGE#` line and immediately before `LABEL PADAIM`: ```basic norun -LABEL HISCORE -IF SCORE# > HIGH# THEN HIGH# = SCORE# +IF DEMO# = 1 THEN GOSUB DEMOAIM +``` + +**In `KEYSPC`** from Step 11, add two lines at the top, so that space starts a real game +rather than launching the demo's ball: + +```basic norun +LABEL KEYSPC +IF DEMO# = 1 THEN STATE# = 5 +IF DEMO# = 1 THEN RETURN +IF HELD# = 0 THEN RETURN +HELD# = 0 +GOSUB CLRMSG RETURN ``` -There is nowhere to save it. There is no disk in this interpreter — see -[Chapter 9](09-files-and-disk.md) — so the high score lasts as long as the process does. +**In `LOSTLIF`** from Step 13, add one line at the top, so a lost ball re-serves rather +than costing a life: + +```basic norun +LABEL LOSTLIF +IF DEMO# = 1 THEN GOTO DEMOSRV +LIVES# = LIVES# - 1 +``` + +with the branch target it needs: + +```basic norun +LABEL DEMOSRV +GOSUB SERVE +HELD# = 0 +STATE# = 0 +GOTO TICK +``` + +**In `SERVE`** from Step 11, add one line after `HELD# = 1`, so the demo's ball launches by +itself: + +```basic norun +IF DEMO# = 1 THEN HELD# = 0 +``` + +**In `KILLBR`** from Step 10, guard the scoreboard, because the machine does not get on it: + +```basic norun +IF DEMO# = 0 THEN GOSUB HISCORE +``` + +Leave it running and watch the score climb. That is your test suite for everything the +unit tests cannot reach. + +## Step 16: Put the program together + +**Goal: one file, in an order that runs.** + +A program runs from the top, so the order of the file matters in three places and nowhere +else: the setup has to come first, `DATA` has to be in the order it will be read, and every +`LABEL` has to exist somewhere. Subroutines can go in any order you like. + +This is the shape of the whole file: + +```text +LABEL SETUP the geometry from Step 2 + the declaration block from Step 3 + the brick faces from Step 5 + SEED# = TI# + the ceiling from Step 9 + GOSUB MKSPR Step 4 + GOSUB SNDPROBE Step 14 + GOTO TITLE + + the sprite routine, the sound routines, the title screen, + the game and level setup, the main loop, the input routines, + the paddle, the ball, the drawing routines, the state changes + --- in any order --- + + the sprite patterns as DATA Step 4 + the level layouts as DATA Step 5 +``` + +The two `DATA` blocks come last because nothing else in the program uses `DATA`, and the +sprite patterns are read before the layouts because `MKSPR` runs before `LOADLAY` does. +Change that order and each loader gets the other one's numbers. + +### Declare all of it + +Step 3 showed the shape of the declaration block. Here it is in full — every name the +finished game uses, and nothing gets to be created later: + +```basic norun +DIM BR#(60) +DIM SB#(63) +DIM SP#(63) +DIM LAY$(6) +DIM BSG$(6) + +SCORE# = 0 +HIGH# = 0 +LIVES# = 3 +LEVEL# = 1 +LEFTN# = 0 +BSPD# = 4 +STATE# = 0 +DEMO# = 0 +PAUSED# = 0 +HELD# = 0 +PDIR# = 0 +PDEC# = 0 +PX# = 0 +BX# = 0 +BY# = 0 +BVX# = 0 +BVY# = 0 +OX# = 0 +OY# = 0 +K# = 0 +HIT# = 0 +PVX# = 0 +DOFF# = 0 +STALL# = 0 +NUDGE# = 0 +BI# = 0 +BR2# = 0 +RB# = 0 +CB# = 0 +RC# = 0 +CC2# = 0 +TX# = 0 +TY# = 0 +PTS# = 0 +V# = 0 +L# = 0 +C2# = 0 +D# = 0 +M# = 0 +X2# = 0 +X3# = 0 +Z# = 0 +BB# = 0 +RX# = 0 +N# = 0 +MROW# = 0 +RMAX# = 2 +RND# = 0 +SND# = 0 +SEED# = 0 +P$ = "" +H$ = "" +S$ = "" +T$ = "" +MSG$ = "" +BSEG$ = "" +FRAME$ = "" +I# = 0 +R# = 0 +R2# = 0 +C# = 0 +CC# = 0 +KI# = 0 +T# = 0 +``` + +That is 69 names out of the 128 the interpreter has. Loop counters are in there too — +`I#`, `R#`, `C#`, `CC#`, `KI#` — because a `FOR` creates its counter the same way a +`GOSUB` creates a local, and declaring them costs nothing. + +### The routines the earlier steps referred to but did not show + +Four small ones, for completeness. + +Loading the sprite patterns at startup, which is Step 4 turned into a routine: + +```basic norun +LABEL MKSPR +FOR I# = 0 TO 62 + READ SB#(I#) +NEXT I# +FOR I# = 0 TO 62 + READ SP#(I#) +NEXT I# +SPRSAV SB#, 1 +SPRITE 1, 1, 2 +SPRSAV SP#, 2 +SPRSAV SP#, 3 +SPRSAV SP#, 4 +SPRITE 2, 1, 15, 0, 1, 0 +SPRITE 3, 1, 15, 0, 1, 0 +SPRITE 4, 1, 15, 0, 1, 0 +RETURN +``` + +Note this reads **all 63 numbers** for each pattern, so the `DATA` at the foot of the file +is 21 lines of three numbers per sprite rather than the shortened form Step 4's figure +used. Here are both, and the ball's is worth reading against the bit diagram in Step 4: + +```basic norun +DATA 60, 0, 0 +DATA 126, 0, 0 +DATA 255, 0, 0 +DATA 255, 0, 0 +DATA 255, 0, 0 +DATA 255, 0, 0 +DATA 126, 0, 0 +DATA 60, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 + +DATA 255, 255, 255 +DATA 255, 255, 255 +DATA 255, 255, 255 +DATA 255, 255, 255 +DATA 255, 255, 255 +DATA 255, 255, 255 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +DATA 0, 0, 0 +``` + +Eight rows of ball in the top-left corner and thirteen empty ones; six solid rows of paddle +and fifteen empty ones. The zeros are not optional — `MKSPR` reads 63 numbers per pattern +and will take them from the level layouts if they are not there. + +The HUD, which is Step 12's block with `CHAR` instead of `PRINT`: + +```basic norun +LABEL DRAWHUD +V# = SCORE# +GOSUB PAD6 +H$ = " SCORE " + P$ +H$ = (H$ + " LIVES ") + LIVES# +H$ = (H$ + " LEVEL ") + LEVEL# +V# = HIGH# +GOSUB PAD6 +H$ = (H$ + " HIGH ") + P$ +CHAR 1, 0, 0, H$ +RETURN +``` + +Pause, which is the `P` key from Step 7, and which the demo ignores: + +```basic norun +LABEL KEYPAU +IF DEMO# = 1 THEN RETURN +PAUSED# = 1 - PAUSED# +IF PAUSED# = 1 THEN MSG$ = "PAUSED" +IF PAUSED# = 1 THEN GOSUB SHOWMSG +IF PAUSED# = 0 THEN GOSUB CLRMSG +RETURN +``` + +Starting a real game, which is what `STATE# = 5` dispatches to, and the title text and its +eraser: + +```basic norun +LABEL NEWGAME +DEMO# = 0 +SCORE# = 0 +LIVES# = 3 +LEVEL# = 1 +BSPD# = 4 +PAUSED# = 0 +GOSUB CLRTIT +GOSUB NEWLEV +STATE# = 0 +GOTO TICK + +LABEL TITTEXT +MROW# = TITROW# +MSG$ = "B R E A K O U T" +GOSUB SHOWAT +MROW# = TITROW# + 2 +MSG$ = "PRESS SPACE TO PLAY" +GOSUB SHOWAT +MROW# = TITROW# + 4 +MSG$ = "ATTRACT MODE" +GOSUB SHOWAT +RETURN + +LABEL CLRTIT +MROW# = TITROW# +GOSUB CLRAT +MROW# = TITROW# + 2 +GOSUB CLRAT +MROW# = TITROW# + 4 +GOSUB CLRAT +RETURN +``` + +### Check it before you have a window + +Run the assembled file through the plain build first: + +```sh norun +$ ./build/basic mybreakout.bas +``` + +It will stop at the first `RGR` with a message about a missing graphics device — which +means everything above that point parsed, and that is the whole of what this check is for. +A parse error, a bad line number or an `OUT OF DATA` here is a real problem, and it is far +easier to find without a window in the way. Then run it properly: + +```sh norun +$ ./build-akgl/basic mybreakout.bas +``` ## The rules this listing never breaks -Every one of these was learned by breaking it. They are the checklist to write your own -game against: +A checklist to write your own game against. | Rule | Because | Where | |---|---|---| -| `DIM` at the top, never in a loop | An array created inside a scope costs pool slots for good; 4096 ends the run. A scalar is free | Step 3 | -| Build a text row whole and write it from column 0 | `CHAR` terminates the row where it stops, so a second write erases the tail of the first | Step 4 | -| Tell rows apart by shape, not colour | `CHAR` ignores its colour argument | Step 4 | -| Compute a `MOVSPR` coordinate into a variable first | A leading sign means *move by*, not *move to* | Step 5 | -| Loop the game with `GOTO`, not `DO` | 32 scopes, and jumping out of a loop leaks one | Step 6 | -| Parenthesise every mixed `+` and `-` | `a - b + c` parses as `a - (b + c)` | Step 7 | -| One `AND` or `OR` per expression unless parenthesised | The parser matches one | Step 7 | -| Keep a line under 32 tokens | The 33rd kills the interpreter with a stack trace, not a BASIC error you can `TRAP` | — | -| Span a loop across lines, and never `FOR I = 1 TO 1` | A whole loop on one line does not loop; equal bounds run zero times | [Ch 13](13-differences.md) | -| Probe for a device once and remember | An untrapped refusal ends the program | Step 11 | +| `DIM` at the top, never inside a loop or a `GOSUB` | An array created inside a scope takes pool storage that is never returned | [Step 3](#step-3-declare-every-name-first) | +| Declare any name a subroutine has to answer through | A name first seen inside a `GOSUB` dies at `RETURN`, silently | [Step 3](#step-3-declare-every-name-first) | +| Parenthesise every mixed `+` and `-` | `a - b + c` is currently evaluated as `a - (b + c)` | [Step 3](#step-3-declare-every-name-first) | +| Parenthesise a second `AND` or `OR` | Only one unparenthesised `AND` or `OR` is matched per expression | [Step 3](#step-3-declare-every-name-first) | +| Compute a `MOVSPR` coordinate into a variable first | A leading sign means *move by*, not *move to* | [Step 4](#step-4-make-the-ball-and-the-paddle) | +| Build a text row whole and write it from column 0 | `CHAR` terminates the row where it stops | [Step 5](#step-5-build-the-brick-wall) | +| Tell rows apart by shape, not colour | `CHAR` ignores its colour argument | [Step 5](#step-5-build-the-brick-wall) | +| Loop the game with `GOTO`, and never jump out of a `DO` | A `GOTO` out of a loop does not release the loop's scope, and there are 32 | [Step 6](#step-6-write-the-main-loop) | +| Name things with letters and digits only | There is no underscore in an identifier, and the error names the character rather than the name | [Step 6](#step-6-write-the-main-loop) | +| Keep a line under 32 tokens | The 33rd stops the interpreter rather than raising an error you can `TRAP` | [Step 8](#step-8-move-the-paddle) | +| Span a loop across lines, and never write `FOR I# = 1 TO 1` | A whole loop on one line does not loop; equal bounds run the body zero times | [Chapter 13](13-differences.md) | +| Probe for a device once and remember the answer | An untrapped refusal ends the program | [Step 14](#step-14-add-sound) | + +## The picture at the top of this chapter + +Every figure in this guide is generated by running the listing beside it, and the one at +the top of this chapter is no exception. It is the game's own drawing code — the row +builder from Step 5, the sprite patterns from Step 4, the HUD from Step 12 — with the +numbers filled in by hand and no main loop, so it draws one frame and stops. + +Type it in and you have a still of the game before you have written any of the game: + +```basic requires=akgl screenshot=breakout-game size=800x600 text=1 +DIM SB#(63) +DIM SP#(63) +DIM BSG$(6) +DIM LAY$(6) +I# = 0 +R# = 0 +C# = 0 +S$ = "" +T$ = "" +SCNCLR +COLS# = RWINDOW(1) +BLEFT# = (COLS# - 40) / 2 +BSG$(0) = "[##]" +BSG$(1) = "[##]" +BSG$(2) = "[==]" +BSG$(3) = "[==]" +BSG$(4) = "[--]" +BSG$(5) = "[--]" +LAY$(0) = "1111111111" +LAY$(1) = "1111111111" +LAY$(2) = "1111111111" +LAY$(3) = "0111111110" +LAY$(4) = "0110110110" +LAY$(5) = "0010110100" +CHAR 1, 0, 0, " SCORE 001250 LIVES 3 LEVEL 2 HIGH 009900" +CHAR 1, 0, 1, "=" * COLS# +FOR R# = 0 TO 5 + S$ = " " * BLEFT# + FOR C# = 0 TO 9 + T$ = MID(LAY$(R#), C#, 1) + IF T$ = "1" THEN S$ = S$ + BSG$(R#) + IF T$ = "0" THEN S$ = S$ + " " + NEXT C# + I# = 4 + R# + CHAR 1, 0, I#, S$ +NEXT R# +CHAR 1, 0, 35, " PRESS SPACE TO LAUNCH" +FOR I# = 0 TO 62 + SB#(I#) = 0 + SP#(I#) = 0 +NEXT I# +FOR I# = 0 TO 7 + READ SB#(I# * 3) +NEXT I# +FOR I# = 0 TO 17 + SP#(I#) = 255 +NEXT I# +SPRSAV SB#, 1 +SPRSAV SP#, 2 +SPRSAV SP#, 3 +SPRSAV SP#, 4 +SPRITE 1, 1, 2 +SPRITE 2, 1, 15, 0, 1, 0 +SPRITE 3, 1, 15, 0, 1, 0 +SPRITE 4, 1, 15, 0, 1, 0 +MOVSPR 1, 396, 518 +MOVSPR 2, 328, 528 +MOVSPR 3, 376, 528 +MOVSPR 4, 424, 528 + +DATA 60, 126, 255, 255, 255, 255, 126, 60 +``` + +![](images/breakout-game.png) ## Where to go next -- **[Chapter 18](18-tutorial-breakout-artwork.md)** builds Breakout again with sprite - artwork, powerups and a coloured HUD. Every architectural decision comes out different, - and the chapter is about why. +- **[Chapter 18](18-tutorial-breakout-artwork.md)** builds Breakout again out of loaded + artwork, with powerups and a coloured HUD. It is a bigger program and it teaches the + drawing verbs, which this chapter never uses. - **[Chapter 13](13-differences.md)** is the full list of what this dialect does differently from BASIC 7.0. -- **[Chapter 14](14-architecture.md)** is the interpreter itself — the step loop, the - pools and how to debug a program that stops for no visible reason. -- `TODO.md` §6 and §9 carry the thirteen defects these two games found, with the - reduction for each. Eleven have since been fixed and are struck; the entries are kept - because the reduction and the cause are worth more than the tidiness of deleting them. +- **[Chapter 8](08-sprites.md)** is the sprite reference, and + **[Chapter 4](04-control-flow.md)** is the whole of `GOSUB`, `TRAP` and the loop forms. +- **[Chapter 14](14-architecture.md)** is the interpreter itself: the step loop, the pools + and how to debug a program that stops for no visible reason. diff --git a/docs/18-tutorial-breakout-artwork.md b/docs/18-tutorial-breakout-artwork.md index b53c12a..15aca20 100644 --- a/docs/18-tutorial-breakout-artwork.md +++ b/docs/18-tutorial-breakout-artwork.md @@ -1,14 +1,30 @@ # 18. Tutorial: Breakout with artwork -[Chapter 17](17-tutorial-breakout.md) built Breakout out of text and two `DATA` sprites. -This chapter builds it again out of **downloaded artwork**, with powerups, a coloured HUD -and three voices of sound — and almost nothing about the shape of the program survives the -change. The finished listing is +This chapter builds Breakout a second time, out of loaded PNG artwork, with a drawn +brick field, five powerups, three voices of sound and a HUD in colour. It is a bigger +program than [Chapter 17](17-tutorial-breakout.md)'s and it uses a completely different set +of verbs: everything here is drawn or loaded, and nothing is written into the text grid. + +This is what you are building: + +![The finished game: a coloured HUD across the top, five rows of coloured bricks, a gem falling, the ball above the paddle](images/breakout-game-artwork.png) + +The finished listing is [`examples/breakout/sprites/breakout.bas`](../examples/breakout/sprites/breakout.bas). Read Chapter 17 first if you have not. The rules it teaches — declare every name up front, -loop with `GOTO`, parenthesise mixed `+` and `-` — all still apply here and are not -repeated. +loop with `GOTO`, parenthesise mixed `+` and `-`, build a random-number generator — all +apply here too, and are not repeated. + +One of them is worth repeating, because this chapter leans on it harder than Chapter 17 +does. **`#` is an integer, `%` is a float and `$` is a string.** That is the opposite way +round from Commodore BASIC, and it is why ball positions and velocities here are `BLX%` and +`BLVX%` while counters and flags are `BLN#` and `BLON#`. Get one wrong and the ball moves in +whole pixels, or stops. + +[Chapter 3](03-the-language.md) is the language reference for the rest — `MOD`, `INSTR`, +`MID`, `LEN` — and [Chapter 4](04-control-flow.md) is `BEGIN`/`BEND`, `DO ... LOOP UNTIL` +and `GOSUB`. ```sh norun $ ./build-akgl/basic examples/breakout/sprites/breakout.bas @@ -33,13 +49,42 @@ tells you which it is. | blue | STICKY | the ball sticks where it lands; space fires it | 18 seconds | | purple | CATCH | a second bar appears higher up the field | 24 seconds | +## What you will do + +- **[Step 1](#step-1-put-artwork-on-the-screen)** — load PNG artwork into sprites +- **[Step 2](#step-2-budget-the-eight-sprite-slots)** — decide what each of the eight + sprites is, before writing anything else +- **[Step 3](#step-3-turn-a-drawing-into-a-sprite)** — draw something and turn it into a + sprite so it stays on the screen +- **[Step 4](#step-4-make-the-brick-stamps)** — make one brick per colour and stamp the + field out of them +- **[Step 5](#step-5-find-the-frame-boundary)** — find the host's frame boundary, so a + drawing survives being captured +- **[Step 6](#step-6-draw-at-most-one-thing-per-frame)** — draw at most one thing per + frame, chosen by dirty flags +- **[Step 7](#step-7-keep-a-flattened-list-of-live-bricks)** — keep a flattened list of the + bricks still standing, so the draw routine decides nothing +- **[Step 8](#step-8-draw-lettering-with-a-stroke-font)** — draw lettering with a stroke + font, because text has no colour +- **[Step 9](#step-9-move-and-bounce-the-ball)** — move the ball and bounce it off bars and + bricks without a square root +- **[Step 10](#step-10-gems-and-powerups)** — drop gems and apply what catching one does +- **[Step 11](#step-11-three-voices-and-a-mute)** — three voices of sound and a mute that + costs nothing +- **[Step 12](#step-12-the-states)** — the frame loop's states: title, serve, play, lost, + cleared +- **[Step 13](#step-13-put-the-program-together)** — put the pieces in one file, in the + right order + --- -## Step 1: Put the artwork on the screen +## Step 1: Put artwork on the screen -`SPRSAV` loads an image file straight into a sprite slot, and a sprite loaded that way -keeps **the image's own size** rather than being forced to 24 by 21 — see -[Chapter 8](08-sprites.md#from-an-image-file). +**Goal: a picture from a file, on the screen, at its own size.** + +`SPRSAV` takes an image file path as well as an array of pattern bytes, and a sprite +loaded from a file **keeps the image's own size** rather than being squeezed into 24 by +21. That is the whole of it: ```basic requires=akgl setup=breakout_art screenshot=breakout-artwork I# = 0 @@ -60,25 +105,41 @@ MOVSPR 7, 130, 120 MOVSPR 8, 230, 120 ``` -![](images/breakout-artwork.png) +![Two paddles, a ball and three coloured gems](images/breakout-artwork.png) -That is the whole game's cast: two bars, a ball and five gems. The path is tried against -the working directory first and then against the directory the program was loaded from, -so a `.bas` stored beside its `art/` runs from anywhere. +That is the whole game's cast: two bars, a ball and five gems. -The artwork here is Kenney's [Puzzle Pack 1](https://kenney.nl/assets/puzzle-pack-1), -released under [CC0](http://creativecommons.org/publicdomain/zero/1.0/); -`examples/breakout/sprites/art/PROVENANCE.md` records which file is used for what. -Crediting Kenney is not required by CC0 — do it anyway. +**Write down the sizes now**, because every collision test later in this chapter is built +out of them: -**`SPRITE n, 1, 2` turns a sprite on in colour 2.** A sprite's colour *multiplies* the -artwork rather than replacing it, so colour 2 (white) is what leaves the artwork looking -like itself. +| Artwork | Is | Shows up later as | +|---|---|---| +| `paddleBlu.png`, `paddleRed.png` | 104 by 24 | `PDW# = 104`, and the bar's height in `HITBAR` | +| `ballBlue.png`, `ballGrey.png` | 22 by 22 | `+ 21` on every edge of the ball's box | +| the five gems | 48 by 46 | the `+ 48` and `+ 46` in the catch test | -## Step 2: Budget the eight sprite slots before you write anything else +Three more things to note. -There are eight sprites. That is not a limit you will design your way around, so decide -what they are first: +**The path is tried against the working directory first, then against the directory the +program was loaded from.** A `.bas` stored beside its own `art/` folder therefore runs from +anywhere. + +**`SPRITE n, 1, 2` turns sprite `n` on in colour 2.** A sprite's colour *multiplies* the +artwork rather than replacing it, so colour 2 — white — is the one that leaves loaded +artwork looking like itself. Any other colour tints it. + +**Put the art where the licence lets you.** The artwork here is Kenney's +[Puzzle Pack 1](https://kenney.nl/assets/puzzle-pack-1), released under +[CC0](http://creativecommons.org/publicdomain/zero/1.0/), and +`examples/breakout/sprites/art/PROVENANCE.md` records which file is used for what. CC0 +does not require crediting Kenney. Do it anyway. + +## Step 2: Budget the eight sprite slots + +**Goal: know what all eight sprites are before you write the program.** + +There are eight sprite slots and no more. That is not a limit you will design your way +around later, so spend them on paper first. This game spends them like this: | Slot | Is | |---|---| @@ -89,32 +150,28 @@ what they are first: | 5, 6, 7 | up to three balls | | 8 | the falling gem | -Two of the eight are **the screen**, and Step 3 is why. That leaves six for everything -else, which is the reason **the bricks are drawn rather than made of artwork** — sixty of -them will not fit in six slots, and there is no way to get artwork onto the screen other -than a sprite. `GSHAPE` cannot stamp a sprite and `SPRSAV` cannot read one back out. +Two of the eight are **the screen itself**, and Step 3 explains why. That leaves six for +everything that moves, and two consequences fall straight out of it: -It is also the reason for "one gem at a time": there is one slot for it, so a brick -broken while a gem is falling drops nothing. +- **The bricks are drawn rather than made of artwork.** Sixty bricks will not fit in six + slots, and a sprite is the only way to get an image file onto the screen — `GSHAPE` + cannot stamp a sprite and `SPRSAV` cannot read one back out. +- **Only one gem falls at a time.** There is one slot for it, so a brick broken while a gem + is already falling drops nothing. -## Step 3: Turn what you drew into a sprite +Deciding this first is what stops a feature costing an afternoon before it is abandoned. -In the standalone SDL build the text layer repaints every row of the window, opaque, -after your program's steps have run and before the frame is presented — so by default -**anything `DRAW`, `BOX` or `CIRCLE` puts on the screen is painted over before anybody -sees it**. Sprites are drawn after the text layer, which makes a sprite the one thing a -program can rely on being visible without doing anything else about it. +## Step 3: Turn a drawing into a sprite -`WINDOW 0, 0, 49, 1` would hand the rest of the window to the drawing verbs. This program -does not, and would not want to: a drawing has to be re-issued every frame to survive -double buffering *and* fit inside one 256-line batch to survive the capture, which is -Step 4. A sprite has neither constraint — it is drawn from state the interpreter keeps. +**Goal: something you drew, still on the screen on the next frame.** -So: **draw it, capture it with `SSHAPE`, install the capture with `SPRSAV`.** Once it is -a sprite it stays on the screen for nothing. +Two layers are redrawn for you every frame from state the interpreter keeps: the text grid +and the sprites. **The drawing verbs are not one of them.** `DRAW`, `BOX`, `CIRCLE` and +`PAINT` write straight into the renderer's buffer, and by default the text layer owns +every row of the window and repaints over them before the frame is shown. -(The figures in this chapter are rendered by a tool that draws no text layer, which is -why they can show a bare drawing at all.) +So a drawing has to be turned into something that persists. The sequence is: draw it, +capture it with `SSHAPE`, install the capture into a sprite with `SPRSAV`. ```basic norun SSHAPE Z$, 0, 60, 800, 600 @@ -123,13 +180,33 @@ SPRITE 2, 1, 2 MOVSPR 2, 0, 60 ``` -Four lines, and they are the last four of every draw routine in the game. `Z$` holds a -handle rather than pixels — see [Chapter 6](06-graphics.md#saving-and-stamping-regions) — -which is all `SPRSAV` needs. +Four lines, and they are the last four lines of every draw routine in this game. `SSHAPE` +captures the rectangle from (0, 60) to (800, 600) into `Z$`; `SPRSAV` installs it into +slot 2; `SPRITE` turns the slot on; `MOVSPR` puts it back where it was drawn. -### Stamp the bricks; do not paint them +`Z$` holds a **handle**, not pixels — see +[Chapter 6](06-graphics.md#saving-and-stamping-regions). You can pass it to `GSHAPE` and +`SPRSAV`. You cannot print it, save it or take its `LEN`. -Draw one brick per colour, capture the six of them, and stamp them with `GSHAPE`: +Once it is a sprite it stays on the screen for nothing, and that is the point: a sprite is +drawn from state the interpreter keeps, so it costs the program no work per frame at all. + +`WINDOW 0, 0, 49, 1` would shrink the text area and hand the rest of the window to the +drawing verbs, which is the other way to make a drawing visible. This program does not use +it, because a drawing kept that way still has to be re-issued every frame *and* fit inside +one batch (Step 5), and a sprite has neither constraint. Making the drawing verbs visible +without a `WINDOW` call is [`TODO.md` §9 item 3](../TODO.md). + +The figures in this chapter are rendered by a tool that draws no text layer, which is why +they can show a bare drawing at all. + +## Step 4: Make the brick stamps + +**Goal: six coloured bricks, and a field stamped out of them.** + +Draw one brick per row colour, capture the six of them, and then stamp them wherever a +brick belongs with `GSHAPE`. Drawing six things and stamping sixty is far cheaper than +drawing sixty. ```basic requires=akgl screenshot=breakout-stamps size=100x130 DIM BRC#(6) @@ -155,60 +232,48 @@ NEXT R# DATA 3, 9, 8, 6, 4, 5 ``` -![](images/breakout-stamps.png) +![Six bricks, one per row colour, stacked vertically](images/breakout-stamps.png) -Six 68 by 16 bricks, filled **two scan lines at a time** so the set costs about a hundred -and thirty lines rather than the two hundred and seventy a line-at-a-time loop would take. -Step 4 explains why that number matters. +Reading that from the top: `GRAPHIC 1, 1` selects the graphics mode and clears it. +`COLOR 1, BRC#(R#)` sets **colour source 1** to the palette entry that row wants — a +drawing verb names a source, not a colour, and there are seven sources. `DRAW 1, x1, y1 TO +x2, y2` draws a line using source 1. -**`PAINT` would be one statement instead of sixteen and is not an option.** It costs -nearly four milliseconds a call; sixty of those is seven frames. +Each brick is 68 by 16 and is filled with horizontal lines. The inner loop draws **two +scan lines per pass**, `T1# + K#` and `T2# + K#`, so eight passes fill sixteen rows. That +costs about a hundred and thirty lines for the whole set instead of the two hundred and +seventy a line-at-a-time loop would take, and Step 5 explains why that number matters. -`SSHAPE` has sixteen slots, nothing gives one back, and `GRAPHIC 5` gives back all of -them at once. So the game counts what it has spent and rebuilds the stamps from scratch -whenever the pool runs dry: +Capturing the six is six `SSHAPE`s: ```basic norun -LABEL DRAWJOB -IF SHN# < 14 THEN GOTO DRAWJOB2 -GOSUB DRAWPROTOS -RETURN +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$ ``` -### Flatten the field before you draw it - -The draw routine should not be deciding anything. When a brick breaks, walk the grid and -write out a **list of the bricks still standing**, row by row — so a row is a contiguous -run of that list, and drawing it is a stamp and an advance: +**`SSHAPE` has sixteen slots and nothing gives one back.** `GRAPHIC 5` gives back all of +them at once, so count what you have spent and rebuild the stamps when the pool runs dry: ```basic norun -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 DRAWPROTOS +GRAPHIC 5 +GRAPHIC 1, 1 +WIDTH 1 +SHN# = 99 ``` -`RS#(R#)` is where row `R#`'s run starts and `RC#(R#)` is how long it is. This costs about -four lines a brick and has all the time in the world; the draw costs two and has a -deadline. **That trade is the spine of this program** and it comes back in Step 6 for the -lettering. +with the six `DRAW` loops and six `SSHAPE`s after it, and `SHN# = 6` at the end. Every +routine that captures adds one to `SHN#`, and the frame loop rebuilds when it reaches 14. -Now the whole screen, drawn and captured and dressed with artwork: +**`PAINT` would be one statement instead of sixteen, and is not usable here.** It costs +nearly four milliseconds a call; sixty of those is seven frames' worth of time. + +Now the whole screen — walls, the brick field stamped out of the six, and the artwork on +top: ```basic requires=akgl setup=breakout_art screenshot=breakout-screen size=800x600 DIM BRC#(6) @@ -265,26 +330,31 @@ MOVSPR 5, 389, 517 DATA 3, 9, 8, 6, 4, 5 ``` -![](images/breakout-screen.png) +![The full field: a double border, six rows of coloured bricks, the paddle and the ball](images/breakout-screen.png) -Everything above the last four lines is a drawing nobody would ever see. `SSHAPE` and -`SPRSAV` are what make it the screen. +Everything above the `SSHAPE Z$, 0, 60, 800, 600` is a drawing nobody would ever see. +Those four lines are what make it the screen. -The game keeps its six stamps in **six separate scalars** rather than an array, which it -had to when it was written — see the fourth trap at the end of this chapter. An array -works now. +A brick is 68 by 16 on a 72 by 24 pitch, ten columns by six rows, with the field's top-left +corner at (42, 108). Those numbers are worth writing down once; they come back in Steps 7 +and 9. -## Step 4: Find the frame boundary +The finished game keeps its six stamps in six separate scalars — `S0$` to `S5$` — rather +than in an array. An array works too. + +## Step 5: Find the frame boundary + +**Goal: a drawing that is not cut in half by the host presenting the frame.** The host runs **256 source lines and then presents the frame**, and presenting throws the -drawing buffer away. So everything between a `GRAPHIC 1, 1` and its `SSHAPE` has to -happen inside one of those batches. Draw more than that and the capture comes back -holding only the tail of what you drew, over whatever the frame before it left behind — +drawing buffer away. So all the drawing between a `GRAPHIC 1, 1` and its `SSHAPE` has to +happen inside one of those batches. Draw more than fits and the capture comes back holding +only what was issued since the present, over whatever the frame before it left behind — which looks exactly like a ghost. -`TI#` is refreshed from the host's clock once per batch, so **the step on which `TI#` -changes is the first step of a batch**. Spinning until it changes is the only way a -program in this dialect can locate a frame boundary: +`TI#` is the host's clock in sixtieths of a second, and it is refreshed **once per batch**. +So the step on which `TI#` changes is the first step of a batch, and spinning until it +changes is how a program finds a frame boundary: ```basic norun LABEL PACE @@ -294,23 +364,29 @@ IF TI# - LASTT# < 2 THEN GOTO PACEEDGE RETURN ``` -Two jiffies is thirty frames a second. **`LASTT#` is sampled on entry rather than carried -over from the last frame, and that is the whole correctness of the routine.** Carried -over, a frame whose work ran long finds the time already spent, returns immediately from -somewhere in the middle of a batch, and the capture that follows is ruined. Sampling here -means the loop always sees `TI#` change under it, and a change is only ever seen on the -first step of a batch. +Two jiffies is thirty frames a second. + +**`LASTT#` is sampled on entry rather than carried over from the last frame, and that is +the whole correctness of the routine.** Carried over, a frame whose work ran long would +find the time already spent, return immediately from somewhere in the middle of a batch, +and ruin the capture that followed. Sampling here means the loop always sees `TI#` change +under it, and a change is only ever seen on the first step of a batch. Measured on one machine: after a jiffy edge, 220 lines of drawing survive the capture -intact and 250 do not. Every draw routine in the game is written to stay near 200. +intact and 250 do not. Write every draw routine to stay near 200 and you have margin. -**Arithmetic is free.** A routine that computes for two thousand steps costs frame rate -and nothing else. Only drawing has a deadline — which is what makes Step 3's flattening -and Step 6's stroke lists worth their complexity. +**Arithmetic is free.** A routine that computes for two thousand steps costs frame rate and +nothing else — only *drawing* has a deadline. That single fact is what Steps 7 and 8 are +built on: move everything that is not a drawing verb out of the routine that draws. -## Step 5: Do at most one capture per frame +This is the host's budget rather than the interpreter's, so the pacing routine is the +answer rather than a workaround for something being fixed; +[`TODO.md` §9 item 5](../TODO.md) records the measurements and why no fix belongs in the +library. -The frame loop paces first, then draws at most one thing, then plays the game: +## Step 6: Draw at most one thing per frame + +**Goal: a frame loop that paces, draws one thing, and then plays the game.** ```basic norun LABEL FRAME @@ -326,7 +402,11 @@ IF RUNNING# = 0 THEN GOTO SHUTDOWN GOTO FRAME ``` -`DRAWJOB` is a queue of one, chosen by dirty flags — stamps first because everything else +Pace first, because that is what puts the drawing at the top of a batch. Then at most one +capture, because a capture is the only thing with a deadline. Then the game, which may +take as long as it likes. + +`DRAWJOB` is a queue of one, chosen by dirty flags — stamps first, because everything else draws with them, then the field, then the HUD: ```basic norun @@ -346,24 +426,123 @@ DHUD# = 0 RETURN ``` -The game sets `DPLAY# = 1` or `DHUD# = 1` when something changes and never draws -directly. One consequence is visible and deliberate: **the score lags the bricks by one -frame**, because the field goes first. At thirty frames a second nobody can see it. +The rest of the game never draws. It sets `DPLAY# = 1` or `DHUD# = 1` when something has +changed and gets on with its frame. One consequence is visible and deliberate: **the score +lags the bricks by one frame**, because the field is drawn first. At thirty frames a second +nobody can see it. -Those are `LABEL`s and `GOTO`s rather than `BEGIN` blocks, and when this was written that -was not a style choice: a loop inside a block that was skipped left the interpreter with -no `GOSUB` to return from. That is fixed — see trap 3 below — and the shape is kept -because it costs nothing and reads the same. +Those are `LABEL`s and `GOTO`s rather than `BEGIN` blocks. Either works; the labels keep +each arm to one `RETURN` and read the same. -## Step 6: Draw the lettering, because text has no colour +## Step 7: Keep a flattened list of live bricks -The interpreter's text sink is white and has no verb that changes it; `CHAR` parses a +**Goal: a draw routine that decides nothing.** + +The draw routine has a deadline and the rest of the program does not, so any decision that +can be made earlier should be. When a brick breaks, walk the grid once and write out a +**list of the bricks still standing**, row by row — so a row becomes a contiguous run of +that list, and drawing it is a stamp and an advance: + +```basic norun +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 +``` + +`LX#()` and `LY#()` are the pixel positions of the bricks still alive. `RS#(R#)` is where +row `R#`'s run starts in that list and `RC#(R#)` is how long it is. + +Drawing a row is then one loop with no tests in it: + +```basic norun +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 +``` + +and `DRAWPLAY` calls it six times, once per stamp: + +```basic norun +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 +``` + +Building the list costs about four lines a brick and has all the time in the world. +Drawing costs two and has a deadline. **That trade is the spine of this program**, and it +comes back in Step 8 for the lettering. + +### Write the one-item case out beside every loop + +Notice the two lines before the `FOR` in `STAMPROW`. **A `FOR` whose bounds are equal does +not run its body**, and the last brick left in a row is exactly that case: + +```basic +FOR I# = 0 TO 0 + PRINT "THE BODY RAN" +NEXT I# +PRINT "AFTER THE LOOP" +``` + +```output +AFTER THE LOOP +``` + +So a loop over a list of unknown length needs its count of one written out beside it, as +`STAMPROW` does. The alternative is a `DO ... LOOP UNTIL`, which tests at the bottom and +therefore always runs once: + +```basic +I# = 0 +DO + PRINT "PASS " + I# + I# = I# + 1 +LOOP UNTIL I# >= 1 +``` + +```output +PASS 0 +``` + +Use whichever reads better. This is [`TODO.md` §6 item 19](../TODO.md), which is waiting on +a decision about a checked-in acceptance file rather than on the work; when it lands, +`FOR I# = 0 TO 0` will run once and both shapes above will still be correct. + +## Step 8: Draw lettering with a stroke font + +**Goal: a HUD in more than one colour.** + +The text grid draws in one colour and has no verb that changes it — `CHAR` accepts a colour argument and ignores it. A coloured HUD therefore has to be *drawn*, which means carrying a font. -The one here is four units wide and seven tall, one glyph per `DATA` line: how many -strokes, then that many pairs of points. **A point is coded `X * 10 + Y`**, so 0 is the -top-left corner, 30 the top right and 36 the bottom right. +The font here is four units wide and seven tall, one glyph per `DATA` line: **how many +strokes, then that many pairs of points**. A point is coded `X * 10 + Y`, so 0 is the +top-left corner, 30 the top right, 6 the bottom left and 36 the bottom right. ```basic requires=akgl screenshot=breakout-lettering size=260x110 DIM FNC#(5) @@ -432,17 +611,21 @@ REM E DATA 4, 0,6, 0,30, 3,33, 6,36 ``` -![](images/breakout-lettering.png) +![The word SCORE drawn as line strokes](images/breakout-lettering.png) -`SZ#` is the scale, so the same table draws a 12-unit `BREAKOUT` on the title screen and -a 4-unit `SCORE` in the HUD. The game reads a character to a glyph number with -`INSTR(ALPHA$, MID(TX$, TXI#, 1))` over a 41-character alphabet, which is why the order of -the `DATA` lines matters. +`SZ#` is the scale, so the same table draws a 12-unit `BREAKOUT` on the title screen and a +4-unit `SCORE` in the HUD. -**Building the strokes and drawing them are separate jobs, on different frames.** Turning -a string into strokes costs about sixteen lines a character and happens when a number -changes; the draw routine merely replays the list at two lines a stroke, and it is the -one with the deadline: +Turning a character into a glyph number is one call: `INSTR(ALPHA$, MID(TX$, TXI#, 1))` +over an alphabet string. That is why the order of the `DATA` lines matters — they have to +match the order of the characters in `ALPHA$`. The finished game uses a 41-character +alphabet covering `A` to `Z`, `0` to `9`, space, colon, dash, full stop and exclamation +mark. + +**Building the strokes and drawing them are separate jobs, on different frames.** Turning a +string into strokes costs about sixteen lines a character and happens when a number +changes; the draw routine merely replays the list at two lines a stroke, and it is the one +with the deadline: ```basic norun LABEL DRAWHUD @@ -461,28 +644,36 @@ SSHAPE Z$, 0, 0, 800, 60 SPRSAV Z$, 1 SPRITE 1, 1, 2 MOVSPR 1, 0, 0 +SHN# = SHN# + 1 RETURN ``` -Each stroke carries its own colour source, so one pass over the list draws in as many -colours as it likes: labels cyan, the score yellow, the lives green, the level red. -**Seven colour sources is the ceiling** — which is why the purple gem's banner is the -closest purple the palette has rather than the gem's own. +`HX1#()` to `HY2#()` are the stroke endpoints and `HC#()` is the colour source each stroke +wants, so one pass over the list draws in as many colours as it likes: labels cyan, the +score yellow, the lives green, the level red. **Seven colour sources is the ceiling**, +which is why the purple gem's banner is the closest purple the palette has rather than the +gem's own. -The one-stroke case is written out beside the loop. That is the second trap below, and -every loop over a list in this program has it. +Note the one-stroke case written out beside the loop, for the reason in Step 7. And note +`SHN# = SHN# + 1`: every routine that captures counts the slot it spent, which is what +Step 4's rebuild-when-dry test reads. -## Step 7: Bounce the ball without a square root +## Step 9: Move and bounce the ball -There is no `SQR` in this dialect, so the game never computes a magnitude. Eight landing -zones across the bar, each holding **very nearly a unit vector**, and a velocity is -always one of them times the current speed: +**Goal: bounces at sensible angles, computed without a square root.** + +There is no `SQR` in this dialect, so never compute a magnitude. Instead, tabulate eight +landing zones across the bar, each holding **very nearly a unit vector**, and make every +velocity one of them times the current speed: ```basic norun 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 ``` +Read those into `ZVX%()` and `ZVY%()`. A bar bounce is then: work out which zone the ball +landed in, and assign. + ```basic norun LABEL HITBAR IF BLY%(B#) + 21 < T2# THEN RETURN @@ -495,12 +686,62 @@ 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 RETURN ``` +`T1#` is the bar's left edge, `T2#` its top and `T3#` its width, so the same routine serves +the paddle and the catcher bar — `BALLPADDLE` in Step 13 calls it twice with different +values and reads `HIT#` to find out whether the first call already caught the ball. The +four tests at the top are the overlap; each can bail out, and reaching the fifth line means +the ball is on the bar. + +Step 10 adds two more lines before that `RETURN`, for the sticky gem. + +### Which type a variable is decides the arithmetic + +Two rules matter here and they matter a lot, because breaking either produces **no error at +all** — the program computes something else and carries on. + +**A ball's velocity must be a float.** `BLVX%` has a `%` suffix; a `#` variable holds +`-0.85 * 6.0` as `-5`, and a ball whose velocity is quantised to whole pixels bleeds speed +away at every bounce. + +**The left operand of an expression decides whether it is done in integers or floats.** +Put the float on the left: + +```basic +LEVEL# = 4 +SPD% = 5.6 + LEVEL# * 0.45 +PRINT "INTEGER FIRST " + SPD% +SPD% = 5.6 + 0.45 * LEVEL# +PRINT "FLOAT FIRST " + SPD% +V% = 6.4 +PRINT "0 - V% " + (0 - V%) +PRINT "0.0 - V% " + (0.0 - V%) +``` + +```output +INTEGER FIRST 5.600000 +FLOAT FIRST 7.400000 +0 - V% -6 +0.0 - V% -6.400000 +``` + +`LEVEL# * 0.45` is zero because `LEVEL#` is an integer, so reversing a ball is +`0.0 - BLVX%(B#)` and never `0 - BLVX%(B#)`. This is a decision of the dialect rather than +a defect — [Chapter 3](03-the-language.md#the-left-operand-decides-whether-the-arithmetic-is-integer-or-float) +has the two rules that keep you out of it, and +[Chapter 13](13-differences.md) names it as the difference from BASIC 7.0 most likely to +turn a working listing into a quietly wrong one. + +### Bricks + A bounce off a wall or a brick only ever flips a sign, so a ball is always travelling at -exactly the `SPD%` that was in force when it last left a bar. That makes the SLOW gem a -**ratio of two speeds** rather than a change of magnitude: +exactly the `SPD%` that was in force when it last left a bar. That makes changing the speed +a **ratio of two speeds** rather than a change of magnitude, which is what the SLOW gem +needs: ```basic norun LABEL RESCALE @@ -516,35 +757,108 @@ NEXT B# RETURN ``` -`RAT%` is a **float** variable on purpose: an integer one holds the 0.75 of a slowdown as -0 and stops the ball dead. That is the first trap below, and it is the expensive one. +`RAT%` is a float on purpose: an integer one holds the 0.75 of a slowdown as 0 and stops +the ball dead. -Scaling by the vertical component instead — which is what this routine did first — is not -a slowdown at all. A shallow ball's small vertical gets stretched up to the new speed and -drags the large horizontal with it, so SLOW made the ball *faster*. Sixty degrees of the -eight zones are shallow enough to do it. +Scaling by the *vertical component* instead — setting `BLVY%` to the new speed and taking +the horizontal along with it — is not a slowdown at all. A shallow ball's small vertical +gets stretched up and drags the large horizontal with it, so SLOW makes the ball faster. The +table above is why: the outermost zones keep only 0.53 of their speed in the vertical, so +scaling that up to the new speed multiplies everything by 1/0.53, and the ball ends up +nearly twice as fast as intended. Every zone inflates; only the middle two are close enough +to vertical for it not to show. -Brick collision reflects off whichever face the ball has less of itself past, which is the -standard box resolution and the reason a ball clipping the end of a row goes sideways -instead of straight back down: +A ball can only be over four cells at once, so work out **which** cells from its bounding +box rather than walking all sixty bricks: ```basic norun +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 +``` + +`LFT#`, `TOP#`, `RGT#` and `BOT#` are the ball's own box — the artwork is 22 pixels across, +so the ball's right edge is its left plus 21. `CC1#` to `RR2#` are the range of cells that +box can touch, clamped to the field. Both loops are `DO ... LOOP UNTIL` rather than `FOR`, +because a ball entirely inside one column gives `CC1#` and `CC2#` the same value and a +`FOR` would not run at all. + +One brick at a time, then. A collision reflects off whichever face the ball has less of +itself past. `T1#` to `T4#` are the **overlapping** rectangle, and its width against its +height is the whole test: + +```basic norun +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#) +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 ``` -`T1#` to `T4#` are the overlapping rectangle; its width against its height is the whole -test. A ball can only be over four cells at once, so the cells are worked out from its box -rather than by walking sixty bricks. +That is the standard box resolution, and it is the reason a ball clipping the end of a row +goes sideways instead of straight back down. -## Step 8: Gems and powerups +Everything after `BRK#(N#) = 0` is the consequences of the hit, in one place: score it, +sound it, rebuild the live list from Step 7, rebuild the HUD text, mark the field dirty so +Step 6's queue redraws it, and roll one chance in seven for a gem — but only if there is not +one already falling, because Step 2 left exactly one slot for it. `BX1#` and `BY1#` are the +broken brick's own corner, which is where the gem will appear. + +## Step 10: Gems and powerups + +**Goal: one falling gem that can be any of five things.** A gem is one sprite, one type number and two timers. The type picks the artwork, the -banner colour and what `TAKEGEM` does: +banner colour and what catching it does: ```basic norun LABEL SPAWNGEM @@ -564,14 +878,125 @@ MOVSPR 8, GMX%, GMY% RETURN ``` -**Reloading slot 8 is how one sprite becomes five gems.** `SPRSAV` over a live slot -replaces the artwork; there is no need for a slot per gem, and there was never a slot to -spare. +**Reloading slot 8 is how one sprite becomes five gems.** `SPRSAV` over a live slot replaces +its artwork, so there is no need for a slot per gem — which is just as well, because Step 2 +did not leave one. -Every timer is a frame count decremented in one place, so an expiry is where the effect is -undone: +Catching one is one routine with five arms. Each sets a timer, sets the banner text, and +does whatever that gem does: ```basic norun +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 +``` + +EXPAND is `SPRITE 3, 1, 2, 0, 1, 0` — the x-expand flag, which is the only scaling a sprite +has, and doubling the artwork is exactly what it wants. `PDW#` is the width the collision +test uses, so it has to be doubled with it. + +MULTI throws two more balls off the one in play, at slightly different angles so they do +not travel as a stack: + +```basic norun +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 +``` + +STICKY is three lines at the end of `HITBAR` in Step 9, replacing its final `RETURN` — the +ball stops where it landed and remembers its offset along the bar: + +```basic norun +IF STKON# = 0 THEN RETURN +BLST#(B#) = 1 +BLOFF# = BLX%(B#) - PDX% +RETURN +``` + +and space releases it, straight up the middle: + +```basic norun +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 +``` + +The CATCH bar **mirrors** the paddle rather than following it — the line is in `MOVEPADDLE` +in Step 13: + +```basic norun +CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104 +``` + +A second bar directly above the first is worth nothing; a mirrored one turns a dive across +the field into a save at both ends. Note the leading `0.0` rather than `0`, for the reason +in Step 9. + +### Every timer in one place + +A timer is a frame count decremented in one routine, so an expiry is exactly where the +effect is undone and there is only one place to look: + +```basic norun +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 @@ -579,39 +1004,54 @@ IF PDEXP# > 0 THEN BEGIN 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 ``` -EXPAND is `SPRITE 3, 1, 2, 0, 1, 0` — the x-expand bit, which is the only scaling a sprite -has, and doubling the artwork is exactly what it wants. +Each arm is the same shape: count down, and on the frame it reaches zero, put back what the +gem changed. SLOW expiring calls `LEVELSPEED` rather than assigning a number, so the speed +it returns to is the speed the current level should be running at. -The CATCH bar **mirrors** the paddle rather than following it: +## Step 11: Three voices and a mute -```basic norun -CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104 -``` +**Goal: sound that does not cut itself off, and a mute that costs one line.** -That is deliberate. A second bar directly above the first is worth nothing; a mirrored one -turns a dive across the field into a save at both ends. Note the leading `0.0` — trap one -again, and this line was wrong before it was right. - -## Step 9: Three voices, and a mute that costs nothing - -Voice 1 is the ball hitting things, voice 2 is the gem and the ball being lost, and voice -3 is reserved for `PLAY` so a brick going cannot cut a tune off mid-note. +Give each kind of sound its own voice. Voice 1 is the ball hitting things, voice 2 is the +gem and the ball being lost, and voice 3 is reserved for `PLAY` — so a brick going cannot +cut a tune off mid-note. **`SOUND`'s frequency argument is a SID register value, not hertz.** The pitch is `register * 1022730 / 16777216` — see [Chapter 7](07-sound.md#sound) — so work the notes -out once and write them down: 17175 is C6, 8579 C5, 4298 C4, 3609 A3. The brick tone is -pitched by the row it came from — +out once and write them down: 17175 is C6, 8579 C5, 4298 C4, 3609 A3. + +Pitch the brick tone by the row it came from and the wall plays itself down a scale as it +comes apart: ```basic norun SOUND 1, 17175 - R# * 2100, 4 ``` -— so the top row rings at C6, each row down drops about a third, and a wall coming apart -plays itself down a scale. - -Mute with `VOL 0` rather than a flag tested at eleven call sites: +Mute with `VOL 0` rather than a flag tested at every call site: ```basic norun LABEL PRESSMUTE @@ -621,166 +1061,963 @@ IF SNDON# = 0 THEN VOL 0 RETURN ``` -A silenced voice costs nothing to issue. One line beats eleven scattered through the game. +A silenced voice costs nothing to issue, so one line here beats eleven scattered through +the game. -## Five things that did not do what they looked like - -Each cost an evening, and each was filed in `TODO.md` §9 with a reduction and the file -and line of the cause. **Three of the five have since been fixed** — the interpreter -changed, not the game — and they are kept here because the shape of the program is still -theirs: a listing whose comments explain what it was avoiding is worth more than one -quietly rewritten to pretend the problem never existed. - -**Two still stand, and they are the first two.** Read those; the rest is history. - -### 1. The left operand decides integer or float arithmetic - -```basic -LEVEL# = 4 -SPD% = 5.6 + LEVEL# * 0.45 -PRINT "INTEGER FIRST " + SPD% -SPD% = 5.6 + 0.45 * LEVEL# -PRINT "FLOAT FIRST " + SPD% -V% = 6.4 -PRINT "0 - V% " + (0 - V%) -PRINT "0.0 - V% " + (0.0 - V%) -FOR I# = 0 TO 0 - PRINT "THE BODY RAN" -NEXT I# -PRINT "AFTER THE LOOP" -``` - -```output -INTEGER FIRST 5.600000 -FLOAT FIRST 7.400000 -0 - V% -6 -0.0 - V% -6.400000 -AFTER THE LOOP -``` - -**This is the dangerous one, because nothing fails.** The program computes something else -and carries on. Two live bugs in this game came from it: `SPD% = 5.6 + LEVEL# * 0.45` was -a flat 5.6, so no level ever got faster than level one; and `0 - BLVX%(B#)`, the obvious -way to reverse a ball, quantised its velocity to whole pixels on every bounce and bled -speed out of it. Neither produced a diagnostic. - -**Put the float on the left, and put the answer somewhere with a `%` on it.** A float -expression landing in a `#` variable truncates. - -### 2. A `FOR` whose bounds are equal does not run its body - -The last two lines of that output are the second trap: `FOR I# = 0 TO 0` runs zero times. -Knowing it and remembering it while writing a loop over "the bricks still standing" are -different things, and the last brick of a row is exactly that case — which is why every -loop over a list in this program has its one-item case written out beside it: +Set up the envelope and tempo once at startup: ```basic norun -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#) +VOL 8 +ENVELOPE 0, 0, 6, 0, 4 +TEMPO 12 +``` + +`PLAY` takes a string of notes, which is what the fanfares are: + +```basic norun +PLAY "V3 T2 U8 O4 QC QE QG O5 HC" +``` + +## Step 12: The states + +**Goal: the five states the frame loop in Step 6 dispatches to.** + +Each is a subroutine called once a frame while that state is current. This is a different +shape from Chapter 17's branch targets, and it works here because nothing jumps out of a +loop to reach it. + +```basic norun +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 +``` + +`STIMER#` is a frame countdown, which is how "hold this message up for a second and a half" +is written without stopping the game. + +Laying a level out is one routine. Every third level is a row shorter, so the field changes +shape as well as pace: + +```basic norun +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 +STATE# = 1 +DPLAY# = 1 +RETURN + +LABEL LEVELSPEED +SPD% = 5.6 + 0.45 * LEVEL# +IF SPD% > 11.0 THEN SPD% = 11.0 RETURN ``` -### 3. A skipped `BEGIN` block containing a loop — *fixed* +`CLEARPOWERS` turns off every powerup, every timer and the two sprites they use, and is +called from level setup, from a re-serve and from the title screen — so no state can leak +from one game into the next. -This one is here as history rather than as a warning. A loop inside a block that was not -taken used to leave a scope behind, and inside a routine the orphan sat between it and -its caller — so the `RETURN` after the block reported `RETURN outside the context of -GOSUB` from a routine that plainly *was* entered by a `GOSUB`. The error named the one -construct that was not at fault, which is why it cost an evening. +## Step 13: Put the program together -```basic -T# = 0 -GOSUB DOIT -PRINT "CAME BACK" -END -LABEL DOIT -IF 1 = 0 THEN BEGIN - FOR I# = 0 TO 2 - T# = T# + 1 - NEXT I# -BEND -RETURN +**Goal: one file, in an order that runs.** + +The file runs from the top, so three things about the order matter and nothing else does: +setup first, `DATA` in the order it will be read, and every `LABEL` present somewhere. + +```text + the declaration block, below + GOSUB LOADTABLES the row colours, values and bounce zones + GOSUB LOADFONT the stroke font + SEED# = MOD(1 + TI# * 7919, 2147483648) + the artwork from Step 1 + VOL 8 / ENVELOPE / TEMPO from Step 11 + GOSUB TITLESCREEN + +LABEL FRAME the frame loop from Step 6 + + the pacing, the draw jobs, the input, the states, the ball, + the gems, the text builders, the generator + --- in any order --- + + the tables as DATA read by LOADTABLES + the font as DATA read by LOADFONT ``` -```output -CAME BACK +**`LOADTABLES` must run before `LOADFONT`, because the tables' `DATA` is written first.** +One `READ` cursor walks every `DATA` item in the file in the order they appear, no matter +which routine is reading, so whichever loader runs first gets whichever `DATA` comes first. +Get that backwards and the font table fills with brick colours, which does not fail — it +just draws nonsense. + +### The declaration block + +```basic norun +STATE# = 0 +RUNNING# = 1 +SCORE# = 0 +LIVES# = 3 +LEVEL# = 1 +HISCORE# = 0 +STIMER# = 0 +LASTT# = 0 +KEYV# = 0 +SNDON# = 1 + +WALLL# = 12 +WALLR# = 788 +BRKX# = 42 +BRKY# = 108 + +PDX% = 348 +PDY# = 540 +PDW# = 104 +PDDIR# = 0 +PDCO# = 0 +PDEXP# = 0 +CTON# = 0 +CTX% = 348 +CTTM# = 0 + +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 +BSPD% = 6.0 +RAT% = 1.0 +SLOWT# = 0 +STKON# = 0 +STKT# = 0 + +GMON# = 0 +GMTYP# = 0 +GMX% = 0 +GMY% = 0 + +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 + +S0$ = "" : S1$ = "" : S2$ = "" +S3$ = "" : S4$ = "" : S5$ = "" +Z$ = "" +SHN# = 99 +DHUD# = 1 +DPLAY# = 1 +BAN$ = "" +BANT# = 0 + +ALPHA$ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 :-.!" +DIM FNS#(280) +DIM FNI#(41) +DIM FNC#(41) + +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 + +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$ = "" + +DIM ZVX%(8) +DIM ZVY%(8) + +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 ``` -`FOR` creates its environment when the line is **parsed** and the skip is decided when it -is **evaluated**; the skip now releases what parsing pushed. **The listing still guards -its loops with `GOTO`**, which is where this chapter's own history shows: written that way -because it had to be, and left that way because it works. +`SHN# = 99` means "the brick stamps do not exist", so the frame loop's first job is to +build them. `DPLAY#` and `DHUD#` start at 1 so the first frame draws both. -### 4. `SSHAPE` and `GSHAPE` ignoring a subscript — *fixed* +That block is 121 of the interpreter's 128 variables, which is why several constants in the +geometry are spelled out where they are used rather than given names: a name costs a slot +whether it holds a constant or not. -Also history. The handle used to land in element zero whatever subscript you wrote, and -`GSHAPE SH$(2)` stamped whatever was in `SH$(0)` — while ordinary assignment and `PRINT` -honoured the subscript, which is what made it expensive to find. The symptom was that -every brick came out the colour of the last stamp captured. +### The tables and the generator -```basic requires=akgl -DIM SH$(4) -SSHAPE SH$(2), 0, 0, 61, 4 -PRINT "[" + SH$(0) + "] [" + SH$(2) + "]" -``` - -```output -[] [SHAPE:0] -``` - -An array of shapes works now. **The listing in `examples/` still keeps its six brick -stamps in six scalars** — `S0$` to `S5$` — because that is what it had to do, and the -field is still drawn as six runs of one colour rather than brick by brick, which turned -out to be cheaper anyway. - -### 5. `READ` walks one cursor through every `DATA` item in the file - -```basic -DIM T#(3) -DIM F#(3) -I# = 0 -GOSUB LOADFONT -GOSUB LOADTABLE -PRINT "TABLE " + T#(0) + " " + T#(1) + " " + T#(2) -PRINT "FONT " + F#(0) + " " + F#(1) + " " + F#(2) -END - -LABEL LOADTABLE -FOR I# = 0 TO 2 - READ T#(I#) +```basic norun +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 +LABEL NEXTRAND +SEED# = MOD(SEED# * 1103515245 + 12345, 2147483648) +RNVAL# = MOD(SHR(SEED#, 16), RNMAX#) +RETURN +``` + +```basic norun +DATA 3, 9, 8, 6, 4, 5 +DATA 60, 50, 40, 30, 20, 10 +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 +``` + +Row colours, what a brick in each row is worth, and the eight bounce zones from Step 9. + +`NEXTRAND` is a `GOSUB` over globals rather than a `DEF` function. Set `RNMAX#` to the +number of answers you want and read `RNVAL#`. + +The font loader is the same shape, and it needs a subroutine of its own for the inner read +rather than a `BEGIN` block, because a `FOR` nested inside a block that is itself inside a +`FOR` does not find its own `NEXT`: + +```basic norun LABEL LOADFONT -FOR I# = 0 TO 2 - READ F#(I#) +GX# = 0 +FOR I# = 0 TO 40 + READ N# + FNC#(I#) = N# + FNI#(I#) = GX# + IF N# > 0 THEN GOSUB READGLYPH NEXT I# RETURN -DATA 11, 12, 13 -DATA 21, 22, 23 +LABEL READGLYPH +FOR K# = 1 TO N# * 2 + READ D# + FNS#(GX#) = D# + GX# = GX# + 1 +NEXT K# +RETURN ``` -```output -TABLE 21 22 23 -FONT 11 12 13 +`FNC#(g)` is glyph `g`'s stroke count and `FNI#(g)` is where its points start in `FNS#()`. + +The 41 `DATA` lines are one per character of `ALPHA$`, **in that order** — the `INSTR` in +`BUILDTEXT` gives a position in `ALPHA$` and that position is used directly as the glyph +number, so getting the two out of step silently draws the wrong letters. Here is the whole +font, in the format Step 8 gives: + +```basic norun +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 ``` -The `DATA` written first went to whichever routine ran first, not to the one it was -written for. Two loaders means the one whose `DATA` comes first in the file has to be -called first — obvious in hindsight, and not obvious when the symptom is a font table -full of brick colours. `RESTORE` to a label is the way out when the order cannot be -arranged; Chapter 17 uses it to pick a level layout. +Space is `DATA 0` — no strokes — which is what the `IF N# > 0` in `LOADFONT` and the +`IF GN# = 0 THEN RETURN` in `BUILDGLYPH` are for. + +### Input + +```basic norun +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 + +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 +``` + +`READKEYS` empties the queue by tail-calling itself with `GOTO`, which costs no scope. +A press buys seven frames of paddle travel in `PDCO#` and a held key keeps renewing it — +the same countdown idea Chapter 17 uses, sized for this game's frame rate. Seven frames +matters because the first key repeat is a quarter of a second behind the press, and a +paddle that stalled for a quarter of a second would be unusable. + +### The paddle and the balls + +```basic norun +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 +CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104 +MOVSPR 4, CTX%, 470 +RETURN + +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 + +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 +``` + +Balls 0, 1 and 2 live in sprites 5, 6 and 7, which is why `MOVSPR 5 + B#` works. +`BLST#(B#)` is 1 while a ball is stuck to the paddle. Note the `0.0 -` on every sign flip, +for the reason in Step 9. + +`BALLBRICKS` and `TESTCELL` are in Step 9. + +### The gem's fall + +```basic norun +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 +``` + +`TAKEGEM` is in Step 10. `HIT#` is doing double duty here as "this gem is finished with" — +it is set both by the gem going off the bottom and by it being caught. + +### Turning text into strokes + +Step 8 drew a glyph directly. The game builds a list instead, so the drawing can happen on +a later frame: + +```basic norun +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 +``` + +Set `TX$`, `TXX#`, `TXY#`, `TXS#` (the scale) and `TXC#` (the colour source), then set +`BUILDH#` to 1 for the HUD list or 0 for the field list, and `GOSUB BUILDTEXT`. Both `DO` +loops test at the bottom, so a one-character string and a one-stroke glyph both work. + +The bounds tests — `HN# < 120` and `PN# < 110` — are what stops a long string running off +the end of the arrays. + +```basic norun +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 + +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 +``` + +`HUDTEXT` rebuilds the whole list from `HN# = 0` and sets `DHUD#`; the frame loop draws it +whenever it next gets a turn. + +`SETBANNER` is the same idea for the field list — the message that flashes over the play +area, in the colour of the gem that put it there: + +```basic norun +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 +TX$ = BAN$ +GOSUB BUILDTEXT +RETURN +``` + +Setting `BAN$` to `""` and calling it is how a banner is cleared: `PN# = 0` empties the +field's stroke list and `DPLAY# = 1` asks for a redraw without it. Sources 1 to 6 are cyan, +yellow, purple, light red, light grey and green, and the five gems are red, yellow, green, +blue and purple — so the banner is as close to the gem as seven colour sources allow. + +### The state changes + +Six routines, each the same shape: set `STATE#`, set `STIMER#` if the state has a duration, +set the banner, and mark whatever is now out of date. + +```basic norun +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 LEVELDONE +PLAY "V3 T2 U8 O4 QC QE QG O5 HC" +STATE# = 4 +STIMER# = 60 +GMTYP# = 0 +BAN$ = "CLEAR" +GOSUB SETBANNER +RETURN + +LABEL STARTGAME +PLAY "V3 T2 U8 O4 IC IE IG O5 IC" +SCORE# = 0 +LIVES# = 3 +LEVEL# = 1 +GOSUB SETUPLEVEL +RETURN +``` + +`GMTYP# = 0` before a banner that is not a gem's is what makes `SETBANNER` pick its default +colour rather than the last gem's. + +`TITLESCREEN` and `GAMEISOVER` are longer only because both draw a screenful of lettering. +Both clear the brick array and call `BUILDLIVE` so the field draws empty, turn the paddle +and ball sprites off, reset `PN#`, and then build their text at a large `TXS#`: + +```basic norun +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 +``` + +`GAMEISOVER` is the same routine with `"GAME OVER"`, the final score and the best score, +and `STATE# = 5` — the state whose only exit is space, back to the title. + +### Serving, and clearing up + +```basic norun +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 + +LABEL FILLROW +FOR C# = 0 TO 9 + BRK#(R# * 10 + C#) = 1 + BRN# = BRN# + 1 +NEXT C# +RETURN + +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 +``` + +The six state changes — `BALLGONE`, `SERVEAGAIN`, `LEVELDONE`, `GAMEISOVER`, +`TITLESCREEN` and `STARTGAME` — are above, with `SETBANNER`. + +### The two draw routines, whole + +Step 4 built the stamps and Step 7 stamped a row; these are the two routines Step 6's queue +actually calls, with those pieces in place. + +```basic norun +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 +``` + +`GRAPHIC 5` hands all sixteen `SSHAPE` slots back, which is the only way to get any of them +back — so `SHN#` goes to 99 first and to 6 at the end, and both flags are set because the +field and the HUD were drawn with stamps that no longer exist. + +```basic norun +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 +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 +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 +``` + +Same shape as `DRAWHUD` in Step 8: the walls, then the bricks, then the field's own stroke +list with its one-item case beside the loop, then Step 3's four lines that turn all of it +into sprite 2. + +**A label is one bare word.** `PLDONE` and `HUDONE` have no underscore in them, and cannot: +underscores are not part of an identifier here. + +### Game over + +`TITLESCREEN` is above. `GAMEISOVER` is the same routine with different text, `STATE# = 5`, +and the high score recorded on the way in: + +```basic norun +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 +``` + +Three sizes of lettering on one screen, from one font table: `TXS# = 9` for the message, +5 for the score and 3 for the HUD strip. Setting `HN# = 0` partway through is what switches +from building the field's list to rebuilding the HUD's. + +### Check it before you have a window + +Run the assembled file through the plain build first: + +```sh norun +$ ./build/basic mybreakout.bas +``` + +It will stop at the first `SPRSAV` with a message about a missing sprite device, which +means everything above it parsed. A parse error, a bad `DATA` read or an `OUT OF DATA` at +this stage is a real problem, and it is much easier to find without a window in the way. +Then run it properly: + +```sh norun +$ ./build-akgl/basic mybreakout.bas +``` ## The budgets -This program sits close to four ceilings at once, and knowing where they are is what -stops a feature costing an afternoon before it is abandoned: +This program sits close to five ceilings at once, and knowing where they are is what stops +a feature costing an afternoon before it is abandoned. | Resource | There are | This game uses | |---|---|---| @@ -788,20 +2025,217 @@ stops a feature costing an afternoon before it is abandoned: | Variables | 128 | 121, plus 4 the interpreter makes | | Labels | 64 | 61 | | `SSHAPE` slots | 16, none reclaimed except by `GRAPHIC 5` | 6 stamps plus 1 capture a frame | -| Value-pool slots | 4096 for arrays and structures; a scalar costs none | six arrays, declared once — see [Chapter 17](17-tutorial-breakout.md#step-3-declare-the-names-a-subroutine-has-to-answer-through) | +| Colour sources | 7 | 7 | | Scopes | 32 | 6 deep at most | -| Tokens on a line | 32, and the 33rd kills the interpreter rather than raising | short lines, temporaries instead of long conditions | +| Tokens on a line | 32, and the 33rd stops the interpreter rather than raising | short lines, temporaries instead of long conditions | +| Value-pool slots | 4096 for arrays; a plain number costs none | fifteen arrays, all declared once | -That is also why several things are **not** in the game, and they are worth naming -honestly rather than leaving to be discovered: no music under the play, only event sounds -and two four-note stings; one gem at a time; sticky and multiball share one offset, so two -balls stuck to the paddle sit on top of each other; and no high score on disk, because -there is no disk. +That is also why several things are **not** in the game, and they are worth naming rather +than leaving to be discovered: no music under the play, only event sounds and two four-note +stings; one gem at a time; sticky and multiball share one offset, so two balls stuck to the +paddle sit on top of each other; and no high score on disk, because there is no disk. -**Several of these budgets were tighter when this was written.** A scalar cost a pool -slot, a saved shape could not live in an array, and a loop inside a skipped block leaked -a scope — so a program had to spend variables to avoid all three. The listing has not -been rewritten to take the room back, because what it does instead is show its working. +## The picture at the top of this chapter + +Every figure in this guide is generated by running the listing beside it, and the one at +the top of this chapter is no exception. It is Step 4's field, Step 8's stroke font and +Step 1's artwork, with the numbers filled in by hand and no frame loop, so it draws one +frame and stops. + +The font here carries only the fourteen glyphs the HUD needs, in the order they appear in +`ALPHA$` — which is the ordering rule from Step 8, shown small enough to check by eye: + +```basic requires=akgl setup=breakout_art screenshot=breakout-game-artwork size=800x600 +DIM BRC#(6) +DIM FNC#(14) +DIM FNI#(14) +DIM FNS#(100) +ALPHA$ = "SCOERLIV 01235" +I# = 0 +R# = 0 +C# = 0 +K# = 0 +N# = 0 +D# = 0 +T1# = 0 +T2# = 0 +GP# = 0 +GX# = 0 +GX2# = 0 +GK# = 0 +TLEN# = 0 +P1# = 0 +P2# = 0 +X1# = 0 +Y1# = 0 +X2# = 0 +Y2# = 0 +SZ# = 0 +TX$ = "" +TXX# = 0 +TXY# = 0 +TXC# = 1 +TXI# = 0 +GC# = 0 +Z$ = "" +S0$ = "" +S1$ = "" +S2$ = "" +S3$ = "" +S4$ = "" +FOR I# = 0 TO 5 + READ BRC#(I#) +NEXT I# +GX# = 0 +FOR I# = 0 TO 13 + READ N# + FNC#(I#) = N# + FNI#(I#) = GX# + IF N# > 0 THEN GOSUB READGLYPH +NEXT I# + +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# +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$ + +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 +FOR C# = 0 TO 9 + Z$ = S0$ : GSHAPE Z$, 42 + C# * 72, 108 + Z$ = S1$ : GSHAPE Z$, 42 + C# * 72, 132 + Z$ = S2$ : GSHAPE Z$, 42 + C# * 72, 156 +NEXT C# +FOR C# = 1 TO 8 + Z$ = S3$ : GSHAPE Z$, 42 + C# * 72, 180 +NEXT C# +FOR C# = 2 TO 6 + Z$ = S4$ : GSHAPE Z$, 42 + C# * 72, 204 +NEXT C# +SSHAPE Z$, 0, 60, 800, 600 +SPRSAV Z$, 2 +SPRITE 2, 1, 2 +MOVSPR 2, 0, 60 + +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 +SZ# = 4 +TXY# = 14 +TXC# = 1 : TXX# = 24 : TX$ = "SCORE" : GOSUB DRAWTEXT +TXC# = 2 : TXX# = 144 : TX$ = "001250" : GOSUB DRAWTEXT +TXC# = 1 : TXX# = 316 : TX$ = "LIVES" : GOSUB DRAWTEXT +TXC# = 6 : TXX# = 436 : TX$ = "3" : GOSUB DRAWTEXT +TXC# = 1 : TXX# = 500 : TX$ = "LEVEL" : GOSUB DRAWTEXT +TXC# = 4 : TXX# = 620 : TX$ = "2" : GOSUB DRAWTEXT +SSHAPE Z$, 0, 0, 800, 60 +SPRSAV Z$, 1 +SPRITE 1, 1, 2 +MOVSPR 1, 0, 0 + +SPRSAV "art/paddleBlu.png", 3 +SPRSAV "art/ballBlue.png", 5 +SPRSAV "art/element_green_polygon_glossy.png", 8 +SPRITE 3, 1, 2 +SPRITE 5, 1, 2 +SPRITE 8, 1, 2 +MOVSPR 3, 300, 540 +MOVSPR 5, 420, 400 +MOVSPR 8, 200, 300 +END + +LABEL READGLYPH +FOR K# = 1 TO N# * 2 + READ D# + FNS#(GX#) = D# + GX# = GX# + 1 +NEXT K# +RETURN + +LABEL DRAWTEXT +TLEN# = LEN(TX$) +IF TLEN# = 0 THEN RETURN +TXI# = 0 +DO + GC# = INSTR(ALPHA$, MID(TX$, TXI#, 1)) + IF GC# > 0 - 1 THEN GOSUB DRAWGLYPH + TXI# = TXI# + 1 +LOOP UNTIL TXI# >= TLEN# +RETURN + +LABEL DRAWGLYPH +N# = FNC#(GC#) +IF N# = 0 THEN RETURN +GP# = FNI#(GC#) +GX2# = TXX# + TXI# * SZ# * 5 +GK# = 0 +DO + P1# = FNS#(GP#) + P2# = FNS#(GP# + 1) + GP# = GP# + 2 + X1# = GX2# + (P1# / 10) * SZ# + Y1# = TXY# + MOD(P1#, 10) * SZ# + X2# = GX2# + (P2# / 10) * SZ# + Y2# = TXY# + MOD(P2#, 10) * SZ# + DRAW TXC#, X1#, Y1# TO X2#, Y2# + GK# = GK# + 1 +LOOP UNTIL GK# >= N# +RETURN + +DATA 3, 9, 8, 6, 4, 5 + +REM S +DATA 5, 0,30, 0,3, 3,33, 33,36, 6,36 +REM C +DATA 3, 0,30, 0,6, 6,36 +REM O +DATA 4, 0,30, 6,36, 0,6, 30,36 +REM E +DATA 4, 0,6, 0,30, 3,33, 6,36 +REM R +DATA 5, 0,6, 0,30, 3,33, 30,33, 13,36 +REM L +DATA 2, 0,6, 6,36 +REM I +DATA 1, 10,16 +REM V +DATA 2, 0,16, 16,30 +REM space +DATA 0 +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 5 +DATA 5, 0,30, 0,3, 3,33, 33,36, 6,36 +``` + +![](images/breakout-game-artwork.png) + +`DRAWTEXT` and `DRAWGLYPH` are `DO ... LOOP UNTIL` rather than `FOR`, which is Step 7's +rule earning its keep twice on one screen: `"3"` is one character long and the `I` glyph is +one stroke, and a `FOR` would have drawn neither. ## Where to go next @@ -810,8 +2244,6 @@ been rewritten to take the room back, because what it does instead is show its w - **[Chapter 6](06-graphics.md)** is the drawing reference, including `SSHAPE`, `GSHAPE` and what a shape handle is. - **[Chapter 7](07-sound.md)** is `SOUND`, `PLAY`, `ENVELOPE` and `VOL`. +- **[Chapter 3](03-the-language.md)** has the arithmetic rules from Step 9 in full. - **[Chapter 14](14-architecture.md)** explains the step loop and the pools this chapter keeps running into, from the interpreter's side. -- `TODO.md` §9 is the eight defects this game found, each with a reduction and the cause. - Six are fixed and struck; the two that stand are the left-operand rule, which is a - decision rather than a defect, and the batch tear, which is the host's to own. diff --git a/docs/README.md b/docs/README.md index bb30130..bd3740d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,7 +15,8 @@ embedding it, debugging it or changing it. **[Chapters 17](17-tutorial-breakout.md)** and **[18](18-tutorial-breakout-artwork.md)** are tutorials rather than reference: they build one complete game twice, two different -ways, and are where the rules that a real program runs into are collected. +ways, in numbered steps you can type in one at a time. Start with 17 — it needs nothing +but the earlier chapters, and 18 assumes it. ## Chapters @@ -37,8 +38,8 @@ ways, and are where the rules that a real program runs into are collected. | **[14. Architecture](14-architecture.md)** | How the interpreter is put together, how to debug it, how to change it | | **[15. Error codes](15-error-codes.md)** | Appendix: every value `ER#` can hold and every error line the interpreter prints | | **[16. Structures](16-structures.md)** | `TYPE`, records, strict pointers, and sharing a C struct with an embedding host | -| **[17. Tutorial: Breakout](17-tutorial-breakout.md)** | Build a whole game out of the text grid and two `DATA` sprites, a step at a time | -| **[18. Tutorial: Breakout with artwork](18-tutorial-breakout-artwork.md)** | Build it again out of loaded artwork, powerups and a drawn HUD | +| **[17. Tutorial: Breakout](17-tutorial-breakout.md)** | Build a whole game out of the text grid and two `DATA` sprites, in sixteen steps | +| **[18. Tutorial: Breakout with artwork](18-tutorial-breakout-artwork.md)** | Build it again out of loaded artwork, powerups and a drawn colour HUD, in thirteen | ## The shortest possible start diff --git a/docs/images/README.md b/docs/images/README.md index 79abb9e..ac42df1 100644 --- a/docs/images/README.md +++ b/docs/images/README.md @@ -17,5 +17,6 @@ test will fail on it, because it re-renders every figure and compares byte for b The tag that ties a figure to its listing is `screenshot=NAME` in the fence info string, and `MAINTENANCE.md` documents it along with `size=WxH` for a figure that needs a surface -other than 320 by 200. A tagged block with no image here fails `docs_examples` in both -build configurations, so a figure cannot be added to a chapter and then forgotten. +other than 320 by 200, and `text=1` for one whose subject is the text grid rather than a +drawing. A tagged block with no image here fails `docs_examples` in both build +configurations, so a figure cannot be added to a chapter and then forgotten. diff --git a/docs/images/breakout-game-artwork.png b/docs/images/breakout-game-artwork.png new file mode 100644 index 0000000..2bddd66 Binary files /dev/null and b/docs/images/breakout-game-artwork.png differ diff --git a/docs/images/breakout-game.png b/docs/images/breakout-game.png new file mode 100644 index 0000000..c30dbcc Binary files /dev/null and b/docs/images/breakout-game.png differ diff --git a/examples/breakout/characters/README.md b/examples/breakout/characters/README.md index 00188a5..3efb178 100644 --- a/examples/breakout/characters/README.md +++ b/examples/breakout/characters/README.md @@ -27,10 +27,11 @@ does. ## How it works, and how to write your own -**[Chapter 17 of the guide](../../../docs/17-tutorial-breakout.md)** builds this program a -step at a time: what to make a sprite and what to make a character, why every name is -declared before the game starts, how a text row has to be written whole, and the rest of -the rules this listing never breaks. It is the tutorial; this is the finished thing. +**[Chapter 17 of the guide](../../../docs/17-tutorial-breakout.md)** builds this program in +fifteen steps you can type in one at a time, starting from an empty file: measuring the +screen, declaring every name before anything uses it, making the two sprites out of `DATA`, +building a text row whole, the main loop, the ball, the bounce, and the attract mode. It is +the tutorial; this is the finished thing. [Chapter 18](../../../docs/18-tutorial-breakout-artwork.md) does the same for [`../sprites`](../sprites), which builds the same game out of loaded artwork and comes out diff --git a/examples/breakout/sprites/README.md b/examples/breakout/sprites/README.md index d0e60f1..e3c2457 100644 --- a/examples/breakout/sprites/README.md +++ b/examples/breakout/sprites/README.md @@ -48,10 +48,10 @@ dive across the field into a save at both ends. ## How it works, and how to write your own **[Chapter 18 of the guide](../../../docs/18-tutorial-breakout-artwork.md)** builds this -program a step at a time: budgeting the eight sprite slots, capturing a drawing into one, -finding the host's frame boundary with `TI#`, moving everything that is not drawing out of -the deadline, the stroke font, and the five things in this dialect that do not do what -they look like. +program in twelve steps: loading artwork, budgeting the eight sprite slots, capturing a +drawing into one, stamping the brick field, finding the host's frame boundary with `TI#`, +moving everything that is not drawing out of the deadline, the stroke font, the bounce +without a square root, and the gems. [Chapter 17](../../../docs/17-tutorial-breakout.md) does the same for [`../characters`](../characters), which builds the same game out of the text grid and two diff --git a/tools/docs_screenshots.sh b/tools/docs_screenshots.sh index 5b60969..55a0fb0 100755 --- a/tools/docs_screenshots.sh +++ b/tools/docs_screenshots.sh @@ -103,17 +103,34 @@ attr() echo "" } +# The font a `text=1` figure is drawn with. The bundled one, at the size the +# standalone frontend opens it at, so a figure of a text-grid program has the +# same cell size the reader's own window will have. +FONT="${ROOT}/assets/fonts/C64_Pro_Mono-STYLE.ttf" + # One figure: write the listing to a file, run it, keep the PNG. render() { - local name="$1" body="$2" size="$3" where="$4" setup="$5" - local w=320 h=200 out="${OUTDIR}/${name}.png" log="" + local name="$1" body="$2" size="$3" where="$4" setup="$5" text="$6" + local w=320 h=200 out="${OUTDIR}/${name}.png" log="" font="" if [ -n "${size}" ]; then w="${size%x*}" h="${size#*x}" fi + # `text=1` says this program's output is characters rather than drawing, so + # the tool has to render the grid -- see the comment at the head of + # tools/screenshot.c. Everything else gets no font and no text layer. + if [ -n "${text}" ]; then + if [ ! -r "${FONT}" ]; then + echo "FAIL ${where}: text=1 needs a font at ${FONT}" >&2 + FAILURES=$((FAILURES + 1)) + return + fi + font="${FONT}" + fi + # The same setup scripts tests/docs_examples.sh uses, run in the same place # relative to the program. A listing that loads `ship.png` needs one whether # it is being checked or being photographed. @@ -136,7 +153,7 @@ render() # stderr, so merging the two would make every sprite figure look like a # failing program. stderr is shown when the tool itself refuses, which is # when it is worth reading. - if ! log="$(cd "${WORK}" && "${TOOL}" "${name}.bas" "${out}" "${w}" "${h}" \ + if ! log="$(cd "${WORK}" && "${TOOL}" "${name}.bas" "${out}" "${w}" "${h}" "${font}" \ 2>"${WORK}/${name}.err")"; then echo "FAIL ${where}: ${name} did not render" >&2 cat "${WORK}/${name}.err" >&2 @@ -196,7 +213,8 @@ for DOC in "${DOCS[@]}"; do NAME="$(attr screenshot "${INFO}")" if [ -n "${NAME}" ]; then render "${NAME}" "${BODY}" "$(attr size "${INFO}")" \ - "${DOC}:${START}" "$(attr setup "${INFO}")" + "${DOC}:${START}" "$(attr setup "${INFO}")" \ + "$(attr text "${INFO}")" fi fi ;; diff --git a/tools/screenshot.c b/tools/screenshot.c index 3b81136..00aeb7f 100644 --- a/tools/screenshot.c +++ b/tools/screenshot.c @@ -16,10 +16,23 @@ * completion, read the target back, write it out. deps/libakgl/tests/draw.c and * tests/akgl_backends.c set that pattern; this is the third user of it. * - * **The text layer is deliberately not drawn.** A screenshot in the graphics - * chapter is of the drawing, and a `READY` in the corner is noise in a figure - * about `CIRCLE`. It also means no font has to be found, which keeps this - * runnable from a build tree with no assets resolved. + * **The text layer is not drawn unless a font is named.** A screenshot in the + * graphics chapter is of the drawing, and a `READY` in the corner is noise in a + * figure about `CIRCLE`; leaving the layer out also means no font has to be + * found, which keeps this runnable from a build tree with no assets resolved. + * + * A figure of a program whose *output is text* needs the opposite, and + * docs/17-tutorial-breakout.md is one: that game's wall, HUD and messages are + * characters in the grid, so a picture without the text layer is a picture of + * two sprites on a black field. Naming a font as the fifth argument swaps the + * stdio sink for the akgl one and renders the grid before the sprites, in the + * same order src/frontend_akgl.c uses. The fence attribute that asks for it is + * `text=1`; tools/docs_screenshots.sh turns that into this argument. + * + * The sink is the akgl one *alone* rather than a tee, so a program's output + * lands in the picture rather than on stdout -- which is what the caller reads + * to decide a figure failed. Nothing is lost: `screenshot=` blocks are also run + * by tests/docs_examples.sh, which is where a raised error is caught. */ #include @@ -28,6 +41,7 @@ #include #include +#include #include @@ -46,6 +60,7 @@ #include #include +#include #include #include @@ -53,6 +68,9 @@ static akbasic_Runtime RUNTIME; static akbasic_TextSink SINK; static akbasic_StdioSink SINKSTATE; +/** @brief The text grid, and the font behind it. Only used when one is named. */ +static akbasic_AkglSink GRIDSTATE; +static TTF_Font *FONT = NULL; static akbasic_GraphicsBackend GRAPHICS; static akbasic_AkglGraphics GRAPHICSSTATE; static akbasic_SpriteBackend SPRITES; @@ -68,10 +86,13 @@ static char SOURCE[65536]; static void usage(void) { fprintf(stderr, - "usage: akbasic_screenshot [width] [height]\n" + "usage: akbasic_screenshot [width] [height] [font.ttf]\n" "\n" " Runs the program against an offscreen renderer of the given size\n" - " (default 320x200) and writes what it drew as a PNG.\n"); + " (default 320x200) and writes what it drew as a PNG.\n" + "\n" + " Naming a font draws the text grid as well, for a figure of a\n" + " program whose output is characters rather than drawing.\n"); } /** @brief Slurp the program. A figure's listing is small; a partial read is not tolerated. */ @@ -98,7 +119,8 @@ static akerr_ErrorContext AKERR_NOIGNORE *read_program(const char *path) * Split out so the ATTEMPT in main() has one thing to CATCH and the SDL * teardown has one place to happen. */ -static akerr_ErrorContext AKERR_NOIGNORE *draw_program(const char *outpath, int w, int h) +static akerr_ErrorContext AKERR_NOIGNORE *draw_program(const char *outpath, int w, int h, + const char *fontpath) { PREPARE_ERROR(errctx); SDL_Surface *shot = NULL; @@ -132,9 +154,22 @@ static akerr_ErrorContext AKERR_NOIGNORE *draw_program(const char *outpath, int FAIL_ZERO_RETURN(errctx, SDL_RenderClear(akgl_renderer->sdl_renderer), AKGL_ERR_SDL, "%s", SDL_GetError()); - /* stdout: a program that errors puts its "? line : CLASS message" there, - which is what the caller shows when a figure comes out wrong. */ - PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL)); + /* + * With no font: stdout, so a program that errors puts its + * "? line : CLASS message" where the caller reads it. With one: the grid, + * so the characters the program writes are in the picture. The file comment + * explains why the two are not teed together. + */ + if ( fontpath == NULL ) { + PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL)); + } else { + FAIL_ZERO_RETURN(errctx, TTF_Init(), AKGL_ERR_SDL, + "Couldn't initialize SDL_ttf: %s", SDL_GetError()); + FONT = TTF_OpenFont(fontpath, (float)AKBASIC_FRONTEND_FONT_SIZE); + FAIL_ZERO_RETURN(errctx, (FONT != NULL), AKGL_ERR_SDL, + "Couldn't open the font %s: %s", fontpath, SDL_GetError()); + PASS(errctx, akbasic_sink_init_akgl(&SINK, &GRIDSTATE, akgl_renderer, FONT, w, h)); + } PASS(errctx, akbasic_runtime_init(&RUNTIME, &SINK)); PASS(errctx, akbasic_graphics_init_akgl(&GRAPHICS, &GRAPHICSSTATE, akgl_renderer)); PASS(errctx, akbasic_sprite_init_akgl(&SPRITES, &SPRITESSTATE, akgl_renderer, &GRAPHICSSTATE)); @@ -144,11 +179,16 @@ static akerr_ErrorContext AKERR_NOIGNORE *draw_program(const char *outpath, int PASS(errctx, akbasic_runtime_run(&RUNTIME, 0)); /* - * The drawing verbs are immediate and are already on the target. Sprites are - * not: they are libakgl actors and reach the target through a render pass, - * which in the standalone frontend is part of a frame. There is no frame - * here, so this is it. + * The drawing verbs are immediate and are already on the target. The text + * grid and the sprites are not: both are redrawn from state the interpreter + * keeps, which in the standalone frontend happens once a frame. There is no + * frame here, so this is it -- and the order is the frontend's, grid first + * and sprites over it, because that ordering is what a program written + * against the real host will have assumed. */ + if ( fontpath != NULL ) { + PASS(errctx, akbasic_sink_akgl_render(&SINK)); + } PASS(errctx, akbasic_sprite_akgl_render(&SPRITES)); shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL); @@ -167,6 +207,7 @@ int main(int argc, char **argv) PREPARE_ERROR(errctx); int w = 320; int h = 200; + const char *fontpath = NULL; if ( argc < 3 ) { usage(); @@ -178,6 +219,9 @@ int main(int argc, char **argv) if ( argc > 4 ) { h = atoi(argv[4]); } + if ( argc > 5 && argv[5][0] != '\0' ) { + fontpath = argv[5]; + } if ( w <= 0 || h <= 0 ) { usage(); return 2; @@ -194,8 +238,13 @@ int main(int argc, char **argv) ATTEMPT { CATCH(errctx, read_program(argv[1])); - CATCH(errctx, draw_program(argv[2], w, h)); + CATCH(errctx, draw_program(argv[2], w, h, fontpath)); } CLEANUP { + if ( FONT != NULL ) { + TTF_CloseFont(FONT); + FONT = NULL; + TTF_Quit(); + } if ( akgl_window != NULL ) { SDL_DestroyWindow(akgl_window); akgl_window = NULL;