From 1fb808f480ce93f63334612aa37289b2feaca0b8 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sun, 2 Aug 2026 08:08:23 -0400 Subject: [PATCH] Rewrite the two game tutorials as instructions rather than commentary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chapters 17 and 18 read as a code review of a finished listing: they explained why each decision had been made, walked through the project's own history, and led with what had once been broken. A reader who wanted to build the game got the reasoning and had to reconstruct the program. Both are now step-by-step. Each opens with a picture of the finished game and a bullet list of the steps, each bullet is a section, and each section states its goal, shows the code, and says how to check it. Chapter 17 is sixteen steps and Chapter 18 is thirteen, and the last of each is the assembly: the order of the file, the full declaration block, and the routines the earlier steps referred to. Project history is gone -- it belongs in Chapter 14 and in git -- and where a listing has to do something awkward, the tutorial shows how first and names the `TODO.md` item that will make it unnecessary second. **Three defects had no entry anywhere**, which the rewrite found by trying to state each rule as a rule. §6 item 35: `a - b + c` computes `a - (b + c)`, because `subtraction()` sits above `addition()` as its own precedence level and the inner loop eats the `+`. Item 36: only one unparenthesised `AND` or `OR` is matched, which is item 12's `if`-where-`while` on the one operator pair item 12 did not reach. Item 37: a `GOTO` out of a `FOR` or a `DO` leaks the loop's scope, so a main loop written that way stops on the thirty-second lost life -- which is why both games are built out of `LABEL` and `GOTO`, and it is a workaround rather than a preference. Chapter 3 gains the identifier rule the third trial ran into: there is no underscore in a name, and the error says `UNKNOWN TOKEN _`. **`tools/screenshot.c` learned to draw the text layer**, behind a new `text=1` fence attribute, because Chapter 17's game is characters in the grid and a figure without that layer is two sprites on black. It opens the bundled font at the size the standalone frontend uses, so a figure's cell size is the reader's cell size, and it uses the akgl sink alone rather than a tee so the program's output lands in the picture instead of on the stdout the caller reads to decide a figure failed. Both new figures -- `breakout-game.png` and `breakout-game-artwork.png` -- are generated from listings in the chapters like every other one. Verified by handing each chapter, alone, to an agent on a much smaller model and telling it to build the game from the tutorial text with the `examples/` tree off limits. The first pass scored 3.5 and 3 out of 10 and named what was missing: routines referred to but never shown, the third level layout, the sprite `DATA`, the font table, edits to earlier routines that were never marked as edits. Those are now in. The second pass built a 658-line Chapter 17 game that plays itself for ninety seconds with the score at 1890 and no error line, and the third built a 1053-line Chapter 18 game with 61 labels, no invented routines, no gaps found, and forty seconds clean. 9/10 and 8/10. One real bug in the new prose, caught in review: Chapter 18's `HITBAR` did not set the `HIT#` that `BALLPADDLE` reads to decide whether the paddle already caught the ball. Both suites green in both configurations, `docs_examples` and `docs_screenshots --check` pass. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL --- MAINTENANCE.md | 15 +- TODO.md | 114 ++ docs/03-the-language.md | 5 + docs/06-graphics.md | 2 +- docs/13-differences.md | 2 +- docs/14-architecture.md | 2 +- docs/17-tutorial-breakout.md | 1686 +++++++++++++++---- docs/18-tutorial-breakout-artwork.md | 2098 ++++++++++++++++++++---- docs/README.md | 7 +- docs/images/README.md | 5 +- docs/images/breakout-game-artwork.png | Bin 0 -> 7789 bytes docs/images/breakout-game.png | Bin 0 -> 3493 bytes examples/breakout/characters/README.md | 9 +- examples/breakout/sprites/README.md | 8 +- tools/docs_screenshots.sh | 26 +- tools/screenshot.c | 79 +- 16 files changed, 3391 insertions(+), 667 deletions(-) create mode 100644 docs/images/breakout-game-artwork.png create mode 100644 docs/images/breakout-game.png 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 0000000000000000000000000000000000000000..2bddd66fecee5dfa0884689841922c4b676ce374 GIT binary patch literal 7789 zcmeHMc~sL`vi`9M3JR?#j-o^xl|~5Ig6xaCbP&+61qg%&S;J1)vtT35K^oLH5ZR$M zAcRd>!;*lCsO-=IfdDRqB@mD;A!K`Dy64S*(=+q>oOkA(m%ox*Rrh}NRo%L$Dk(Q? zuI-UKEC&F|*nGSg&p-SegJ?5vr<<<&spVik3*#rE)l}n|4Tnb;zsTY&*p!2&y(b$| z;wEk0PuWCFKe%?u-HE_6ObetA>rZUD^Y|}0=bn!?fVP4c*RGDfS-6!bimzLU+$TKj z_lhGNwFtgdV=E}J@MBxf>2-m5HCU}8FawsKB`m3^_WIVP`PO?ueD zTTKG>mgnvr8JWSdkw=UDP=U7>5n}V&_T9d6X?iE1XXD{!mTioQ%%Eks@AjQYzACmf zkyZxIpSc~G;m2I3ITVku4<-yN!P>4mbBgx5tFQ0B9eM5yhSumlJ_Vy3yQ9{EdS}(?{vp+A-Coh3h4mdWRhC0Yzm>Ey4D@k=jsGO`IYfG{mSm@ z$Eqh^CQAao=(mkaylg!s$hmV%ituBHvx* zw8+&Hne(LA8)L%ytLjvfxg%mO3~$$+Do_C0GWNnIeAVpEw(92%A$jAFVT|9MkzHn* zi%=t#+(mzO?zl6gln0 zp~w*&{li*4vO5i)-p@e&!!-T7FyZy|sZ*!M%w&KBQYV43PK;8N_`4LyWMm3oM(>gU z^ygkIE6RxjmIiaSwN#7c66zinkFui8hlM z<80E}Xw{PEZy;N%5f#2Ne+ui_UY?$wT=sUfKU<2U-SIBK#o5m1k7D~*4&U2RX_FGt z#{Bjv1SN=ST<{xz@2i#u<$1KYKiuS>%9%nVaV}?ufMTZy^wXt*Lf7*0;*4|Nf9U;z zakB){&@6~jHF0U@8Y`L7j2_2En)7IGi%!w=NrJEM{x63(N--fg{9K+IONBG_-`E_N z4z8clg#keOZ2|!xuUtj7!qNJqM)o@SzaH(9A1p2{_jzjOV&nEs4w*0rT z{J-H{@v~fBi93TEk#ee` zF6f7VCpn6p(QsYEGxnzIMM(WLh25OA<*^z;Dd^~UJZ9_DGMpy}$ukN=6-~U9POu8S zZ<6;(3UoO3ivee`(I>RC@`1_nSVJu83?ZD1S5X;9jeInTVomCwv2R%hv4k12_4V;j zfY4n&)qYfAJKsz(*dnH@*z;Se=*Ja=adi;4Ae@Yblv1gpmsE6D3c+2p;6-ixtmMje zH`tsIMaB6%AEO+XYKxOr4yq5b6=4uZK;x5Cf7FNK@yZEP{mn^SA9%_*bYe2dq~3Jx z*HjInEH0u?QMkC6YLSIxc}Nr(S zP)K~`sF8j>cfQ2ka-KfLC5#x~R0fV-@fZl`Dj^%kyj)5&SaipIrjH>|M-Lt1ZmOH} zKgo9@Orz*2&w;sbBvXf-&3kAcZitRXtG+kYZxOQV;)qb!InDBhQQ6?8U`~FXEq~5m z{hefCgrTA`_&GY8B!&#)O2C2+PwP7!XUdJ{kFQ%u`Y+MD zT|+1sbO0Mw+ETsV;@j6Dw&)&k@a1{hs2|kg3$yQ-bf!&*~w* z*kPfed20i%O;F^%Ws>o3oBPT3k4(*SwmtbpH(j#dQ!>>@T#(F(>+I4j2kLGZX=a#e zZ}Zurj>()d$TdY`zDi(MU$(5tHEg=!^3DtKSDb`{r$3JS7DtfLna`n-K0YgHZ@Gk-t`brmv^CRh zb+2}*dtX`IAey^QPEwjx#@;OJy`|B`Shag7Ul-@iWIf-w0J!vtSwVf8ZC-(24qBQRygYPWZD!%nL2^Sn7D8A5XI67%DRbHlv=SZ zcxW~{J{Y?~-w~NHIeyitylE^EkE|)NwJu)sO$8E^m0)@|W1nI9+)@1CdcXq8IWvH^ z=bU3}zBkFOGj=PG=x${fLg>ra3YlJ!`0#G0R`$ZHwKWZ5ox&ZLNMUIhvgbC~EKiJf zOk9BA>iGyujh^d{PI={HA5MRC!PM)0?<3|iYA;N0WQ%miU+N(YS)r)iO)FQF?01(; zEsp<8?5Jg*{&)cNW8LuL2w6AdmRNuL9#0IGn!j|~HDa7L-0i%O2-Ie^{W9-!isU;9 zyIw@P39zI!mg zwbiAPa1cq@NFc+t3lI#gFKa_}+h;{7!D?T0--=|iR^PLQ^KY5MD-?NQmyWNWVudb|n=UZ&GKQXvK&mATXZ0nxjeJ1_6Ft1Fe@=A=P%0lPkG4jn|!e~H=# zo7eWsmCQ_*wqb9pV+Df#-Ja6~Z8!{W3@l8qbZ8z`173Xvt9>*2kWt#uwc(O&?&>UL z@UMf%+sO2TGXY)dNSF&q3v5s5m6HI1PE`Bl5j&b0%lovu9g)b~uCfcxnKQjrMwA-! z&D?GBZ9N8zt!~%VLSPy+t5gDJj@A)S7x;aBHZ(h@^rWnZME@al>iuD(*cN-2rwY3d z^yQUKhpS{ayOh?#`WQ&vhYs7U?Yw=(DlaBj_}b(e0#~ZhS>!5a_7d#tCq(QQapt{X z&PGfX4qn`loyrP)ttQpRIGcFID&ydSh%wbNwsaFXAMwY%#(!A%_D?kpzqO-|fgAuB zQ!_9%UVp&*(L5X%923YTjPiedp>$8nbY&tC+mz-d)s)vJ8D|q5gIXX6S^d{6@<4~0 zHxbDeaX3tHz{hJibEB1WSv%l|Ztqyn{3N{enJNeko2B3ft-X@WE+th6$Y22%Kj;ZI zA>Xhd--th+mPTLW$S7d=>m#5t3LoB_yc-1)8{emYnS;8qRNsjbr4n6vI;IL6Gtb23 zKx;(AOX)C(UQ!XUlBf9Z%x0b?bTN)a7bgECS}M?@(L&%Bs4zG1_*j7`Z6P`Mu;%`4 z-3beS#?ITML^1e|8Cj}v=$HETi(7P_%#U(phlWEMGf}%|q7@ho>$5^OPbONCA*(Q1 z&uy=&k9m5&K?!!=_RXGq>E@v)i0d51-K;~W%bR-69}fa`D;|tUOrmdkU>ooA#JfQu zj}?!+-E*%V)p(w;ZT1&3PoDeWnN*IPtU-ml_@4D(ma!*hBFMsvW4iW@{^j>Uhg8Hm z;6tZEGrrn3;J+!cgMjGulLqlj=lWHusf?}UD?QF^Wny_(r`fr*+ua4xCH!_SgOQ~uoL4B2QTPM_u`qp5dU%= zgJA+SMjuJZwsv6JWkk!Bx77L>8qk<~r6$B(mst^tPcCw`5YBiCFlx)_)X=h@tsMdg zEuV8zH;By!(daf>$G+qJO3DSBYxqG@1flg4XZRxKZ+^ixS2$2er!TT5!wQ6{D~ETv zXIFryjuu{!l%LyGH&oS&5#57AaFI=eo2^)5=Zas4=p510s4ZIYo;{6nyR-_OuG^q1 z;+O|Rz{9whvZB{<{>4#&O^#+8sc%!N&cz3|Ib8LSX#F~;z+AAAEcQ=))>Ldo?fOQW zLk4mb(Q;Er8p_zB|2kh~f$yG7&gq zB8p|^P<@x=8qbPdGaBWulT)gC%-KEG$<#C9o6D6gyKTgmY3Z5hTi$yEYq*5U@#+Ls z2Mm`Sq12Zb0hshlr~lzhxb*fX1Ea z5grxbkiWbY(%Kyrz^^T0^&5Jrj<)%4lxOWAb$05fJF)P1h9Bwmj;}6xT56-on4tZ~qtQ$l9tWjvFw%1d;32?c52P-?^1H&9vzv9US#P^D+C{MIalV`ge|21 zJg}oVGIT}Mn+E6eh=K{@S_}>{L}ac+$TQq{OkZ~^Uw069kuVD#gq?R1H}R-D6xU_d zCSE&UXFnVCZuxz)cXf;(dg$cBX(9@PbCMa}5M)>d>l)uM)uZ@#d3;Ot3i)1CgxC2A z&F;c9Oiu6zBm(hmKd`%&$1K)Mlc5l<8!tRj?PUkQ9ek;SAD4Vvhg=LBbu2f)pshm} zZj?Ugxyz-CWiLC>!J#LpNCd=x&Q6hWwKK=mI3aBxtRH&`w^qdeU>Ury#D5=ioG?iZ zYJAWHaceejobLbNsg`qKjV$GwEJwzJRQl3bZxqvaV^`ODR8_bL0>N=1A2H~b8%AdKJ?u(ov@m8+e9Jfm85ZQ+YkB+ zRKY_HBK>_#<&Enati$sme99i}+Q{P9QkukCuctahyypIhoQ7ijRtUBEyT_6{k5?D= z$eAcU!LM$w>T?brh99}UN9S2&c@a9w`q{vt#@n9Q#z(dq4E-Q$jRTyC!B-C?0v~N3 zWfE!y(J(O09yT1JxirOLLf?A#v<0btQ@n9CG%GoGOHzKUiv4aNsgs%kEo)u9h?xmg zi_U_V7vpX0We+mlxZ2pqW7J2CA?{J~ldbO2F-@H(cx6>l=DcR%S|sC|E*t~Faan^y z_lR*7!mYl;&YHPK7Ar%>e=#9V?D>V$edgw_`A7M$5GikEN#F6QRiocH@ELd#bSfZ#{&~cxyjhp3e zCjf;%)Jeh#E;}+Q&c=m=U0QROGkif+ro0$6>y^t8MSYAR09TbIOZ{6jzpJAOJJrSiV;L&^?a!-(;B}FNZ}GXk TUGY?`Q)uzHhrB@G4b@liJ^6N zR?pt-i~s4|cPIb&aFgR-$z8dhAKF*YbiwO0uNO)`%R}ET)n82KKRSBa1&SV1Wuwxe zLc!1rHSZogF1$2;!kM9Djk^go`s8eXA#_;1)^}jM#`n(xkd>+JkPktyX22V*5{uy+ z|H!9O-KxR?Pq6SdzYx*tqIo%aE+u~Rfwtv2F;jmTaZn?WJotc49_%OI!OrF{ku9os zo-DirUF>ypd>T#MrB!BwFtsWeeOG!{DzuxzQxO}&`9AQP%O z@8MayIs!qI4n6!DaU_Do=hcWgw46}ouuj+EJ+$tHR9MU%?rpaKY|?gEBN+-c&sMO3 zl^Mi2=S6pAOGA1d^-8BCqykN*c&d*uk}}BRlyJ)#up8(SuA#Y1l*9S^HTsV7ONmia zWq;R4uJMMCLhd7Q5vmsX`pN6Qy8-}*qpw*Z64V8WO5ItstG+gm-3)g&QAp2eP?37_ z`b*{h?`vz=SNlhAZpdsQ1hbS2k<2r6P1(=O9d?MR;4P8C6r)G*5Q}o1%Ad$nq`c1Y z-ynTJ{86;ORZu7K4&t2ZPpQ1`O^7LqxTz7Ifs`LsbX9nPO8m5#x+N&a&M-{`cUzPAd|JJS*>At*^y z_^;d<2I_n$UyUF~?5d=^^D%s?e7vVN917)Y3>CnnjlgW*$uTd?Wj9*Q`taocRX;e# zFTuL>m6?;An+KS_Xwsu0R%Qe;;*K9Iuda_lWxw^=+X3ae@TLR6lTNsA8lmn6P9q_n zOC2B$GeXd2<3N>(i)|MG8?UIk2anyO{3uQes>vLzDW5mY;W%tU)gLHS?!-pxS-|2_ zPog-Zvz604GtN>qB6Wqde#0do#hVBK?G5z21bqG~5U)a#f=)uM36_}h*h_C~tQRR) zenK*OWuyek*UFS9$!O&(jL_ZfYVM6E*GIFPwYp*O6oe0e8)Qn#SRVmJXJ5xSk4YP$ zq1Eo0q+(LzB}ro6sDZm9+(Ff($d@ITmAHJ{`OznofI1NkMYZb)%Oev?;~7qFSNHlH z*QVUXmtaG9DB5Bo9oX-)qmv&H1m8-cufJ>x`d##xl1ESWxsUNPVUB1$7c40y;9zWI=w=?{<{5~gm3uC^LneD1#uP~fXl=xW*{%*!q&$`h#Q{00tUK=dE+ZbV zVrisZdk0fmqtF}~bVtlMM~PHvJYFZefuL0kwgr<|w@9+hzO=54`jp2k<%Uyw4_OHk zTm(ho%e5ow2WdiRdJ=XGoB#QXa3)W}^C|f4G%x0c{gmK8=nX=$(g5^;?jOH0!ySb~ zjODc@6pQ-QiM%Y;=xJ~@Hn1hCDI-t@3(GWK$tt0|>Moh>^0&7}gmk>S5e9E!7}o%U z>6qc{z4r^`0!n4LW_7h_&xjPh&tqwI4%CL?_qARZXQ~F4@j+G-ad90lWw5p~7cX1_ z;@*1P)2L;_ju_zVFRmZ6FE|h(osr!zfs?_@^ieXnr-o)Xh4lRCw-^n(_ZFV< z!}E;&v%B52_%6A!KvUwP1ze8n$^U-F)6(px_`RuX$A(Dyp4O1s)OVFTLN}$j=eCx) z)nM5u4KH@*-|Fh><1`$3h6^=xDVBn&(++d{5&6{3L?*_qmn(0aB|cwKEswcP8R&Xg zgl~}yG5o-^J^?-}M|8o+WJ~M<{=slz*CbtOIH?`MzmrQrZY%pUhQ$! zG`7E;H5oEz?_s3zVeDEsGS6;^Y2@~&e^bBr>{^o}RKhI;6~78R&1qXo!^_xuPrGz} zZD%5<(bF7%xNy1%yT2*+t|5lNdGdi<|6XbgrgAbr-AMdx7%|JRO+2gPmWhO~ud>^S zyL!G{fF*@We!yp$i4|zU3a%~K8yD}Qm#BCis02gKoa%h6;G)>NyrI;9T3Sh;tue$t z2QZ`hqn>R-`_uRdflmnh9t37(cGv>H@JFXqVm;p3zv*C)s=|VhUJ=bNjhl9S%5?^A zf|e%VT`+?w5l9S%)8mXS#MqAlXSa>x4p={&be*yE*}6GKw|2qRPJ72-JX+>-$O_cy zG!mPQYr^MQ0G1y#pts%Z_XPUU-n@(s+ctYTOnFXrDI|llcO#LFRF0O}8m2=d1*6FW z>U(rwPdF^CQ==i#Mf{%REW&*G8x7=K?1m`kcF5z>3@=WL%F71`&%#^e=#&zhrfPJX z>Zq2cXUSPq1(~cLtgE@SXf4W$Xjxt*=D?2%K|L>9^mB!e|3;ko<-=W&*U(XFpWlE~{F@A*`xp3iw6Zh*^wj^i;^0J^&>+|L(?ZrYV zQpASlE5}j;NupKpckfbYIH~)SW7yx_8YkT4N6Nay)?E|Z!{-&)Wrf9c66)BC;CY<% kM#lPBOG66(lh{nUubr!pjtdSvgW1^{xP1$IbL`>&0CzYMX8-^I literal 0 HcmV?d00001 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;