Merge branch 'main' into 10
Some checks failed
akbasic CI Build / mutation_test (push) Failing after 11m28s
akbasic CI Build / akgl_build (push) Failing after 11m46s
akbasic CI Build / coverage (push) Failing after 12m10s
akbasic CI Build / sanitizers (push) Failing after 12m21s
akbasic CI Build / cmake_build (push) Failing after 12m33s

This commit is contained in:
2026-08-06 12:50:30 -04:00
committed by Starfort Source Vault
75 changed files with 2335 additions and 328 deletions

View File

@@ -146,6 +146,143 @@ condition at all loops forever until an `EXIT` or a `GOTO` leaves it.
`EXIT` works here too.
## GEN ... EMIT and FOR EACH / DO EACH
A `GEN` is a subroutine that hands back more than one value, one at a time, instead of
returning once. It looks like a multi-line `DEF`, except its body runs `EMIT` where a
function would `RETURN`, and it ends in `END GEN` rather than `RETURN`:
```basic
10 GEN COUNTUP(N#)
20 FOR I# = 1 TO N#
30 EMIT I#
40 NEXT I#
50 END GEN
60 FOR EACH V# IN COUNTUP(3)
70 PRINT V#
80 NEXT V#
```
```output
1
2
3
```
`FOR EACH` is what runs a `GEN`: it calls `COUNTUP(3)` the way `FOR EACH V# IN` says,
and each `EMIT` inside the generator's body becomes one trip through the loop, with `V#`
holding whatever was emitted. When the generator's body reaches `END GEN` with nothing
left to emit, the loop ends -- there is no separate "no more values" check to write.
`DO EACH ... LOOP` does the same thing:
```basic
10 GEN COUNTUP(N#)
20 FOR I# = 1 TO N#
30 EMIT I#
40 NEXT I#
50 END GEN
60 DO EACH V# IN COUNTUP(3)
70 PRINT V#
80 LOOP
```
```output
1
2
3
```
A `GEN`'s body is ordinary BASIC: it may hold its own `FOR`, `DO`, `IF` or `GOSUB`
around the `EMIT`s, and even invoke another `GEN` with its own `FOR EACH`/`DO EACH` --
the same generator invoked with different arguments, nested or side by side, is not
recursion. A `GEN` invoking *itself* from within its own currently-running body is
refused: unlike a function call, which runs to completion and returns, the outer
invocation is suspended mid-body waiting on the same loop, and there is no answer to
"which EMIT feeds which loop" that is not a surprise.
`EXIT` leaves a `FOR EACH`/`DO EACH` loop early, exactly as it does a plain `FOR` or
`DO`, and the generator it was consuming stops there -- nothing forces the rest of it to
run just because the loop started it:
```basic
10 GEN COUNTUP(N#)
20 FOR I# = 1 TO N#
30 EMIT I#
40 NEXT I#
50 END GEN
60 FOR EACH V# IN COUNTUP(100)
70 PRINT V#
80 IF V# = 3 THEN EXIT
90 NEXT V#
100 PRINT "STOPPED"
```
```output
1
2
3
STOPPED
```
`RETURN` ends a `GEN` from the inside, exactly as it ends a multi-line `DEF` or a
`GOSUB`: the generator is done, the loop consuming it ends, and the program carries on
after the loop. What a generator's `RETURN` cannot do is carry a value -- values leave
a `GEN` one at a time, through `EMIT`, and `RETURN 99` inside one is an error. Like a
`GOSUB`'s or a `DEF`'s, the `RETURN` must stand in the `GEN`'s own scope: from inside
a `FOR` or `DO` the body opened, it is an error, though `IF ... THEN RETURN` is fine
because `IF` opens no scope of its own.
```basic
10 GEN FIRSTFEW(N#)
20 EMIT 1
30 IF N# < 2 THEN RETURN
40 EMIT 2
50 IF N# < 3 THEN RETURN
60 EMIT 3
70 END GEN
80 FOR EACH V# IN FIRSTFEW(2)
90 PRINT V#
100 NEXT V#
110 PRINT "DONE"
```
```output
1
2
DONE
```
A `DO EACH`'s `LOOP` may carry a `WHILE` or `UNTIL`, and the two compose: the condition
is checked after each trip through the body, with the loop variable still holding that
trip's value, and a condition that says stop abandons the rest of the generator exactly
as `EXIT` does. The condition belongs on the `LOOP` -- putting it on the `DO` line is
an error, since the `DO` line already says what the loop consumes.
```basic
10 GEN COUNTUP(N#)
20 FOR I# = 1 TO N#
30 EMIT I#
40 NEXT I#
50 END GEN
60 DO EACH V# IN COUNTUP(10)
70 PRINT V#
80 LOOP UNTIL V# = 3
90 PRINT "STOPPED"
```
```output
1
2
3
STOPPED
```
A `GEN` shares its namespace with `DEF` -- the same name cannot be both -- but it is not
a function and cannot be called like one: `X# = COUNTUP(3)` is refused, because nothing
about an ordinary call means "resume where the last `EMIT` left off." `FOR EACH`/`DO
EACH` is the only thing that consumes a `GEN`.
## GOTO and GOSUB
```basic

View File

@@ -126,7 +126,7 @@ exists, and reading into one you never sized writes over something else.
| `COLLECT` | validates a disk's block allocation map. There is no map |
| `BACKUP` | duplicates one disk onto another. There are no disks |
| `BOOT` | loads and runs a boot sector. There is no boot sector |
| `DIRECTORY` / `CATALOG` | needs a directory-reading wrapper the standard library does not have yet. Filed upstream |
| `DIRECTORY` / `CATALOG` | not written yet. It was blocked on a directory-reading wrapper in the standard library; that landed, so only the verb is outstanding |
`DCLEAR` is the exception among the drive verbs: resetting a drive also closes its
channels, and closing the channels is real, so that is what it does.

View File

@@ -35,19 +35,22 @@ for the reasoning in each case.
| `DIALOG` | `DIALOG ["text"]` | Show a text panel across the bottom of the screen. No argument takes it down. See Chapter 19. |
| `DIM` | `DIM A#(n [,...])` | Make an array. Subscripts start at zero; `n` is the count. |
| `DIM``AS` | `DIM S@ AS T`, `DIM P@ AS PTR TO T` | Make a structure, or a strict pointer to one. See Chapter 16. |
| `DIRECTORY` | `DIRECTORY` | **Refused.** Needs a directory-reading wrapper that does not exist yet. |
| `DIRECTORY` | `DIRECTORY` | **Refused.** Not written yet; the standard-library wrapper it waited on has landed. |
| `DLOAD` | `DLOAD "name"` | Load a program from a file. |
| `DO` | `DO [WHILE c | UNTIL c]` | Start a loop. The condition may be here, on the `LOOP`, or neither. |
| `DO` | `DO [WHILE c | UNTIL c]`, `DO EACH V IN gen(args)` | Start a loop. The condition may be here, on the `LOOP`, or neither. `EACH` consumes a `GEN` instead, and takes its condition only on the `LOOP`; see Chapter 4. |
| `DOPEN` | `DOPEN n, "name" [,W]` | Open a file on channel `n`. `W` opens it for writing. |
| `DRAW` | `DRAW src, x, y [TO x, y ...]` | Plot a point or draw a polyline. |
| `DSAVE` | `DSAVE "name"` | Save the program to a file. |
| `DVERIFY` | `DVERIFY "name"` | The other name for `VERIFY`. |
| `EMIT` | `EMIT expr` | Yield one value from a `GEN` body. Only valid inside one; see Chapter 4. |
| `END` | `END` | Stop the program. Does not arm `CONT`. |
| `END GEN` | `END GEN` | Close a `GEN` body, the way `RETURN` closes a multi-line `DEF`. |
| `ENVELOPE` | `ENVELOPE n, a, d, s, r` | Define one of `PLAY`'s ten envelope presets. |
| `EXIT` | `EXIT` | Leave the innermost `FOR` or `DO` loop. |
| `EXIT` | `EXIT` | Leave the innermost `FOR`, `FOR EACH`, `DO` or `DO EACH` loop. |
| `FETCH` | `FETCH count, from, to` | Copy bytes. The same as `STASH`; there is no expansion RAM. |
| `FILTER` | `FILTER ...` | **Refused.** There is no filter stage in the audio backend. |
| `FOR` | `FOR V = a TO b [STEP c]` | Start a counted loop, ended by `NEXT`. |
| `FOR` | `FOR V = a TO b [STEP c]`, `FOR EACH V IN gen(args)` | Start a counted loop, ended by `NEXT`. `EACH` consumes a `GEN` instead; see Chapter 4. |
| `GEN` | `GEN NAME(args) ... END GEN` | Define a generator: a subroutine that yields more than once via `EMIT`, consumed by `FOR EACH`/`DO EACH`. See Chapter 4. |
| `GET` | `GET V` | Take a keystroke if one is waiting, without stopping. |
| `GETKEY` | `GETKEY V` | Wait for a keystroke, holding the program but not the host. |
| `GETMENU` | `GETMENU n, V%` | Wait for a menu choice, holding the program but not the host. Assigns the entry number. See Chapter 19. |
@@ -67,11 +70,11 @@ for the reasoning in each case.
| `LIST` | `LIST [n][-n]` | List the program, or part of it. |
| `LOAD` | `LOAD "name"` | The other name for `DLOAD`. |
| `LOCATE` | `LOCATE x, y` | Move the pixel cursor. |
| `LOOP` | `LOOP [WHILE c | UNTIL c]` | End a `DO` loop. |
| `LOOP` | `LOOP [WHILE c | UNTIL c]` | End a `DO` loop, including a `DO EACH`. |
| `MENU` | `MENU [n [,"item", ...]]` | Show a menu the player picks from. No entries retires it; no arguments retire them all. See Chapter 19. |
| `MOVSPR` | `MOVSPR n, ...` | Move a sprite. Four forms; see Chapter 8. |
| `NEW` | `NEW` | Erase the program and every variable. |
| `NEXT` | `NEXT V` | End a `FOR` loop and advance its counter. |
| `NEXT` | `NEXT V` | End a `FOR` loop and advance its counter, or resume a `FOR EACH` for its next value. |
| `ON` | `ON e GOTO|GOSUB t [,...]` | Branch to the `e`th target, counting from one. |
| `PAINT` | `PAINT src, x, y` | Flood-fill the region containing a point. |
| `PLAY` | `PLAY "notes"` | Queue notes. Does not block. |
@@ -87,7 +90,7 @@ for the reasoning in each case.
| `RENUMBER` | `RENUMBER [start [,step [,from]]]` | Renumber lines, rewriting every branch to match. |
| `RESTORE` | `RESTORE [line]` | Reset the `READ` cursor, optionally to a line. |
| `RESUME` | `RESUME [NEXT | line]` | Return from a `TRAP` handler. |
| `RETURN` | `RETURN [expr]` | Return from a `GOSUB` or a multi-line `DEF`. |
| `RETURN` | `RETURN [expr]` | Return from a `GOSUB` or a multi-line `DEF`. Inside a `GEN`, a bare `RETURN` ends the generator early; `RETURN expr` there is an error. |
| `RUN` | `RUN [line]` | Run the program, optionally from a line. |
| `SAVE` | `SAVE "name"` | The other name for `DSAVE`. |
| `SCALE` | `SCALE on [,xmax, ymax]` | Turn user coordinates on or off. |

