Add two Breakout examples and the tutorials that build them
Some checks failed
akbasic CI Build / cmake_build (push) Failing after 3m10s
akbasic CI Build / sanitizers (push) Failing after 4m5s
akbasic CI Build / coverage (push) Failing after 3m29s
akbasic CI Build / akgl_build (push) Failing after 21s
akbasic CI Build / mutation_test (push) Failing after 3m19s
Two complete games in `examples/breakout/`, both 100% BASIC: `characters/` draws its wall in the text grid with two `DATA` sprites for the ball and paddle, and `sprites/` loads CC0 artwork and captures its whole screen with `SSHAPE`/`SPRSAV`. They take opposite shapes for reasons that are entirely this interpreter's, which is what the chapters are for. `docs/17-tutorial-breakout.md` and `docs/18-tutorial-breakout-artwork.md` build each one a step at a time, and end in a checklist of the rules a real program runs into: create every name before the loop starts, write a text row whole, loop with `GOTO` rather than `DO`, put the float on the left. Every trap is a runnable block with its own output rather than a claim -- the value pool dying at four thousand names, the skipped `BEGIN` block that breaks its caller's `RETURN`, `SSHAPE` ignoring a subscript, `READ`'s single cursor. Five figures, generated from the listings beside them by `docs_screenshots`, and a `breakout_art` setup so the ones that load artwork load the example's own. The character game's wall cannot be photographed -- the screenshot host omits the text layer on purpose -- so it is shown as compared output instead. `docs/07-sound.md` never said `SOUND`'s frequency is a SID register value rather than hertz, which both games depend on. It says so now, with the conversion from `src/audio_tables.c:84`. `TODO.md` gains the thirteen defects the two games turned up -- §6 items 30 to 33 and all of §9 -- each with a reduction that fits on a screen, the file and line of the cause, and what a fix would touch. Verified: `docs_examples` passes in both build configurations, `docs_screenshots --check` re-renders all thirteen figures and byte-compares them, the full 109-test suite passes in both builds, every quoted fragment was checked to appear verbatim in the listing it came from, and every relative link and anchor resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@@ -25,7 +25,7 @@ scripting engine for game authors.
|
||||
| [`MAINTENANCE.md`](MAINTENANCE.md) | Everything above. Start here |
|
||||
| [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence. §0.1 first — it retires the byte-for-byte fidelity constraint several later sections were written on |
|
||||
| [`README.md`](README.md) | What the project is and why, for somebody who has not seen it |
|
||||
| [`docs/`](docs/README.md) | The language itself: fourteen chapters, verb and function reference. [Chapter 14](docs/14-architecture.md) is the interpreter's architecture — the step loop, the pools, the two kinds of error, and how to debug it. [Chapter 15](docs/15-error-codes.md) is the error-code appendix |
|
||||
| [`docs/`](docs/README.md) | The language itself: eighteen chapters, verb and function reference. [Chapter 14](docs/14-architecture.md) is the interpreter's architecture — the step loop, the pools, the two kinds of error, and how to debug it. [Chapter 15](docs/15-error-codes.md) is the error-code appendix. [Chapters 17](docs/17-tutorial-breakout.md) and [18](docs/18-tutorial-breakout-artwork.md) are tutorials that build the games in `examples/breakout/` |
|
||||
| `deps/libakerror/AGENTS.md` | The `ATTEMPT`/`CLEANUP`/`PROCESS`/`HANDLE`/`FINISH` protocol, authoritatively |
|
||||
| `deps/libakerror/UPGRADING.md` | 1.0.0's status registry. Required before writing an error code |
|
||||
| `deps/<library>/AGENTS.md` | Per-repo rules. Read the relevant one **before editing a submodule** |
|
||||
|
||||
@@ -126,7 +126,7 @@ version are catalogued in [`TODO.md`](TODO.md) and summarised for a BASIC progra
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| [`docs/`](docs/README.md) | The guide: fourteen chapters, the language then each hardware area then a reference section for every verb and function, [Chapter 14](docs/14-architecture.md) on the interpreter's own architecture, and [Chapter 15](docs/15-error-codes.md) listing every error code |
|
||||
| [`docs/`](docs/README.md) | The guide: eighteen chapters, the language then each hardware area then a reference section for every verb and function, [Chapter 14](docs/14-architecture.md) on the interpreter's own architecture, [Chapter 15](docs/15-error-codes.md) listing every error code, and [Chapters 17](docs/17-tutorial-breakout.md) and [18](docs/18-tutorial-breakout-artwork.md) building a whole game twice |
|
||||
| [`MAINTENANCE.md`](MAINTENANCE.md) | For contributors and maintainers: the documentation-example harness, the three test lists, mutation testing, error-code allocation, style |
|
||||
| [`TODO.md`](TODO.md) | Outstanding defects, with file, line and consequence |
|
||||
| [`tests/reference/README.md`](tests/reference/README.md) | Where the golden corpus came from, and the rule for changing it |
|
||||
|
||||
424
TODO.md
@@ -1858,6 +1858,147 @@ the reference's semantics reproduced faithfully; the third is the port's own.
|
||||
reentrant" and "do not grow the runtime by a third for a scratch buffer" are both right
|
||||
and picking between them is a decision.
|
||||
|
||||
### Found while writing a game against the interpreter
|
||||
|
||||
A complete Breakout, sprites and text grid and keyboard, running for minutes at a time. Nothing
|
||||
in either corpus runs for minutes, which is why the first of these had never been seen.
|
||||
|
||||
30. **A variable created inside a scope costs pool slots that never come back, so a long-running
|
||||
program has a hard budget of 4096 name creations.** `akbasic_ValuePool` is a bump allocator
|
||||
on purpose (`include/akbasic/value.h:30`), and the reason given is that "nothing in BASIC
|
||||
destroys a variable". **Scope exit destroys variables.**
|
||||
`akbasic_runtime_prev_environment()` (`src/runtime.c:121`) marks a popped environment's
|
||||
variables `used = false`; `akbasic_runtime_new_variable()` (`src/runtime.c:31`) `memset`s
|
||||
the slot it hands back, clearing `values` and `valuecount`; and `akbasic_variable_init()`
|
||||
(`src/variable.c:98`) therefore takes *fresh* slots for a variable whose old ones are still
|
||||
counted against `obj->next`. Nothing decrements it, so every `GOSUB` that creates a local
|
||||
leaks the local's slots for the life of the run.
|
||||
|
||||
Reduced, and it fails on both builds:
|
||||
|
||||
```basic
|
||||
X# = 0
|
||||
FOR T# = 1 TO 6000
|
||||
GOSUB SUBA
|
||||
NEXT T#
|
||||
PRINT "OK " + X#
|
||||
END
|
||||
|
||||
LABEL SUBA
|
||||
LOC# = 1
|
||||
X# = X# + LOC#
|
||||
RETURN
|
||||
```
|
||||
|
||||
`? 110 : RUNTIME ERROR Array of 1 elements does not fit in the 0 remaining value slots`,
|
||||
at `LOC# = 1`, on the 4091st call -- 4096 less the handful of names the program itself
|
||||
declared. Both builds, same line, same count. Declare `LOC#` alongside `X#` and the same
|
||||
6000 calls cost nothing at all. A `FOR` counter behaves the same way: name one that does
|
||||
not already exist and every entry to the loop spends a slot.
|
||||
|
||||
**What it costs a program:** a game loop that creates one name per tick is dead in half a
|
||||
minute, and the error names the line that happened to be unlucky rather than the line that
|
||||
caused it. Breakout ran twenty-five seconds before this was understood. The workaround is
|
||||
real and not onerous -- declare every name, loop counters included, at the top -- but it
|
||||
has to be *known*, and right now the only way to learn it is to hit it.
|
||||
|
||||
Three ways out, in increasing order of work. Reclaim on scope exit: give the pool a mark
|
||||
and release, take the mark when an environment is pushed and rewind to it when it pops,
|
||||
which is exactly the discipline the environments themselves already have and is why they
|
||||
are a pool in the first place. Or keep the slice on the recycled variable: stop the
|
||||
`memset` in `new_variable()` from clearing `values`/`valuecount` and let
|
||||
`variable_init()`'s existing "reuse the slice when it is already big enough" branch do the
|
||||
work -- much smaller, and it fixes the common case of the same-sized scalar being recreated
|
||||
over and over. Or, at minimum, say so in the manual and in `value.h`'s comment, which
|
||||
currently states the opposite.
|
||||
|
||||
Whichever is chosen, the test is a CTest program that enters and leaves a scope creating a
|
||||
variable tens of thousands of times and returns zero -- and it wants to exist even for the
|
||||
documentation-only outcome, pinned at whatever the real budget turns out to be. Item 33
|
||||
has to be settled with it: while the pool is dry the failure cannot be trapped, so a
|
||||
program cannot even report this one against itself.
|
||||
|
||||
31. **`WINDOW` cannot be reached in the standalone AKGL build.** `akbasic_sink_init_tee()`
|
||||
(`src/sink_tee.c:143`) wires `write`, `writeln`, `readline`, `clear` and -- conditionally
|
||||
-- `moveto`, and never sets `window`. The standalone frontend runs the interpreter against
|
||||
the tee (`src/frontend_akgl.c:215`), so the fully implemented `sink_window()` underneath it
|
||||
(`src/sink_akgl.c:413`) is unreachable: `WINDOW 0, 0, 10, 10` in the windowed build answers
|
||||
`? RUNTIME ERROR WINDOW needs a text device with a character grid, and this one has none`.
|
||||
The verb is only usable by a host that hands the runtime the akgl sink directly.
|
||||
|
||||
It looks like an oversight rather than a decision, because `moveto` right above it *is*
|
||||
forwarded, and with the same reasoning that would apply here. The fix is a `tee_window()`
|
||||
that forwards to whichever half offers one, offered only when one does, and a case in
|
||||
`tests/sink_tee.c` beside the `moveto` one. §3's "Still missing from the sink" closes by
|
||||
calling `WINDOW` "ordinary work here rather than anything blocked", which is now half true:
|
||||
the work was done and then not plumbed.
|
||||
|
||||
Worth deciding at the same time: **a program still has no way to ask how big the text grid
|
||||
is.** `RGR(1)` and `RGR(2)` give the window in pixels, and nothing gives columns, rows or
|
||||
the cell size, so anything that wants to place a character *and* a sprite at the same spot
|
||||
has to hardcode a cell size measured against the bundled font -- which is what the Breakout
|
||||
in `examples/breakout/characters` does, and it is the one thing in that listing that will break
|
||||
on a different font or window. Two more `RGR()` indices would answer it.
|
||||
|
||||
32. **A character written past a short row's terminator is stored where nothing will render
|
||||
it.** A grid row is a NUL-terminated string and `putchar_at()` (`src/sink_akgl.c:89`)
|
||||
writes the character, advances, and terminates -- so `CHAR 1, 40, 1, "#"` on an empty row
|
||||
stores `#` at column 40 with `text[1][0]` still `'\0'`, and `sink_akgl_render()` draws
|
||||
nothing at all. The same call on a row already 50 characters long draws the `#` and erases
|
||||
columns 41 onward, which is the documented truncation and is fine.
|
||||
|
||||
It is the *silent nothing* that is the trap: the write succeeds, the cursor moves, stdout's
|
||||
mirror shows the character, and the window stays blank. Either pad the gap with spaces on
|
||||
a `moveto` past the terminator, so a positioned write always lands, or say plainly beside
|
||||
`CHAR` in `docs/11-verb-reference.md` and deviation 49 that a row is written from column 0
|
||||
or not at all. Padding is the friendlier of the two and costs one loop in `sink_moveto()`;
|
||||
the argument against it is that it makes `moveto` write to the grid, which it currently
|
||||
does not.
|
||||
|
||||
33. **A `TRAP` handler cannot be entered once the value pool is dry, and the error is dropped
|
||||
in silence.** `akbasic_trap_set_error_variables()` (`src/runtime_trap.c:36`) sets `ER#`
|
||||
and `EL#` through `akbasic_runtime_global()` (`:51` and `:53`), which *creates* them if
|
||||
the program never named them. Creating them needs pool slots. So the one error most
|
||||
likely to leave the pool empty -- item 30's -- is the one error whose handler cannot be
|
||||
reached, and because the dispatch fails inside the error path rather than raising, **the
|
||||
program keeps running with the failed statement's effect quietly missing**.
|
||||
|
||||
It is easy to see, and it is much worse than an abort:
|
||||
|
||||
```basic
|
||||
X# = 0
|
||||
TRAP RANOUT
|
||||
FOR T# = 1 TO 6000
|
||||
GOSUB SUBA
|
||||
NEXT T#
|
||||
PRINT "OK " + X#
|
||||
END
|
||||
|
||||
LABEL SUBA
|
||||
LOC# = 1
|
||||
X# = X# + LOC#
|
||||
RETURN
|
||||
|
||||
LABEL RANOUT
|
||||
PRINT "DIED AT X " + X#
|
||||
QUIT
|
||||
```
|
||||
|
||||
prints `OK 4092`. The handler never ran, nothing was reported, and `X#` is 1908 short of
|
||||
the 6000 the loop counted -- a wrong answer delivered as a right one. Add `ER# = 0` and
|
||||
`EL# = 0` at the top, so the dispatch has nothing left to create, and the same program
|
||||
prints `DIED AT X 4090` from its handler as it should.
|
||||
|
||||
Two things want fixing and they are separable. The dispatch should not allocate: create
|
||||
`ER#` and `EL#` at runtime init, where there is always room, rather than at the moment the
|
||||
program is in trouble -- they are reserved names and §1's other reserved globals are
|
||||
already there. And a failure *inside* the trap dispatch should not be swallowed; if the
|
||||
handler cannot be entered, the original error is the thing to report, and reporting it is
|
||||
what the untrapped path already does correctly.
|
||||
|
||||
`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.
|
||||
|
||||
## 7. Filing gaps against `libakgl`
|
||||
|
||||
When phase 7 or phase 8 needs something `libakgl` does not have, **stop and file it** in
|
||||
@@ -2285,3 +2426,286 @@ What remains, in priority order:
|
||||
own body said the fill was filed rather than implemented. Drawing the figure and getting
|
||||
an outline back is what caught it. `akbasic_GraphicsBackend::filled_rect` is implemented,
|
||||
recorded by the mock, and reached by no verb at all as a result.
|
||||
|
||||
---
|
||||
|
||||
## 9. Defects found by writing a game against it
|
||||
|
||||
`examples/breakout/sprites/breakout.bas` is a complete Breakout — sprite artwork,
|
||||
powerups, a stroke-font HUD — written in this dialect, for this interpreter, by somebody who
|
||||
had not written a line of it before. Eight things came out of that, and they are here for the
|
||||
same reason §8's three are: **none of them was on any list, and all of them were found by
|
||||
using the thing rather than by testing it.** The corpus reaches none of them.
|
||||
|
||||
Ordered by blast radius. Each has a minimal reproduction that fits on a screen; every one was
|
||||
reduced against `build/basic`, the stdio build, unless it says otherwise.
|
||||
|
||||
1. **Every call to a user-defined function leaks value-pool slots, so a program may call one
|
||||
about four thousand times and then die.**
|
||||
|
||||
```basic
|
||||
DEF ADDIT(N#)
|
||||
RETURN N# + 1
|
||||
R# = 0
|
||||
FOR I# = 1 TO 8000
|
||||
R# = ADDIT(I#)
|
||||
NEXT I#
|
||||
```
|
||||
|
||||
```output
|
||||
? 5 : RUNTIME ERROR Array of 1 elements does not fit in the 0 remaining value slots
|
||||
```
|
||||
|
||||
It fails between 4000 and 5000 calls, which is `AKBASIC_MAX_ARRAY_VALUES` (4096,
|
||||
`include/akbasic/types.h:29`) at one slot per call — the call scope's parameter. **Both
|
||||
`DEF` forms leak**; the single-expression `DEF SQR1(X#) = X# * X#` fails at the same point.
|
||||
A `GOSUB` doing the same work runs eight thousand times without complaint, which is the
|
||||
comparison that isolates it.
|
||||
|
||||
`akbasic_valuepool_take()` (`src/value.c:142`) is a bump allocator — `obj->next += count` at
|
||||
`:160` — and **nothing anywhere returns a slot**. The only reset is
|
||||
`akbasic_valuepool_init()`, from `src/runtime.c:209` at startup and
|
||||
`src/runtime_housekeeping.c:53` for `CLR`/`NEW`. Environments *are* released back to their
|
||||
own pool (§1.4, and §6 item 11 says so), but the value slots the variables in them held are
|
||||
not: `src/environment.c:321` takes them through `akbasic_variable_init()` and the release
|
||||
path has no counterpart.
|
||||
|
||||
The consequence is a hard ceiling on a whole language feature. A game calling one function
|
||||
per frame at 30fps gets a little over two minutes. It is why the game in question spells its
|
||||
pseudo-random generator as a `GOSUB` over globals rather than the `DEF` it wants to be.
|
||||
|
||||
Fixing it means giving the pool a free list, or giving each environment a value arena
|
||||
released with it. The second is closer to the house style and to what §1.4 already does for
|
||||
environments; it touches `src/value.c`, `src/variable.c`, `src/environment.c` and
|
||||
`include/akbasic/value.h`. A test belongs in `tests/user_functions.c`: call a `DEF` ten
|
||||
thousand times and then `DIM` an array.
|
||||
|
||||
2. **A `BEGIN` block that is skipped leaks a scope for every loop inside it**, because `FOR`
|
||||
and `DO` create their environment at *parse* time and the skip is decided at *evaluate*
|
||||
time.
|
||||
|
||||
```basic
|
||||
N# = 0
|
||||
LABEL AGAIN
|
||||
N# = N# + 1
|
||||
IF 1 = 0 THEN BEGIN
|
||||
FOR I# = 0 TO 2
|
||||
N# = N# + 1000
|
||||
NEXT I#
|
||||
BEND
|
||||
IF N# < 40 THEN GOTO AGAIN
|
||||
```
|
||||
|
||||
```output
|
||||
? 5 : PARSE ERROR Environment pool exhausted at line 5 (32 in use)
|
||||
```
|
||||
|
||||
Thirty-two skips is `AKBASIC_MAX_ENVIRONMENTS` (`include/akbasic/types.h:31`) at one per
|
||||
skip. Inside a subroutine it announces itself on the very first skip and much more
|
||||
confusingly — the orphaned scope sits between the routine and its caller, so the `RETURN`
|
||||
after the block reports from a routine that plainly *was* entered by a `GOSUB`:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
```output
|
||||
? 11 : RUNTIME ERROR RETURN outside the context of GOSUB
|
||||
```
|
||||
|
||||
That is the form it was found in, and it costs an evening because the error names the one
|
||||
construct that is not at fault.
|
||||
|
||||
**The cause is an ordering mismatch and it is exact.** `akbasic_parse_do()`
|
||||
(`src/parser_commands.c:266`) and `akbasic_parse_for()` (`:849`) both call
|
||||
`akbasic_runtime_new_environment()` while parsing the line. The block skip is tested in
|
||||
`akbasic_runtime_interpret_line()` (`src/runtime.c:767`), which runs *after* the line has
|
||||
been parsed — so a skipped `FOR` pushes a scope, its execution is skipped, and the `NEXT`
|
||||
that would pop it is skipped too. `BEGIN` itself pushes nothing, which is why a nested
|
||||
`BEGIN`/`BEND` inside a skipped block is harmless, and a `GOSUB` inside one is harmless for
|
||||
the same reason.
|
||||
|
||||
Confirmed for both loop forms: `DO`/`LOOP` in a skipped block fails identically. Confirmed
|
||||
for both kinds of enclosing routine: a multi-line `DEF` reports the same thing.
|
||||
|
||||
A fix has to either push the loop's scope at execution rather than at parse, or have the
|
||||
skip release what parsing pushed. Touches `src/parser_commands.c`, `src/runtime.c` and
|
||||
`tests/structure_verbs.c`, which today only exercises blocks whose bodies are plain
|
||||
statements.
|
||||
|
||||
3. **In the standalone SDL build, nothing the graphics verbs draw can ever be seen.** Not
|
||||
"does not persist" — *never visible at all*, in any frame.
|
||||
|
||||
```basic
|
||||
GRAPHIC 1, 1
|
||||
COLOR 1, 3
|
||||
BOX 1, 100, 300, 700, 500
|
||||
PRINT "THE TEXT SHOWS AND THE BOX DOES NOT"
|
||||
LABEL SPIN
|
||||
GOTO SPIN
|
||||
```
|
||||
|
||||
The text appears; the box never does. Two mechanisms combine, and each alone would be
|
||||
survivable:
|
||||
|
||||
- `akbasic_sink_akgl_render()` fills **every row of the text area, opaque, every frame**
|
||||
(`src/sink_akgl.c:564`), and the area is the whole window — `state->rows = h / cellh` at
|
||||
`:505`. `akbasic_frontend_akgl_pump()` calls it after `akbasic_runtime_run()` and before
|
||||
`SDL_RenderPresent`, so it paints over everything the program drew during those steps.
|
||||
The comment at `:564` explains why it must repaint rather than track dirty rows, and that
|
||||
reasoning is sound; what it does not account for is that the rows it owns are all of them.
|
||||
- `WINDOW` would shrink the text area out of the way, and the akgl sink implements it
|
||||
(`sink_window`, `src/sink_akgl.c:413`) — but **the tee sink in front of it never assigns
|
||||
`obj->window`** (`src/sink_tee.c:143-151` assigns `clear` and conditionally `moveto`, and
|
||||
stops). So `WINDOW` refuses with "needs a text device with a character grid"
|
||||
(`src/runtime_console.c:179`) in the one build that has a character grid.
|
||||
|
||||
The result is that **chapter 6 cannot be run in the interpreter it documents**. Its figures
|
||||
are right because `tools/screenshot.c` deliberately omits the text layer — the comment in
|
||||
that file says so — so the harness that proves the chapter is exactly the harness that hides
|
||||
this. `docs_screenshots` passes and always would.
|
||||
|
||||
Sprites are unaffected: `akbasic_sprite_akgl_render()` runs after the sink, which is why the
|
||||
game draws its entire screen by `SSHAPE`-ing what it drew and installing it with `SPRSAV`.
|
||||
That is a fine trick and it should not be the only way.
|
||||
|
||||
The tee half is three lines and is worth doing whatever is decided about the rest.
|
||||
|
||||
4. **Mixed-type arithmetic is decided by the left operand alone, and nothing says so.**
|
||||
|
||||
```basic
|
||||
A# = 3
|
||||
PRINT A# * 0.45
|
||||
PRINT 0.45 * A#
|
||||
```
|
||||
|
||||
```output
|
||||
0
|
||||
1.350000
|
||||
```
|
||||
|
||||
`akbasic_value_math_plus()` and its siblings branch on `self->valuetype`
|
||||
(`src/value.c:510`, `:512`, `:539`) and convert the right operand to match, so an integer on
|
||||
the left silently truncates a float on the right. **This is inherited, not introduced** —
|
||||
`deps/basicinterpret/basicvalue.go:190-193` has the same shape — and §6 item 5 already fixed
|
||||
the other half of that expression without changing the branch.
|
||||
|
||||
It may well be intended; a dialect is allowed to say the left operand wins, and this one is
|
||||
consistent about it. The defect is that **it is documented nowhere**. Chapter 3's "Numbers"
|
||||
does not mention it, chapter 13 does not list it among the differences, and a C128 promotes
|
||||
to float instead. Nothing fails when you get it wrong — the program computes something else
|
||||
and carries on.
|
||||
|
||||
It cost the game two live bugs before either was noticed. `SPD% = 5.6 + LEVEL# * 0.45`
|
||||
evaluated to a flat 5.6, so no level ever ran 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. Both read correctly. Neither produced a diagnostic.
|
||||
|
||||
The cheap fix is documentation: a paragraph in chapter 3 and a row in chapter 13. The other
|
||||
fix is promotion, which would be a real change of semantics and wants its own decision.
|
||||
|
||||
5. **A drawing that spans a 256-line batch boundary is torn, not merely transient.** §5 and
|
||||
chapter 13 record that drawing does not persist across frames. What neither says is that a
|
||||
single uninterrupted sequence of drawing verbs longer than one `akbasic_runtime_run()` budget
|
||||
comes back **half drawn**, because the present in the middle of it discards the buffer — so
|
||||
an `SSHAPE` at the end captures only the statements issued since that present, over whatever
|
||||
the frame before left behind.
|
||||
|
||||
Measured against `AKBASIC_FRONTEND_STEPS_PER_FRAME` of 256, in the SDL build: after
|
||||
synchronising to a jiffy edge, 110 `DRAW` statements in a `FOR` loop (220 executed lines)
|
||||
are captured whole; 125 (250 lines) capture nothing at all; 140 (280 lines) capture the last
|
||||
fifteen. A program cannot see the boundary except by watching `TI#` change, which is what
|
||||
the game's `PACE` routine does and what makes the whole thing workable.
|
||||
|
||||
This is worth a paragraph in chapter 13 beside the existing note, because "redraw it every
|
||||
frame" is the advice there and it is not sufficient advice — the redraw also has to fit.
|
||||
|
||||
6. **`SSHAPE` and `GSHAPE` ignore the subscript on a string array element.** Needs a graphics
|
||||
device, so this one is reduced against `build-akgl/basic`.
|
||||
|
||||
```basic
|
||||
DIM SH$(4)
|
||||
SSHAPE SH$(2), 0, 0, 61, 4
|
||||
PRINT "[" + SH$(0) + "] [" + SH$(2) + "]"
|
||||
```
|
||||
|
||||
```output
|
||||
[SHAPE:0] []
|
||||
```
|
||||
|
||||
The handle lands in `SH$(0)`; `SH$(2)` stays empty. `GSHAPE SH$(2)` then stamps whatever is
|
||||
in `SH$(0)`. Ordinary assignment and `PRINT` honour the subscript, so a program that keeps
|
||||
several saved shapes in an array gets every one of them resolving to element zero, silently.
|
||||
|
||||
`shape_variable()` (`src/runtime_graphics.c:625`) takes `arg->identifier` and calls
|
||||
`akbasic_environment_get()` — it never evaluates the subscript — and both verbs then use a
|
||||
literal zero: `src/runtime_graphics.c:688` writes with `zerosubscript`, `:724` reads with it.
|
||||
**`SPRSAV` is the counter-example and the model for the fix**: it calls
|
||||
`akbasic_runtime_evaluate()` on its argument (`src/runtime_sprite.c:480`) and handles an
|
||||
array element correctly.
|
||||
|
||||
The game keeps six brick stamps in six separate scalars because of this, and the workaround
|
||||
is not obvious from the failure — every brick simply comes out the colour of the last one
|
||||
captured. `tests/graphics_verbs.c` is where the regression goes.
|
||||
|
||||
7. **`GRAPHIC CLR` is documented and refused.** `docs/06-graphics.md:65` and
|
||||
`docs/11-verb-reference.md:54` both give the form as `GRAPHIC mode | CLR`.
|
||||
|
||||
```basic
|
||||
GRAPHIC CLR
|
||||
```
|
||||
|
||||
```output
|
||||
? 1 : PARSE ERROR 1 at 'CLR', Expected expression or literal
|
||||
```
|
||||
|
||||
`GRAPHIC` parses with `akbasic_parse_arglist` (`src/verbs.c:86`), so `CLR` scans as the `CLR`
|
||||
verb rather than as a keyword argument. `GRAPHIC 5` does the job and is what the game calls.
|
||||
Either the parse handler learns the word or both documents lose it; the second is smaller and
|
||||
the first is what a C128 programmer will type.
|
||||
|
||||
Worth noting because `docs_examples` cannot catch it: every fenced `GRAPHIC CLR` in the
|
||||
documentation is prose, not a tagged runnable block.
|
||||
|
||||
8. **A `DATA` line holding a negative number cannot be executed.** §4 settled that "`DATA` at
|
||||
run time is now a no-op: it is a declaration, and reaching the statement means walking past
|
||||
it." Walking past a negative one raises instead:
|
||||
|
||||
```basic
|
||||
READ A#
|
||||
PRINT "READ GAVE " + A#
|
||||
DATA -5
|
||||
PRINT "REACHED THE LINE AFTER THE DATA"
|
||||
```
|
||||
|
||||
```output
|
||||
READ GAVE -5
|
||||
? 3 : PARSE ERROR Expected literal
|
||||
```
|
||||
|
||||
`READ` handles the value correctly — the negative comes back intact — so this is only the
|
||||
statement's own argument parse, in `akbasic_parse_data()`
|
||||
(`src/parser_commands.c:934`), refusing a leading `-` where the prescan accepted it. The
|
||||
positive form falls through cleanly.
|
||||
|
||||
Reachable in ordinary code: a table of `DATA` at the foot of a program is walked into by any
|
||||
routine above it that ends in a fall-through rather than an `END`, and a table of angles or
|
||||
offsets is exactly where negative numbers live. `tests/read_data.c`.
|
||||
|
||||
**A note on what this list is.** Items 1, 2, 6, 7 and 8 are defects by any reading. Item 3 is a
|
||||
design consequence nobody chose. Item 4 may be intended and is a documentation gap either way.
|
||||
Item 5 sharpens something already recorded. None of them is a complaint about the interpreter,
|
||||
which was a pleasure to write a real program against — they are the kind of thing one real
|
||||
program finds and a test suite written alongside the implementation structurally cannot,
|
||||
because the suite tests the constructs the implementer had in mind and a game reaches for
|
||||
whatever it needs.
|
||||
|
||||
@@ -13,6 +13,12 @@ name, and a machine with no sound card still runs the interpreter — only `SOUN
|
||||
Voice, frequency, duration. Voices are numbered from 1. The duration is in *jiffies* —
|
||||
sixtieths of a second — as on a C128, so 60 is one second.
|
||||
|
||||
**The frequency is a SID register value, not hertz.** As on a C128 the pitch is
|
||||
`frequency * 1022730 / 16777216` — the NTSC system clock over the oscillator's 24-bit
|
||||
accumulator — so the 4000 above is about 244 Hz, and the argument ranges from 0 to
|
||||
65535. Work the notes you need out once and write the arithmetic down beside them: 4298
|
||||
is C4, 8579 is C5, 17175 is C6. `PLAY` takes note names and does the conversion for you.
|
||||
|
||||
Further arguments give a frequency sweep: a step, a direction and a range, which is
|
||||
what makes a siren or a laser.
|
||||
|
||||
|
||||
763
docs/17-tutorial-breakout.md
Normal file
@@ -0,0 +1,763 @@
|
||||
# 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.
|
||||
|
||||
**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.
|
||||
|
||||
Run the finished game with the SDL build:
|
||||
|
||||
```sh norun
|
||||
$ ./build-akgl/basic examples/breakout/characters/breakout.bas
|
||||
```
|
||||
|
||||
| Key | Does |
|
||||
|---|---|
|
||||
| left / right | move the paddle |
|
||||
| space | start a game, then launch the ball |
|
||||
| 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.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Decide what is a sprite and what is a character
|
||||
|
||||
Two layers of this interpreter are redrawn from state it keeps for you, every frame,
|
||||
without your program doing anything:
|
||||
|
||||
- the **text grid**, repainted row by row by the akgl sink, and
|
||||
- the **sprites**, walked slot by slot after it.
|
||||
|
||||
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 paints over the whole window on its
|
||||
way past — so in the standalone SDL build **nothing you draw with them is ever visible**.
|
||||
That is recorded as a defect (`TODO.md` §9 item 3) rather than a design, but it is the
|
||||
interpreter you have.
|
||||
|
||||
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.
|
||||
|
||||
**If you want artwork on the screen, that rule flips** and the whole architecture changes
|
||||
with it. That is Chapter 18.
|
||||
|
||||
## Step 2: Measure the screen once, at the top
|
||||
|
||||
```basic norun
|
||||
CW# = 16
|
||||
CH# = 16
|
||||
SCW# = RGR(1)
|
||||
SCH# = RGR(2)
|
||||
COLS# = SCW# / CW#
|
||||
ROWS# = SCH# / CH#
|
||||
```
|
||||
|
||||
`RGR(1)` and `RGR(2)` are the window in pixels — see
|
||||
[Chapter 6](06-graphics.md#rgr) — and the game asks for them rather than assuming 800 by
|
||||
600, so it survives a host that opens a different window.
|
||||
|
||||
**The cell size is a constant and has to be**, which is the one thing here you cannot
|
||||
derive. `WINDOW` knows the grid, but the standalone frontend's tee sink never offers it
|
||||
(`TODO.md` §9 item 3 again), so a program has no way to ask. 16 by 16 is the bundled
|
||||
C64_Pro_Mono at 16 points. Change the font or the point size and change these two with
|
||||
it; everything below is derived from them, including the ball's speed relative to the
|
||||
wall.
|
||||
|
||||
Lay the rest of the geometry out in the same block — the wall in cells, the play area in
|
||||
pixels:
|
||||
|
||||
```basic norun
|
||||
BRW# = 4
|
||||
BCOLS# = 10
|
||||
BROWS# = 6
|
||||
BTOP# = 4
|
||||
BLEFT# = (COLS# - (BRW# * BCOLS#)) / 2
|
||||
TOPY# = 2 * CH#
|
||||
MAXX# = SCW# - 8
|
||||
PW# = 144
|
||||
PY# = 528
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Step 3: Create every name before the game starts
|
||||
|
||||
This is the rule that decides whether your game runs for four minutes or for ever, and it
|
||||
is worth getting straight before you write a single subroutine.
|
||||
|
||||
**A name that is created costs value-pool slots that are never handed back.** The pool is
|
||||
a bump allocator with 4096 slots for the life of the run. Scope exit *does* destroy
|
||||
variables — a `GOSUB`'s locals go when it returns — but their slots are not reclaimed, so
|
||||
recreating the same local on the next call takes fresh ones.
|
||||
|
||||
Here is the whole defect on one screen:
|
||||
|
||||
```basic
|
||||
X# = 0
|
||||
FOR T# = 1 TO 6000
|
||||
GOSUB SUBA
|
||||
NEXT T#
|
||||
PRINT "OK " + X#
|
||||
END
|
||||
|
||||
LABEL SUBA
|
||||
LOC# = 1
|
||||
X# = X# + LOC#
|
||||
RETURN
|
||||
```
|
||||
|
||||
```output
|
||||
? 8 : RUNTIME ERROR Array of 1 elements does not fit in the 0 remaining value slots
|
||||
|
||||
```
|
||||
|
||||
Six thousand calls, one new name each, and it dies on the four-thousand-and-somethingth
|
||||
— naming the line that was unlucky rather than the line that was wrong. A game loop that
|
||||
creates one name per tick is dead in half a minute. This one was, and it took twenty-five
|
||||
seconds to do it.
|
||||
|
||||
The fix is one line, moved:
|
||||
|
||||
```basic
|
||||
X# = 0
|
||||
LOC# = 0
|
||||
FOR T# = 1 TO 6000
|
||||
GOSUB SUBA
|
||||
NEXT T#
|
||||
PRINT "OK " + X#
|
||||
END
|
||||
|
||||
LABEL SUBA
|
||||
LOC# = 1
|
||||
X# = X# + LOC#
|
||||
RETURN
|
||||
```
|
||||
|
||||
```output
|
||||
OK 6000
|
||||
```
|
||||
|
||||
So **declare everything at the top** — variables, scratch temporaries, loop counters, all
|
||||
of it — and after that the program only ever stores into names that already exist:
|
||||
|
||||
```basic norun
|
||||
DIM BR#(60)
|
||||
DIM LAY$(6)
|
||||
DIM BSG$(6)
|
||||
SCORE# = 0
|
||||
LIVES# = 3
|
||||
BX# = 0
|
||||
BY# = 0
|
||||
HIT# = 0
|
||||
BI# = 0
|
||||
I# = 0
|
||||
R# = 0
|
||||
C# = 0
|
||||
KI# = 0
|
||||
```
|
||||
|
||||
`I#`, `R#`, `C#` and `KI#` are in that list because a `FOR` counter is a name like any
|
||||
other: name one that does not exist yet and every entry to the loop spends slots.
|
||||
|
||||
The same block does a second job. A `GOSUB` gets its own scope, and assignment walks up
|
||||
the chain to find an outer name — but a name *first seen* inside a subroutine is created
|
||||
in that subroutine and dies at `RETURN`. `HIT#` and `BI#` are how `HITTEST` answers its
|
||||
caller, so they have to exist before anybody calls it. See
|
||||
[Chapter 4](04-control-flow.md#goto-and-gosub) for the scope rules.
|
||||
|
||||
This is filed as `TODO.md` §6 item 30. Until it is fixed, the declaration block is not a
|
||||
style preference — it is the price of a program that runs.
|
||||
|
||||
## Step 4: Build the wall, and write a row whole
|
||||
|
||||
Levels are `DATA`, one string per row, `1` for a brick and `0` for a hole:
|
||||
|
||||
```basic norun
|
||||
LABEL LAY1
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
|
||||
LABEL LAY2
|
||||
DATA "0011111100"
|
||||
DATA "0111111110"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "0111111110"
|
||||
DATA "0011111100"
|
||||
```
|
||||
|
||||
`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`:
|
||||
|
||||
```basic norun
|
||||
LABEL LOADLAY
|
||||
N# = MOD((LEVEL# - 1), 3)
|
||||
IF N# = 0 THEN RESTORE LAY1
|
||||
IF N# = 1 THEN RESTORE LAY2
|
||||
IF N# = 2 THEN RESTORE LAY3
|
||||
FOR R# = 0 TO 5
|
||||
READ LAY$(R#)
|
||||
NEXT R#
|
||||
RETURN
|
||||
```
|
||||
|
||||
Now draw it — and here is the second rule that shapes this listing:
|
||||
|
||||
**`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, and writing *past* the terminator of a short
|
||||
row draws nothing at all. There is no way to poke one character into the middle of a row
|
||||
and leave the rest alone.
|
||||
|
||||
So a row is rebuilt whole and written from column 0, in one `CHAR`:
|
||||
|
||||
```basic
|
||||
DIM LAY$(6)
|
||||
DIM BSG$(6)
|
||||
BSG$(0) = "[##]"
|
||||
BSG$(1) = "[##]"
|
||||
BSG$(2) = "[==]"
|
||||
BSG$(3) = "[==]"
|
||||
BSG$(4) = "[--]"
|
||||
BSG$(5) = "[--]"
|
||||
LAY$(0) = "1111111111"
|
||||
LAY$(1) = "1111111111"
|
||||
LAY$(2) = "1101111011"
|
||||
LAY$(3) = "1111111111"
|
||||
LAY$(4) = "1011110111"
|
||||
LAY$(5) = "0110110111"
|
||||
S$ = ""
|
||||
T$ = ""
|
||||
R# = 0
|
||||
C# = 0
|
||||
FOR R# = 0 TO 5
|
||||
GOSUB BUILDROW
|
||||
PRINT S$
|
||||
NEXT R#
|
||||
END
|
||||
|
||||
LABEL BUILDROW
|
||||
S$ = " "
|
||||
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#
|
||||
RETURN
|
||||
```
|
||||
|
||||
```output
|
||||
[##][##][##][##][##][##][##][##][##][##]
|
||||
[##][##][##][##][##][##][##][##][##][##]
|
||||
[==][==] [==][==][==][==] [==][==]
|
||||
[==][==][==][==][==][==][==][==][==][==]
|
||||
[--] [--][--][--][--] [--][--][--]
|
||||
[--][--] [--][--] [--][--][--]
|
||||
```
|
||||
|
||||
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#`.
|
||||
|
||||
**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:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
## Step 5: Make the ball and the paddle
|
||||
|
||||
`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
|
||||
```
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||
```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* — 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.
|
||||
|
||||
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).
|
||||
|
||||
## Step 6: Write the main loop as a `GOTO` loop
|
||||
|
||||
```basic norun
|
||||
LABEL TICK
|
||||
GOSUB READKEY
|
||||
IF STATE# <> 0 THEN GOTO DISPATCH
|
||||
IF PAUSED# = 1 THEN GOTO TICKEND
|
||||
GOSUB MOVEPAD
|
||||
IF HELD# = 1 THEN GOSUB HOLDBAL
|
||||
IF HELD# = 0 THEN GOSUB MOVEBAL
|
||||
GOSUB SHOWSPR
|
||||
LABEL TICKEND
|
||||
SLEEP 0.02
|
||||
GOTO TICK
|
||||
|
||||
LABEL DISPATCH
|
||||
IF STATE# = 1 THEN GOTO LOSTLIF
|
||||
IF STATE# = 2 THEN GOTO LEVELUP
|
||||
IF STATE# = 3 THEN GOTO BYE
|
||||
IF STATE# = 5 THEN GOTO NEWGAME
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
`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#`.
|
||||
|
||||
## Step 7: Read the keyboard through a countdown
|
||||
|
||||
`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.
|
||||
|
||||
So drain the queue every tick, and let a keystroke refill a countdown:
|
||||
|
||||
```basic norun
|
||||
LABEL READKEY
|
||||
FOR KI# = 1 TO 8
|
||||
GET K#
|
||||
IF K# <> 0 THEN GOSUB HANDKEY
|
||||
NEXT KI#
|
||||
RETURN
|
||||
|
||||
LABEL HANDKEY
|
||||
IF K# = 1073741904 THEN PDIR# = 0 - 1
|
||||
IF K# = 1073741904 THEN PDEC# = 12
|
||||
IF K# = 1073741903 THEN PDIR# = 1
|
||||
IF K# = 1073741903 THEN PDEC# = 12
|
||||
IF K# = 32 THEN GOSUB KEYSPC
|
||||
IF K# = 112 THEN GOSUB KEYPAU
|
||||
IF K# = 113 THEN STATE# = 3
|
||||
IF K# = 27 THEN STATE# = 3
|
||||
RETURN
|
||||
```
|
||||
|
||||
```basic norun
|
||||
LABEL MOVEPAD
|
||||
IF PDEC# < 1 THEN GOTO PADCLMP
|
||||
PDEC# = PDEC# - 1
|
||||
PX# = PX# + (PDIR# * PSPD#)
|
||||
LABEL PADCLMP
|
||||
IF PX# < 0 THEN PX# = 0
|
||||
M# = SCW# - PW#
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
## Step 8: Move the ball one axis at a time
|
||||
|
||||
```basic norun
|
||||
LABEL MOVEBAL
|
||||
OX# = BX#
|
||||
BX# = BX# + BVX#
|
||||
IF BX# < 0 THEN GOSUB WALLL
|
||||
IF BX# > MAXX# THEN GOSUB WALLR
|
||||
GOSUB XBRICK
|
||||
OY# = BY#
|
||||
BY# = BY# + BVY#
|
||||
IF BY# < TOPY# THEN GOSUB WALLT
|
||||
GOSUB YBRICK
|
||||
GOSUB PADHIT
|
||||
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.
|
||||
|
||||
The brick test converts a pixel to a cell, and it is the only place in the program that
|
||||
does:
|
||||
|
||||
```basic norun
|
||||
LABEL HITTEST
|
||||
HIT# = 0
|
||||
RC# = TY# / CH#
|
||||
IF RC# < BTOP# THEN RETURN
|
||||
RB# = RC# - BTOP#
|
||||
IF RB# > (BROWS# - 1) THEN RETURN
|
||||
CC2# = TX# / CW#
|
||||
IF CC2# < BLEFT# THEN RETURN
|
||||
CB# = (CC2# - BLEFT#) / BRW#
|
||||
IF CB# > (BCOLS# - 1) THEN RETURN
|
||||
BI# = (RB# * BCOLS#) + CB#
|
||||
BR2# = RB#
|
||||
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.
|
||||
|
||||
**Test the ball's leading edge, not its centre:**
|
||||
|
||||
```basic norun
|
||||
LABEL XBRICK
|
||||
TX# = BX#
|
||||
IF BVX# > 0 THEN TX# = BX# + 7
|
||||
TY# = BY# + 4
|
||||
GOSUB HITTEST
|
||||
IF HIT# = 0 THEN RETURN
|
||||
BX# = OX#
|
||||
BVX# = 0 - BVX#
|
||||
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.
|
||||
|
||||
## Step 9: Make the paddle bounce mean something
|
||||
|
||||
```basic norun
|
||||
LABEL PADHIT
|
||||
IF BVY# < 1 THEN RETURN
|
||||
BB# = BY# + 8
|
||||
IF BB# < PY# THEN RETURN
|
||||
IF BY# > (PY# + 10) THEN RETURN
|
||||
RX# = PX# + PW#
|
||||
IF (BX# + 8) < PX# THEN RETURN
|
||||
IF BX# > RX# THEN RETURN
|
||||
BY# = PY# - 8
|
||||
BVY# = 0 - BSPD#
|
||||
Z# = ((BX# + 4) - PX#) / (PW# / 5)
|
||||
IF Z# < 0 THEN Z# = 0
|
||||
IF Z# > 4 THEN Z# = 4
|
||||
PVX# = BVX#
|
||||
BVX# = (Z# - 2) * 3
|
||||
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 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.
|
||||
|
||||
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:
|
||||
|
||||
```basic norun
|
||||
LABEL UNSTICK
|
||||
NUDGE# = 0
|
||||
STALL# = 0
|
||||
RMAX# = 4
|
||||
GOSUB RANDOM
|
||||
BVX# = (RND# * 3) - 6
|
||||
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:
|
||||
|
||||
```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.
|
||||
|
||||
## Step 10: The HUD and the messages
|
||||
|
||||
Same rule as the wall: build the whole line, write it once.
|
||||
|
||||
```basic
|
||||
V# = 0
|
||||
P$ = ""
|
||||
H$ = ""
|
||||
SCORE# = 1250
|
||||
LIVES# = 3
|
||||
LEVEL# = 2
|
||||
HIGH# = 9900
|
||||
V# = SCORE#
|
||||
GOSUB PAD6
|
||||
H$ = " SCORE " + P$
|
||||
H$ = (H$ + " LIVES ") + LIVES#
|
||||
H$ = (H$ + " LEVEL ") + LEVEL#
|
||||
V# = HIGH#
|
||||
GOSUB PAD6
|
||||
H$ = (H$ + " HIGH ") + P$
|
||||
PRINT H$
|
||||
END
|
||||
|
||||
LABEL PAD6
|
||||
P$ = "" + V#
|
||||
DO WHILE LEN(P$) < 6
|
||||
P$ = "0" + P$
|
||||
LOOP
|
||||
RETURN
|
||||
```
|
||||
|
||||
```output
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```basic norun
|
||||
LABEL SHOWAT
|
||||
L# = LEN(MSG$)
|
||||
C2# = (COLS# - L#) / 2
|
||||
S$ = MSG$
|
||||
IF C2# > 0 THEN S$ = (" " * C2#) + MSG$
|
||||
CHAR 1, 0, MROW#, S$
|
||||
RETURN
|
||||
|
||||
LABEL CLRAT
|
||||
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.
|
||||
|
||||
## Step 11: Ask for sound once, then believe the answer
|
||||
|
||||
`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:
|
||||
|
||||
```basic requires=noakgl
|
||||
SND# = 1
|
||||
TRAP NOAUDIO
|
||||
SOUND 1, 2000, 1
|
||||
TRAP
|
||||
IF SND# = 1 THEN PRINT "SOUND IS AVAILABLE"
|
||||
IF SND# = 0 THEN PRINT "NO AUDIO DEVICE, PLAYING SILENTLY"
|
||||
END
|
||||
|
||||
LABEL NOAUDIO
|
||||
SND# = 0
|
||||
RESUME NEXT
|
||||
```
|
||||
|
||||
```output
|
||||
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
|
||||
[Chapter 4](04-control-flow.md#trapping-errors).
|
||||
|
||||
After that every sound is guarded by the flag it recorded:
|
||||
|
||||
```basic norun
|
||||
LABEL BEEPB
|
||||
IF SND# = 1 THEN SOUND 1, 12000, 2
|
||||
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.
|
||||
|
||||
## Step 12: Give it an attract mode, and test with it
|
||||
|
||||
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:
|
||||
|
||||
```basic norun
|
||||
LABEL DEMOPAD
|
||||
TX# = (BX# + 4) - (PW# / 2)
|
||||
TX# = TX# + DOFF#
|
||||
D# = TX# - PX#
|
||||
IF D# > PSPD# THEN D# = PSPD#
|
||||
M# = 0 - PSPD#
|
||||
IF D# < M# THEN D# = M#
|
||||
PX# = PX# + D#
|
||||
RETURN
|
||||
|
||||
LABEL DEMOAIM
|
||||
RMAX# = 81
|
||||
GOSUB RANDOM
|
||||
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.
|
||||
|
||||
Leave it running for two minutes and watch the score climb: that exercises the ball, the
|
||||
bricks, the level change, the speed-up and — most usefully — the value pool from Step 3,
|
||||
which is the failure that only shows up after thousands of ticks.
|
||||
|
||||
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:
|
||||
|
||||
```basic norun
|
||||
LABEL HISCORE
|
||||
IF SCORE# > HIGH# THEN HIGH# = SCORE#
|
||||
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.
|
||||
|
||||
## 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:
|
||||
|
||||
| Rule | Because | Where |
|
||||
|---|---|---|
|
||||
| Create every name at the top, loop counters included | A name created inside a scope costs pool slots for good; 4096 ends the run | Step 3 |
|
||||
| Build a text row whole and write it from column 0 | `CHAR` terminates the row where it stops | 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 |
|
||||
|
||||
## 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 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 defects this game found, each with a reduction that fits
|
||||
on a screen and what a fix would touch.
|
||||
796
docs/18-tutorial-breakout-artwork.md
Normal file
@@ -0,0 +1,796 @@
|
||||
# 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
|
||||
[`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.
|
||||
|
||||
```sh norun
|
||||
$ ./build-akgl/basic examples/breakout/sprites/breakout.bas
|
||||
```
|
||||
|
||||
| Key | Does |
|
||||
|---|---|
|
||||
| left / right | move the paddle |
|
||||
| space | start a game, launch the ball, release a stuck ball |
|
||||
| P | pause |
|
||||
| S | sound on and off |
|
||||
| Q or escape | quit |
|
||||
|
||||
A broken brick drops a gem about one time in seven. Catch it with the paddle; the colour
|
||||
tells you which it is.
|
||||
|
||||
| Gem | Name | Does | For |
|
||||
|---|---|---|---|
|
||||
| red | EXPAND | doubles the paddle's width | 20 seconds |
|
||||
| yellow | MULTI | throws two more balls off the one in play | until they are lost |
|
||||
| green | SLOW | drops the ball's speed to about two thirds | 16 seconds |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Put the 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).
|
||||
|
||||
```basic requires=akgl setup=breakout_art screenshot=breakout-artwork
|
||||
I# = 0
|
||||
SPRSAV "art/paddleBlu.png", 3
|
||||
SPRSAV "art/paddleRed.png", 4
|
||||
SPRSAV "art/ballBlue.png", 5
|
||||
SPRSAV "art/element_red_polygon_glossy.png", 6
|
||||
SPRSAV "art/element_green_polygon_glossy.png", 7
|
||||
SPRSAV "art/element_purple_polygon_glossy.png", 8
|
||||
FOR I# = 3 TO 8
|
||||
SPRITE I#, 1, 2
|
||||
NEXT I#
|
||||
MOVSPR 3, 20, 20
|
||||
MOVSPR 4, 20, 60
|
||||
MOVSPR 5, 160, 30
|
||||
MOVSPR 6, 30, 120
|
||||
MOVSPR 7, 130, 120
|
||||
MOVSPR 8, 230, 120
|
||||
```
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
**`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.
|
||||
|
||||
## Step 2: Budget the eight sprite slots before you write anything else
|
||||
|
||||
There are eight sprites. That is not a limit you will design your way around, so decide
|
||||
what they are first:
|
||||
|
||||
| Slot | Is |
|
||||
|---|---|
|
||||
| 1 | the HUD strip — a captured drawing |
|
||||
| 2 | the playing field — a captured drawing |
|
||||
| 3 | the paddle |
|
||||
| 4 | the catcher bar (the purple gem) |
|
||||
| 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.
|
||||
|
||||
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.
|
||||
|
||||
## Step 3: Turn what you drew into a sprite
|
||||
|
||||
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 **anything
|
||||
`DRAW`, `BOX` or `CIRCLE` puts on the screen is painted over before anybody sees it**.
|
||||
Sprites are drawn after the text layer. A sprite is the only thing on the screen a
|
||||
program can rely on being visible. (`TODO.md` §9 item 3; the figures in this chapter are
|
||||
rendered by a tool that omits the text layer, which is why they can show a drawing at
|
||||
all.)
|
||||
|
||||
That leaves exactly one way to put a picture up: **draw it, capture it with `SSHAPE`,
|
||||
install the capture with `SPRSAV`.**
|
||||
|
||||
```basic norun
|
||||
SSHAPE Z$, 0, 60, 800, 600
|
||||
SPRSAV Z$, 2
|
||||
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.
|
||||
|
||||
### Stamp the bricks; do not paint them
|
||||
|
||||
Draw one brick per colour, capture the six of them, and stamp them with `GSHAPE`:
|
||||
|
||||
```basic requires=akgl screenshot=breakout-stamps size=100x130
|
||||
DIM BRC#(6)
|
||||
I# = 0
|
||||
R# = 0
|
||||
K# = 0
|
||||
T1# = 0
|
||||
T2# = 0
|
||||
FOR I# = 0 TO 5
|
||||
READ BRC#(I#)
|
||||
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#
|
||||
|
||||
DATA 3, 9, 8, 6, 4, 5
|
||||
```
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
**`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.
|
||||
|
||||
`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:
|
||||
|
||||
```basic norun
|
||||
LABEL DRAWJOB
|
||||
IF SHN# < 14 THEN GOTO DRAWJOB2
|
||||
GOSUB DRAWPROTOS
|
||||
RETURN
|
||||
```
|
||||
|
||||
### 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:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
Now the whole screen, drawn and captured and dressed with artwork:
|
||||
|
||||
```basic requires=akgl setup=breakout_art screenshot=breakout-screen size=800x600
|
||||
DIM BRC#(6)
|
||||
I# = 0
|
||||
R# = 0
|
||||
C# = 0
|
||||
K# = 0
|
||||
T1# = 0
|
||||
T2# = 0
|
||||
Z$ = ""
|
||||
FOR I# = 0 TO 5
|
||||
READ BRC#(I#)
|
||||
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$
|
||||
SSHAPE Z$, 0, 100, 68, 116 : S5$ = Z$
|
||||
GRAPHIC 1, 1
|
||||
WIDTH 2
|
||||
COLOR 5, 16 : COLOR 1, 4
|
||||
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
|
||||
Z$ = S3$ : GSHAPE Z$, 42 + C# * 72, 180
|
||||
Z$ = S4$ : GSHAPE Z$, 42 + C# * 72, 204
|
||||
Z$ = S5$ : GSHAPE Z$, 42 + C# * 72, 228
|
||||
NEXT C#
|
||||
SSHAPE Z$, 0, 60, 800, 600
|
||||
SPRSAV Z$, 2
|
||||
SPRITE 2, 1, 2
|
||||
MOVSPR 2, 0, 60
|
||||
SPRSAV "art/paddleBlu.png", 3
|
||||
SPRSAV "art/ballBlue.png", 5
|
||||
SPRITE 3, 1, 2
|
||||
SPRITE 5, 1, 2
|
||||
MOVSPR 3, 348, 540
|
||||
MOVSPR 5, 389, 517
|
||||
|
||||
DATA 3, 9, 8, 6, 4, 5
|
||||
```
|
||||
|
||||

|
||||
|
||||
Everything above the last four lines is a drawing nobody would ever see. `SSHAPE` and
|
||||
`SPRSAV` are what make it the screen.
|
||||
|
||||
The game keeps its six stamps in **six separate scalars** rather than an array, and that
|
||||
is not a style choice — see the fourth trap at the end of this chapter.
|
||||
|
||||
## Step 4: Find the frame boundary
|
||||
|
||||
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 —
|
||||
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:
|
||||
|
||||
```basic norun
|
||||
LABEL PACE
|
||||
LASTT# = TI#
|
||||
LABEL PACEEDGE
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
## Step 5: Do at most one capture per frame
|
||||
|
||||
The frame loop paces first, then draws at most one thing, then plays the game:
|
||||
|
||||
```basic norun
|
||||
LABEL FRAME
|
||||
GOSUB PACE
|
||||
GOSUB DRAWJOB
|
||||
GOSUB READKEYS
|
||||
IF STATE# = 0 THEN GOSUB TITLETICK
|
||||
IF STATE# = 1 THEN GOSUB SERVETICK
|
||||
IF STATE# = 2 THEN GOSUB PLAYTICK
|
||||
IF STATE# = 3 THEN GOSUB LOSTTICK
|
||||
IF STATE# = 4 THEN GOSUB CLEARTICK
|
||||
IF RUNNING# = 0 THEN GOTO SHUTDOWN
|
||||
GOTO FRAME
|
||||
```
|
||||
|
||||
`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
|
||||
LABEL DRAWJOB
|
||||
IF SHN# < 14 THEN GOTO DRAWJOB2
|
||||
GOSUB DRAWPROTOS
|
||||
RETURN
|
||||
LABEL DRAWJOB2
|
||||
IF DPLAY# = 0 THEN GOTO DRAWJOB3
|
||||
GOSUB DRAWPLAY
|
||||
DPLAY# = 0
|
||||
RETURN
|
||||
LABEL DRAWJOB3
|
||||
IF DHUD# = 0 THEN RETURN
|
||||
GOSUB DRAWHUD
|
||||
DHUD# = 0
|
||||
RETURN
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Those are `LABEL`s and `GOTO`s rather than `BEGIN` blocks, and again that is not a style
|
||||
choice — a `RETURN` inside a block leaves the interpreter with no `GOSUB` to return from.
|
||||
The last trap in this chapter is exactly that.
|
||||
|
||||
## Step 6: Draw the lettering, because text has no colour
|
||||
|
||||
The interpreter's text sink is white and has no verb that changes it; `CHAR` parses 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.
|
||||
|
||||
```basic requires=akgl screenshot=breakout-lettering size=260x110
|
||||
DIM FNC#(5)
|
||||
DIM FNI#(5)
|
||||
DIM FNS#(60)
|
||||
I# = 0
|
||||
K# = 0
|
||||
N# = 0
|
||||
D# = 0
|
||||
GP# = 0
|
||||
GX# = 0
|
||||
GX2# = 0
|
||||
P1# = 0
|
||||
P2# = 0
|
||||
X1# = 0
|
||||
Y1# = 0
|
||||
X2# = 0
|
||||
Y2# = 0
|
||||
FOR I# = 0 TO 4
|
||||
READ N#
|
||||
FNC#(I#) = N#
|
||||
FNI#(I#) = GX#
|
||||
GOSUB READGLYPH
|
||||
NEXT I#
|
||||
GRAPHIC 1, 1
|
||||
WIDTH 2
|
||||
COLOR 1, 8
|
||||
SZ# = 9
|
||||
FOR I# = 0 TO 4
|
||||
GOSUB DRAWGLYPH
|
||||
NEXT I#
|
||||
END
|
||||
|
||||
LABEL READGLYPH
|
||||
FOR K# = 1 TO N# * 2
|
||||
READ D#
|
||||
FNS#(GX#) = D#
|
||||
GX# = GX# + 1
|
||||
NEXT K#
|
||||
RETURN
|
||||
|
||||
LABEL DRAWGLYPH
|
||||
GP# = FNI#(I#)
|
||||
GX2# = 20 + I# * SZ# * 5
|
||||
FOR K# = 1 TO FNC#(I#)
|
||||
P1# = FNS#(GP#)
|
||||
P2# = FNS#(GP# + 1)
|
||||
GP# = GP# + 2
|
||||
X1# = GX2# + (P1# / 10) * SZ#
|
||||
Y1# = 20 + MOD(P1#, 10) * SZ#
|
||||
X2# = GX2# + (P2# / 10) * SZ#
|
||||
Y2# = 20 + MOD(P2#, 10) * SZ#
|
||||
DRAW 1, X1#, Y1# TO X2#, Y2#
|
||||
NEXT K#
|
||||
RETURN
|
||||
|
||||
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 R
|
||||
DATA 5, 0,6, 0,30, 3,33, 30,33, 13,36
|
||||
REM E
|
||||
DATA 4, 0,6, 0,30, 3,33, 6,36
|
||||
```
|
||||
|
||||

|
||||
|
||||
`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.
|
||||
|
||||
**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
|
||||
GRAPHIC 1, 1
|
||||
WIDTH 1
|
||||
COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5
|
||||
COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6
|
||||
BOX 5, 0, 56, 799, 57
|
||||
IF HN# = 1 THEN DRAW HC#(0), HX1#(0), HY1#(0) TO HX2#(0), HY2#(0)
|
||||
IF HN# < 2 THEN GOTO HUDONE
|
||||
FOR I# = 0 TO HN# - 1
|
||||
DRAW HC#(I#), HX1#(I#), HY1#(I#) TO HX2#(I#), HY2#(I#)
|
||||
NEXT I#
|
||||
LABEL HUDONE
|
||||
SSHAPE Z$, 0, 0, 800, 60
|
||||
SPRSAV Z$, 1
|
||||
SPRITE 1, 1, 2
|
||||
MOVSPR 1, 0, 0
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Step 7: Bounce the ball without a square root
|
||||
|
||||
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:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
```basic norun
|
||||
LABEL HITBAR
|
||||
IF BLY%(B#) + 21 < T2# THEN RETURN
|
||||
IF BLY%(B#) > T2# + 23 THEN RETURN
|
||||
IF BLX%(B#) + 21 < T1# THEN RETURN
|
||||
IF BLX%(B#) > T1# + T3# THEN RETURN
|
||||
T4# = (BLX%(B#) + 11 - T1#) * 8 / T3#
|
||||
IF T4# < 0 THEN T4# = 0
|
||||
IF T4# > 7 THEN T4# = 7
|
||||
BLVX%(B#) = ZVX%(T4#) * SPD%
|
||||
BLVY%(B#) = ZVY%(T4#) * SPD%
|
||||
BLY%(B#) = T2# - 23
|
||||
RETURN
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```basic norun
|
||||
LABEL RESCALE
|
||||
IF BSPD% < 0.1 THEN BSPD% = SPD%
|
||||
RAT% = SPD% / BSPD%
|
||||
BSPD% = SPD%
|
||||
FOR B# = 0 TO 2
|
||||
IF BLON#(B#) = 1 THEN BEGIN
|
||||
BLVX%(B#) = BLVX%(B#) * RAT%
|
||||
BLVY%(B#) = BLVY%(B#) * RAT%
|
||||
BEND
|
||||
NEXT B#
|
||||
RETURN
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```basic norun
|
||||
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#)
|
||||
```
|
||||
|
||||
`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.
|
||||
|
||||
## Step 8: Gems and powerups
|
||||
|
||||
A gem is one sprite, one type number and two timers. The type picks the artwork, the
|
||||
banner colour and what `TAKEGEM` does:
|
||||
|
||||
```basic norun
|
||||
LABEL SPAWNGEM
|
||||
RNMAX# = 5
|
||||
GOSUB NEXTRAND
|
||||
GMTYP# = RNVAL# + 1
|
||||
GMX% = BX1# + 10
|
||||
GMY% = BY1#
|
||||
IF GMTYP# = 1 THEN SPRSAV "art/element_red_polygon_glossy.png", 8
|
||||
IF GMTYP# = 2 THEN SPRSAV "art/element_yellow_polygon_glossy.png", 8
|
||||
IF GMTYP# = 3 THEN SPRSAV "art/element_green_polygon_glossy.png", 8
|
||||
IF GMTYP# = 4 THEN SPRSAV "art/element_blue_polygon_glossy.png", 8
|
||||
IF GMTYP# = 5 THEN SPRSAV "art/element_purple_polygon_glossy.png", 8
|
||||
GMON# = 1
|
||||
SPRITE 8, 1, 2
|
||||
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.
|
||||
|
||||
Every timer is a frame count decremented in one place, so an expiry is where the effect is
|
||||
undone:
|
||||
|
||||
```basic norun
|
||||
IF PDEXP# > 0 THEN BEGIN
|
||||
PDEXP# = PDEXP# - 1
|
||||
IF PDEXP# = 0 THEN BEGIN
|
||||
PDW# = 104
|
||||
SPRITE 3, 1, 2, 0, 0, 0
|
||||
BEND
|
||||
BEND
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
The CATCH bar **mirrors** the paddle rather than following it:
|
||||
|
||||
```basic norun
|
||||
CTX% = 0.0 - PDX% + WALLL# + WALLR# - 104
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
**`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 —
|
||||
|
||||
```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:
|
||||
|
||||
```basic norun
|
||||
LABEL PRESSMUTE
|
||||
SNDON# = 1 - SNDON#
|
||||
IF SNDON# = 1 THEN VOL 8
|
||||
IF SNDON# = 0 THEN VOL 0
|
||||
RETURN
|
||||
```
|
||||
|
||||
A silenced voice costs nothing to issue. One line beats eleven scattered through the game.
|
||||
|
||||
## Five things in this dialect that do not do what they look like
|
||||
|
||||
Each of these cost an evening. All five are filed in `TODO.md` §9 with a reduction and
|
||||
the file and line of the cause.
|
||||
|
||||
### 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:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### 3. A skipped `BEGIN` block containing a loop breaks the enclosing `RETURN`
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
```output
|
||||
? 11 : RUNTIME ERROR RETURN outside the context of GOSUB
|
||||
|
||||
```
|
||||
|
||||
The routine plainly *was* called by a `GOSUB`. `FOR` creates its environment when the
|
||||
line is **parsed**, and the block skip is decided when it is **evaluated**, so a skipped
|
||||
loop pushes a scope that its skipped `NEXT` never pops — and the orphan sits between the
|
||||
routine and its caller. Outside a routine the same thing exhausts the pool of 32 after
|
||||
thirty-two skips instead.
|
||||
|
||||
Guard loops with `GOTO` rather than wrapping them in a block, which is what every draw
|
||||
routine in this game does.
|
||||
|
||||
### 4. `SSHAPE` and `GSHAPE` ignore the subscript on a string array
|
||||
|
||||
```basic requires=akgl
|
||||
DIM SH$(4)
|
||||
SSHAPE SH$(2), 0, 0, 61, 4
|
||||
PRINT "[" + SH$(0) + "] [" + SH$(2) + "]"
|
||||
```
|
||||
|
||||
```output
|
||||
[SHAPE:0] []
|
||||
```
|
||||
|
||||
The handle lands in element zero whatever subscript you write, and `GSHAPE SH$(2)` then
|
||||
stamps whatever is in `SH$(0)`. Ordinary assignment and `PRINT` honour the subscript, so
|
||||
this is the two verbs reading their leaf directly rather than evaluating it. The symptom
|
||||
is that every brick comes out the colour of the last stamp captured.
|
||||
|
||||
**Keep saved shapes in separate scalars.** The six brick stamps are `S0$` to `S5$` for
|
||||
this reason, and the field is 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#)
|
||||
NEXT I#
|
||||
RETURN
|
||||
|
||||
LABEL LOADFONT
|
||||
FOR I# = 0 TO 2
|
||||
READ F#(I#)
|
||||
NEXT I#
|
||||
RETURN
|
||||
|
||||
DATA 11, 12, 13
|
||||
DATA 21, 22, 23
|
||||
```
|
||||
|
||||
```output
|
||||
TABLE 21 22 23
|
||||
FONT 11 12 13
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## 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:
|
||||
|
||||
| Resource | There are | This game uses |
|
||||
|---|---|---|
|
||||
| Sprites | 8 | 8 |
|
||||
| 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, none reclaimed | none after startup — see [Chapter 17](17-tutorial-breakout.md#step-3-create-every-name-before-the-game-starts) |
|
||||
| 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 |
|
||||
|
||||
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.
|
||||
|
||||
## Where to go next
|
||||
|
||||
- **[Chapter 8](08-sprites.md)** is the sprite reference: every form of `SPRSAV`, the
|
||||
`MOVSPR` forms, collision and what `RSPPOS` reads back.
|
||||
- **[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 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, the cause and
|
||||
what a fix would touch.
|
||||
@@ -13,6 +13,10 @@ everything that behaves differently here and why. **[Chapter 14](14-architecture
|
||||
the odd one out: it is about the interpreter rather than the language, for anyone
|
||||
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.
|
||||
|
||||
## Chapters
|
||||
|
||||
| | |
|
||||
@@ -33,6 +37,8 @@ embedding it, debugging it or changing it.
|
||||
| **[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 |
|
||||
|
||||
## The shortest possible start
|
||||
|
||||
|
||||
BIN
docs/images/breakout-artwork.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
docs/images/breakout-lettering.png
Normal file
|
After Width: | Height: | Size: 451 B |
BIN
docs/images/breakout-paddle.png
Normal file
|
After Width: | Height: | Size: 262 B |
BIN
docs/images/breakout-screen.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
docs/images/breakout-stamps.png
Normal file
|
After Width: | Height: | Size: 388 B |
74
examples/breakout/characters/README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Breakout
|
||||
|
||||
Breakout, written entirely in BASIC for the AKGL BASIC interpreter. The ball and the
|
||||
paddle are sprites defined from `DATA` in the listing; the bricks, the HUD and the
|
||||
messages are characters in the text grid.
|
||||
|
||||
```sh
|
||||
../../../build-akgl/basic breakout.bas
|
||||
```
|
||||
|
||||
It needs the SDL build. The stdio build has no graphics device, so `RGR` refuses on the
|
||||
fourth line and the game stops there — which is exactly what it should do.
|
||||
|
||||
| Key | Does |
|
||||
|---|---|
|
||||
| left / right | move the paddle |
|
||||
| space | start a game from the title screen, then launch the ball |
|
||||
| P | pause |
|
||||
| Q or escape | quit |
|
||||
|
||||
The title screen plays itself. Attract mode is a real feature and it is also how the game
|
||||
was tested without a hand on the keyboard.
|
||||
|
||||
Three lives, six rows of bricks worth 60 down to 10 points a row, three level layouts that
|
||||
repeat with a faster ball each time, and a high score that lasts as long as the process
|
||||
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 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
|
||||
a completely different program.
|
||||
|
||||
## Known warts, this game's own
|
||||
|
||||
- **The cell size is a constant.** `CW#` and `CH#` are 16, measured with the bundled
|
||||
C64_Pro_Mono at 16 points in the 800×600 window; the grid is 50×37. BASIC cannot ask —
|
||||
`WINDOW` knows, but the standalone frontend never offers it. Change the font or the
|
||||
window size and those two numbers have to change with them.
|
||||
- **Every character the game draws also goes to stdout**, because the frontend tees the
|
||||
text sink to the terminal. It made the whole game auditable from a log file during
|
||||
development and it is pure noise the rest of the time. Redirect it.
|
||||
- **Collision is against the cell the ball's leading edge is in**, so a brick clipped at
|
||||
the very corner can be missed by up to seven pixels' worth of ball. Testing the centre
|
||||
instead was worse: the ball sank four pixels into a brick before it turned.
|
||||
- **There is a stall watchdog.** Ten seconds without touching a brick and the ball gets a
|
||||
new angle — at the *next paddle bounce*, never in mid-air.
|
||||
- **The demo cannot lose**, it re-serves. It also cannot be paused; `P` is ignored until a
|
||||
real game starts.
|
||||
- **Sound is asked for once and then believed.** On a machine with no audio device the
|
||||
game is silent rather than dead.
|
||||
- **No high score on disk.** There is no disk in this interpreter.
|
||||
|
||||
The interpreter defects this game turned up are filed in `TODO.md` §6 item 30 and §9, each
|
||||
with a reduction that fits on a screen.
|
||||
|
||||
## How this was checked
|
||||
|
||||
- Parses clean under the stdio `basic` (it then stops at the first graphics verb, as it
|
||||
should).
|
||||
- Attract mode run for 145 seconds and again for 110: no runtime error, score climbing
|
||||
throughout, 39 bricks in the second run.
|
||||
- Level clear verified against a copy whose first layout is a single row: `LEVEL CLEARED`,
|
||||
layout 2 loaded, ball speed up, play continues.
|
||||
- Input driven with `xdotool` against the real window: paddle moves and clamps at both
|
||||
edges, ball launches, `P` pauses and unpauses, three lives drain to `GAME OVER`, space
|
||||
starts a new game, `Q` exits printing the final and high scores.
|
||||
- Every direction change logged for a whole run and read back: each one is a wall, a brick
|
||||
or the paddle. Nothing turns without a reason.
|
||||
843
examples/breakout/characters/breakout.bas
Normal file
@@ -0,0 +1,843 @@
|
||||
REM ####################################################################
|
||||
REM BREAKOUT
|
||||
REM
|
||||
REM One hundred percent BASIC, for the AKGL BASIC interpreter.
|
||||
REM ../akbasic/build-akgl/basic breakout.bas
|
||||
REM
|
||||
REM The ball and the paddle are sprites, defined from DATA in this listing.
|
||||
REM The bricks, the HUD and the messages are characters in the text grid.
|
||||
REM Nothing here loads an asset and nothing here uses DRAW, BOX or CIRCLE:
|
||||
REM the drawing verbs do not survive a frame in this interpreter, while the
|
||||
REM text grid and the sprites are both redrawn from persistent state every
|
||||
REM frame. See README.md for the whole list of things that shaped this.
|
||||
REM
|
||||
REM Three rules this listing never breaks, each one earned the hard way:
|
||||
REM * Every name is created once, in the pool block below -- variables,
|
||||
REM loop counters, all of it. A name first created inside a GOSUB or a
|
||||
REM FOR costs a value slot that is never handed back, and there are 4096
|
||||
REM of them for the whole run. A game that creates one name per tick is
|
||||
REM dead in half a minute. This one creates none after startup.
|
||||
REM * A row of text is written whole, from column 0. CHAR terminates the
|
||||
REM row where it stops, so writing at column N erases N+1 onwards -- and
|
||||
REM writing PAST the terminator of a short row draws nothing at all.
|
||||
REM * Mixed + and - are parenthesised. This parser reads a - b + c as
|
||||
REM a - (b + c), and matches only ONE unparenthesised AND or OR.
|
||||
REM
|
||||
REM Written by Claude Opus (1M)
|
||||
REM ####################################################################
|
||||
|
||||
LABEL SETUP
|
||||
SCNCLR
|
||||
|
||||
REM --- geometry -------------------------------------------------------
|
||||
REM The cell is 16x16 and the grid is 50x37, measured with the bundled
|
||||
REM C64_Pro_Mono at 16 points in the 800x600 window. BASIC cannot ask for
|
||||
REM the cell size -- WINDOW would tell us, but the standalone frontend's
|
||||
REM tee sink never offers it -- so these two are constants. Change them
|
||||
REM together with the font or the window and everything else follows.
|
||||
CW# = 16
|
||||
CH# = 16
|
||||
SCW# = RGR(1)
|
||||
SCH# = RGR(2)
|
||||
COLS# = SCW# / CW#
|
||||
ROWS# = SCH# / CH#
|
||||
|
||||
REM --- the brick wall, in cells ---------------------------------------
|
||||
BRW# = 4
|
||||
BCOLS# = 10
|
||||
BROWS# = 6
|
||||
BTOP# = 4
|
||||
BLEFT# = (COLS# - (BRW# * BCOLS#)) / 2
|
||||
|
||||
REM --- the play area, in pixels ---------------------------------------
|
||||
TOPY# = 2 * CH#
|
||||
MAXX# = SCW# - 8
|
||||
PW# = 144
|
||||
PY# = 528
|
||||
LOSEY# = PY# + 32
|
||||
MSGROW# = 35
|
||||
TITROW# = 20
|
||||
PSPD# = 10
|
||||
|
||||
REM --- pools ----------------------------------------------------------
|
||||
REM Every name in the program is created HERE, and this is load-bearing
|
||||
REM twice over.
|
||||
REM
|
||||
REM A GOSUB gets its own scope. Assignment walks up the chain and finds an
|
||||
REM outer variable, but a name first seen inside a subroutine is created in
|
||||
REM the subroutine and dies at RETURN -- so HIT# and BI# would mean nothing
|
||||
REM to the caller if they were not already out here.
|
||||
REM
|
||||
REM And a name that is created costs a value slot for good. The pool is a
|
||||
REM bump allocator with no free (../akbasic/src/value.c:142) and recycling a
|
||||
REM variable takes fresh slots, so 4096 creations ends the run whenever they
|
||||
REM happen. Declared once, the whole game then runs on nothing but stores.
|
||||
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
|
||||
P$ = ""
|
||||
H$ = ""
|
||||
S$ = ""
|
||||
T$ = ""
|
||||
MSG$ = ""
|
||||
BSEG$ = ""
|
||||
FRAME$ = ""
|
||||
I# = 0
|
||||
R# = 0
|
||||
R2# = 0
|
||||
C# = 0
|
||||
CC# = 0
|
||||
KI# = 0
|
||||
T# = 0
|
||||
|
||||
REM Brick faces, one per wall row. CHAR ignores its colour argument --
|
||||
REM the sink draws in one colour -- so the rows are told apart by shape.
|
||||
BSG$(0) = "[##]"
|
||||
BSG$(1) = "[##]"
|
||||
BSG$(2) = "[==]"
|
||||
BSG$(3) = "[==]"
|
||||
BSG$(4) = "[--]"
|
||||
BSG$(5) = "[--]"
|
||||
|
||||
REM --- the seed -------------------------------------------------------
|
||||
REM There is no RND in this dialect. TI# is jiffies off the host's clock
|
||||
REM and is host uptime rather than zero-based, which makes it a fine seed.
|
||||
SEED# = TI#
|
||||
|
||||
REM The ceiling, drawn once. TOPY# is the bottom edge of this row, so the
|
||||
REM ball turns against something the player can see rather than against an
|
||||
REM invisible line two rows down from the top of the window.
|
||||
FRAME$ = "=" * COLS#
|
||||
CHAR 1, 0, 1, FRAME$
|
||||
|
||||
GOSUB MKSPR
|
||||
GOSUB SNDPROBE
|
||||
GOTO TITLE
|
||||
|
||||
REM ####################################################################
|
||||
REM SPRITES
|
||||
REM ####################################################################
|
||||
|
||||
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
|
||||
|
||||
REM ####################################################################
|
||||
REM SOUND
|
||||
REM
|
||||
REM SOUND refuses on a machine with no audio device, and an untrapped
|
||||
REM refusal would end the game. Ask once, remember the answer, and never
|
||||
REM ask again -- the game is silent where there is nothing to play on.
|
||||
REM ####################################################################
|
||||
|
||||
LABEL SNDPROBE
|
||||
SND# = 1
|
||||
TRAP NOAUDIO
|
||||
SOUND 1, 2000, 1
|
||||
TRAP
|
||||
RETURN
|
||||
|
||||
LABEL NOAUDIO
|
||||
SND# = 0
|
||||
RESUME NEXT
|
||||
|
||||
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
|
||||
|
||||
REM ####################################################################
|
||||
REM TITLE AND ATTRACT MODE
|
||||
REM
|
||||
REM The title screen plays itself. It is also how this game gets tested
|
||||
REM without a human at the keyboard.
|
||||
REM ####################################################################
|
||||
|
||||
LABEL TITLE
|
||||
DEMO# = 1
|
||||
SCORE# = 0
|
||||
LIVES# = 3
|
||||
LEVEL# = 1
|
||||
BSPD# = 4
|
||||
PAUSED# = 0
|
||||
GOSUB LOADLAY
|
||||
GOSUB BUILDW
|
||||
GOSUB DRAWW
|
||||
GOSUB DRAWHUD
|
||||
GOSUB TITTEXT
|
||||
GOSUB SERVE
|
||||
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$ = "LEFT AND RIGHT MOVE P PAUSES Q QUITS"
|
||||
GOSUB SHOWAT
|
||||
MROW# = TITROW# + 6
|
||||
MSG$ = "ATTRACT MODE"
|
||||
GOSUB SHOWAT
|
||||
RETURN
|
||||
|
||||
LABEL CLRTIT
|
||||
MROW# = TITROW#
|
||||
GOSUB CLRAT
|
||||
MROW# = TITROW# + 2
|
||||
GOSUB CLRAT
|
||||
MROW# = TITROW# + 4
|
||||
GOSUB CLRAT
|
||||
MROW# = TITROW# + 6
|
||||
GOSUB CLRAT
|
||||
RETURN
|
||||
|
||||
REM ####################################################################
|
||||
REM GAME AND LEVEL SETUP
|
||||
REM ####################################################################
|
||||
|
||||
LABEL NEWGAME
|
||||
DEMO# = 0
|
||||
SCORE# = 0
|
||||
LIVES# = 3
|
||||
LEVEL# = 1
|
||||
BSPD# = 4
|
||||
PAUSED# = 0
|
||||
GOSUB CLRTIT
|
||||
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
|
||||
|
||||
REM Three layouts, then round again with a faster ball. RESTORE takes a
|
||||
REM label -- it wants a line and a label evaluates to one -- so a layout is
|
||||
REM simply a named place in the DATA stream.
|
||||
LABEL LOADLAY
|
||||
N# = MOD((LEVEL# - 1), 3)
|
||||
IF N# = 0 THEN RESTORE LAY1
|
||||
IF N# = 1 THEN RESTORE LAY2
|
||||
IF N# = 2 THEN RESTORE LAY3
|
||||
FOR R# = 0 TO 5
|
||||
READ LAY$(R#)
|
||||
NEXT R#
|
||||
RETURN
|
||||
|
||||
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
|
||||
|
||||
LABEL DRAWW
|
||||
FOR R# = 0 TO 5
|
||||
BR2# = R#
|
||||
GOSUB DRAWBR
|
||||
NEXT R#
|
||||
RETURN
|
||||
|
||||
REM One brick row, rebuilt whole and written in a single CHAR. Anything
|
||||
REM else would truncate the row at the first character it wrote.
|
||||
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
|
||||
|
||||
REM Put the ball back on the paddle, pointing up, left or right at random.
|
||||
LABEL SERVE
|
||||
PX# = (SCW# - PW#) / 2
|
||||
HELD# = 1
|
||||
IF DEMO# = 1 THEN HELD# = 0
|
||||
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
|
||||
|
||||
REM A linear congruential generator, because this dialect has no RND.
|
||||
REM The multiply stays inside int64 for any seed under 2^31.
|
||||
LABEL RANDOM
|
||||
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
|
||||
RND# = MOD((SEED# / 65536), RMAX#)
|
||||
RETURN
|
||||
|
||||
REM ####################################################################
|
||||
REM THE MAIN LOOP
|
||||
REM
|
||||
REM A GOTO loop rather than DO/LOOP on purpose: every loop and every
|
||||
REM GOSUB pushes one of 32 scopes, and a state change that jumped out of
|
||||
REM a DO would leak one every time. This loop pushes nothing.
|
||||
REM ####################################################################
|
||||
|
||||
LABEL TICK
|
||||
GOSUB READKEY
|
||||
IF STATE# <> 0 THEN GOTO DISPATCH
|
||||
IF PAUSED# = 1 THEN GOTO TICKEND
|
||||
GOSUB MOVEPAD
|
||||
IF HELD# = 1 THEN GOSUB HOLDBAL
|
||||
IF HELD# = 0 THEN GOSUB MOVEBAL
|
||||
GOSUB SHOWSPR
|
||||
LABEL TICKEND
|
||||
SLEEP 0.02
|
||||
GOTO TICK
|
||||
|
||||
LABEL DISPATCH
|
||||
IF STATE# = 1 THEN GOTO LOSTLIF
|
||||
IF STATE# = 2 THEN GOTO LEVELUP
|
||||
IF STATE# = 3 THEN GOTO BYE
|
||||
IF STATE# = 5 THEN GOTO NEWGAME
|
||||
STATE# = 0
|
||||
GOTO TICK
|
||||
|
||||
REM ####################################################################
|
||||
REM INPUT
|
||||
REM
|
||||
REM GET is the only non-blocking read there is, and there is no key-up
|
||||
REM event and no held-key state. What there IS is the host's own key
|
||||
REM repeat, which arrives as ordinary key-down events about once per tick.
|
||||
REM So a keystroke refills a countdown and the paddle coasts to a stop
|
||||
REM when the repeats stop: held reads as held, tapped reads as a nudge.
|
||||
REM ####################################################################
|
||||
|
||||
LABEL READKEY
|
||||
FOR KI# = 1 TO 8
|
||||
GET K#
|
||||
IF K# <> 0 THEN GOSUB HANDKEY
|
||||
NEXT KI#
|
||||
RETURN
|
||||
|
||||
LABEL HANDKEY
|
||||
IF K# = 1073741904 THEN PDIR# = 0 - 1
|
||||
IF K# = 1073741904 THEN PDEC# = 12
|
||||
IF K# = 1073741903 THEN PDIR# = 1
|
||||
IF K# = 1073741903 THEN PDEC# = 12
|
||||
IF K# = 32 THEN GOSUB KEYSPC
|
||||
IF K# = 112 THEN GOSUB KEYPAU
|
||||
IF K# = 113 THEN STATE# = 3
|
||||
IF K# = 27 THEN STATE# = 3
|
||||
RETURN
|
||||
|
||||
LABEL KEYSPC
|
||||
IF DEMO# = 1 THEN STATE# = 5
|
||||
IF DEMO# = 1 THEN RETURN
|
||||
IF HELD# = 0 THEN RETURN
|
||||
HELD# = 0
|
||||
GOSUB CLRMSG
|
||||
RETURN
|
||||
|
||||
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
|
||||
|
||||
REM ####################################################################
|
||||
REM THE PADDLE
|
||||
REM ####################################################################
|
||||
|
||||
LABEL MOVEPAD
|
||||
IF DEMO# = 1 THEN GOSUB DEMOPAD
|
||||
IF DEMO# = 1 THEN GOTO PADCLMP
|
||||
IF PDEC# < 1 THEN GOTO PADCLMP
|
||||
PDEC# = PDEC# - 1
|
||||
PX# = PX# + (PDIR# * PSPD#)
|
||||
LABEL PADCLMP
|
||||
IF PX# < 0 THEN PX# = 0
|
||||
M# = SCW# - PW#
|
||||
IF PX# > M# THEN PX# = M#
|
||||
RETURN
|
||||
|
||||
LABEL DEMOPAD
|
||||
TX# = (BX# + 4) - (PW# / 2)
|
||||
TX# = TX# + DOFF#
|
||||
D# = TX# - PX#
|
||||
IF D# > PSPD# THEN D# = PSPD#
|
||||
M# = 0 - PSPD#
|
||||
IF D# < M# THEN D# = M#
|
||||
PX# = PX# + D#
|
||||
RETURN
|
||||
|
||||
LABEL HOLDBAL
|
||||
BX# = PX# + ((PW# / 2) - 4)
|
||||
BY# = PY# - 10
|
||||
RETURN
|
||||
|
||||
REM ####################################################################
|
||||
REM THE BALL
|
||||
REM
|
||||
REM X first and then Y, each move tested on its own, so a ball arriving
|
||||
REM at the corner of a brick reverses one axis rather than both.
|
||||
REM ####################################################################
|
||||
|
||||
LABEL MOVEBAL
|
||||
OX# = BX#
|
||||
BX# = BX# + BVX#
|
||||
IF BX# < 0 THEN GOSUB WALLL
|
||||
IF BX# > MAXX# THEN GOSUB WALLR
|
||||
GOSUB XBRICK
|
||||
OY# = BY#
|
||||
BY# = BY# + BVY#
|
||||
IF BY# < TOPY# THEN GOSUB WALLT
|
||||
GOSUB YBRICK
|
||||
GOSUB PADHIT
|
||||
IF BY# > LOSEY# THEN STATE# = 1
|
||||
STALL# = STALL# + 1
|
||||
IF STALL# > 500 THEN NUDGE# = 1
|
||||
RETURN
|
||||
|
||||
LABEL WALLL
|
||||
BX# = 0
|
||||
BVX# = 0 - BVX#
|
||||
GOSUB BEEPW
|
||||
RETURN
|
||||
|
||||
LABEL WALLR
|
||||
BX# = MAXX#
|
||||
BVX# = 0 - BVX#
|
||||
GOSUB BEEPW
|
||||
RETURN
|
||||
|
||||
LABEL WALLT
|
||||
BY# = TOPY#
|
||||
BVY# = 0 - BVY#
|
||||
GOSUB BEEPW
|
||||
RETURN
|
||||
|
||||
REM The leading edge, not the centre: a ball tested by its centre sinks
|
||||
REM four pixels into a brick before it turns, which looks like the brick
|
||||
REM was hit late and the one beside it was not hit at all.
|
||||
LABEL XBRICK
|
||||
TX# = BX#
|
||||
IF BVX# > 0 THEN TX# = BX# + 7
|
||||
TY# = BY# + 4
|
||||
GOSUB HITTEST
|
||||
IF HIT# = 0 THEN RETURN
|
||||
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
|
||||
|
||||
REM Which brick, if any, holds the pixel (TX#, TY#). This is the only
|
||||
REM place in the game that converts pixels into cells.
|
||||
LABEL HITTEST
|
||||
HIT# = 0
|
||||
RC# = TY# / CH#
|
||||
IF RC# < BTOP# THEN RETURN
|
||||
RB# = RC# - BTOP#
|
||||
IF RB# > (BROWS# - 1) THEN RETURN
|
||||
CC2# = TX# / CW#
|
||||
IF CC2# < BLEFT# THEN RETURN
|
||||
CB# = (CC2# - BLEFT#) / BRW#
|
||||
IF CB# > (BCOLS# - 1) THEN RETURN
|
||||
BI# = (RB# * BCOLS#) + CB#
|
||||
BR2# = RB#
|
||||
IF BR#(BI#) = 1 THEN HIT# = 1
|
||||
RETURN
|
||||
|
||||
LABEL KILLBR
|
||||
BR#(BI#) = 0
|
||||
LEFTN# = LEFTN# - 1
|
||||
STALL# = 0
|
||||
PTS# = (BROWS# - BR2#) * 10
|
||||
SCORE# = SCORE# + PTS#
|
||||
IF DEMO# = 0 THEN GOSUB HISCORE
|
||||
GOSUB DRAWBR
|
||||
GOSUB DRAWHUD
|
||||
GOSUB BEEPB
|
||||
IF LEFTN# < 1 THEN STATE# = 2
|
||||
RETURN
|
||||
|
||||
REM Paddle bounce. Five zones across the face, so where you catch it
|
||||
REM decides where it goes -- the whole game, really.
|
||||
LABEL PADHIT
|
||||
IF BVY# < 1 THEN RETURN
|
||||
BB# = BY# + 8
|
||||
IF BB# < PY# THEN RETURN
|
||||
IF BY# > (PY# + 10) THEN RETURN
|
||||
RX# = PX# + PW#
|
||||
IF (BX# + 8) < PX# THEN RETURN
|
||||
IF BX# > RX# THEN RETURN
|
||||
BY# = PY# - 8
|
||||
BVY# = 0 - BSPD#
|
||||
Z# = ((BX# + 4) - PX#) / (PW# / 5)
|
||||
IF Z# < 0 THEN Z# = 0
|
||||
IF Z# > 4 THEN Z# = 4
|
||||
PVX# = BVX#
|
||||
BVX# = (Z# - 2) * 3
|
||||
IF BVX# <> 0 THEN GOTO PADAIM
|
||||
REM Dead centre. Keep the direction it came in on and give it a shallow
|
||||
REM angle: a ball with no sideways speed at all rises and falls down one
|
||||
REM column for ever, which is how attract mode used to hang itself.
|
||||
BVX# = 2
|
||||
IF PVX# < 0 THEN BVX# = 0 - 2
|
||||
LABEL PADAIM
|
||||
IF NUDGE# = 1 THEN GOSUB UNSTICK
|
||||
IF DEMO# = 1 THEN GOSUB DEMOAIM
|
||||
GOSUB BEEPP
|
||||
RETURN
|
||||
|
||||
REM Ten seconds without touching a brick means the ball has found an orbit
|
||||
REM that misses the wall -- straight up and straight back down the same
|
||||
REM empty column, usually. Give it a different angle, but do it HERE, on a
|
||||
REM bounce the player is already watching, rather than in mid-flight where
|
||||
REM it would look like the ball hit something that was not there.
|
||||
LABEL UNSTICK
|
||||
NUDGE# = 0
|
||||
STALL# = 0
|
||||
RMAX# = 4
|
||||
GOSUB RANDOM
|
||||
BVX# = (RND# * 3) - 6
|
||||
IF BVX# = 0 THEN BVX# = 3
|
||||
RETURN
|
||||
|
||||
REM Attract mode aims off-centre by a random amount, so the machine plays
|
||||
REM like something with a hand on the paddle rather than a mirror.
|
||||
LABEL DEMOAIM
|
||||
RMAX# = 81
|
||||
GOSUB RANDOM
|
||||
DOFF# = RND# - 40
|
||||
RETURN
|
||||
|
||||
REM ####################################################################
|
||||
REM DRAWING
|
||||
REM ####################################################################
|
||||
|
||||
REM The sprite position is its top-left corner in device pixels, and the
|
||||
REM paddle is three 48-wide segments laid end to end. Each coordinate is
|
||||
REM worked out into a variable first: MOVSPR reads a leading sign as
|
||||
REM "move by this much" rather than "move to here".
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
LABEL PAD6
|
||||
P$ = "" + V#
|
||||
DO WHILE LEN(P$) < 6
|
||||
P$ = "0" + P$
|
||||
LOOP
|
||||
RETURN
|
||||
|
||||
LABEL SHOWMSG
|
||||
MROW# = MSGROW#
|
||||
GOSUB SHOWAT
|
||||
RETURN
|
||||
|
||||
LABEL CLRMSG
|
||||
MROW# = MSGROW#
|
||||
GOSUB CLRAT
|
||||
RETURN
|
||||
|
||||
REM Centre MSG$ on row MROW#. No padding on the right is needed: CHAR
|
||||
REM terminates the row where the message ends, which erases the rest.
|
||||
LABEL SHOWAT
|
||||
L# = LEN(MSG$)
|
||||
C2# = (COLS# - L#) / 2
|
||||
S$ = MSG$
|
||||
IF C2# > 0 THEN S$ = (" " * C2#) + MSG$
|
||||
CHAR 1, 0, MROW#, S$
|
||||
RETURN
|
||||
|
||||
LABEL CLRAT
|
||||
CHAR 1, 0, MROW#, " "
|
||||
RETURN
|
||||
|
||||
REM ####################################################################
|
||||
REM WHAT HAPPENS WHEN THE BALL GETS PAST YOU
|
||||
REM ####################################################################
|
||||
|
||||
LABEL LOSTLIF
|
||||
GOSUB BEEPL
|
||||
IF DEMO# = 1 THEN GOTO DEMOSRV
|
||||
LIVES# = LIVES# - 1
|
||||
GOSUB DRAWHUD
|
||||
IF LIVES# < 1 THEN GOTO GAMEOVR
|
||||
GOSUB SERVE
|
||||
MSG$ = "BALL LOST -- PRESS SPACE"
|
||||
GOSUB SHOWMSG
|
||||
STATE# = 0
|
||||
GOTO TICK
|
||||
|
||||
LABEL DEMOSRV
|
||||
GOSUB SERVE
|
||||
HELD# = 0
|
||||
STATE# = 0
|
||||
GOTO TICK
|
||||
|
||||
LABEL LEVELUP
|
||||
MSG$ = "LEVEL CLEARED"
|
||||
GOSUB SHOWMSG
|
||||
SLEEP 1.5
|
||||
LEVEL# = LEVEL# + 1
|
||||
IF BSPD# < 7 THEN BSPD# = BSPD# + 1
|
||||
GOSUB NEWLEV
|
||||
IF DEMO# = 1 THEN GOSUB CLRMSG
|
||||
STATE# = 0
|
||||
GOTO TICK
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
REM The machine does not get on the scoreboard: attract mode keeps score
|
||||
REM so the wall behaves, but only a player's score is ever the high one.
|
||||
LABEL HISCORE
|
||||
IF SCORE# > HIGH# THEN HIGH# = SCORE#
|
||||
RETURN
|
||||
|
||||
REM ####################################################################
|
||||
REM SPRITE PATTERNS
|
||||
REM
|
||||
REM 63 bytes each: three per row, twenty-one rows, most significant bit
|
||||
REM leftmost. A clear bit is transparent, which is what lets an 8x8 ball
|
||||
REM live in the corner of a 24x21 pattern with nothing drawn around it.
|
||||
REM
|
||||
REM byte 0 byte 1 byte 2
|
||||
REM 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
|
||||
REM ^ column 0 ^ column 23
|
||||
REM ####################################################################
|
||||
|
||||
REM Ball: an 8x8 disc in the top-left corner, so its pixel rectangle is
|
||||
REM the sprite position and eight pixels each way. No offset arithmetic.
|
||||
REM . . # # # # . . 60
|
||||
REM . # # # # # # . 126
|
||||
REM # # # # # # # # 255
|
||||
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
|
||||
|
||||
REM Paddle segment: the full 24 wide and 6 deep, expanded to 48 wide by
|
||||
REM SPRITE's xexpand bit. Three of them make one 144 pixel paddle.
|
||||
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
|
||||
|
||||
REM ####################################################################
|
||||
REM LEVEL LAYOUTS
|
||||
REM
|
||||
REM Six rows of ten bricks. 1 is a brick, 0 is a hole. RESTORE picks the
|
||||
REM layout by line number, so adding a fourth is a block and one IF.
|
||||
REM ####################################################################
|
||||
|
||||
LABEL LAY1
|
||||
REM Level 1: the whole wall.
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
|
||||
LABEL LAY2
|
||||
REM Level 2: a barrel, open at the corners.
|
||||
DATA "0011111100"
|
||||
DATA "0111111110"
|
||||
DATA "1111111111"
|
||||
DATA "1111111111"
|
||||
DATA "0111111110"
|
||||
DATA "0011111100"
|
||||
|
||||
LABEL LAY3
|
||||
REM Level 3: gaps to shoot through.
|
||||
DATA "1010101010"
|
||||
DATA "0101010101"
|
||||
DATA "1111001111"
|
||||
DATA "1100110011"
|
||||
DATA "1011111101"
|
||||
DATA "0110110110"
|
||||
86
examples/breakout/sprites/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Breakout, with artwork
|
||||
|
||||
Breakout, written entirely in BASIC for the AKGL BASIC interpreter, using downloaded CC0
|
||||
sprite artwork for the paddles, the balls and the powerup gems. The bricks, the HUD and
|
||||
the lettering are drawn, captured with `SSHAPE` and installed as sprites — which is the
|
||||
only way a program in this interpreter can put a picture on the screen.
|
||||
|
||||
```sh
|
||||
../../../build-akgl/basic breakout.bas
|
||||
```
|
||||
|
||||
It needs the SDL build. Without a graphics device the first `SPRSAV` refuses by name and
|
||||
the program stops there, which is the right thing for it to do.
|
||||
|
||||
| Key | Does |
|
||||
|---|---|
|
||||
| left / right | move the paddle |
|
||||
| space | start a game, launch the ball, release a stuck ball |
|
||||
| P | pause |
|
||||
| S | sound on and off |
|
||||
| Q or escape | quit |
|
||||
|
||||
Three lives, six rows of bricks worth 60 down to 10 points a row, and a field that cycles
|
||||
six rows, then five, then four while the ball gets faster every level up to a ceiling. A
|
||||
gem is worth 50 on top of whatever it does.
|
||||
|
||||
## The powerups
|
||||
|
||||
A broken brick drops a gem about one time in seven, and only ever one at a time. Catch it
|
||||
with the paddle. The colour of the gem is what tells you which it is; the banner that
|
||||
flashes over the field names it as well.
|
||||
|
||||
| Gem | Name | Does | For |
|
||||
|---|---|---|---|
|
||||
| red | EXPAND | doubles the paddle's width | 20 seconds |
|
||||
| yellow | MULTI | throws two more balls off the one in play | until they are lost |
|
||||
| green | SLOW | drops the ball's speed to about two thirds | 16 seconds |
|
||||
| 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 |
|
||||
|
||||
The second bar **mirrors** the paddle rather than following it, so it covers the side you
|
||||
just left. 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.
|
||||
|
||||
## 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.
|
||||
|
||||
[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
|
||||
`DATA` sprites — a completely different program, and the shorter one to read first.
|
||||
|
||||
## Where the artwork came from
|
||||
|
||||
Kenney's [Puzzle Pack 1](https://kenney.nl/assets/puzzle-pack-1), released under
|
||||
[CC0](http://creativecommons.org/publicdomain/zero/1.0/). The files in `art/` are the
|
||||
pack's own `PNG/Default/` versions, unmodified, with the pack's licence file beside them.
|
||||
`art/PROVENANCE.md` says which file is used for what and why the double-size set is not.
|
||||
|
||||
Crediting Kenney is not required by CC0. It is here because it should be.
|
||||
|
||||
## What is not in it
|
||||
|
||||
Named honestly rather than left to be discovered:
|
||||
|
||||
- **No music under the game**, only event sounds and two four-note stings. There is room in
|
||||
the third voice for it; there is not much room anywhere else. 121 of the interpreter's
|
||||
128 variables are spoken for, four more are the ones it makes itself, and the label table
|
||||
is at 61 of 64.
|
||||
- **One gem at a time.** There is one sprite slot for it. A brick broken while a gem is
|
||||
falling drops nothing.
|
||||
- **Sticky and multiball share one offset.** Two balls stuck to the paddle at once sit on
|
||||
top of each other. It is rare enough that fixing it would cost a variable I do not have.
|
||||
- **The score lags a frame behind the bricks.** Only one capture happens per frame and the
|
||||
field goes first, so a brick disappears one frame before the score that counts it. At
|
||||
thirty frames a second nobody can see it, and it is a deliberate trade rather than an
|
||||
oversight.
|
||||
- **No high score on disk.** There is no disk.
|
||||
|
||||
The eight interpreter defects this game turned up are filed in `TODO.md` §9, each with a
|
||||
reduction that fits on a screen, the file and line of the cause, and what a fix would
|
||||
touch.
|
||||
21
examples/breakout/sprites/art/License.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
|
||||
Puzzle Pack (1.1)
|
||||
|
||||
Created/distributed by Kenney (www.kenney.nl)
|
||||
|
||||
------------------------------
|
||||
|
||||
License: (Creative Commons Zero, CC0)
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
This content is free to use in personal, educational and commercial projects.
|
||||
Support us by crediting Kenney or www.kenney.nl (this is not mandatory)
|
||||
|
||||
------------------------------
|
||||
|
||||
Donate: http://support.kenney.nl
|
||||
Patreon: http://patreon.com/kenney/
|
||||
|
||||
Follow on Twitter for updates:
|
||||
http://twitter.com/KenneyNL
|
||||
35
examples/breakout/sprites/art/PROVENANCE.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Where this art came from
|
||||
|
||||
Every PNG in this directory is from **Kenney's Puzzle Pack 1**, released into the public
|
||||
domain under [Creative Commons Zero](http://creativecommons.org/publicdomain/zero/1.0/).
|
||||
`License.txt` is the pack's own licence file, copied here unedited.
|
||||
|
||||
* Source: <https://kenney.nl/assets/puzzle-pack-1>
|
||||
* Downloaded: 2026-08-01, `kenney_puzzle-pack-1.zip`
|
||||
* Author: Kenney (<https://www.kenney.nl>)
|
||||
* Licence: CC0 1.0. Crediting is not required; it is here because it should be.
|
||||
|
||||
The files are the pack's `PNG/Default/` versions, byte for byte — nothing is resized,
|
||||
recoloured or re-encoded, so the checksum of any of them still matches the distributed
|
||||
archive.
|
||||
|
||||
| File | Size | Used for |
|
||||
|---|---|---|
|
||||
| `paddleBlu.png` | 104x24 | the player's paddle |
|
||||
| `paddleRed.png` | 104x24 | the second paddle, from the CATCHER powerup |
|
||||
| `ballBlue.png` | 22x22 | the ball |
|
||||
| `ballGrey.png` | 22x22 | the extra balls from MULTIBALL |
|
||||
| `element_red_polygon_glossy.png` | 48x46 | the EXPAND powerup |
|
||||
| `element_yellow_polygon_glossy.png` | 48x46 | the MULTIBALL powerup |
|
||||
| `element_green_polygon_glossy.png` | 48x46 | the SLOW powerup |
|
||||
| `element_blue_polygon_glossy.png` | 48x46 | the STICKY powerup |
|
||||
| `element_purple_polygon_glossy.png` | 48x46 | the CATCHER powerup |
|
||||
|
||||
The pack's `PNG/Double/` versions are twice these sizes and are the wrong scale for an
|
||||
800x600 field. `SPRITE`'s x- and y-expand bits would double them again, which is the only
|
||||
scaling this interpreter has — there is no way to make a sprite *smaller* — so the
|
||||
`Default` set is the one that fits.
|
||||
|
||||
Everything else on screen — the bricks, the walls, the HUD lettering, the title — is drawn
|
||||
by the program with `DRAW` and `BOX`. See `README-sprites.md` for why only eight things in
|
||||
this game can be made of artwork.
|
||||
BIN
examples/breakout/sprites/art/ballBlue.png
Normal file
|
After Width: | Height: | Size: 513 B |
BIN
examples/breakout/sprites/art/ballGrey.png
Normal file
|
After Width: | Height: | Size: 495 B |
BIN
examples/breakout/sprites/art/element_blue_polygon_glossy.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
examples/breakout/sprites/art/element_green_polygon_glossy.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
examples/breakout/sprites/art/element_purple_polygon_glossy.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
examples/breakout/sprites/art/element_red_polygon_glossy.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
examples/breakout/sprites/art/element_yellow_polygon_glossy.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
examples/breakout/sprites/art/paddleBlu.png
Normal file
|
After Width: | Height: | Size: 762 B |
BIN
examples/breakout/sprites/art/paddleRed.png
Normal file
|
After Width: | Height: | Size: 578 B |
1360
examples/breakout/sprites/breakout.bas
Normal file
8
tests/docs_setups/breakout_art.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Chapter 18's figures load the Breakout artwork by the same relative path the
|
||||
# game does, so they need an `art/` beside the program. Copy the example's own
|
||||
# directory rather than a private duplicate: a figure drawn from a second copy
|
||||
# of the artwork stops being a figure of the program it documents the moment
|
||||
# either copy moves, which is the drift this whole arrangement is against.
|
||||
cp -R "$(dirname "${BASH_SOURCE[0]}")/../../examples/breakout/sprites/art" art
|
||||