Merge branch 'main' into 36
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m25s
akbasic CI Build / coverage (push) Successful in 4m18s
akbasic CI Build / sanitizers (push) Successful in 5m4s
akbasic CI Build / akgl_build (push) Successful in 8m17s
akbasic CI Build / mutation_test (push) Successful in 23m54s
All checks were successful
akbasic CI Build / cmake_build (push) Successful in 3m25s
akbasic CI Build / coverage (push) Successful in 4m18s
akbasic CI Build / sanitizers (push) Successful in 5m4s
akbasic CI Build / akgl_build (push) Successful in 8m17s
akbasic CI Build / mutation_test (push) Successful in 23m54s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -409,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
|
||||
@@ -456,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
|
||||
|
||||
@@ -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$ = ""
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user