View File

@@ -9,6 +9,7 @@ so a call with the wrong number is a syntax error rather than a surprise.
| Function | Args | Form | What it gives |
|---|---|---|---|
| `ABS` | 1 | `ABS(n)` | The absolute value of an integer or float. |
| `ASC` | 1 | `ASC(A$)` | The Unicode code point of a string's first character. |
| `ATN` | 1 | `ATN(n)` | Arctangent, in radians. |
| `BUMP` | 1 | `BUMP(1)` | Which sprites have collided, as a bitmask. **Reading clears it.** |
| `CHR` | 1 | `CHR(n)` | The character for a Unicode code point, as a string. |
@@ -29,6 +30,7 @@ so a call with the wrong number is a syntax error rather than a surprise.
| `RGR` | 1 | `RGR(f)` | The `GRAPHIC` mode (0), the drawing surface's width (1) or height (2) in pixels, or a character cell's width (3) or height (4). |
| `RIGHT` | 2 | `RIGHT(A$, n)` | The rightmost `n` characters. Clamped. |
| `RMENU` | 2 | `RMENU(n, f)` | A menu's state: field 0 the highlighted entry, field 1 whether it has been confirmed. **Reading field 1 clears it.** |
| `RND` | 1 | `RND(n)` | A random integer from 0 up to but not including `n`. |
| `RWINDOW` | 1 | `RWINDOW(f)` | The current text window's rows (0) or columns (1). Field 2 is a C128 screen mode and is refused. |
| `RSPCOLOR` | 1 | `RSPCOLOR(n)` | One of `SPRCOLOR`'s two shared registers, 1 or 2. |
| `RSPHIT` | 2 | `RSPHIT(n, f)` | One of `SPRHIT`'s settings for sprite `n`, in `SPRHIT`'s own argument order: 0 the kind, 1 to 4 the two corners. |

View File

@@ -199,7 +199,8 @@ interpreter's error code, which bears no relation to a Commodore error number. P
- **`BLOAD` requires a length.**
- **`HEADER`, `COLLECT`, `BACKUP` and `BOOT` are refused.** They operate on a physical
disk.
- **`DIRECTORY` is refused** pending a wrapper in the standard library.
- **`DIRECTORY` is refused** because it is not written yet. The standard-library
wrapper it was waiting on has landed, so the remaining work is the verb.
## Machine

View File

@@ -380,8 +380,10 @@ no free, so anything drawn from it is spent for the life of the run — and scop
returns a variable's *slot* without returning its storage. A scalar therefore does not
draw from it at all: `akbasic_variable_init()` points a one-element non-`@` variable at
its own `inlinevalue`, which is what makes a local, a `FOR` counter and a `DEF` parameter
free. Arrays and structures still spend, deliberately, because a pointer into a record is
allowed to outlive the scope that DIMmed it.
free. A pointer parameter is also free: it owns only its one-slot reference, so its
`inlinevalue` dies with the call while the target remains in the caller's storage. Arrays,
structures and by-value structure parameters still spend, deliberately, because a pointer
into a record is allowed to outlive the scope that DIMmed it.
[Chapter 13](13-differences.md) states the same budget from a BASIC programmer's side.
The per-environment three are reset at the top of every line, which is what makes
@@ -407,6 +409,8 @@ block structure is executing: the `FOR` bounds and step, the `DO`/`LOOP` conditi
| `GOSUB` | `RETURN` |
| A call to a multi-line user function | that function's `RETURN` |
| An interrupt firing | the handler's `RETURN` |
| `FOR EACH`/`DO EACH` — the loop's own scope, during parsing | the `NEXT`/`LOOP` that finds the generator exhausted, or an `EXIT` |
| A `FOR EACH`/`DO EACH` invoking a `GEN` | `END GEN` reached for real, or the loop's own abandonment |
That `FOR` entry is not a typo. `akbasic_parse_for()` pushes the new environment while
parsing the line, parks `TO` and `STEP` in it as unevaluated leaves, and makes it active
@@ -454,6 +458,59 @@ Three consequences follow, and all three are things people report as bugs:
the value correctly inside the loop and gets `0` immediately after it, with nothing
raised anywhere.
### Generators: a scope that outlives the verb that pushed it
Everything above pops a scope by *releasing* it — `akbasic_runtime_prev_environment()`
gives its variables and its own pool slot back in the same motion that hands control to
its parent. A `GEN` needed a third option, because `EMIT` has to survive being
"returned" from: the next value comes from resuming exactly where the last `EMIT` left
off, not from starting over.
`akbasic_runtime_prev_environment()` is now built from two smaller pieces:
- `akbasic_runtime_detach_environment()` — moves `obj->environment` to the parent,
*without* releasing anything.
- `akbasic_runtime_release_environment()` — gives a scope's variables and pool slot
back, callable on a scope that is not necessarily the active one.
A `FOR EACH`/`DO EACH` invocation therefore holds **two** environments at once, for as
long as the loop is running:
```text
loop environment (isEachLoop) <- pushed like a plain FOR/DO's, at parse time
forGeneratorEnv -----------> generator environment (isGenerator)
<- pushed once, by akbasic_runtime_generator_invoke(),
and never released until the generator is
exhausted or abandoned
```
`EMIT` finds its generator environment by walking *up* from wherever it is actually
standing — a `GEN` body is ordinary BASIC and may nest its own `FOR`, `DO` or `GOSUB`
around an `EMIT`, each pushing scopes of its own — to the nearest ancestor with
`isGenerator` set. It assigns the emitted value into the loop's `forNextVariable`,
records *exactly* where it is standing (which may be several environments below the
generator's own call frame) as `forGeneratorEnv`, and moves `obj->environment` straight
to the loop environment — detaching, not popping, so every environment between the two
survives untouched.
`NEXT`/`LOOP` reactivate a suspended generator by setting `obj->environment` back to
`forGeneratorEnv` and driving the step loop (`akbasic_runtime_pump_generator()`) until
either another `EMIT` detaches it again or `END GEN` is reached for real — meaning
`isGenerator` is set and nothing is skipping forward to it, exactly the same test
`RETURN` makes for a multi-line `DEF`. Real exhaustion releases the generator
environment (`akbasic_runtime_prev_environment()`, same as anything else that pops) and
clears `forGeneratorEnv`, which is what tells the loop apart from one still waiting to
resume.
**Abandoning a live generator has to release it explicitly.** `EXIT` out of a `FOR
EACH`/`DO EACH` pops the loop environment the same way it always has, but a generator
paused mid-run is not on that direct parent chain from the loop back to the root — it
hangs off `forGeneratorEnv` instead, possibly several environments deep if `EMIT` last
ran inside a nested `FOR`/`DO` in the `GEN`'s own body. `akbasic_runtime_release_generator()`
is what walks that chain and releases all of it; every place that pops a `FOR EACH`/`DO
EACH` loop out from under a live generator calls it first, or the pool leaks one
generator at a time.
## Values
`akbasic_Value` carries its string **inline**, not behind a pointer, so a copy is a struct

View File

@@ -266,6 +266,13 @@ The function saw 99; the caller still has 5. To change a caller's record on purp
a pointer — `DEF POKEIT(P@ AS PTR TO CRATE)` — and reach through it with `->`. Neither is
a special rule: both fall out of the parameter being assigned like any other variable.
A pointer parameter's own reference is kept in the call variable's inline slot, so repeated
calls do not spend the value pool. Its target remains the caller's structure. A by-value
structure parameter is different: its copied slots remain in the value pool because
`POINT Q@ AT B@` may retain a pointer to that copy after the function returns. Until escape
analysis can distinguish that case, **do not call a by-value structure-parameter function in
a loop**; bind a host global and rebind it per instance instead.
## What is checked, and what is not
A field name is checked against the set the type declared, and the refusal lists the

View File

@@ -1005,57 +1005,26 @@ IF NUDGE# = 1 THEN GOSUB UNSTICK
LABEL UNSTICK
NUDGE# = 0
STALL# = 0
RMAX# = 4
GOSUB RANDOM
BVX# = (RND# * 3) - 6
BVX# = (RND(4) * 3) - 6
IF BVX# = 0 THEN BVX# = 3
RETURN
```
### You have to write your own random numbers
### Random numbers are built in
**There is no `RND` in this dialect**, and no `INT`, `SQR`, `ASC` or `TIMER` either. A
linear congruential generator is nine tokens and does the job. Put the number of possible
answers in `RMAX#` and read the result from `RND#`:
There is no `INT`, `SQR` or `TIMER` in this dialect, but
`RND(n)` returns an integer from zero through `n - 1`. It seeds itself
from the host clock the first time it is called, so a program only needs the bound:
```basic
SEED# = 12345
RMAX# = 6
RND# = 0
I# = 0
FOR I# = 1 TO 5
GOSUB RANDOM
PRINT "ROLL " + (RND# + 1)
PRINT "ROLL " + (RND(6) + 1)
NEXT I#
END
LABEL RANDOM
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
RND# = MOD((SEED# / 65536), RMAX#)
RETURN
```
```output
ROLL 1
ROLL 5
ROLL 2
ROLL 1
ROLL 2
```
The multiplication stays inside a 64-bit integer for any seed below 2147483648, which is
why the modulus is that number. The answer is taken from the middle bits — `SEED# / 65536`
— because the low bits of a power-of-two modulus barely change from one call to the next.
Integer division truncating for free is the `INT` you do not have.
Seed it from the clock at startup. `TI#` is the host's uptime in sixtieths of a second,
which is different every time the game is run:
```basic norun
SEED# = TI#
```
Use `RANDOM` for the serve, too, so the ball does not always leave in the same direction:
Use `RND` for the serve, too, so the ball does not always leave in the same direction:
```basic norun
LABEL SERVE
@@ -1063,16 +1032,43 @@ PX# = (SCW# - PW#) / 2
HELD# = 1
BX# = PX# + ((PW# / 2) - 4)
BY# = PY# - 10
RMAX# = 2
GOSUB RANDOM
BVX# = BSPD#
IF RND# = 0 THEN BVX# = 0 - BSPD#
IF RND(2) = 0 THEN BVX# = 0 - BSPD#
BVY# = 0 - BSPD#
PDEC# = 0
GOSUB SHOWSPR
RETURN
```
<details>
<summary>Historical aside: the LCG this chapter used to teach</summary>
Before `RND` existed, this nine-token linear congruential generator was copied into
every program. It remains a useful from-scratch PRNG example:
```basic norun
SEED# = 12345
RMAX# = 6
ROLL# = 0
I# = 0
FOR I# = 1 TO 5
GOSUB RANDOM
PRINT "ROLL " + (ROLL# + 1)
NEXT I#
END
LABEL RANDOM
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
ROLL# = MOD((SEED# / 65536), RMAX#)
RETURN
```
The multiplication stays inside a 64-bit integer for any seed below 2147483648. The
answer is taken from the middle bits because the low bits of a power-of-two modulus
barely change from one call to the next. This used to be required; it is now built in.
</details>
`HELD#` is the flag Step 6's loop tests: while it is 1 the ball sits on the paddle, and
`HOLDBAL` keeps it there:
@@ -1422,9 +1418,7 @@ PX# = PX# + D#
RETURN
LABEL DEMOAIM
RMAX# = 81
GOSUB RANDOM
DOFF# = RND# - 40
DOFF# = RND(81) - 40
RETURN
```
@@ -1501,7 +1495,7 @@ This is the shape of the whole file:
LABEL SETUP the geometry from Step 2
the declaration block from Step 3
the brick faces from Step 5
SEED# = TI#
RND(n) seeds itself from the host clock
the ceiling from Step 9
GOSUB MKSPR Step 4
GOSUB SNDPROBE Step 14
@@ -1576,10 +1570,7 @@ BB# = 0
RX# = 0
N# = 0
MROW# = 0
RMAX# = 2
RND# = 0
SND# = 0
SEED# = 0
P$ = ""
H$ = ""
S$ = ""

View File

@@ -20,7 +20,7 @@ whole development loop; the engine never rebuilds.
- **[Step 2](#step-2-bind-the-engines-own-actor)** — bind the engine's own
actor as the second type, which is the point of the whole exercise
- **[Step 3](#step-3-share-the-frame-and-the-dice)** — share the frame state,
and give the script randomness it cannot make itself
and hand the script dice the engine controls
- **[Step 4](#step-4-why-bindings-and-not-arguments)** — see why the structures
are bindings rather than function arguments
- **[Step 5](#step-5-the-shape-of-the-script)** — learn the three language
@@ -75,7 +75,7 @@ typedef struct galaga_Enemy
float t; /* parametric clock for the current maneuver */
int32_t hp;
int32_t fire; /* outbox: script sets 1, engine consumes */
float rnd; /* inbox: engine writes fresh 0..1 each call */
float rnd; /* inbox: fresh 0..1 each call; ROLL% in BASIC */
} galaga_Enemy;
```
@@ -107,7 +107,7 @@ static const akbasic_HostField ENEMY_FIELDS[] = {
AKBASIC_HOST_FIELD( galaga_Enemy, t, "T%", AKBASIC_HOSTFIELD_FLOAT ),
AKBASIC_HOST_FIELD( galaga_Enemy, hp, "HP#", AKBASIC_HOSTFIELD_INT32 ),
AKBASIC_HOST_FIELD( galaga_Enemy, fire, "FIRE#", AKBASIC_HOSTFIELD_INT32 ),
AKBASIC_HOST_FIELD( galaga_Enemy, rnd, "RND%", AKBASIC_HOSTFIELD_FLOAT )
AKBASIC_HOST_FIELD( galaga_Enemy, rnd, "ROLL%", AKBASIC_HOSTFIELD_FLOAT )
};
static const akbasic_HostType ENEMY_TYPE = {
"ENEMY", sizeof(galaga_Enemy), ENEMY_FIELDS, 8
@@ -194,7 +194,7 @@ typedef struct galaga_Shared
float playerx; /* the player actor's position, this frame */
float playery;
int32_t wave;
float rnd; /* fresh 0..1 each frame; the issue #16 route */
float rnd; /* fresh 0..1 each frame; ROLL% to the script */
} galaga_Shared;
```
@@ -203,13 +203,24 @@ the engine refreshes it at the top of every frame. The boss reads
`GAME@.PLAYERX%` to lead its dive; the fire decision reads it to know whether
anything is worth shooting at.
The `rnd` fields — one here per frame, one on each enemy per call — exist
because the engine's PRNG is the script's **only** source of randomness: write
`SELF@.RND% < DT% * 1.5` and an enemy's trigger finger is a dice roll. There
is no `RND` verb in this dialect; issue #16 tracks adding one, and Chapter
17's breakout hand-rolls a linear congruential generator in BASIC as the other
route. Here the engine fills the field, which also keeps a headless run the
same game on every machine — the PRNG is the example's own, not libc's.
The `rnd` fields — one here per frame, one on each enemy per call — carry the
engine's PRNG into the script: write `SELF@.ROLL% < DT% * 1.5` and an enemy's
trigger finger is a dice roll.
The dialect does now have a native `RND` function — issue #16 closed, and
[Chapter 12](12-function-reference.md) documents it — so this is no longer the
*only* route; Chapter 17's breakout hand-rolls a linear congruential generator
in BASIC as a third. The engine keeps filling the field here on purpose,
because it buys something `RND` cannot: the numbers come from the example's own
PRNG rather than libc's, so a headless run is the same game on every machine,
which is what makes `example_galaga` a test and not just a demo.
**The BASIC name is `ROLL%`, not `RND%`.** A host field is a bare word and
shares a namespace with every verb and function, so once `RND` became a
function name a field could no longer be called that — the scanner refuses it
with *"Reserved word in variable name"*. The C member stays `rnd`; only the
name the script sees had to move. [Chapter 16](16-structures.md) has the same
rule for `TYPE` declarations.
## Step 4: Why bindings, and not arguments
@@ -351,7 +362,7 @@ DEF DECIDEFIRE(DT%)
DX% = GAME@.PLAYERX% - ACTOR@.X%
IF ABS(DX%) > 140 THEN RETURN 0
IF ACTOR@.Y% > GAME@.PLAYERY% THEN RETURN 0
IF SELF@.RND% < DT% * 1.5 THEN SELF@.FIRE# = 1
IF SELF@.ROLL% < DT% * 1.5 THEN SELF@.FIRE# = 1
RETURN 0
END
```
@@ -386,7 +397,7 @@ DEF UPDATEBEE(DT%)
IF (S# AND 2) > 0 THEN BEGIN
ACTOR@.X% = SELF@.HOMEX% + SIN(SELF@.T% * 1.7) * 16
ACTOR@.Y% = SELF@.HOMEY%
IF SELF@.RND% < DT% * 0.04 THEN SELF@.STATE# = 4 : SELF@.T% = 0
IF SELF@.ROLL% < DT% * 0.04 THEN SELF@.STATE# = 4 : SELF@.T% = 0
BEND
IF (S# AND 4) > 0 THEN BEGIN
R# = DIVESTEP(DT%, 130, 0.2)