Compare commits
26 Commits
fix/megade
...
57-impleme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cb68665d0 | ||
|
f7e8d4b82b
|
|||
|
11e4e96daf
|
|||
|
cdb9be03c6
|
|||
|
d047ef9c80
|
|||
|
9a22fde5ee
|
|||
|
809a9bf64d
|
|||
|
13f1df03ef
|
|||
|
a15e172c2a
|
|||
|
ae2c702c2b
|
|||
|
5c58f4c16d
|
|||
|
ed6f463897
|
|||
|
d5647758d0
|
|||
|
1a7226eef1
|
|||
|
4bfcf72240
|
|||
|
6ba8a59c5e
|
|||
|
bb2d896e4b
|
|||
|
3183908012
|
|||
|
ed99d38534
|
|||
|
36fa1285b4
|
|||
|
8ef0b253b2
|
|||
|
61e08812e3
|
|||
|
a0ac978541
|
|||
|
f9742ed5a5
|
|||
|
350deb3a45
|
|||
|
8a02674af5
|
@@ -17,7 +17,7 @@ jobs:
|
|||||||
# Not recursive, deliberately. The top-level build needs
|
# Not recursive, deliberately. The top-level build needs
|
||||||
# deps/libakerror and deps/libakstdlib, via add_subdirectory, and
|
# deps/libakerror and deps/libakstdlib, via add_subdirectory, and
|
||||||
# nothing else: the golden corpus and the Commodore font now live in
|
# nothing else: the golden corpus and the Commodore font now live in
|
||||||
# this repository (tests/reference/ and assets/fonts/), so
|
# this repository (tests/language/ and assets/fonts/), so
|
||||||
# deps/basicinterpret is no longer a build dependency at all.
|
# deps/basicinterpret is no longer a build dependency at all.
|
||||||
# It does *not* need deps/libakgl, which is guarded behind
|
# It does *not* need deps/libakgl, which is guarded behind
|
||||||
# AKBASIC_WITH_AKGL and defaults OFF -- and recursing into it would
|
# AKBASIC_WITH_AKGL and defaults OFF -- and recursing into it would
|
||||||
@@ -62,11 +62,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cmake -S . -B build
|
cmake -S . -B build
|
||||||
cmake --build build --parallel 2
|
cmake --build build --parallel 2
|
||||||
# The suite is 78 cases: 41 golden files byte-compared against the Go
|
# The suite is 112 cases: 65 language files with sibling expectations,
|
||||||
# reference's own corpus (checked in at tests/reference/, see its README),
|
# 43 unit tests, 3 embedding examples, and docs_examples.
|
||||||
# 9 local golden cases for verbs the reference never implemented, 25 unit
|
# Some unit tests assert the *correct* contract for known defects (TODO.md
|
||||||
# tests, 2 embedding examples, and 1 known-failing test that asserts the
|
|
||||||
# *correct* contract for defects carried over from the reference (TODO.md
|
|
||||||
# section 6). A green run therefore does not mean defect-free -- see
|
# section 6). A green run therefore does not mean defect-free -- see
|
||||||
# AKBASIC_KNOWN_FAILING_TESTS.
|
# AKBASIC_KNOWN_FAILING_TESTS.
|
||||||
#
|
#
|
||||||
|
|||||||
48
.gitea/workflows/mutation.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: akbasic Mutation Testing
|
||||||
|
run-name: ${{ gitea.actor }} akbasic mutation test
|
||||||
|
|
||||||
|
# Mutation testing is intentionally isolated from push and merge checks. The
|
||||||
|
# complete src/ tree takes hours and must not consume the shared CI capacity
|
||||||
|
# during normal development.
|
||||||
|
on:
|
||||||
|
# Gitea schedules use UTC. 07:00 UTC is 02:00 EST (the requested fixed
|
||||||
|
# Eastern Standard Time slot).
|
||||||
|
schedule:
|
||||||
|
- cron: '0 7 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
full_mutation:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 720
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- name: dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y cmake gcc python3 moreutils
|
||||||
|
- name: mutation testing (full tree)
|
||||||
|
run: |
|
||||||
|
python3 scripts/mutation_test.py \
|
||||||
|
--junit mutation-junit.xml \
|
||||||
|
--threshold 65
|
||||||
|
- name: publish mutation results
|
||||||
|
if: always()
|
||||||
|
uses: mikepenz/action-junit-report@v4
|
||||||
|
with:
|
||||||
|
report_paths: 'mutation-junit.xml'
|
||||||
|
annotate_only: true
|
||||||
|
detailed_summary: true
|
||||||
|
include_passed: true
|
||||||
|
fail_on_failure: 'false'
|
||||||
|
- name: upload mutation report
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: mutation-report
|
||||||
|
path: mutation-junit.xml
|
||||||
|
if-no-files-found: warn
|
||||||
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
@@ -1,24 +1,13 @@
|
|||||||
name: akbasic Release Build
|
name: akbasic Release Build
|
||||||
run-name: ${{ gitea.actor }} akbasic release checks
|
run-name: ${{ gitea.actor }} akbasic release checks
|
||||||
# Manual only. Nothing here runs on push: the full mutation set is thousands of
|
# Manual only. Nothing here runs on push: documentation is a release gate, not
|
||||||
# mutants and hours of runner time, which is a release-gate cost, not a
|
# a per-commit check. The expensive mutation suite has its own daily schedule.
|
||||||
# per-commit one. Trigger it from the Actions tab.
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
mutation_threshold:
|
|
||||||
description: "Fail if the mutation score falls below this percentage"
|
|
||||||
required: false
|
|
||||||
default: "65"
|
|
||||||
mutation_targets:
|
|
||||||
description: "Space-separated files to mutate; empty means the whole src/ tree"
|
|
||||||
required: false
|
|
||||||
default: ""
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Moved here from ci.yaml. The docs are wanted for a release, not on every
|
# Moved here from ci.yaml. The docs are wanted for a release, not on every
|
||||||
# push, and building them beside the release mutation run keeps the two
|
# push.
|
||||||
# artefacts a release needs in one place.
|
|
||||||
docs:
|
docs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -52,89 +41,3 @@ jobs:
|
|||||||
path: build/docs/html/
|
path: build/docs/html/
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
|
|
||||||
full_mutation:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# 3675 mutants across the whole src/ tree. Cost per mutant is one incremental
|
|
||||||
# rebuild plus one full ctest run, which measures at roughly 2s for a leaf
|
|
||||||
# file and 11s for src/value.c, where almost everything links against it.
|
|
||||||
# Budget most of a day and expect it to finish well inside that; the ceiling
|
|
||||||
# exists so a mutant that wedges the runner cannot hold it forever.
|
|
||||||
timeout-minutes: 720
|
|
||||||
steps:
|
|
||||||
- name: Check out repository code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
# The harness copies the repo and configures a build inside the copy,
|
|
||||||
# so it needs the same submodules the main build does. The golden
|
|
||||||
# corpus is part of what kills mutants and it is checked in now, so
|
|
||||||
# that is libakerror and libakstdlib and nothing else.
|
|
||||||
submodules: true
|
|
||||||
# moreutils for errno(1), which deps/libakerror's scripts/generrno.sh needs
|
|
||||||
# to generate its errno table. Without it the generated table is empty and
|
|
||||||
# every AKERR_* code collapses onto a low integer -- see the long note on
|
|
||||||
# ci.yaml's cmake_build job for what that breaks and how it presents.
|
|
||||||
- name: dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y cmake gcc python3 moreutils
|
|
||||||
# The whole akbasic-owned src/ tree. ci.yaml runs a two-file subset on every
|
|
||||||
# push; this is the one that actually covers the interpreter.
|
|
||||||
#
|
|
||||||
# Mutation testing is the only gate that sees the error-handling control
|
|
||||||
# flow at all: libakerror's ATTEMPT/CATCH/PASS macros expand at their call
|
|
||||||
# sites, so gcov attributes them to the caller and line coverage cannot
|
|
||||||
# measure them.
|
|
||||||
#
|
|
||||||
# The default threshold matches ci.yaml's rather than being stricter.
|
|
||||||
# Whole-tree coverage is uneven -- src/symtab.c is the only file measured
|
|
||||||
# on the push path, at 74.1%, and the rest is unmeasured, so a tighter
|
|
||||||
# number here would be a guess. Raise it with the workflow input once a
|
|
||||||
# full run has established a real baseline.
|
|
||||||
#
|
|
||||||
# The two inputs arrive through the environment rather than being
|
|
||||||
# interpolated straight into the script. ${{ }} substitution happens before
|
|
||||||
# the shell sees the line, so a value containing shell metacharacters would
|
|
||||||
# otherwise run as code. This workflow is manual and owner-triggered, but
|
|
||||||
# the safe form costs nothing.
|
|
||||||
- name: mutation testing (full tree)
|
|
||||||
env:
|
|
||||||
MUTATION_TARGETS: ${{ gitea.event.inputs.mutation_targets }}
|
|
||||||
MUTATION_THRESHOLD: ${{ gitea.event.inputs.mutation_threshold }}
|
|
||||||
run: |
|
|
||||||
set -eu
|
|
||||||
# Word-splitting is the point here: the input is a space-separated
|
|
||||||
# list. An empty input leaves $targets empty and the harness falls
|
|
||||||
# through to its own default, which is the whole src/ tree.
|
|
||||||
targets=""
|
|
||||||
for f in ${MUTATION_TARGETS:-}; do
|
|
||||||
targets="$targets --target $f"
|
|
||||||
done
|
|
||||||
# shellcheck disable=SC2086
|
|
||||||
python3 scripts/mutation_test.py \
|
|
||||||
$targets \
|
|
||||||
--junit mutation-junit.xml \
|
|
||||||
--threshold "${MUTATION_THRESHOLD:-65}"
|
|
||||||
# Publish even when the threshold gate fails, so survivors are visible --
|
|
||||||
# each one is a missing test. Display-only (fail_on_failure: false); the
|
|
||||||
# --threshold above is the gate. annotate_only avoids the Checks API 404
|
|
||||||
# on Gitea (mikepenz/action-junit-report#23).
|
|
||||||
- name: publish mutation results
|
|
||||||
if: always()
|
|
||||||
uses: mikepenz/action-junit-report@v4
|
|
||||||
with:
|
|
||||||
report_paths: 'mutation-junit.xml'
|
|
||||||
annotate_only: true
|
|
||||||
detailed_summary: true
|
|
||||||
include_passed: true
|
|
||||||
fail_on_failure: 'false'
|
|
||||||
# Keep the raw report as well as the annotations: a release wants the
|
|
||||||
# survivor list on file, and the job summary is not durable.
|
|
||||||
- name: upload mutation report
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: mutation-report
|
|
||||||
path: mutation-junit.xml
|
|
||||||
if-no-files-found: warn
|
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ scripting engine for game authors.
|
|||||||
| [the issue tracker](https://source.starfort.tech/andrew/akbasic/issues) | **Outstanding defects and gaps.** Labelled by kind and blast radius; `status::grooming` means the scope is not settled yet |
|
| [the issue tracker](https://source.starfort.tech/andrew/akbasic/issues) | **Outstanding defects and gaps.** Labelled by kind and blast radius; `status::grooming` means the scope is not settled yet |
|
||||||
| [`TODO.md`](TODO.md) | The record: settled design decisions, the deviation register, defects already fixed, and the reasoning behind the measurements. §0.1 first — it retires the byte-for-byte fidelity constraint several later sections were written on |
|
| [`TODO.md`](TODO.md) | The record: settled design decisions, the deviation register, defects already fixed, and the reasoning behind the measurements. §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 |
|
| [`README.md`](README.md) | What the project is and why, for somebody who has not seen it |
|
||||||
| [`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/` |
|
| [`docs/`](docs/README.md) | The language itself: twenty-one 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/`; [Chapters 20](docs/20-tutorial-galaga.md) and [21](docs/21-tutorial-galaga-enemies.md) build the embedding host in `examples/galaga/` |
|
||||||
| `deps/libakerror/AGENTS.md` | The `ATTEMPT`/`CLEANUP`/`PROCESS`/`HANDLE`/`FINISH` protocol, authoritatively |
|
| `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/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** |
|
| `deps/<library>/AGENTS.md` | Per-repo rules. Read the relevant one **before editing a submodule** |
|
||||||
@@ -57,9 +57,9 @@ repeating where you will see them:
|
|||||||
`include/akgl/SDL_GameControllerDB.h`. Change the template or the generator script.
|
`include/akgl/SDL_GameControllerDB.h`. Change the template or the generator script.
|
||||||
- **Do not reformat code you are not otherwise changing.** Several files mix tabs and spaces
|
- **Do not reformat code you are not otherwise changing.** Several files mix tabs and spaces
|
||||||
and there is no repo-wide formatter; style conversions get their own commit.
|
and there is no repo-wide formatter; style conversions get their own commit.
|
||||||
- **Do not edit `tests/reference/`.** Those expectations came from the Go implementation and
|
- **Keep `tests/language/` editable.** Its `.bas` programs and sibling `.txt` expectations
|
||||||
are never edited to suit this interpreter. A deliberate divergence goes in
|
are changed together when behavior changes. Record deliberate language decisions in
|
||||||
`tests/reference/README.md`'s divergence table and `docs/13-differences.md`.
|
`TODO.md` or `docs/13-differences.md`.
|
||||||
- **Open an issue for outstanding work; do not add it to `TODO.md`.**
|
- **Open an issue for outstanding work; do not add it to `TODO.md`.**
|
||||||
<https://source.starfort.tech/andrew/akbasic/issues>, or `tea issues create --repo
|
<https://source.starfort.tech/andrew/akbasic/issues>, or `tea issues create --repo
|
||||||
andrew/akbasic`. Name the file and line, the functional consequence, and what closing it would
|
andrew/akbasic`. Name the file and line, the functional consequence, and what closing it would
|
||||||
|
|||||||
152
CMakeLists.txt
@@ -51,12 +51,14 @@ option(AKBASIC_SANITIZE "Build with ASan + UBSan" OFF
|
|||||||
# through: the dependencies set target and directory properties that their own
|
# through: the dependencies set target and directory properties that their own
|
||||||
# builds depend on.
|
# builds depend on.
|
||||||
#
|
#
|
||||||
# libakerror additionally namespaces its `mutation` target when embedded but not
|
# All three dependencies now namespace both their `coverage` and their
|
||||||
# its `coverage` target (deps/libakerror/CMakeLists.txt:194 vs :172), so a
|
# `mutation` targets when embedded, so there is no custom-target collision left
|
||||||
# coverage build collides on the `coverage` target and fails to configure at all.
|
# to work around. libakerror was the last holdout -- it namespaced `mutation`
|
||||||
# Rename the dependency's on the way past. Remove this once libakerror applies
|
# but not `coverage`, and a coverage build collided on the bare name and failed
|
||||||
# the same CMAKE_SOURCE_DIR test to `coverage` that it already applies to
|
# to configure at all. 2.0.2 applies the same CMAKE_SOURCE_DIR test to both
|
||||||
# `mutation` -- filed as libakerror issue #15.
|
# (deps/libakerror/CMakeLists.txt:429-434), closing libakerror issue #15, and
|
||||||
|
# the add_custom_target() shadow that renamed it on the way past is gone with
|
||||||
|
# this comment.
|
||||||
#
|
#
|
||||||
# **Only one project in a tree may shadow add_test(), and this is that project.**
|
# **Only one project in a tree may shadow add_test(), and this is that project.**
|
||||||
# CMake exposes an overridden command as `_name` and chains exactly one level: a
|
# CMake exposes an overridden command as `_name` and chains exactly one level: a
|
||||||
@@ -86,14 +88,6 @@ function(set_property _scope)
|
|||||||
endif()
|
endif()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
function(add_custom_target _name)
|
|
||||||
if(AKBASIC_SUPPRESS_ADD_TEST AND _name STREQUAL "coverage")
|
|
||||||
_add_custom_target(akerror_coverage ${ARGN})
|
|
||||||
else()
|
|
||||||
_add_custom_target(${ARGV})
|
|
||||||
endif()
|
|
||||||
endfunction()
|
|
||||||
|
|
||||||
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
||||||
add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL)
|
add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL)
|
||||||
if(AKBASIC_WITH_AKGL)
|
if(AKBASIC_WITH_AKGL)
|
||||||
@@ -149,6 +143,7 @@ set(AKBASIC_SOURCES
|
|||||||
src/runtime_disk.c
|
src/runtime_disk.c
|
||||||
src/runtime_format.c
|
src/runtime_format.c
|
||||||
src/runtime_functions.c
|
src/runtime_functions.c
|
||||||
|
src/runtime_generator.c
|
||||||
src/runtime_graphics.c
|
src/runtime_graphics.c
|
||||||
src/runtime_housekeeping.c
|
src/runtime_housekeeping.c
|
||||||
src/runtime_machine.c
|
src/runtime_machine.c
|
||||||
@@ -268,6 +263,69 @@ if(AKBASIC_BUILD_EXAMPLES)
|
|||||||
endforeach()
|
endforeach()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# The galaga example: a C game on libakgl with the interpreter embedded as its
|
||||||
|
# enemy-behavior engine. Chapters 20 and 21 build it from an empty file, so it
|
||||||
|
# is compiled and run by every AKGL build rather than rotting in a document.
|
||||||
|
# The asset, script and font paths are baked in so the smoke test can launch
|
||||||
|
# from any working directory; --assets and --script override them at runtime.
|
||||||
|
if(AKBASIC_BUILD_EXAMPLES AND AKBASIC_WITH_AKGL)
|
||||||
|
add_executable(akbasic_example_galaga
|
||||||
|
examples/galaga/main.c
|
||||||
|
examples/galaga/script.c
|
||||||
|
examples/galaga/enemies.c
|
||||||
|
examples/galaga/player.c)
|
||||||
|
target_compile_options(akbasic_example_galaga PRIVATE -Wall -Wextra)
|
||||||
|
target_compile_definitions(akbasic_example_galaga PRIVATE
|
||||||
|
GALAGA_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/assets"
|
||||||
|
GALAGA_SCRIPT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/galaga.bas"
|
||||||
|
GALAGA_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/C64_Pro_Mono-STYLE.ttf")
|
||||||
|
target_link_libraries(akbasic_example_galaga PRIVATE akbasic akgl
|
||||||
|
SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image)
|
||||||
|
akbasic_instrument(akbasic_example_galaga)
|
||||||
|
# Ten seconds of scripted play under the headless drivers: the script boots,
|
||||||
|
# a wave enters and forms, the autoplay pilot shoots at it, and the program
|
||||||
|
# tears down and exits 0. A tutorial that stops working fails here rather
|
||||||
|
# than in front of a reader.
|
||||||
|
_add_test(NAME example_galaga COMMAND akbasic_example_galaga --frames 600 --autoplay)
|
||||||
|
_set_tests_properties(example_galaga PROPERTIES TIMEOUT 120
|
||||||
|
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
|
||||||
|
|
||||||
|
# The boundary's round-trip test: links the real script.c and the real
|
||||||
|
# galaga.bas, and fails the moment the two sides of the interop disagree.
|
||||||
|
add_executable(akbasic_example_galaga_interop
|
||||||
|
examples/galaga/interop_test.c
|
||||||
|
examples/galaga/script.c)
|
||||||
|
target_compile_options(akbasic_example_galaga_interop PRIVATE -Wall -Wextra)
|
||||||
|
target_compile_definitions(akbasic_example_galaga_interop PRIVATE
|
||||||
|
GALAGA_SCRIPT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/examples/galaga/galaga.bas")
|
||||||
|
target_link_libraries(akbasic_example_galaga_interop PRIVATE akbasic akgl
|
||||||
|
SDL3::SDL3 m)
|
||||||
|
akbasic_instrument(akbasic_example_galaga_interop)
|
||||||
|
_add_test(NAME example_galaga_interop COMMAND akbasic_example_galaga_interop)
|
||||||
|
_set_tests_properties(example_galaga_interop PROPERTIES TIMEOUT 120)
|
||||||
|
|
||||||
|
# Regenerating the game figures in docs/ is a deliberate act, never part of
|
||||||
|
# a build, for the same reason docs_screenshots is: the PNGs are checked in.
|
||||||
|
# Wall-clock dt makes each regeneration differ by a few pixels of starfield,
|
||||||
|
# so expect a binary diff every time this runs; commit one only when the
|
||||||
|
# content changed on purpose. (docs_galaga_figures, not docs_game_figures:
|
||||||
|
# the libakgl submodule already owns that target name.)
|
||||||
|
add_custom_target(docs_galaga_figures
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy
|
||||||
|
SDL_RENDER_DRIVER=software
|
||||||
|
$<TARGET_FILE:akbasic_example_galaga> --frames 40
|
||||||
|
--screenshot "${CMAKE_CURRENT_SOURCE_DIR}/docs/images/galaga-title.png"
|
||||||
|
--screenshot-frame 30
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy
|
||||||
|
SDL_RENDER_DRIVER=software
|
||||||
|
$<TARGET_FILE:akbasic_example_galaga> --autoplay --frames 370
|
||||||
|
--screenshot "${CMAKE_CURRENT_SOURCE_DIR}/docs/images/galaga-wave.png"
|
||||||
|
--screenshot-frame 360
|
||||||
|
DEPENDS akbasic_example_galaga
|
||||||
|
COMMENT "Regenerating the galaga figures in docs/images"
|
||||||
|
VERBATIM)
|
||||||
|
endif()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Tests.
|
# Tests.
|
||||||
#
|
#
|
||||||
@@ -295,6 +353,7 @@ set(AKBASIC_TESTS
|
|||||||
error_codes
|
error_codes
|
||||||
for_next
|
for_next
|
||||||
format_verbs
|
format_verbs
|
||||||
|
generators
|
||||||
grammar_leaves
|
grammar_leaves
|
||||||
graphics_verbs
|
graphics_verbs
|
||||||
hoststruct
|
hoststruct
|
||||||
@@ -448,7 +507,7 @@ if(AKBASIC_WITH_AKGL)
|
|||||||
# against.
|
# against.
|
||||||
#
|
#
|
||||||
# **A byte comparison of a rendered PNG is a deliberate bet**, the same bet
|
# **A byte comparison of a rendered PNG is a deliberate bet**, the same bet
|
||||||
# tests/reference/ already makes about golden output: that the dummy video
|
# the language corpus makes about golden output: that the dummy video
|
||||||
# driver and the software renderer are reproducible. They are, run to run and
|
# driver and the software renderer are reproducible. They are, run to run and
|
||||||
# build to build. What is untested is an SDL upgrade that shifts one pixel of
|
# build to build. What is untested is an SDL upgrade that shifts one pixel of
|
||||||
# a diagonal, and the answer to that is to regenerate the figures in the same
|
# a diagonal, and the answer to that is to regenerate the figures in the same
|
||||||
@@ -531,63 +590,20 @@ if(AKBASIC_WILL_FAIL_TESTS OR AKBASIC_KNOWN_FAILING_TESTS)
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# The reference's own corpus, byte-compared against the sibling .txt. One CTest
|
# The editable language corpus. One CTest case per .bas so a failure names the
|
||||||
# case per .bas so a failure names the file.
|
# file. Each program is paired with a sibling .txt expectation; see
|
||||||
|
# tests/language/README.md for the editing rule.
|
||||||
#
|
#
|
||||||
# It used to be driven in place out of deps/basicinterpret, on the reasoning that
|
# The corpus includes programs carried over from the deprecated Go implementation
|
||||||
# copying a submodule's corpus guarantees drift. That reasoning was sound and it
|
# and cases written for this interpreter. Their provenance is useful when
|
||||||
# has been overruled deliberately: the Go dependency is being deprecated, and a
|
# investigating a regression, but it does not make any case immutable.
|
||||||
# build that cannot run its acceptance suite without cloning the implementation
|
|
||||||
# it replaced is not finished. The copy is byte-identical to
|
|
||||||
# basicinterpreter@d76162c and tests/reference/README.md records
|
|
||||||
# where it came from and what the drift now costs.
|
|
||||||
file(GLOB_RECURSE AKBASIC_GOLDEN_CASES
|
|
||||||
RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}/tests/reference"
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/reference/*.bas"
|
|
||||||
)
|
|
||||||
|
|
||||||
foreach(_case IN LISTS AKBASIC_GOLDEN_CASES)
|
|
||||||
string(REGEX REPLACE "^tests/" "" _name "${_case}")
|
|
||||||
string(REGEX REPLACE "\\.bas$" "" _name "${_name}")
|
|
||||||
string(REPLACE "/" "_" _name "${_name}")
|
|
||||||
_add_test(
|
|
||||||
NAME golden_${_name}
|
|
||||||
COMMAND ${CMAKE_COMMAND}
|
|
||||||
-DBASIC=$<TARGET_FILE:basic>
|
|
||||||
-DCASE=${CMAKE_CURRENT_SOURCE_DIR}/tests/reference/${_case}
|
|
||||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/golden.cmake
|
|
||||||
)
|
|
||||||
endforeach()
|
|
||||||
|
|
||||||
if(AKBASIC_GOLDEN_CASES)
|
|
||||||
set(AKBASIC_GOLDEN_NAMES)
|
|
||||||
foreach(_case IN LISTS AKBASIC_GOLDEN_CASES)
|
|
||||||
string(REGEX REPLACE "^tests/" "" _name "${_case}")
|
|
||||||
string(REGEX REPLACE "\\.bas$" "" _name "${_name}")
|
|
||||||
string(REPLACE "/" "_" _name "${_name}")
|
|
||||||
list(APPEND AKBASIC_GOLDEN_NAMES golden_${_name})
|
|
||||||
endforeach()
|
|
||||||
_set_tests_properties(${AKBASIC_GOLDEN_NAMES} PROPERTIES TIMEOUT 30)
|
|
||||||
# An AKGL build of `basic` opens a window, and forty-one of them is not what
|
|
||||||
# anybody running the suite wanted. The dummy driver produces the same stdout,
|
|
||||||
# which is the only thing a golden case compares.
|
|
||||||
if(AKBASIC_WITH_AKGL)
|
|
||||||
_set_tests_properties(${AKBASIC_GOLDEN_NAMES} PROPERTIES
|
|
||||||
ENVIRONMENT "SDL_VIDEODRIVER=dummy;SDL_AUDIODRIVER=dummy;SDL_RENDER_DRIVER=software")
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# The local golden corpus, for verbs the reference never implemented.
|
|
||||||
#
|
#
|
||||||
# Still separate now that the reference's corpus lives in this repository too,
|
# Programs carried over from the deprecated Go implementation and cases written
|
||||||
# and the reason changed rather than went away: tests/reference/ is a *record* of
|
# for this interpreter use the same editable `.bas`/`.txt` contract. Registered
|
||||||
# what the Go implementation did and nothing in it should ever be edited to suit
|
# under local_ so every failure identifies the program that produced it.
|
||||||
# this one, while tests/language/ is ours to change. Registered under local_ so a
|
|
||||||
# failure says at a glance which of the two it came from -- and so a diff that
|
|
||||||
# touches tests/reference/ stands out as the thing it is.
|
|
||||||
#
|
#
|
||||||
# Note what this can and cannot cover. The graphics and sound verbs draw and play
|
# Note what this can and cannot cover. The graphics and sound verbs draw and play
|
||||||
# rather than print, so what a golden file sees of them is their *refusals* and
|
# rather than print, so what a golden file sees of them is their refusals and
|
||||||
# whatever a program can PRINT about the state they changed. The behaviour that
|
# whatever a program can PRINT about the state they changed. The behaviour that
|
||||||
# reaches a device is asserted against tests/mockdevice.h instead.
|
# reaches a device is asserted against tests/mockdevice.h instead.
|
||||||
file(GLOB_RECURSE AKBASIC_LOCAL_CASES
|
file(GLOB_RECURSE AKBASIC_LOCAL_CASES
|
||||||
|
|||||||
1
Doxyfile
@@ -12,3 +12,4 @@ WARN_AS_ERROR = FAIL_ON_WARNINGS
|
|||||||
GENERATE_HTML = YES
|
GENERATE_HTML = YES
|
||||||
GENERATE_LATEX = NO
|
GENERATE_LATEX = NO
|
||||||
QUIET = YES
|
QUIET = YES
|
||||||
|
DOT_GRAPH_MAX_NODES = 100
|
||||||
|
|||||||
128
MAINTENANCE.md
@@ -68,12 +68,19 @@ the `akgl_*` or `aksl_*` entry point should look like, and what tests would cove
|
|||||||
decision, which is true of *changing* it and not of *reporting* it. Follow the prose-paragraph style of the entries already
|
decision, which is true of *changing* it and not of *reporting* it. Follow the prose-paragraph style of the entries already
|
||||||
there. Growing the dependency to serve the interpreter is a wanted outcome, not a detour.
|
there. Growing the dependency to serve the interpreter is a wanted outcome, not a detour.
|
||||||
|
|
||||||
It works. Four gaps were filed this way — text measurement, immediate-mode drawing, audio,
|
It works. **Five** gaps were filed this way and all five landed upstream: text measurement,
|
||||||
and a non-blocking keystroke read — and all four landed upstream as `akgl_text_measure`, the
|
immediate-mode drawing, audio and a non-blocking keystroke read became `akgl_text_measure`,
|
||||||
`akgl_draw_*` family, `akgl_audio_*` and `akgl_controller_poll_key`. `FILTER` is the one verb
|
the `akgl_draw_*` family, `akgl_audio_*` and `akgl_controller_poll_key`; and the
|
||||||
still blocked on a gap, and `DIRECTORY` is refused pending an `opendir`/`readdir` wrapper in
|
directory-reading wrapper `DIRECTORY` was waiting on became `aksl_opendir`, `aksl_readdir`,
|
||||||
`libakstdlib`. Both refuse at execution and say so, rather than being silently ignored: a
|
`aksl_closedir` and `aksl_rewinddir` — `libakstdlib` issue #10, in the revision this tree
|
||||||
program that asks for a low-pass filter and gets an unfiltered square wave has been lied to.
|
pins.
|
||||||
|
|
||||||
|
That leaves the two refusals in different positions, and the difference is worth keeping
|
||||||
|
straight. **`FILTER` is the one verb still blocked on a gap** — there is no filter stage in
|
||||||
|
`akgl_audio_*` to configure. **`DIRECTORY` is no longer blocked on anything**; it is simply
|
||||||
|
unwritten, and its refusal says so rather than naming a wrapper that now exists. Both refuse
|
||||||
|
at execution and say so, rather than being silently ignored: a program that asks for a
|
||||||
|
low-pass filter and gets an unfiltered square wave has been lied to.
|
||||||
|
|
||||||
### The Go reference
|
### The Go reference
|
||||||
|
|
||||||
@@ -88,7 +95,7 @@ source stays readable as documentation of what the original did. Neither is bind
|
|||||||
|
|
||||||
**It is not a build or test dependency.** Both configurations have been configured, built and
|
**It is not a build or test dependency.** Both configurations have been configured, built and
|
||||||
run from scratch with it moved out of the tree. Its acceptance corpus is checked in at
|
run from scratch with it moved out of the tree. Its acceptance corpus is checked in at
|
||||||
`tests/reference/` and its Commodore font at `assets/fonts/`.
|
`tests/language/` and its Commodore font at `assets/fonts/`.
|
||||||
|
|
||||||
```sh norun
|
```sh norun
|
||||||
cd deps/basicinterpret
|
cd deps/basicinterpret
|
||||||
@@ -156,10 +163,10 @@ guards every dependency with `if(NOT TARGET ...)`, so a top-level build must def
|
|||||||
`akerror::akerror` and `akstdlib::akstdlib` from `deps/libakerror` and `deps/libakstdlib`
|
`akerror::akerror` and `akstdlib::akstdlib` from `deps/libakerror` and `deps/libakstdlib`
|
||||||
**before** `add_subdirectory(deps/libakgl)`, or the targets are declared twice.
|
**before** `add_subdirectory(deps/libakgl)`, or the targets are declared twice.
|
||||||
|
|
||||||
That order is load-bearing for a second reason: `deps/libakerror` is at **2.0.1**, whose 2.0.0
|
That order is load-bearing for a second reason: `deps/libakerror` is at **2.0.3**, whose 2.0.0
|
||||||
was a source and ABI break carrying an soname (`libakerror.so.2`). `libakstdlib` and `libakgl`
|
was a source and ABI break carrying an soname (`libakerror.so.2`). `libakstdlib` and `libakgl`
|
||||||
must be compiled against that header, not a 1.x one, and an installed `libakerror.so.1` must
|
must be compiled against that header, not a 1.x one, and an installed `libakerror.so.1` must
|
||||||
not be picked up. The break is quiet if you get it wrong: `__akerr_last_ignored` became
|
not be picked up. The break is quiet if you get it wrong: the context behind `IGNORE` became
|
||||||
thread-local and `akerr_next_error()` now returns a context that already holds a reference, so
|
thread-local and `akerr_next_error()` now returns a context that already holds a reference, so
|
||||||
a mixed build leaks pool slots or frees one twice rather than failing to link.
|
a mixed build leaks pool slots or frees one twice rather than failing to link.
|
||||||
|
|
||||||
@@ -167,10 +174,23 @@ a mixed build leaks pool slots or frees one twice rather than failing to link.
|
|||||||
|
|
||||||
| Submodule | Version | soname | ABI rule | Version API |
|
| Submodule | Version | soname | ABI rule | Version API |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `deps/libakerror` | 2.0.1 | `libakerror.so.2` | major only | **none** — no version macro; `include/akbasic/error.h` feature-tests `AKERR_THREAD_SAFE` and `AKERR_EXIT_STATUS_UNREPRESENTABLE` instead |
|
| `deps/libakerror` | 2.0.3 | `libakerror.so.2` | major only | **none** — no version macro; `include/akbasic/error.h` feature-tests `AKERR_THREAD_SAFE` and `AKERR_EXIT_STATUS_UNREPRESENTABLE` instead |
|
||||||
| `deps/libakstdlib` | 0.2.0 | `libakstdlib.so.0.2` | **`MAJOR.MINOR` while major is 0** | `AKSL_VERSION_*`, `aksl_version()`, `AKSL_VERSION_CHECK()` |
|
| `deps/libakstdlib` | 0.2.0 | `libakstdlib.so.0.2` | **`MAJOR.MINOR` while major is 0** | `AKSL_VERSION_*`, `aksl_version()`, `AKSL_VERSION_CHECK()` |
|
||||||
| `deps/libakgl` | 0.9.0 | `libakgl.so.0.9` | **`MAJOR.MINOR` while major is 0** | `AKGL_VERSION*`, `akgl_version()`, `AKGL_VERSION_AT_LEAST()` |
|
| `deps/libakgl` | 0.9.0 | `libakgl.so.0.9` | **`MAJOR.MINOR` while major is 0** | `AKGL_VERSION*`, `akgl_version()`, `AKGL_VERSION_AT_LEAST()` |
|
||||||
|
|
||||||
|
`project(akerror VERSION ...)` now stamps 2.0.3, matching that library's own "Release 2.0.3"
|
||||||
|
release notes. It previously disagreed — the soname and `akerrorConfigVersion.cmake` still
|
||||||
|
said 2.0.2 while the notes described 2.0.3 — but that was `libakerror` issue #38, which is
|
||||||
|
closed; the table and the notes agree again.
|
||||||
|
|
||||||
|
`IGNORE` still takes a *copy* of the ignored context so the pool slot can be released, which
|
||||||
|
fixes a real leak, but the copy no longer costs TLS per translation unit. It was a file-scope
|
||||||
|
`static` in the public header — one copy per TU, 37,296 bytes each, 1.35 MiB of thread-local
|
||||||
|
storage in `build/basic` alone, plus 84 `-Wunused-variable` warnings for every TU that never
|
||||||
|
called `IGNORE`. `libakerror` issue #37 moved it to an `extern` declaration in the header with
|
||||||
|
a single definition in `src/error.c`, which restores one copy per thread and silences the
|
||||||
|
warning. Issue #37 is closed and this subsection is gone accordingly.
|
||||||
|
|
||||||
For both 0.x libraries the soname carries `MAJOR.MINOR` deliberately: 0.1 and 0.2 are
|
For both 0.x libraries the soname carries `MAJOR.MINOR` deliberately: 0.1 and 0.2 are
|
||||||
*different* ABIs, and both become major-only at 1.0. Do not read `0.1 → 0.2` as a compatible
|
*different* ABIs, and both become major-only at 1.0. Do not read `0.1 → 0.2` as a compatible
|
||||||
bump — both libraries have actually made that jump, so anything built against the 0.1 headers
|
bump — both libraries have actually made that jump, so anything built against the 0.1 headers
|
||||||
@@ -196,16 +216,32 @@ precedes the build tree on the include path and a stray copy there would shadow
|
|||||||
one and pin every consumer. It publishes `AKGL_VERSION_AT_LEAST(major, minor, patch)` — the
|
one and pin every consumer. It publishes `AKGL_VERSION_AT_LEAST(major, minor, patch)` — the
|
||||||
compile-time test `libakstdlib` could not write against `libakerror` — and `akgl_version()`.
|
compile-time test `libakstdlib` could not write against `libakerror` — and `akgl_version()`.
|
||||||
|
|
||||||
**Version-pinning in `find_package` is asymmetric, and that is deliberate.**
|
**Version-pinning in `find_package` used to be asymmetric. It no longer is.**
|
||||||
`find_package(akstdlib 0.1)` and `find_package(akgl 0.1)` both work; each ships a
|
`find_package(akstdlib 0.1)` and `find_package(akgl 0.1)` both work; each ships a
|
||||||
`ConfigVersion.cmake` at `SameMinorVersion`, mirroring its soname. `find_package(akerror 1.0)`
|
`ConfigVersion.cmake` at `SameMinorVersion`, mirroring its soname. `libakerror` shipped
|
||||||
**fails against a correct install**, because `libakerror` ships `akerrorConfig.cmake` and
|
`akerrorConfig.cmake` and `akerrorTargets.cmake` but no `akerrorConfigVersion.cmake`, so any
|
||||||
`akerrorTargets.cmake` but no `akerrorConfigVersion.cmake`. Ask for `akerror` unversioned. Its
|
versioned request failed against a correct install and the advice here was to ask for
|
||||||
floor is enforced instead by an `#error` feature-testing `AKERR_FIRST_CONSUMER_STATUS`, which
|
`akerror` unversioned. That was `libakerror` issue #16 — closed — and `libakstdlib` issue #5,
|
||||||
`akstdlib.h`, `akgl/error.h` and our own `include/akbasic/error.h` all carry — include any of
|
which tracks the same fix from the other side and is still open only because nobody has shut
|
||||||
them and you inherit the guard. The missing version file is filed in
|
it. It has landed: `libakerror` now writes `akerrorConfigVersion.cmake` at
|
||||||
`libakstdlib` issue #5 and `libakerror` issue #16; when it lands, add the `1.0` floor to the `find_dependency`
|
**`SameMajorVersion`**, matching the soname's major-only rule, rather than the
|
||||||
calls.
|
`SameMinorVersion` the other two use to match theirs.
|
||||||
|
|
||||||
|
Two things follow, and the second is the one that bites:
|
||||||
|
|
||||||
|
- **A versioned request now works** — but the floor to ask for is **`2.0`**, not the `1.0`
|
||||||
|
this file used to say. `find_package(akerror 1.0)` fails *harder* than before: it is a
|
||||||
|
request for major 1 against a major-2 install, which `SameMajorVersion` correctly rejects.
|
||||||
|
- **There is nothing in this repository to change.** `akbasic` reaches all three dependencies
|
||||||
|
by `add_subdirectory`, not `find_package`, and ships no CMake package config of its own —
|
||||||
|
so it has no `find_dependency` calls to add a floor to. The instruction that used to live
|
||||||
|
here was written for a consumer this project never became. It matters to anyone *installing*
|
||||||
|
these libraries and linking `akbasic` against the installed copies, which is why it is
|
||||||
|
recorded rather than deleted.
|
||||||
|
|
||||||
|
The compile-time floor is unchanged and still the real guard: an `#error` feature-testing
|
||||||
|
`AKERR_FIRST_CONSUMER_STATUS`, which `akstdlib.h`, `akgl/error.h` and our own
|
||||||
|
`include/akbasic/error.h` all carry — include any of them and you inherit it.
|
||||||
|
|
||||||
### Embedding all three dependencies collides four ways
|
### Embedding all three dependencies collides four ways
|
||||||
|
|
||||||
@@ -250,14 +286,19 @@ CTest names. `libakstdlib` still uses bare `test_<name>` targets. **Name every t
|
|||||||
this repo `akbasic_test_<name>`** — it costs nothing and it is the collision that actually
|
this repo `akbasic_test_<name>`** — it costs nothing and it is the collision that actually
|
||||||
stopped a build.
|
stopped a build.
|
||||||
|
|
||||||
**3. Duplicate custom targets.** `libakerror` namespaces its `mutation` target when embedded
|
**3. Duplicate custom targets — fixed upstream, and the workaround is gone.** `libakerror`
|
||||||
but **not** its `coverage` target, so any coverage-enabled top-level build fails with *"another
|
used to namespace its `mutation` target when embedded but **not** its `coverage` target, so
|
||||||
target with the same name already exists"*. We shadow `add_custom_target` and rename that one
|
any coverage-enabled top-level build failed with *"another target with the same name already
|
||||||
to `akerror_coverage` on the way past. `libakstdlib` (both targets) and `libakgl` (its
|
exists"*. This project shadowed `add_custom_target` and renamed that one to
|
||||||
`mutation` target) namespace themselves correctly. **The real fix is upstream in
|
`akerror_coverage` on the way past, and recorded the real fix as `libakerror` issue #15: the
|
||||||
`libakerror`** — the same `CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test it already
|
same `CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR` test it already applied to
|
||||||
applies to `mutation` — and it is filed as `libakerror` issue #15. Delete the
|
`mutation`.
|
||||||
workaround when it lands.
|
|
||||||
|
That landed in 2.0.2. `libakerror` now picks `akerror_coverage` itself when embedded, so the
|
||||||
|
shadow was dead code that could only ever fire on a name the dependency had stopped using —
|
||||||
|
and it is deleted. All three dependencies namespace both targets correctly now, so **there is
|
||||||
|
no custom-target collision left**; only collisions 1, 2 and 4 below are live. The heading says
|
||||||
|
four because four is what a reader coming from the issue tracker will be looking for.
|
||||||
|
|
||||||
**4. Stale build trees poison the coverage report.** See below; it is the reason for
|
**4. Stale build trees poison the coverage report.** See below; it is the reason for
|
||||||
`cmake -S . -B build`.
|
`cmake -S . -B build`.
|
||||||
@@ -351,15 +392,10 @@ name. That is not cosmetic: `add_executable` creates a dependency's targets even
|
|||||||
|
|
||||||
### The golden corpora
|
### The golden corpora
|
||||||
|
|
||||||
`tests/reference/` is the Go implementation's own acceptance suite, byte-compared.
|
`tests/language/` is the editable language corpus. It includes cases carried over from the
|
||||||
**Nothing in it is ever edited to suit this interpreter.** If a case fails, either this
|
deprecated Go implementation as well as cases written for this interpreter. Every `.bas` file
|
||||||
interpreter is wrong or the divergence is deliberate — and a deliberate one goes in
|
has a sibling `.txt` expectation, and a new language feature needs that pair as well as unit
|
||||||
`tests/reference/README.md`'s divergence table and `docs/13-differences.md`, not into the
|
tests. Change both deliberately in the same commit; provenance does not make a case immutable.
|
||||||
expectation file. `tests/reference/README.md`
|
|
||||||
says the same thing at more length.
|
|
||||||
|
|
||||||
`tests/language/` is ours and may be changed freely. A new language feature needs a
|
|
||||||
`.bas`/`.txt` pair there as well as unit tests.
|
|
||||||
|
|
||||||
### Mutation-check a fix before you believe it
|
### Mutation-check a fix before you believe it
|
||||||
|
|
||||||
@@ -662,6 +698,26 @@ as a commit co-author, and this repository follows the same rule.
|
|||||||
Each dependency carries its own `AGENTS.md` with authoritative per-repo rules. Read the
|
Each dependency carries its own `AGENTS.md` with authoritative per-repo rules. Read the
|
||||||
relevant one before editing a submodule.
|
relevant one before editing a submodule.
|
||||||
|
|
||||||
|
### Abandoned generators
|
||||||
|
|
||||||
|
The one invariant generators (`GEN`/`EMIT`, `FOR EACH`/`DO EACH`) add to the environment
|
||||||
|
pool: **a suspended generator hangs off its loop scope's `forGeneratorEnv` as a *child*,
|
||||||
|
off the parent chain, so no walk up `->parent` ever finds it.** Any path that discards a
|
||||||
|
loop scope — `EXIT`, a mismatched `NEXT`, a `LOOP` condition saying stop, an error unwind
|
||||||
|
through `akbasic_runtime_pump_generator()` or `akbasic_runtime_call_function()` — must
|
||||||
|
release that generator too, or its pool slot is stranded until the next `RUN`; with
|
||||||
|
twelve slots, a loop that abandons a dozen of them kills the program with "Environment
|
||||||
|
pool exhausted" far from the cause.
|
||||||
|
|
||||||
|
Two functions own the invariant, and every discard path goes through one of them:
|
||||||
|
`akbasic_runtime_release_generator()` (walks a detached generator chain up through its
|
||||||
|
call frame, recursing into any `forGeneratorEnv` it passes) and
|
||||||
|
`akbasic_runtime_unwind_to_environment()` (pops the *active* chain down to a target,
|
||||||
|
releasing each popped scope's suspended generator on the way). If you add a new path
|
||||||
|
that pops or discards environments, use one of these — a bare
|
||||||
|
`akbasic_runtime_prev_environment()` loop reintroduces the leak, and
|
||||||
|
`tests/generators.c` holds the pool-exhaustion tests that will say so.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Editing the documentation
|
## Editing the documentation
|
||||||
|
|||||||
13
README.md
@@ -10,7 +10,7 @@ implementation that started from the Java Lox instructions in
|
|||||||
[craftinginterpreters.com](https://craftinginterpreters.com) and then struck off on its own. That
|
[craftinginterpreters.com](https://craftinginterpreters.com) and then struck off on its own. That
|
||||||
project is deprecated. It is vendored here as the behavioural spec to read when a question about
|
project is deprecated. It is vendored here as the behavioural spec to read when a question about
|
||||||
semantics comes up, and its acceptance corpus is checked in at
|
semantics comes up, and its acceptance corpus is checked in at
|
||||||
[`tests/reference/`](tests/reference/README.md) and runs on every build — so nothing about
|
[`tests/language/`](tests/language/README.md) and runs on every build — so nothing about
|
||||||
building or testing this project needs it.
|
building or testing this project needs it.
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
@@ -24,7 +24,7 @@ ctest --test-dir build --output-on-failure
|
|||||||
|
|
||||||
```sh norun
|
```sh norun
|
||||||
./build/basic # the REPL
|
./build/basic # the REPL
|
||||||
./build/basic tests/reference/language/functions.bas # run a program
|
./build/basic tests/language/functions.bas # run a program
|
||||||
```
|
```
|
||||||
|
|
||||||
```basic
|
```basic
|
||||||
@@ -126,10 +126,10 @@ version are catalogued in [`TODO.md`](TODO.md) and summarised for a BASIC progra
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [`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 |
|
| [`docs/`](docs/README.md) | The guide: twenty-one 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, [Chapters 17](docs/17-tutorial-breakout.md) and [18](docs/18-tutorial-breakout-artwork.md) building a whole game twice, and [Chapters 20](docs/20-tutorial-galaga.md) and [21](docs/21-tutorial-galaga-enemies.md) building a C game that embeds the interpreter |
|
||||||
| [`MAINTENANCE.md`](MAINTENANCE.md) | For contributors and maintainers: the documentation-example harness, the three test lists, mutation testing, error-code allocation, style |
|
| [`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 |
|
| [`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 |
|
| [`tests/language/README.md`](tests/language/README.md) | The editable language corpus and the rule for changing it |
|
||||||
|
|
||||||
API documentation builds with `doxygen Doxyfile`, into `build/docs/html`.
|
API documentation builds with `doxygen Doxyfile`, into `build/docs/html`.
|
||||||
|
|
||||||
@@ -138,12 +138,13 @@ API documentation builds with `doxygen Doxyfile`, into `build/docs/html`.
|
|||||||
Everything is a submodule; `git submodule update --init --recursive` gets all of it. There is
|
Everything is a submodule; `git submodule update --init --recursive` gets all of it. There is
|
||||||
nothing to install first.
|
nothing to install first.
|
||||||
|
|
||||||
* [libakerror](https://source.starfort.tech/andrew/libakerror) 2.0.1 — TRY/CATCH-style error
|
* [libakerror](https://source.starfort.tech/andrew/libakerror) 2.0.2 — TRY/CATCH-style error
|
||||||
contexts. Every function that can fail returns one. 2.0.0 made it thread safe and broke the
|
contexts. Every function that can fail returns one. 2.0.0 made it thread safe and broke the
|
||||||
ABI; anything built against a 1.x header must be rebuilt rather than relinked.
|
ABI; anything built against a 1.x header must be rebuilt rather than relinked.
|
||||||
* [libakstdlib](https://source.starfort.tech/andrew/libakstdlib) 0.2.0 — libc wrappers that
|
* [libakstdlib](https://source.starfort.tech/andrew/libakstdlib) 0.2.0 — libc wrappers that
|
||||||
report through `libakerror`. String-to-number conversion goes straight to it, which is why
|
report through `libakerror`. String-to-number conversion goes straight to it, which is why
|
||||||
`VAL("garbage")` is an error rather than a silent `0`.
|
`VAL("garbage")` is an error rather than a silent `0`. Its public header now also pulls in
|
||||||
|
`<dirent.h>` and `<sys/stat.h>` for the directory and file-metadata wrappers.
|
||||||
* [libakgl](https://source.starfort.tech/andrew/libakgl) 0.9.0 — **optional**, only for
|
* [libakgl](https://source.starfort.tech/andrew/libakgl) 0.9.0 — **optional**, only for
|
||||||
`-DAKBASIC_WITH_AKGL=ON`. Pulls in SDL3. Its soname carries `MAJOR.MINOR` while the major is 0,
|
`-DAKBASIC_WITH_AKGL=ON`. Pulls in SDL3. Its soname carries `MAJOR.MINOR` while the major is 0,
|
||||||
so rebuild rather than relink.
|
so rebuild rather than relink.
|
||||||
|
|||||||
73
TODO.md
@@ -38,7 +38,7 @@ What it changes:
|
|||||||
- **§1.8's message-text contract is now a convention.** Improving a message is allowed; it costs
|
- **§1.8's message-text contract is now a convention.** Improving a message is allowed; it costs
|
||||||
a golden file, which is a cost rather than a veto.
|
a golden file, which is a cost rather than a veto.
|
||||||
- **§5's bar drops** from "defensible against the golden suite" to defensible on its own merits.
|
- **§5's bar drops** from "defensible against the golden suite" to defensible on its own merits.
|
||||||
- **`tests/reference/` becomes a regression suite rather than a specification.** Diverging from
|
- **`tests/language/` is the editable language corpus rather than a protected specification.** Diverging from
|
||||||
it is allowed and must be deliberate and recorded — see its README.
|
it is allowed and must be deliberate and recorded — see its README.
|
||||||
|
|
||||||
What it does **not** change:
|
What it does **not** change:
|
||||||
@@ -306,13 +306,13 @@ to `Println`, which adds another.
|
|||||||
**This used to be a hard contract and is now a default.** The Go implementation is deprecated
|
**This used to be a hard contract and is now a default.** The Go implementation is deprecated
|
||||||
and will not be updated, so the two projects are no longer required to match — see §0.1. What
|
and will not be updated, so the two projects are no longer required to match — see §0.1. What
|
||||||
survives is the practical half: these strings and this newline behaviour are what every
|
survives is the practical half: these strings and this newline behaviour are what every
|
||||||
expectation in `tests/reference/` was written against, so changing one means changing golden
|
expectation in `tests/language/` was written against, so changing one means changing the paired
|
||||||
files, and that is worth doing on purpose rather than by accident. A message that reads
|
files, and that is worth doing on purpose rather than by accident. A message that reads
|
||||||
awkwardly *may* now be improved; do it deliberately, move the expectations in the same commit,
|
awkwardly *may* now be improved; do it deliberately, move the expectations in the same commit,
|
||||||
and add a line to §5.
|
and add a line to §5.
|
||||||
|
|
||||||
Numeric formatting still matches the reference: integers via `%" PRId64 "`, floats via `%f`
|
Numeric formatting still matches the reference: integers via `%" PRId64 "`, floats via `%f`
|
||||||
(Go's `%f` and C's `%f` both give six decimals — `tests/reference/language/arithmetic/float.txt`
|
(Go's `%f` and C's `%f` both give six decimals — `tests/language/arithmetic/float.txt`
|
||||||
confirms). No reason to change it, which is different from not being allowed to.
|
confirms). No reason to change it, which is different from not being allowed to.
|
||||||
|
|
||||||
### 1.9 Which `libakstdlib` calls are cleared for use — **the bans are lifted**
|
### 1.9 Which `libakstdlib` calls are cleared for use — **the bans are lifted**
|
||||||
@@ -366,6 +366,36 @@ One caveat survives the upgrade unchanged: `aksl_strhash_djb2` still sign-extend
|
|||||||
high-bit byte hashes differently from the `unsigned char` answer. BASIC identifiers are 7-bit
|
high-bit byte hashes differently from the `unsigned char` answer. BASIC identifiers are 7-bit
|
||||||
ASCII so the symbol tables cannot reach it — see §1.3, which is still accurate.
|
ASCII so the symbol tables cannot reach it — see §1.3, which is still accurate.
|
||||||
|
|
||||||
|
### 1.10 Generators share the `DEF` namespace, and the rest of their v1 semantics
|
||||||
|
|
||||||
|
Decided with issue #57 and its review. `GEN` and `DEF` live in the same functions table —
|
||||||
|
one lookup, one "unknown function" error path, a name cannot be both. What follows from
|
||||||
|
that and from the review of PR #61, all settled:
|
||||||
|
|
||||||
|
- A `GEN` called like a function (`X# = COUNTUP(3)`) is refused at the call, before
|
||||||
|
anything is pushed — `akbasic_FunctionDef.isGenerator` exists for exactly this check.
|
||||||
|
- Bare `RETURN` standing in a `GEN`'s own frame ends the generator exactly as `END GEN`
|
||||||
|
does; `RETURN expr` there is an error, because values leave a `GEN` only through `EMIT`.
|
||||||
|
Both inherit the interpreter-wide restriction that `RETURN` does not unwind nested
|
||||||
|
`FOR`/`DO` scopes — lifting that everywhere (the C64 reference *does* unwind) is filed
|
||||||
|
separately.
|
||||||
|
- `DO EACH ... LOOP WHILE c | UNTIL c` composes: the condition is checked after each trip,
|
||||||
|
with the loop variable still holding that trip's value, and stopping abandons the
|
||||||
|
generator exactly as `EXIT` does. A condition on the `DO EACH` line itself is a parse
|
||||||
|
error.
|
||||||
|
- Self-recursion — a `GEN` reached again down its own parent chain — is refused; sibling
|
||||||
|
and nested invocations of the same `GEN` are each a fresh pool environment and are fine.
|
||||||
|
- Every path that discards a `FOR EACH`/`DO EACH` scope must release the generator
|
||||||
|
suspended off it; see MAINTENANCE.md's "Abandoned generators" note for the invariant
|
||||||
|
and `akbasic_runtime_unwind_to_environment()` for the one primitive that enforces it.
|
||||||
|
|
||||||
|
One known limitation, pre-existing and shared with `DEF`: `parse_def_parameters()` does
|
||||||
|
not accept an empty parameter list, so `GEN NAME()` cannot be written — every generator
|
||||||
|
takes at least one parameter whether it wants one or not. Location:
|
||||||
|
`src/parser_commands.c`, `parse_def_parameters()`. Consequence: pointless parameters in
|
||||||
|
programs. Blast radius: cosmetic, both `DEF` and `GEN` headers. Closure: teach the shared
|
||||||
|
helper to accept `()`, one test each for `DEF` and `GEN`; filed as its own issue.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. What exists — **the core port is complete and green**
|
## 2. What exists — **the core port is complete and green**
|
||||||
@@ -375,7 +405,7 @@ Phases 0 through 6 of the original plan are done. The interpreter builds clean u
|
|||||||
|
|
||||||
It *did* reproduce the reference byte for byte, and that claim is retired rather than broken:
|
It *did* reproduce the reference byte for byte, and that claim is retired rather than broken:
|
||||||
§0.1 released it, and one case has since diverged deliberately (§6 item 16, listed in
|
§0.1 released it, and one case has since diverged deliberately (§6 item 16, listed in
|
||||||
`tests/reference/README.md`). Everything else still matches, which is worth knowing but is no
|
`tests/language/README.md`). Everything else still matches, which is worth knowing but is no
|
||||||
longer a gate.
|
longer a gate.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -408,17 +438,16 @@ and the only path that existed — `AKBASIC_MODE_RUNSTREAM` reading through the
|
|||||||
is the code the README quotes, built by every build and registered as a CTest case so a
|
is the code the README quotes, built by every build and registered as a CTest case so a
|
||||||
signature change breaks the build rather than rotting the document.
|
signature change breaks the build rather than rotting the document.
|
||||||
|
|
||||||
**The acceptance suite is the reference's own corpus, checked in at `tests/reference/`.** All
|
**The acceptance suite is the editable language corpus, checked in at `tests/language/`.** All
|
||||||
41 `.bas` files are registered as individual CTest cases and byte-compared against their `.txt`
|
65 `.bas` files are registered as individual CTest cases and compared against their `.txt`
|
||||||
— including the trailing double newline on an error line (§1.8).
|
— including the trailing double newline on an error line (§1.8).
|
||||||
|
|
||||||
It was driven *in place* out of `deps/basicinterpret` until 2026-07-31, on the reasoning that
|
It was driven *in place* out of `deps/basicinterpret` until 2026-07-31, on the reasoning that
|
||||||
copying a submodule's corpus guarantees drift. That reasoning was sound and was overruled
|
copying a submodule's corpus guarantees drift. That reasoning was sound and was overruled
|
||||||
deliberately: the Go dependency is being deprecated, and a build that cannot run its own
|
deliberately: the Go dependency is being deprecated, and a build that cannot run its own
|
||||||
acceptance suite without cloning the implementation it replaced is not finished. The copy is
|
acceptance suite without cloning the implementation it replaced is not finished. The copy is
|
||||||
byte-identical to `basicinterpreter@d76162c`, and `tests/reference/README.md` records the
|
originally byte-identical to `basicinterpreter@d76162c`, and `tests/language/README.md` records the
|
||||||
provenance, the cost of the drift nobody is watching for now, and the rule that those
|
provenance and the rule that programs and expectations are edited together deliberately.
|
||||||
expectations are never edited to suit this interpreter.
|
|
||||||
|
|
||||||
**Nothing in the build or the suite needs `deps/basicinterpret` any more**, and that is checked
|
**Nothing in the build or the suite needs `deps/basicinterpret` any more**, and that is checked
|
||||||
rather than assumed — both configurations were configured, built and run from scratch with the
|
rather than assumed — both configurations were configured, built and run from scratch with the
|
||||||
@@ -1026,7 +1055,7 @@ deviations from the reference's *program*: `main.go` and the SDL half of
|
|||||||
|
|
||||||
**What it costs a program:** a listing that used `INPUT$`, `LEN#` or `GOTO%` as a variable
|
**What it costs a program:** a listing that used `INPUT$`, `LEN#` or `GOTO%` as a variable
|
||||||
stops parsing, and the fix is to rename the variable. One case in the reference's own corpus
|
stops parsing, and the fix is to rename the variable. One case in the reference's own corpus
|
||||||
did exactly that; see `tests/reference/README.md`.
|
did exactly that; see `tests/language/examples/strreverse.bas`.
|
||||||
|
|
||||||
### Deviations in statement separation
|
### Deviations in statement separation
|
||||||
|
|
||||||
@@ -1391,10 +1420,10 @@ deviations from the reference's *program*: `main.go` and the SDL half of
|
|||||||
**This one moved a golden file.** A line with no number used to be filed under the loader's
|
**This one moved a golden file.** A line with no number used to be filed under the loader's
|
||||||
cursor unchanged — that is, on top of the line before it — so two unnumbered lines in a row
|
cursor unchanged — that is, on top of the line before it — so two unnumbered lines in a row
|
||||||
silently lost the first, and a *blank* line erased whatever preceded it. The reference does
|
silently lost the first, and a *blank* line erased whatever preceded it. The reference does
|
||||||
the same, and `tests/reference/language/arithmetic/integer.bas` is the proof: four `PRINT`
|
the same, and `tests/language/arithmetic/integer.bas` is the proof: four `PRINT`
|
||||||
statements, an expectation with three values, and a trailing blank line that erased
|
statements, an expectation with three values, and a trailing blank line that erased
|
||||||
`40 PRINT 4 - 2` before the program ran. The expectation is now `4 4 2 2` and
|
`40 PRINT 4 - 2` before the program ran. The expectation is now `4 4 2 2` and
|
||||||
`tests/reference/README.md` records it.
|
`tests/language/README.md` records it.
|
||||||
|
|
||||||
In its place: `akbasic_runtime_file_line()` (`src/runtime.c`) is the one implementation of
|
In its place: `akbasic_runtime_file_line()` (`src/runtime.c`) is the one implementation of
|
||||||
the rule, shared by `akbasic_runtime_load()`, RUNSTREAM and `DLOAD`. A numbered line is
|
the rule, shared by `akbasic_runtime_load()`, RUNSTREAM and `DLOAD`. A numbered line is
|
||||||
@@ -1586,9 +1615,9 @@ be reproduced before it can be fixed.
|
|||||||
|
|
||||||
**It cost one golden case, exactly as predicted, and the cost turned out to be nothing.**
|
**It cost one golden case, exactly as predicted, and the cost turned out to be nothing.**
|
||||||
The reference's `examples/strreverse.bas` names a variable `INPUT$`; the variable is renamed
|
The reference's `examples/strreverse.bas` names a variable `INPUT$`; the variable is renamed
|
||||||
to `SOURCE$` in `tests/reference/`, the expectation is byte-for-byte unchanged — the program
|
to `SOURCE$` in `tests/language/examples/strreverse.bas`, the expectation is byte-for-byte unchanged — the program
|
||||||
still prints `REVERSED: OLLEH` — and the case keeps every bit of its coverage. Recorded in
|
still prints `REVERSED: OLLEH` — and the case keeps every bit of its coverage. Recorded in
|
||||||
`tests/reference/README.md`'s divergence table, which this is the first entry in.
|
`tests/language/README.md` records the corpus editing rule.
|
||||||
|
|
||||||
This item sat parked because the corpus was the acceptance contract and lived in a submodule
|
This item sat parked because the corpus was the acceptance contract and lived in a submodule
|
||||||
this repository could not edit. Both premises are gone: the corpus is checked in, and
|
this repository could not edit. Both premises are gone: the corpus is checked in, and
|
||||||
@@ -2232,10 +2261,10 @@ update --init --recursive` gets them.
|
|||||||
|
|
||||||
## 8. Status
|
## 8. Status
|
||||||
|
|
||||||
**The port is done.** The C interpreter passes the Go reference's entire corpus and passes clean
|
**The port is done.** The C interpreter passes the language corpus and passes clean
|
||||||
under ASan and UBSan. It reproduced that corpus byte for byte until §0.1 retired the
|
under ASan and UBSan. It reproduced that corpus byte for byte until §0.1 retired the
|
||||||
requirement; two cases have diverged on purpose since, and
|
requirement; two cases have diverged on purpose since, and
|
||||||
`tests/reference/README.md` lists them.
|
`tests/language/README.md` lists them.
|
||||||
|
|
||||||
| Gate | Result |
|
| Gate | Result |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -2243,7 +2272,7 @@ requirement; two cases have diverged on purpose since, and
|
|||||||
| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 112/112 headless, with `akgl_typing` skipping itself. The same set minus the four `no_device` cases the SDL driver contradicts, plus `akgl_backends`, `akgl_frontend`, `docs_screenshots` and `akgl_typing` — the last of which is the skip, and the `akgl_build` CI job is where it skips |
|
| `ctest` with `-DAKBASIC_WITH_AKGL=ON` | 112/112 headless, with `akgl_typing` skipping itself. The same set minus the four `no_device` cases the SDL driver contradicts, plus `akgl_backends`, `akgl_frontend`, `docs_screenshots` and `akgl_typing` — the last of which is the skip, and the `akgl_build` CI job is where it skips |
|
||||||
| `docs_examples` | Every fenced block in `README.md`, `MAINTENANCE.md` and `docs/` executed and byte-compared: 71 programs, 9 transcripts, 79 output comparisons, 4 C snippets, 2 excerpts, 2 shell blocks and 19 figures in the default build. The C-snippet count reads 0 when the harness is run by hand without `--cflags-file`; CTest passes it. `MAINTENANCE.md` documents the fence-tag convention |
|
| `docs_examples` | Every fenced block in `README.md`, `MAINTENANCE.md` and `docs/` executed and byte-compared: 71 programs, 9 transcripts, 79 output comparisons, 4 C snippets, 2 excerpts, 2 shell blocks and 19 figures in the default build. The C-snippet count reads 0 when the harness is run by hand without `--cflags-file`; CTest passes it. `MAINTENANCE.md` documents the fence-tag convention |
|
||||||
| `docs_screenshots` | 19/19 figures re-rendered and byte-identical to the checked-in PNGs. AKGL build only — rendering a picture needs the SDL half |
|
| `docs_screenshots` | 19/19 figures re-rendered and byte-identical to the checked-in PNGs. AKGL build only — rendering a picture needs the SDL half |
|
||||||
| Golden corpus | 41/41 byte-exact from `tests/reference/` — **and 41/41 again through the SDL binary**, which is most of what proves the frontend changes no output |
|
| Language corpus | 65/65 paired expectations — **and 65/65 again through the SDL binary**, which is most of what proves the frontend changes no output |
|
||||||
| ASan + UBSan | 112/112 |
|
| ASan + UBSan | 112/112 |
|
||||||
| Line coverage | 94.1% (7227/7681) — above the 90% gate |
|
| Line coverage | 94.1% (7227/7681) — above the 90% gate |
|
||||||
| Function coverage | 97.9% (474/484) |
|
| Function coverage | 97.9% (474/484) |
|
||||||
@@ -2340,9 +2369,9 @@ Dependency baseline:
|
|||||||
|
|
||||||
| Submodule | Version | Notes |
|
| Submodule | Version | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `deps/libakerror` | 2.0.1 | Private ownership-enforced status registry. akbasic reserves 512–767 in `akbasic_error_register()`. Does not namespace its `coverage` target when embedded — worked around in our `CMakeLists.txt`. **2.0.0 is thread safe and an ABI break** (`libakerror.so.2`): `__akerr_last_ignored` is thread-local and `akerr_next_error()` returns an owned reference, neither of which fails to link when mismatched. **2.0.1 fixes an exit status that mattered more to this band than to any other** — see below. |
|
| `deps/libakerror` | 2.0.2 | Private ownership-enforced status registry. akbasic reserves 512–767 in `akbasic_error_register()`. **2.0.0 is thread safe and an ABI break** (`libakerror.so.2`): the `IGNORE` context is thread-local and `akerr_next_error()` returns an owned reference, neither of which fails to link when mismatched. **2.0.1 fixes an exit status that mattered more to this band than to any other** — see below. **2.0.2** namespaces its embedded `coverage` target (issue #15 — our `add_custom_target` shadow is deleted), installs `akerrorConfigVersion.cmake` at `SameMajorVersion` (issue #16), fixes the `AKERR_USE_STDLIB=OFF` build, retires `PATH_MAX` for `AKERR_MAX_ERROR_FNAME_LENGTH`, and turns the `IGNORE` slot into a released copy — `__akerr_last_ignored` was an `extern` pointer and is now a per-TU `static akerr_last_ignored`. **That last one costs us 1.35 MiB of thread-local storage**: 38 translation units x 37,296 bytes, measured as the whole TLS segment of `build/basic`, where 2.0.1 produced no TLS segment at all. Filed upstream as libakerror issue #37, along with the 84 `-Wunused-variable` warnings it emits. Its `UPGRADING.md` describes a 2.0.3 that `project()` never stamped — libakerror issue #38, which is why the version above reads 2.0.2. |
|
||||||
| `deps/libakstdlib` | 0.2.0 | soname `libakstdlib.so.0.2`. `AKSL_VERSION_CHECK()` asserted in `tests/version_check.c`. This release fixed all six confirmed defects the port was working around — see §1.9, where the bans are now lifted. |
|
| `deps/libakstdlib` | 0.2.0 | soname `libakstdlib.so.0.2`. `AKSL_VERSION_CHECK()` asserted in `tests/version_check.c`. This release fixed all six confirmed defects the port was working around — see §1.9, where the bans are now lifted. Since then, and with no version bump: **directory wrappers landed** (`aksl_opendir`/`readdir`/`closedir`/`rewinddir`, issue #10) — the gap `DIRECTORY` was refused for, so that refusal now says only that the verb is unwritten; file-metadata wrappers landed; and `aksl_snprintf` keeps its `int *count` but now reports the *required* length on truncation rather than 0. Only read on success here, so nothing moved. |
|
||||||
| `deps/libakgl` | 0.7.0 | soname `libakgl.so.0.7`. Owns status codes 256–260. Linked and tested under `-DAKBASIC_WITH_AKGL=ON`, which still defaults OFF so the core library and its whole suite build on a machine with no SDL. **0.3.0 closed every API gap this port had filed** — see §7 — so the four workarounds §3 used to list are gone. 0.4.0 was a leak-and-overread release that changed no public struct. **0.5.0 is the first that broke our source as well as our ABI**: it namespaced every exported symbol, so `akgl_render_bind2d` is `akgl_render_2d_bind`, `akgl_sprite_sheet_coords_for_frame` is `akgl_spritesheet_coords_for_frame`, the `renderer`/`camera`/`window` globals carry the prefix, and `_akgl_renderer`/`_akgl_camera` are `akgl_default_renderer`/`akgl_default_camera`. `include/akbasic/akgl.h` asserts the floor. **0.6.0 and 0.7.0 broke nothing here** — 0.6.0 is three arcade-physics fixes and a `physics.max_timestep` property this port does not use, and 0.7.0 reports failures `libakstdlib`'s wrappers were already catching and takes `libakerror` 2.0.1. The floor moved anyway, because deciding for ourselves which of libakgl's minor releases were really compatible is the judgement the soname exists to take away. |
|
| `deps/libakgl` | 0.9.0 | soname `libakgl.so.0.9`. Owns status codes 256–262. Linked and tested under `-DAKBASIC_WITH_AKGL=ON`, which still defaults OFF so the core library and its whole suite build on a machine with no SDL. **0.3.0 closed every API gap this port had filed** — see §7 — so the four workarounds §3 used to list are gone. 0.4.0 was a leak-and-overread release that changed no public struct. **0.5.0 is the first that broke our source as well as our ABI**: it namespaced every exported symbol, so `akgl_render_bind2d` is `akgl_render_2d_bind`, `akgl_sprite_sheet_coords_for_frame` is `akgl_spritesheet_coords_for_frame`, the `renderer`/`camera`/`window` globals carry the prefix, and `_akgl_renderer`/`_akgl_camera` are `akgl_default_renderer`/`akgl_default_camera`. `include/akbasic/akgl.h` asserts the floor. **0.6.0 and 0.7.0 broke nothing here** — 0.6.0 is three arcade-physics fixes and a `physics.max_timestep` property this port does not use, and 0.7.0 reports failures `libakstdlib`'s wrappers were already catching and takes `libakerror` 2.0.1. **0.8.0 and 0.9.0 broke nothing here either** — 0.8.0 is the collision subsystem (`AKGL_ERR_COLLISION`, code 261, and the vendored `libccd` and `tg` submodules that come with it), and 0.9.0 is the `akgl_ui` subsystem (`AKGL_ERR_UI`, code 262, and vendored `clay`), which is what `src/ui_akgl.c` draws through. Both widen libakgl's reserved band from five codes to seven; the range map in `MAINTENANCE.md` carries it. The floor moved anyway, because deciding for ourselves which of libakgl's minor releases were really compatible is the judgement the soname exists to take away. **Note `deps/libakgl/deps/` pins its own `libakerror` and `libakstdlib` older than ours**; the top-level build declares both targets first and libakgl's `if(NOT TARGET ...)` guards mean its copies are never configured, so what libakgl actually compiles against is what this tree pins. |
|
||||||
|
|
||||||
**An unhandled error in this band used to exit zero, and 512 is the worst possible base for
|
**An unhandled error in this band used to exit zero, and 512 is the worst possible base for
|
||||||
that.** `libakerror`'s default unhandled-error handler ended in `exit(errctx->status)`, and a
|
that.** `libakerror`'s default unhandled-error handler ended in `exit(errctx->status)`, and a
|
||||||
@@ -2553,7 +2582,7 @@ What remains, in priority order:
|
|||||||
this whole arrangement exists against.
|
this whole arrangement exists against.
|
||||||
|
|
||||||
The byte comparison is a deliberate bet that the dummy driver and the software renderer
|
The byte comparison is a deliberate bet that the dummy driver and the software renderer
|
||||||
are reproducible, which is the same bet `tests/reference/` already makes about golden
|
are reproducible, which is the same bet `tests/language/` makes about golden
|
||||||
output. Verified run-to-run and build-to-build here; what is untested is an SDL upgrade
|
output. Verified run-to-run and build-to-build here; what is untested is an SDL upgrade
|
||||||
that moves one pixel of a diagonal. **If that happens, regenerate the figures in the same
|
that moves one pixel of a diagonal. **If that happens, regenerate the figures in the same
|
||||||
commit as the bump** — do not weaken the test to a size check, which would pass for every
|
commit as the bump** — do not weaken the test to a size check, which would pass for every
|
||||||
@@ -2636,7 +2665,7 @@ reduced against `build/basic`, the stdio build, unless it says otherwise.
|
|||||||
|
|
||||||
**It is narrower than it looks, and the golden corpus is why.** The first attempt
|
**It is narrower than it looks, and the golden corpus is why.** The first attempt
|
||||||
released the scope on *any* skip, which broke
|
released the scope on *any* skip, which broke
|
||||||
`tests/reference/language/flowcontrol/nestedforloopwaitingforcommand.bas`: a zero-
|
`tests/language/flowcontrol/nestedforloopwaitingforcommand.bas`: a zero-
|
||||||
iteration `FOR` skips its body by the same mechanism, and there the orphan is
|
iteration `FOR` skips its body by the same mechanism, and there the orphan is
|
||||||
load-bearing -- it is what absorbs the inner `NEXT` so the outer one still finds its
|
load-bearing -- it is what absorbs the inner `NEXT` so the outer one still finds its
|
||||||
`FOR`. Releasing it turns that case into "NEXT outside the context of FOR". So the
|
`FOR`. Releasing it turns that case into "NEXT outside the context of FOR". So the
|
||||||
|
|||||||
2
deps/libakerror
vendored
2
deps/libakgl
vendored
2
deps/libakstdlib
vendored
@@ -146,6 +146,143 @@ condition at all loops forever until an `EXIT` or a `GOTO` leaves it.
|
|||||||
|
|
||||||
`EXIT` works here too.
|
`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
|
## GOTO and GOSUB
|
||||||
|
|
||||||
```basic
|
```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 |
|
| `COLLECT` | validates a disk's block allocation map. There is no map |
|
||||||
| `BACKUP` | duplicates one disk onto another. There are no disks |
|
| `BACKUP` | duplicates one disk onto another. There are no disks |
|
||||||
| `BOOT` | loads and runs a boot sector. There is no boot sector |
|
| `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
|
`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.
|
channels, and closing the channels is real, so that is what it does.
|
||||||
|
|||||||
@@ -100,6 +100,50 @@ bounded run is usually inside a `FOR` or `GOSUB` body, and a variable created th
|
|||||||
dies when the body pops — silently, with the script reading it correctly right up until
|
dies when the body pops — silently, with the script reading it correctly right up until
|
||||||
it stops.
|
it stops.
|
||||||
|
|
||||||
|
## Calling a function every frame
|
||||||
|
|
||||||
|
`akbasic_runtime_call_function()` calls a `DEF` by name with values you already
|
||||||
|
hold — the entry point a game loop wants. A host that calls it repeatedly signs
|
||||||
|
up for three rules the one-shot examples never meet:
|
||||||
|
|
||||||
|
```c wrap=hostcalls
|
||||||
|
CATCH(errctx, akbasic_runtime_call_function(&SCRIPT, "THINK", argp, 1, &result));
|
||||||
|
/* ...consume the result... */
|
||||||
|
CATCH(errctx, akbasic_environment_zero(SCRIPT.environment));
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Reset the value scratch after every call, once the result is consumed.**
|
||||||
|
Each call parks its result in the caller environment's per-line scratch
|
||||||
|
(`AKBASIC_MAX_VALUES` slots), and a host calling in a loop never crosses the
|
||||||
|
line boundary that would reset it. Skip the `akbasic_environment_zero()` and
|
||||||
|
the pool drains — measured at under two frames of forty calls — after which
|
||||||
|
every call fails with `Maximum values per line reached`. The reset also
|
||||||
|
invalidates `result`, which is why it comes after the consumption.
|
||||||
|
2. **Force RUN mode once after the boot run.** A multi-line `DEF` body only
|
||||||
|
runs while the runtime is in RUN mode, and by the time a host can call, the
|
||||||
|
program that filed the definitions has ended. One
|
||||||
|
`akbasic_runtime_set_mode(&SCRIPT, AKBASIC_MODE_RUN)` after
|
||||||
|
`akbasic_runtime_run()` makes the bodies run, and the mode stays put because
|
||||||
|
nothing steps the runtime between calls. Issue #8 tracks making this
|
||||||
|
unnecessary.
|
||||||
|
3. **Revive after a script error, deliberately.** A BASIC-level error inside a
|
||||||
|
called body reports through the sink, answers a stale value, and latches:
|
||||||
|
the runtime leaves RUN mode and every later call does nothing. When your
|
||||||
|
policy is to absorb the error and keep calling — a game marking one actor
|
||||||
|
dumb rather than killing the frame — the revival is two calls:
|
||||||
|
`akbasic_runtime_clear_error()`, then `akbasic_runtime_set_mode(RUN)` again.
|
||||||
|
The latch is deliberate for *programs* — the first error ends a run, once,
|
||||||
|
with one line — so nothing clears it for you.
|
||||||
|
|
||||||
|
Do not pass structures as per-frame arguments. A structure or pointer parameter
|
||||||
|
spends a value-pool slot on every call and the pool never reclaims, so the
|
||||||
|
interface dies after about a thousand calls — issue #36 has the measurements.
|
||||||
|
Bind the instance once with `akbasic_host_bind()` and point it at each object
|
||||||
|
with `akbasic_host_rebind()` ([Chapter 16](16-structures.md)), which spends
|
||||||
|
nothing per call. The GALAGA tutorial ([Chapters 20](20-tutorial-galaga.md)
|
||||||
|
and [21](21-tutorial-galaga-enemies.md)) is this whole recipe as a working
|
||||||
|
game, forty calls a frame.
|
||||||
|
|
||||||
## Where the output goes
|
## Where the output goes
|
||||||
|
|
||||||
`PRINT` writes through an `akbasic_TextSink`, which is a record of function pointers plus
|
`PRINT` writes through an `akbasic_TextSink`, which is a record of function pointers plus
|
||||||
|
|||||||
@@ -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. |
|
| `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` | `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `DRAW` | `DRAW src, x, y [TO x, y ...]` | Plot a point or draw a polyline. |
|
||||||
| `DSAVE` | `DSAVE "name"` | Save the program to a file. |
|
| `DSAVE` | `DSAVE "name"` | Save the program to a file. |
|
||||||
| `DVERIFY` | `DVERIFY "name"` | The other name for `VERIFY`. |
|
| `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` | `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `LIST` | `LIST [n][-n]` | List the program, or part of it. |
|
||||||
| `LOAD` | `LOAD "name"` | The other name for `DLOAD`. |
|
| `LOAD` | `LOAD "name"` | The other name for `DLOAD`. |
|
||||||
| `LOCATE` | `LOCATE x, y` | Move the pixel cursor. |
|
| `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. |
|
| `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. |
|
| `MOVSPR` | `MOVSPR n, ...` | Move a sprite. Four forms; see Chapter 8. |
|
||||||
| `NEW` | `NEW` | Erase the program and every variable. |
|
| `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. |
|
| `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. |
|
| `PAINT` | `PAINT src, x, y` | Flood-fill the region containing a point. |
|
||||||
| `PLAY` | `PLAY "notes"` | Queue notes. Does not block. |
|
| `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. |
|
| `RENUMBER` | `RENUMBER [start [,step [,from]]]` | Renumber lines, rewriting every branch to match. |
|
||||||
| `RESTORE` | `RESTORE [line]` | Reset the `READ` cursor, optionally to a line. |
|
| `RESTORE` | `RESTORE [line]` | Reset the `READ` cursor, optionally to a line. |
|
||||||
| `RESUME` | `RESUME [NEXT | line]` | Return from a `TRAP` handler. |
|
| `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. |
|
| `RUN` | `RUN [line]` | Run the program, optionally from a line. |
|
||||||
| `SAVE` | `SAVE "name"` | The other name for `DSAVE`. |
|
| `SAVE` | `SAVE "name"` | The other name for `DSAVE`. |
|
||||||
| `SCALE` | `SCALE on [,xmax, ymax]` | Turn user coordinates on or off. |
|
| `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 |
|
| Function | Args | Form | What it gives |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `ABS` | 1 | `ABS(n)` | The absolute value of an integer or float. |
|
| `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. |
|
| `ATN` | 1 | `ATN(n)` | Arctangent, in radians. |
|
||||||
| `BUMP` | 1 | `BUMP(1)` | Which sprites have collided, as a bitmask. **Reading clears it.** |
|
| `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. |
|
| `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). |
|
| `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. |
|
| `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.** |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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.**
|
- **`BLOAD` requires a length.**
|
||||||
- **`HEADER`, `COLLECT`, `BACKUP` and `BOOT` are refused.** They operate on a physical
|
- **`HEADER`, `COLLECT`, `BACKUP` and `BOOT` are refused.** They operate on a physical
|
||||||
disk.
|
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
|
## Machine
|
||||||
|
|
||||||
|
|||||||
@@ -407,6 +407,8 @@ block structure is executing: the `FOR` bounds and step, the `DO`/`LOOP` conditi
|
|||||||
| `GOSUB` | `RETURN` |
|
| `GOSUB` | `RETURN` |
|
||||||
| A call to a multi-line user function | that function's `RETURN` |
|
| A call to a multi-line user function | that function's `RETURN` |
|
||||||
| An interrupt firing | the handler'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
|
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
|
parsing the line, parks `TO` and `STEP` in it as unevaluated leaves, and makes it active
|
||||||
@@ -454,6 +456,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
|
the value correctly inside the loop and gets `0` immediately after it, with nothing
|
||||||
raised anywhere.
|
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
|
## Values
|
||||||
|
|
||||||
`akbasic_Value` carries its string **inline**, not behind a pointer, so a copy is a struct
|
`akbasic_Value` carries its string **inline**, not behind a pointer, so a copy is a struct
|
||||||
@@ -743,8 +798,8 @@ way to see that something pushed a scope and never popped it.
|
|||||||
|
|
||||||
```sh norun
|
```sh norun
|
||||||
ctest --test-dir build --output-on-failure -R for_next # one unit test
|
ctest --test-dir build --output-on-failure -R for_next # one unit test
|
||||||
ctest --test-dir build --output-on-failure -R golden_ # the reference corpus
|
ctest --test-dir build --output-on-failure -R local_ # the language corpus
|
||||||
./build/basic tests/reference/language/functions.bas | diff - tests/reference/language/functions.txt
|
./build/basic tests/language/functions.bas | diff - tests/language/functions.txt
|
||||||
./tests/docs_examples.sh --root . --basic ./build/basic \
|
./tests/docs_examples.sh --root . --basic ./build/basic \
|
||||||
--cflags-file build/docs_cflags.txt docs/14-architecture.md
|
--cflags-file build/docs_cflags.txt docs/14-architecture.md
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -685,9 +685,9 @@ seconds asks for fifty frames a second.
|
|||||||
### Why `GOTO` rather than `DO ... LOOP`
|
### Why `GOTO` rather than `DO ... LOOP`
|
||||||
|
|
||||||
A `DO ... LOOP` around the frame would read better, and it is not usable here: **a `GOTO`
|
A `DO ... LOOP` around the frame would read better, and it is not usable here: **a `GOTO`
|
||||||
that jumps out of a `FOR` or a `DO` does not release the loop's scope.** There are 32
|
that jumps out of a `FOR` or a `DO` does not release the loop's scope.** There are 12
|
||||||
scopes, so a game that leaves its main loop once per lost life stops on the
|
scopes, so a game that leaves its main loop once per lost life stops on the
|
||||||
thirty-second one:
|
twelfth one:
|
||||||
|
|
||||||
```basic
|
```basic
|
||||||
N# = 0
|
N# = 0
|
||||||
@@ -700,7 +700,7 @@ PRINT "SURVIVED " + N#
|
|||||||
```
|
```
|
||||||
|
|
||||||
```output
|
```output
|
||||||
? 3 : PARSE ERROR Environment pool exhausted at line 3 (32 in use)
|
? 3 : PARSE ERROR Environment pool exhausted at line 3 (12 in use)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1005,57 +1005,26 @@ IF NUDGE# = 1 THEN GOSUB UNSTICK
|
|||||||
LABEL UNSTICK
|
LABEL UNSTICK
|
||||||
NUDGE# = 0
|
NUDGE# = 0
|
||||||
STALL# = 0
|
STALL# = 0
|
||||||
RMAX# = 4
|
BVX# = (RND(4) * 3) - 6
|
||||||
GOSUB RANDOM
|
|
||||||
BVX# = (RND# * 3) - 6
|
|
||||||
IF BVX# = 0 THEN BVX# = 3
|
IF BVX# = 0 THEN BVX# = 3
|
||||||
RETURN
|
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
|
There is no `INT`, `SQR` or `TIMER` in this dialect, but
|
||||||
linear congruential generator is nine tokens and does the job. Put the number of possible
|
`RND(n)` returns an integer from zero through `n - 1`. It seeds itself
|
||||||
answers in `RMAX#` and read the result from `RND#`:
|
from the host clock the first time it is called, so a program only needs the bound:
|
||||||
|
|
||||||
```basic
|
```basic
|
||||||
SEED# = 12345
|
|
||||||
RMAX# = 6
|
|
||||||
RND# = 0
|
|
||||||
I# = 0
|
I# = 0
|
||||||
FOR I# = 1 TO 5
|
FOR I# = 1 TO 5
|
||||||
GOSUB RANDOM
|
PRINT "ROLL " + (RND(6) + 1)
|
||||||
PRINT "ROLL " + (RND# + 1)
|
|
||||||
NEXT I#
|
NEXT I#
|
||||||
END
|
END
|
||||||
|
|
||||||
LABEL RANDOM
|
|
||||||
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
|
|
||||||
RND# = MOD((SEED# / 65536), RMAX#)
|
|
||||||
RETURN
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```output
|
Use `RND` for the serve, too, so the ball does not always leave in the same direction:
|
||||||
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:
|
|
||||||
|
|
||||||
```basic norun
|
```basic norun
|
||||||
LABEL SERVE
|
LABEL SERVE
|
||||||
@@ -1063,16 +1032,43 @@ PX# = (SCW# - PW#) / 2
|
|||||||
HELD# = 1
|
HELD# = 1
|
||||||
BX# = PX# + ((PW# / 2) - 4)
|
BX# = PX# + ((PW# / 2) - 4)
|
||||||
BY# = PY# - 10
|
BY# = PY# - 10
|
||||||
RMAX# = 2
|
|
||||||
GOSUB RANDOM
|
|
||||||
BVX# = BSPD#
|
BVX# = BSPD#
|
||||||
IF RND# = 0 THEN BVX# = 0 - BSPD#
|
IF RND(2) = 0 THEN BVX# = 0 - BSPD#
|
||||||
BVY# = 0 - BSPD#
|
BVY# = 0 - BSPD#
|
||||||
PDEC# = 0
|
PDEC# = 0
|
||||||
GOSUB SHOWSPR
|
GOSUB SHOWSPR
|
||||||
RETURN
|
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
|
`HELD#` is the flag Step 6's loop tests: while it is 1 the ball sits on the paddle, and
|
||||||
`HOLDBAL` keeps it there:
|
`HOLDBAL` keeps it there:
|
||||||
|
|
||||||
@@ -1422,9 +1418,7 @@ PX# = PX# + D#
|
|||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
LABEL DEMOAIM
|
LABEL DEMOAIM
|
||||||
RMAX# = 81
|
DOFF# = RND(81) - 40
|
||||||
GOSUB RANDOM
|
|
||||||
DOFF# = RND# - 40
|
|
||||||
RETURN
|
RETURN
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1501,7 +1495,7 @@ This is the shape of the whole file:
|
|||||||
LABEL SETUP the geometry from Step 2
|
LABEL SETUP the geometry from Step 2
|
||||||
the declaration block from Step 3
|
the declaration block from Step 3
|
||||||
the brick faces from Step 5
|
the brick faces from Step 5
|
||||||
SEED# = TI#
|
RND(n) seeds itself from the host clock
|
||||||
the ceiling from Step 9
|
the ceiling from Step 9
|
||||||
GOSUB MKSPR Step 4
|
GOSUB MKSPR Step 4
|
||||||
GOSUB SNDPROBE Step 14
|
GOSUB SNDPROBE Step 14
|
||||||
@@ -1576,10 +1570,7 @@ BB# = 0
|
|||||||
RX# = 0
|
RX# = 0
|
||||||
N# = 0
|
N# = 0
|
||||||
MROW# = 0
|
MROW# = 0
|
||||||
RMAX# = 2
|
|
||||||
RND# = 0
|
|
||||||
SND# = 0
|
SND# = 0
|
||||||
SEED# = 0
|
|
||||||
P$ = ""
|
P$ = ""
|
||||||
H$ = ""
|
H$ = ""
|
||||||
S$ = ""
|
S$ = ""
|
||||||
|
|||||||
@@ -1423,7 +1423,8 @@ IF STATE# = 2 THEN GOSUB UNSTICK
|
|||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
LABEL PRESSPAUSE
|
LABEL PRESSPAUSE
|
||||||
IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED" : GOSUB SETBANNER : RETURN
|
IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED"
|
||||||
|
IF STATE# = 2 THEN BAN$ = "PAUSED" : GOSUB SETBANNER : RETURN
|
||||||
IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER
|
IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER
|
||||||
RETURN
|
RETURN
|
||||||
```
|
```
|
||||||
|
|||||||
849
docs/20-tutorial-galaga.md
Normal file
@@ -0,0 +1,849 @@
|
|||||||
|
# 20. Tutorial: GALAGA — a C engine with a BASIC brain
|
||||||
|
|
||||||
|
This chapter and [Chapter 21](21-tutorial-galaga-enemies.md) build a GALAGA-style
|
||||||
|
fixed shooter from an empty file. The engine — window, starfield, bullets,
|
||||||
|
collision, score, screens — is C on libakgl. The enemies think in BASIC: one
|
||||||
|
script of `DEF` functions is called once per enemy per frame, and it reads and
|
||||||
|
writes the engine's own structures with no marshalling in either direction.
|
||||||
|
This chapter builds the engine and proves the boundary works; the next one
|
||||||
|
fills in the data structures and the AI.
|
||||||
|
|
||||||
|
The split is the point. Everything mechanical stays compiled, and everything an
|
||||||
|
enemy *decides* is a text file you can edit and re-run without rebuilding. It is
|
||||||
|
an academic exercise in *how* such an embed is done, not a claim that it is the
|
||||||
|
best way to write a GALAGA.
|
||||||
|
|
||||||
|
This is what the two chapters build:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The finished program is [`examples/galaga/`](../examples/galaga/): four C files,
|
||||||
|
one `galaga.bas`, and the assets. You do not need it to follow along, but it is
|
||||||
|
the same program assembled.
|
||||||
|
|
||||||
|
```sh norun
|
||||||
|
$ cmake -S . -B build-akgl -DAKBASIC_WITH_AKGL=ON
|
||||||
|
$ cmake --build build-akgl --target akbasic_example_galaga
|
||||||
|
$ ./build-akgl/akbasic_example_galaga
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Does |
|
||||||
|
|---|---|
|
||||||
|
| left / right | move the ship |
|
||||||
|
| space | fire — two shots on screen at a time, the classic rule |
|
||||||
|
| return | choose a menu entry |
|
||||||
|
|
||||||
|
## What you will do
|
||||||
|
|
||||||
|
- **[Step 1](#step-1-open-a-window)** — open a window, in the one startup order
|
||||||
|
that works
|
||||||
|
- **[Step 2](#step-2-scatter-a-starfield)** — scatter a starfield and scroll it,
|
||||||
|
with no parallax machinery at all
|
||||||
|
- **[Step 3](#step-3-put-a-ship-on-screen)** — put a ship on screen from a
|
||||||
|
sprite and a character file, and drive it from the keyboard
|
||||||
|
- **[Step 4](#step-4-shots-and-collision)** — spawn shots from the actor heap
|
||||||
|
and collide them by hand
|
||||||
|
- **[Step 5](#step-5-boot-the-interpreter)** — link the interpreter in, load a
|
||||||
|
script of definitions, and call one from C
|
||||||
|
- **[Step 6](#step-6-the-update-hook)** — replace an actor's update hook so its
|
||||||
|
every frame is a BASIC call
|
||||||
|
- **[Step 7](#step-7-first-light)** — watch one enemy move under BASIC control,
|
||||||
|
and read the same numbers from both sides
|
||||||
|
- **[Step 8](#step-8-screens)** — add the title, game over and victory screens
|
||||||
|
- **[Step 9](#step-9-run-it-headless)** — run the whole game headless, so CI can
|
||||||
|
play it every night
|
||||||
|
|
||||||
|
Each step compiles and runs. The C fragments quote the finished example; the
|
||||||
|
file layout there — `main.c` for the harness, `script.c` for the boundary,
|
||||||
|
`enemies.c` and `player.c` for the actors — is a good one to copy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Open a window
|
||||||
|
|
||||||
|
**Goal: a black window with a title, from the canonical startup order.**
|
||||||
|
|
||||||
|
libakgl has one startup sequence that works, documented at the top of its
|
||||||
|
`include/akgl/game.h` and walked through in its own tutorial (libakgl
|
||||||
|
docs/20-tutorial-sidescroller.md). The order matters twice: the screen
|
||||||
|
properties are read by the renderer, so they must be set before it exists, and
|
||||||
|
`akgl_game_init()` does **not** install a physics backend, so the application
|
||||||
|
must.
|
||||||
|
|
||||||
|
```c wrap=galagatypes requires=akgl
|
||||||
|
static akerr_ErrorContext *startup(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, aksl_strncpy((char *)&akgl_game.name, sizeof(akgl_game.name),
|
||||||
|
"akbasic galaga tutorial", sizeof(akgl_game.name) - 1));
|
||||||
|
PASS(errctx, aksl_strncpy((char *)&akgl_game.version, sizeof(akgl_game.version),
|
||||||
|
"1.0.0", sizeof(akgl_game.version) - 1));
|
||||||
|
PASS(errctx, aksl_strncpy((char *)&akgl_game.uri, sizeof(akgl_game.uri),
|
||||||
|
"net.aklabs.akbasic.galaga", sizeof(akgl_game.uri) - 1));
|
||||||
|
|
||||||
|
PASS(errctx, akgl_game_init());
|
||||||
|
|
||||||
|
PASS(errctx, akgl_set_property("game.screenwidth", "1280"));
|
||||||
|
PASS(errctx, akgl_set_property("game.screenheight", "960"));
|
||||||
|
PASS(errctx, akgl_render_2d_init(akgl_renderer));
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
SDL_SetRenderLogicalPresentation(
|
||||||
|
akgl_renderer->sdl_renderer,
|
||||||
|
1280,
|
||||||
|
960,
|
||||||
|
SDL_LOGICAL_PRESENTATION_INTEGER_SCALE),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError()
|
||||||
|
);
|
||||||
|
akgl_camera->x = 0.0f;
|
||||||
|
akgl_camera->y = 0.0f;
|
||||||
|
akgl_camera->w = 1280.0f;
|
||||||
|
akgl_camera->h = 960.0f;
|
||||||
|
|
||||||
|
PASS(errctx, akgl_physics_init_null(akgl_physics));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Three of those lines deserve their reasons.
|
||||||
|
|
||||||
|
**The view is 1280x960 because the artwork is ~100 pixels wide.** libakgl draws
|
||||||
|
a sprite at the sprite's own size — `akgl_Actor.scale` is overwritten every
|
||||||
|
frame, so there is no way to draw one smaller (libakgl docs/12-actors.md) — and
|
||||||
|
a ten-column formation of 100-pixel ships needs 1120 pixels plus margins. The
|
||||||
|
view is sized to the art rather than the art resized to a view.
|
||||||
|
|
||||||
|
**`akgl_physics_init_null()` is not optional.** Skip it and the first
|
||||||
|
`akgl_game_update()` calls through a NULL `simulate` pointer. Null physics
|
||||||
|
accepts every call and moves nothing, which is exactly right here: whatever
|
||||||
|
writes `x` and `y` directly is the mover, and in this game that will be BASIC.
|
||||||
|
|
||||||
|
**Error handling is the house protocol.** Every function returns
|
||||||
|
`akerr_ErrorContext *`, `PASS` propagates, `ATTEMPT`/`CATCH`/`CLEANUP` brackets
|
||||||
|
anything that must unwind. libakgl's docs/04-errors.md teaches it; this chapter
|
||||||
|
just uses it, with two rules that keep the fragments compiling: **`CATCH` is
|
||||||
|
only legal inside an `ATTEMPT` block, and `PASS` everywhere else** — swap them
|
||||||
|
and the compiler objects about a stray `break` — and `main()` alone ends its
|
||||||
|
block with `FINISH_NORETURN(errctx)` instead of `FINISH`, because `FINISH`
|
||||||
|
expands a `return` of the context that an `int`-returning function cannot
|
||||||
|
compile:
|
||||||
|
|
||||||
|
```c wrap=galagatypes requires=akgl
|
||||||
|
static int FAILED = 0;
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
(void)argc; (void)argv;
|
||||||
|
ATTEMPT {
|
||||||
|
/* CATCH each stage in order: startup, assets, the script boot,
|
||||||
|
* the spawns, then the frame loop. */
|
||||||
|
} CLEANUP {
|
||||||
|
/* ...teardown, every call wrapped in IGNORE()... */
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE_DEFAULT(errctx) {
|
||||||
|
LOG_ERROR_WITH_MESSAGE(errctx, "galaga could not run");
|
||||||
|
/* Set a flag rather than returning: leaving a HANDLE block early
|
||||||
|
* skips FINISH's release and leaks the context's pool slot. */
|
||||||
|
FAILED = 1;
|
||||||
|
} FINISH_NORETURN(errctx);
|
||||||
|
return FAILED;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The status codes this game raises are `AKERR_NULLPOINTER`,
|
||||||
|
`AKERR_VALUE`, `AKERR_KEY`, `AKERR_IO`, `AKERR_OUTOFBOUNDS`, `AKGL_ERR_SDL`
|
||||||
|
and `AKGL_ERR_HEAP` — there is no code this tutorial invents.
|
||||||
|
|
||||||
|
The includes the engine files draw on, so nothing later has to be guessed —
|
||||||
|
the SDL satellites use their own prefixes (`SDL3_ttf/SDL_ttf.h`, not
|
||||||
|
`SDL3/SDL_ttf.h`):
|
||||||
|
|
||||||
|
```c wrap=galagatypes requires=akgl
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3_image/SDL_image.h>
|
||||||
|
#include <SDL3_ttf/SDL_ttf.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <akgl/actor.h>
|
||||||
|
#include <akgl/character.h>
|
||||||
|
#include <akgl/controller.h>
|
||||||
|
#include <akgl/draw.h>
|
||||||
|
#include <akgl/error.h>
|
||||||
|
#include <akgl/game.h>
|
||||||
|
#include <akgl/heap.h>
|
||||||
|
#include <akgl/physics.h>
|
||||||
|
#include <akgl/registry.h>
|
||||||
|
#include <akgl/renderer.h>
|
||||||
|
#include <akgl/sprite.h>
|
||||||
|
#include <akgl/text.h>
|
||||||
|
#include <akgl/ui.h>
|
||||||
|
#include <akgl/util.h>
|
||||||
|
```
|
||||||
|
|
||||||
|
The frame loop is the standard bracket, with one addition you will meet in
|
||||||
|
Step 6 — for now, events in, world drawn, frame out:
|
||||||
|
|
||||||
|
```c wrap=galagahost requires=akgl
|
||||||
|
while ( SDL_PollEvent(&event) == true ) {
|
||||||
|
CATCH(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
|
||||||
|
}
|
||||||
|
CATCH(errctx, akgl_renderer->frame_start(akgl_renderer));
|
||||||
|
CATCH(errctx, akgl_game_update(NULL));
|
||||||
|
CATCH(errctx, akgl_renderer->frame_end(akgl_renderer));
|
||||||
|
```
|
||||||
|
|
||||||
|
`akgl_game_update(NULL)` is update-every-actor, step-the-physics,
|
||||||
|
draw-the-world. It neither clears nor presents; the `frame_start` and
|
||||||
|
`frame_end` calls own that.
|
||||||
|
|
||||||
|
## Step 2: Scatter a starfield
|
||||||
|
|
||||||
|
**Goal: a scrolling two-depth starfield, from an array and one draw call.**
|
||||||
|
|
||||||
|
No parallax facility exists in libakgl and none is needed. A fixed array of
|
||||||
|
stars, advanced per frame and drawn with `akgl_draw_point()` between
|
||||||
|
`frame_start` and `akgl_game_update()`, is the whole feature. Two speed bands
|
||||||
|
give the depth for free — the slow band reads as far away:
|
||||||
|
|
||||||
|
```c wrap=galagatypes requires=akgl
|
||||||
|
#define GALAGA_STARS 96
|
||||||
|
|
||||||
|
static struct
|
||||||
|
{
|
||||||
|
float x;
|
||||||
|
float y;
|
||||||
|
float speed;
|
||||||
|
Uint8 bright;
|
||||||
|
} STARS[GALAGA_STARS];
|
||||||
|
|
||||||
|
static akerr_ErrorContext *starfield_draw(float dt)
|
||||||
|
{
|
||||||
|
SDL_Color color = { 255, 255, 255, 255 };
|
||||||
|
int i = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
for ( i = 0; i < GALAGA_STARS; i++ ) {
|
||||||
|
STARS[i].y += STARS[i].speed * dt;
|
||||||
|
if ( STARS[i].y > 960.0f ) {
|
||||||
|
STARS[i].y -= 960.0f;
|
||||||
|
}
|
||||||
|
color.r = STARS[i].bright;
|
||||||
|
color.g = STARS[i].bright;
|
||||||
|
color.b = STARS[i].bright;
|
||||||
|
PASS(errctx, akgl_draw_point(akgl_renderer, STARS[i].x, STARS[i].y, color));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Seed the array once at startup — even indexes slow and dim (speed 40, bright
|
||||||
|
110), odd indexes fast and bright (speed 110, bright 220) — and the effect is
|
||||||
|
done. A point is exactly one pixel (libakgl docs/09-drawing.md).
|
||||||
|
|
||||||
|
## Step 3: Put a ship on screen
|
||||||
|
|
||||||
|
**Goal: a player actor, drawn from a character file, moving on key input.**
|
||||||
|
|
||||||
|
The art is Kenney's Space Shooter pack, CC0, used byte for byte — see
|
||||||
|
[`examples/galaga/assets/art/PROVENANCE.md`](../examples/galaga/assets/art/PROVENANCE.md)
|
||||||
|
for what each file is. An actor gets its looks from a **character**, which maps
|
||||||
|
actor state words to **sprites** (libakgl docs/10 and 12). Both are JSON; load
|
||||||
|
sprites first, because a character names its sprites and a character loaded
|
||||||
|
first fails on the first name it cannot find.
|
||||||
|
|
||||||
|
These are the names, so the loading lists and every
|
||||||
|
`akgl_actor_set_character()` call in both chapters agree — each `sprite_*.json`
|
||||||
|
and `character_*.json` lives in `assets/`:
|
||||||
|
|
||||||
|
| Character | Sprite(s) it maps | Worn by |
|
||||||
|
|---|---|---|
|
||||||
|
| `galaga_player` | `galaga_player` | the ship |
|
||||||
|
| `galaga_bee` | `galaga_bee` | bees |
|
||||||
|
| `galaga_butterfly` | `galaga_butterfly` | butterflies |
|
||||||
|
| `galaga_boss` | `galaga_boss`, and `galaga_boss_hurt` on state bit 13 | bosses |
|
||||||
|
| `galaga_playershot` | `galaga_playershot` | the ship's shots |
|
||||||
|
| `galaga_enemyshot` | `galaga_enemyshot` | enemy shots |
|
||||||
|
| `galaga_boom` | `galaga_boom` | explosions |
|
||||||
|
|
||||||
|
The spawn is four decisions after the two boilerplate calls:
|
||||||
|
|
||||||
|
```c wrap=galagagame requires=akgl
|
||||||
|
static akerr_ErrorContext *galaga_player_spawn(void)
|
||||||
|
{
|
||||||
|
akgl_Actor *player = NULL;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, akgl_heap_next_actor(&player));
|
||||||
|
PASS(errctx, akgl_actor_initialize(player, "player"));
|
||||||
|
PASS(errctx, akgl_actor_set_character(player, "galaga_player"));
|
||||||
|
/* AFTER initialize: it resets all seven hooks. */
|
||||||
|
player->updatefunc = &player_update;
|
||||||
|
player->movement_controls_face = false;
|
||||||
|
player->state = AKGL_ACTOR_STATE_ALIVE;
|
||||||
|
player->visible = true;
|
||||||
|
player->x = 590.0f;
|
||||||
|
player->y = 860.0f;
|
||||||
|
|
||||||
|
galaga_game.player = player;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each of the four lines under the comment closes a trap:
|
||||||
|
|
||||||
|
- **`updatefunc` after `akgl_actor_initialize()`**, never before — initialize
|
||||||
|
installs all seven default hooks, and a hook set first is a hook reset.
|
||||||
|
- **`movement_controls_face = false`.** The default facing logic edits the
|
||||||
|
state word, a character mapping matches the **whole** word, and an actor
|
||||||
|
whose state matches no mapping is *silently not drawn*. Nothing here moves by
|
||||||
|
state bits, so facing stays out of the word entirely.
|
||||||
|
- **`state = AKGL_ACTOR_STATE_ALIVE`** — the word the character mapping names.
|
||||||
|
- **`visible = true`.** `akgl_actor_initialize()` does not raise it. In a
|
||||||
|
tilemap game the map loader copies visibility from map data; there is no map
|
||||||
|
here, so an actor that skips this line exists, moves, fires and collides —
|
||||||
|
invisibly. This one line cost this example its first screenshot.
|
||||||
|
|
||||||
|
Input goes through a control map: push a control per key with handlers that set
|
||||||
|
flags, and let the actor's update hook read the flags. A handler receives the
|
||||||
|
map's target actor and the event, and returns through the error protocol like
|
||||||
|
everything else — this pair is the whole pattern, repeated per key:
|
||||||
|
|
||||||
|
```c wrap=galagagame requires=akgl
|
||||||
|
static bool MOVELEFT = false;
|
||||||
|
|
||||||
|
akerr_ErrorContext *left_on(akgl_Actor *obj, SDL_Event *event)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)obj; (void)event;
|
||||||
|
MOVELEFT = true;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *left_off(akgl_Actor *obj, SDL_Event *event)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)obj; (void)event;
|
||||||
|
MOVELEFT = false;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(The example keeps the flags in its `galaga_Game` struct rather than statics;
|
||||||
|
either works.) The bindings themselves are pushes onto map 0:
|
||||||
|
|
||||||
|
```c wrap=galagagame requires=akgl
|
||||||
|
static akerr_ErrorContext *galaga_player_controls(void)
|
||||||
|
{
|
||||||
|
akgl_Control control;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
memset(&control, 0, sizeof(control));
|
||||||
|
control.event_on = SDL_EVENT_KEY_DOWN;
|
||||||
|
control.event_off = SDL_EVENT_KEY_UP;
|
||||||
|
|
||||||
|
control.key = SDLK_LEFT;
|
||||||
|
control.handler_on = &left_on;
|
||||||
|
control.handler_off = &left_off;
|
||||||
|
PASS(errctx, akgl_controller_pushmap(0, &control));
|
||||||
|
|
||||||
|
control.key = SDLK_RIGHT;
|
||||||
|
control.handler_on = &right_on;
|
||||||
|
control.handler_off = &right_off;
|
||||||
|
PASS(errctx, akgl_controller_pushmap(0, &control));
|
||||||
|
|
||||||
|
control.key = SDLK_SPACE;
|
||||||
|
control.handler_on = &fire_on;
|
||||||
|
control.handler_off = &fire_off;
|
||||||
|
PASS(errctx, akgl_controller_pushmap(0, &control));
|
||||||
|
|
||||||
|
akgl_controlmaps[0].target = galaga_game.player;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Hand **every** polled event to `akgl_controller_handle_event()` — one that no
|
||||||
|
control binds is not an error, it is a call that did nothing.
|
||||||
|
|
||||||
|
## Step 4: Shots and collision
|
||||||
|
|
||||||
|
**Goal: bullets that fly, hit, and give their actor slot back.**
|
||||||
|
|
||||||
|
Bullets and collision are C forever — they are engine, not behavior. A shot is
|
||||||
|
an actor from the same 64-slot heap pool, with its own tiny update hook: move,
|
||||||
|
test, release.
|
||||||
|
|
||||||
|
```c wrap=galagagame requires=akgl
|
||||||
|
static akerr_ErrorContext *player_shot_update(akgl_Actor *obj)
|
||||||
|
{
|
||||||
|
SDL_FRect mine;
|
||||||
|
SDL_FRect theirs;
|
||||||
|
bool hit = false;
|
||||||
|
int i = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
|
||||||
|
obj->y -= 900.0f * galaga_game.dt;
|
||||||
|
if ( obj->y < -60.0f ) {
|
||||||
|
galaga_game.player_shots_live -= 1;
|
||||||
|
PASS(errctx, akgl_heap_release_actor(obj));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
shot_box(obj, &mine);
|
||||||
|
for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
|
||||||
|
if ( galaga_enemy_actors[i] == NULL ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
enemy_box(galaga_enemy_actors[i], &theirs);
|
||||||
|
PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
|
||||||
|
if ( !hit ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
galaga_enemies[i].hp -= 1;
|
||||||
|
if ( galaga_enemies[i].hp <= 0 ) {
|
||||||
|
PASS(errctx, kill_enemy(i));
|
||||||
|
}
|
||||||
|
galaga_game.player_shots_live -= 1;
|
||||||
|
PASS(errctx, akgl_heap_release_actor(obj));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Four conventions worth keeping:
|
||||||
|
|
||||||
|
- **`akgl_collide_rectangles()` is the whole collision system.** At most 2
|
||||||
|
shots x 40 enemies of axis-aligned tests per frame is noise; the full
|
||||||
|
`akgl_CollisionWorld` machinery earns its keep on tilemaps, not here. The
|
||||||
|
`shot_box`/`enemy_box` helpers inset each box from the artwork's rectangle,
|
||||||
|
because the PNGs carry transparent margin that should not kill anybody.
|
||||||
|
- **Releasing is despawning.** `akgl_heap_release_actor()` unregisters the
|
||||||
|
actor and stops it drawing; releasing mid-sweep is safe because
|
||||||
|
`akgl_game_update()` re-reads each slot's refcount as it goes.
|
||||||
|
- **Names carry a serial** — `pshot17`, not `pshot1` reused — because the actor
|
||||||
|
registry is keyed by name, and two live actors with one name is a fight.
|
||||||
|
- **Spawn caps are C-side refusals.** Two player shots, eight enemy shots; the
|
||||||
|
spawn functions simply decline past the cap.
|
||||||
|
|
||||||
|
Give the enemy shots the same shape falling downward, and the ship a sweep over
|
||||||
|
both — `examples/galaga/player.c` has all three loops.
|
||||||
|
|
||||||
|
Explosions are the fourth actor kind, and they carry the one place this game
|
||||||
|
*absorbs* an error instead of propagating it. `HANDLE` names the status it
|
||||||
|
forgives; everything else still travels:
|
||||||
|
|
||||||
|
```c wrap=galagagame requires=akgl
|
||||||
|
static float BOOM_TTL[AKGL_MAX_HEAP_ACTOR];
|
||||||
|
static uint32_t BOOM_SERIAL = 0;
|
||||||
|
|
||||||
|
static akerr_ErrorContext *boom_update(akgl_Actor *obj)
|
||||||
|
{
|
||||||
|
ptrdiff_t slot = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
|
||||||
|
slot = obj - akgl_heap_actors;
|
||||||
|
BOOM_TTL[slot] -= galaga_game.dt;
|
||||||
|
if ( BOOM_TTL[slot] <= 0.0f ) {
|
||||||
|
PASS(errctx, akgl_heap_release_actor(obj));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *galaga_boom_spawn(float x, float y)
|
||||||
|
{
|
||||||
|
akgl_Actor *boom = NULL;
|
||||||
|
char name[32];
|
||||||
|
int count = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_heap_next_actor(&boom));
|
||||||
|
BOOM_SERIAL += 1;
|
||||||
|
CATCH(errctx, aksl_snprintf(&count, name, sizeof(name), "boom%u", BOOM_SERIAL));
|
||||||
|
CATCH(errctx, akgl_actor_initialize(boom, name));
|
||||||
|
CATCH(errctx, akgl_actor_set_character(boom, "galaga_boom"));
|
||||||
|
boom->updatefunc = &boom_update;
|
||||||
|
boom->movement_controls_face = false;
|
||||||
|
boom->state = AKGL_ACTOR_STATE_ALIVE;
|
||||||
|
boom->visible = true;
|
||||||
|
boom->x = x;
|
||||||
|
boom->y = y;
|
||||||
|
BOOM_TTL[boom - akgl_heap_actors] = 0.25f;
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE(errctx, AKGL_ERR_HEAP) {
|
||||||
|
/* Explosions are decoration. When the heap is momentarily full the
|
||||||
|
* right outcome is no explosion, not a dead frame. */
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 5: Boot the interpreter
|
||||||
|
|
||||||
|
**Goal: the engine calls a BASIC function and prints its answer.**
|
||||||
|
|
||||||
|
Everything so far was libakgl. Now link the interpreter into the same
|
||||||
|
executable. The whole CMake recipe, inside an akbasic checkout with
|
||||||
|
`AKBASIC_WITH_AKGL=ON`:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
add_executable(mygalaga
|
||||||
|
main.c
|
||||||
|
script.c
|
||||||
|
enemies.c
|
||||||
|
player.c)
|
||||||
|
target_compile_options(mygalaga PRIVATE -Wall -Wextra)
|
||||||
|
target_compile_definitions(mygalaga PRIVATE
|
||||||
|
GALAGA_ASSET_DIR="${CMAKE_CURRENT_SOURCE_DIR}/assets"
|
||||||
|
GALAGA_SCRIPT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/galaga.bas"
|
||||||
|
GALAGA_FONT_PATH="${CMAKE_CURRENT_SOURCE_DIR}/assets/fonts/C64_Pro_Mono-STYLE.ttf")
|
||||||
|
target_link_libraries(mygalaga PRIVATE akbasic akgl
|
||||||
|
SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image)
|
||||||
|
```
|
||||||
|
|
||||||
|
The three baked-in paths are what let the program launch from any working
|
||||||
|
directory; `--assets` and `--script` flags can override them at runtime.
|
||||||
|
|
||||||
|
Link `akbasic` — the interpreter only. Not `akbasic_akgl` (the device backends
|
||||||
|
that let a script draw), and not `akbasic_frontend` (the standalone program's
|
||||||
|
host). This game lends the script **no devices at all**: the scripts compute,
|
||||||
|
the engine draws, and a script that tries `SPRITE` is refused by name. That
|
||||||
|
refusal is enforced by the interpreter, not by convention —
|
||||||
|
[Chapter 10](10-embedding.md) explains the device-lending model this game
|
||||||
|
declines to use.
|
||||||
|
|
||||||
|
The boot is the embedding host from Chapter 10, adapted to a script that only
|
||||||
|
defines. Keep every line that touches the interpreter in one file — the
|
||||||
|
example's `script.c` — so the boundary stays a place rather than a habit. That
|
||||||
|
file's interpreter-facing includes and statics, exactly:
|
||||||
|
|
||||||
|
```c wrap=galagatypes requires=akgl
|
||||||
|
#include <akbasic/environment.h>
|
||||||
|
#include <akbasic/error.h>
|
||||||
|
#include <akbasic/host.h>
|
||||||
|
#include <akbasic/runtime.h>
|
||||||
|
#include <akbasic/sink.h>
|
||||||
|
|
||||||
|
/* Static because an akbasic_Runtime is far too big for a stack frame --
|
||||||
|
* 2.40 MiB on this branch. */
|
||||||
|
static akbasic_Runtime SCRIPT;
|
||||||
|
static akbasic_TextSink SINK;
|
||||||
|
static akbasic_StdioSink SINKSTATE;
|
||||||
|
static char SOURCE[16384];
|
||||||
|
```
|
||||||
|
|
||||||
|
The boot itself:
|
||||||
|
|
||||||
|
```c wrap=galagacalls requires=akgl
|
||||||
|
CATCH(errctx, akbasic_error_register());
|
||||||
|
CATCH(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL));
|
||||||
|
CATCH(errctx, akbasic_runtime_init(&SCRIPT, &SINK));
|
||||||
|
|
||||||
|
CATCH(errctx, akbasic_runtime_load(&SCRIPT, SOURCE));
|
||||||
|
CATCH(errctx, akbasic_runtime_start(&SCRIPT, AKBASIC_MODE_RUN));
|
||||||
|
CATCH(errctx, akbasic_runtime_run(&SCRIPT, 4 * AKBASIC_MAX_SOURCE_LINES));
|
||||||
|
CATCH(errctx, akbasic_runtime_set_mode(&SCRIPT, AKBASIC_MODE_RUN));
|
||||||
|
```
|
||||||
|
|
||||||
|
Two of those lines are the ones a first embedding gets wrong.
|
||||||
|
|
||||||
|
**A "no top level code" script still has to run once.** The script is nothing
|
||||||
|
but `DEF` blocks and a final `END`, and executing the `DEF` statements is what
|
||||||
|
files the functions. The run is bounded — a script that is all definitions has
|
||||||
|
no business taking more than a few steps per line, and an accidental loop at
|
||||||
|
boot should be a diagnosis, not a hang.
|
||||||
|
|
||||||
|
**The `set_mode` after the run is load-bearing.** The program has now ended and
|
||||||
|
the runtime sits in QUIT mode, where a multi-line `DEF` called from the host
|
||||||
|
returns a silent zero. Forcing the mode back to RUN makes the bodies run, and it
|
||||||
|
stays put because nothing here ever steps the runtime again. Issue #8 tracks
|
||||||
|
making this workaround unnecessary.
|
||||||
|
|
||||||
|
`PRINT` inside the script goes through the stdio sink and lands on stdout —
|
||||||
|
that is the script's debug channel for the rest of both chapters.
|
||||||
|
|
||||||
|
Prove the wiring with one function. Put this in the script:
|
||||||
|
|
||||||
|
```basic
|
||||||
|
DEF ADDEM(A#, B#) = A# + B#
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
And call it from C, with values you already have:
|
||||||
|
|
||||||
|
```c wrap=galagacalls requires=akgl
|
||||||
|
memset(&args[0], 0, sizeof(args[0]));
|
||||||
|
memset(&args[1], 0, sizeof(args[1]));
|
||||||
|
args[0].valuetype = AKBASIC_TYPE_INTEGER;
|
||||||
|
args[0].intval = 17;
|
||||||
|
args[1].valuetype = AKBASIC_TYPE_INTEGER;
|
||||||
|
args[1].intval = 25;
|
||||||
|
argp[0] = &args[0];
|
||||||
|
argp[1] = &args[1];
|
||||||
|
CATCH(errctx, akbasic_runtime_call_function(&SCRIPT, "ADDEM", argp, 2, &result));
|
||||||
|
printf("ADDEM(17, 25) = %lld\n", (long long)result->intval);
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
ADDEM(17, 25) = 42
|
||||||
|
```
|
||||||
|
|
||||||
|
`akbasic_runtime_call_function()` is the host's entry point: a name and
|
||||||
|
already-evaluated values in, the function's result out. The engine refuses to
|
||||||
|
start when the script will not boot — a game whose enemies cannot think is not
|
||||||
|
a game missing a feature, it is a game that does not run.
|
||||||
|
|
||||||
|
## Step 6: The update hook
|
||||||
|
|
||||||
|
**Goal: one actor whose every frame is a BASIC call.**
|
||||||
|
|
||||||
|
`akgl_game_update()` calls each live actor's `updatefunc` exactly once per
|
||||||
|
frame. Replacing that pointer is the whole integration: the actor's frame *is*
|
||||||
|
a script call.
|
||||||
|
|
||||||
|
```c wrap=galagagame requires=akgl
|
||||||
|
static akerr_ErrorContext *enemy_update(akgl_Actor *obj)
|
||||||
|
{
|
||||||
|
galaga_Enemy *enemy = NULL;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
|
||||||
|
enemy = (galaga_Enemy *)obj->actorData;
|
||||||
|
FAIL_ZERO_RETURN(errctx, enemy, AKERR_NULLPOINTER, "an enemy actor with no galaga_Enemy attached");
|
||||||
|
|
||||||
|
enemy->rnd = galaga_random();
|
||||||
|
PASS(errctx, galaga_script_update_enemy(enemy, obj, galaga_game.dt));
|
||||||
|
if ( enemy->fire != 0 ) {
|
||||||
|
PASS(errctx, enemy_fire(enemy, obj));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The hook's body is a protocol, and `galaga_script_update_enemy()` is its
|
||||||
|
middle: **rebind, call, recover, reset.**
|
||||||
|
|
||||||
|
```c wrap=galagacalls requires=akgl
|
||||||
|
CATCH(errctx, akbasic_host_rebind(&SCRIPT, "SELF@", enemy));
|
||||||
|
CATCH(errctx, akbasic_host_rebind(&SCRIPT, "ACTOR@", actor));
|
||||||
|
|
||||||
|
memset(&dtval, 0, sizeof(dtval));
|
||||||
|
dtval.valuetype = AKBASIC_TYPE_FLOAT;
|
||||||
|
dtval.floatval = (double)dt;
|
||||||
|
argp[0] = &dtval;
|
||||||
|
CATCH(errctx, akbasic_runtime_call_function(&SCRIPT, "UPDATEBEE", argp, 1, &result));
|
||||||
|
|
||||||
|
CATCH(errctx, akbasic_environment_zero(SCRIPT.environment));
|
||||||
|
```
|
||||||
|
|
||||||
|
`SELF@` and `ACTOR@` are **host bindings** — the enemy's record and the
|
||||||
|
engine's live actor, shared with the script as structures it can read and
|
||||||
|
write directly. [Chapter 21](21-tutorial-galaga-enemies.md) builds them; for
|
||||||
|
this chapter, know that `akbasic_host_rebind()` points an existing binding at
|
||||||
|
a different instance, which is how forty enemies share one script: one name,
|
||||||
|
rebound per enemy, rather than forty names.
|
||||||
|
|
||||||
|
**The `akbasic_environment_zero()` after every call is load-bearing.** Each
|
||||||
|
call parks its result in the caller environment's per-line value scratch, and a
|
||||||
|
host calling in a loop never crosses the line boundary that would reset it.
|
||||||
|
Without this line the scratch drains in under two frames of a 40-enemy wave and
|
||||||
|
every later call fails with `Maximum values per line reached`. Chapter 10's
|
||||||
|
["Calling a function every frame"](10-embedding.md#calling-a-function-every-frame)
|
||||||
|
section is the rule's home.
|
||||||
|
|
||||||
|
## Step 7: First light
|
||||||
|
|
||||||
|
**Goal: a C actor moving under BASIC control, and proof it is one memory.**
|
||||||
|
|
||||||
|
Before any real AI, the smallest demonstration. One enemy, one function, a sine
|
||||||
|
drift written entirely in BASIC through the actor binding:
|
||||||
|
|
||||||
|
```basic
|
||||||
|
DEF UPDATEBEE(DT%)
|
||||||
|
SELF@.T% = SELF@.T% + DT%
|
||||||
|
ACTOR@.X% = 590.0 + SIN(SELF@.T%) * 200
|
||||||
|
ACTOR@.Y% = 300.0
|
||||||
|
PRINT "BASIC SEES X = " + ACTOR@.X%
|
||||||
|
RETURN 0
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
Spawn one enemy with the hook from Step 6, and have the engine print the same
|
||||||
|
actor's position each frame from C:
|
||||||
|
|
||||||
|
```c wrap=galagahost requires=akgl
|
||||||
|
SDL_Log("C SEES X = %f", galaga_enemy_actors[0]->x);
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
BASIC SEES X = 593.191094
|
||||||
|
INFO: C SEES X = 593.191094
|
||||||
|
BASIC SEES X = 596.378593
|
||||||
|
INFO: C SEES X = 596.378593
|
||||||
|
```
|
||||||
|
|
||||||
|
Same numbers, one memory. The script wrote `ACTOR@.X%`; the renderer read
|
||||||
|
`akgl_Actor.x`; nothing copied anything anywhere. The ship swings in a slow
|
||||||
|
arc, and the whole architecture is visible in that one motion: C owns the
|
||||||
|
frame, BASIC owns the decision, and the actor is the same bytes to both.
|
||||||
|
|
||||||
|
## Step 8: Screens
|
||||||
|
|
||||||
|
**Goal: title, playing, game over, victory — a state machine around the loop.**
|
||||||
|
|
||||||
|
The screens are libakgl's UI layer, in the three-state pattern of its uidemo
|
||||||
|
example (libakgl docs/22-ui.md). A `galaga_Screen` enum, one `declare_*()`
|
||||||
|
function per screen, and the UI bracket between `akgl_game_update()` and
|
||||||
|
`frame_end` — exactly where the frame contract puts it:
|
||||||
|
|
||||||
|
```c wrap=galagahost requires=akgl
|
||||||
|
CATCH(errctx, akgl_ui_frame_begin());
|
||||||
|
switch ( galaga_game.screen ) {
|
||||||
|
case GALAGA_SCREEN_TITLE:
|
||||||
|
CATCH(errctx, declare_title());
|
||||||
|
break;
|
||||||
|
case GALAGA_SCREEN_PLAY:
|
||||||
|
CATCH(errctx, declare_play());
|
||||||
|
break;
|
||||||
|
case GALAGA_SCREEN_GAMEOVER:
|
||||||
|
case GALAGA_SCREEN_VICTORY:
|
||||||
|
CATCH(errctx, declare_end());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
CATCH(errctx, akgl_ui_frame_end(akgl_renderer));
|
||||||
|
```
|
||||||
|
|
||||||
|
The playing screen is two `akgl_ui_label()` calls — a widget call per label,
|
||||||
|
not a struct — formatted into `static` buffers, because the UI borrows label
|
||||||
|
text until `frame_end` and a local buffer would be dangling by the time it
|
||||||
|
draws:
|
||||||
|
|
||||||
|
```c wrap=galagagame requires=akgl
|
||||||
|
static char HUD_SCORE[64];
|
||||||
|
static char HUD_LIVES[64];
|
||||||
|
|
||||||
|
static akerr_ErrorContext *declare_play(void)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, aksl_snprintf(&count, HUD_SCORE, sizeof(HUD_SCORE),
|
||||||
|
"SCORE %06d", galaga_game.score));
|
||||||
|
PASS(errctx, aksl_snprintf(&count, HUD_LIVES, sizeof(HUD_LIVES),
|
||||||
|
"LIVES %d WAVE %d", galaga_game.lives, galaga_shared.wave));
|
||||||
|
PASS(errctx, akgl_ui_label("score", HUD_SCORE, AKGL_UI_ANCHOR_TOP_LEFT, NULL));
|
||||||
|
PASS(errctx, akgl_ui_label("lives", HUD_LIVES, AKGL_UI_ANCHOR_TOP_RIGHT, NULL));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The title and end screens are an `akgl_ui_menu()` at the center, fed an
|
||||||
|
`akgl_UiMenu` that lives in a `static` for the same borrowing reason. The
|
||||||
|
struct is an id, the item strings, a count, the selected index, the
|
||||||
|
`activated` output flag, and a style (`NULL` for the default):
|
||||||
|
|
||||||
|
```c wrap=galagatypes requires=akgl
|
||||||
|
static akgl_UiMenu TITLE_MENU = {
|
||||||
|
"titlemenu", { "START", "QUIT" }, 2, 0, false, NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
static akerr_ErrorContext *declare_title(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, akgl_ui_menu(&TITLE_MENU));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Route events to the menu with
|
||||||
|
`akgl_ui_menu_handle_event(&TITLE_MENU, event, &consumed)` — the menu for
|
||||||
|
whichever screen is up, a `bool` out-parameter reporting whether the event was
|
||||||
|
taken. Up and down move `selected`, return sets `activated`.
|
||||||
|
|
||||||
|
The big **GALAGA** headline is direct text rather than a label:
|
||||||
|
|
||||||
|
```c wrap=galagagame requires=akgl
|
||||||
|
static akerr_ErrorContext *draw_banner(char *text)
|
||||||
|
{
|
||||||
|
SDL_Color ink = { 235, 235, 235, 255 };
|
||||||
|
TTF_Font *font = NULL;
|
||||||
|
int w = 0;
|
||||||
|
int h = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "text");
|
||||||
|
font = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "banner", NULL);
|
||||||
|
FAIL_ZERO_RETURN(errctx, font, AKERR_KEY, "the banner font is not loaded");
|
||||||
|
PASS(errctx, akgl_text_measure(font, text, &w, &h));
|
||||||
|
PASS(errctx, akgl_text_rendertextat(font, text, ink, 0, (1280 - w) / 2, 280));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The menu owns `AKGL_UI_ANCHOR_CENTER`, a label anchored there disappears
|
||||||
|
behind it, and there is no top-center anchor — so the headline measures itself
|
||||||
|
and draws at a coordinate, before the UI bracket so the menu still paints over
|
||||||
|
it if the two ever meet.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Screen transitions are three rules read after the world updates: lives spent is
|
||||||
|
GAME OVER, an empty wave is VICTORY, and a menu activation either restarts or
|
||||||
|
quits. The menu never clears its own `activated` flag — the state machine that
|
||||||
|
acts on it does.
|
||||||
|
|
||||||
|
## Step 9: Run it headless
|
||||||
|
|
||||||
|
**Goal: the same game, playable by a script, in CI every night.**
|
||||||
|
|
||||||
|
The example takes five flags, in the pattern of libakgl's sidescroller:
|
||||||
|
|
||||||
|
```sh norun
|
||||||
|
$ SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy SDL_RENDER_DRIVER=software \
|
||||||
|
./build-akgl/akbasic_example_galaga --frames 600 --autoplay
|
||||||
|
```
|
||||||
|
|
||||||
|
`--frames N` bounds the run; `--autoplay` is a scripted pilot that starts the
|
||||||
|
game, sweeps the floor and holds fire until the wave assembles; `--screenshot
|
||||||
|
PATH --screenshot-frame N` write a PNG from the render target — the figures in
|
||||||
|
this chapter are that flag's output, not pictures somebody took once. Synthetic
|
||||||
|
input goes through `akgl_controller_handle_event()` with constructed
|
||||||
|
`SDL_Event`s, never by calling the handlers directly — the point of autoplay is
|
||||||
|
to exercise the same path a keyboard does.
|
||||||
|
|
||||||
|
The last line of every run is the evidence:
|
||||||
|
|
||||||
|
```text
|
||||||
|
galaga: 600 frames, screen 1, score 1910, alive 9, kills bee 19 bfly 12 boss 0, shots bee 0 bfly 3 boss 0, script errors 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Exiting 0 is not proof the wave flew. The readout is: kills and shots counted
|
||||||
|
per kind say the enemies entered, thought and fired, and **`script errors 0`**
|
||||||
|
says every one of the ~24,000 BASIC calls in those ten seconds came back clean.
|
||||||
|
A wave of dumb enemies still exits 0, and that count is how you notice. The
|
||||||
|
CTest entry `example_galaga` runs exactly this under the dummy SDL drivers,
|
||||||
|
which is what keeps both chapters honest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
That is the engine: a window, a starfield, a ship, bullets, screens, and an
|
||||||
|
interpreter that answers when called. Everything on screen so far is C. What
|
||||||
|
turns it into a GALAGA is [Chapter 21](21-tutorial-galaga-enemies.md) — the
|
||||||
|
three shared structures, the script that thinks through them, and a full wave
|
||||||
|
entering, breathing, diving and firing without another line of engine code.
|
||||||
596
docs/21-tutorial-galaga-enemies.md
Normal file
@@ -0,0 +1,596 @@
|
|||||||
|
# 21. Tutorial: GALAGA — the structures and the AI
|
||||||
|
|
||||||
|
[Chapter 20](20-tutorial-galaga.md) built a C engine that boots the interpreter
|
||||||
|
and hands one actor to BASIC. This chapter builds everything that crosses the
|
||||||
|
boundary — the three shared structures — and then the script that thinks
|
||||||
|
through them: a full wave that enters, forms up, breathes, dives, fires and
|
||||||
|
dies, without another line of engine code.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The finished script is
|
||||||
|
[`examples/galaga/galaga.bas`](../examples/galaga/galaga.bas) — six `DEF`
|
||||||
|
functions and an `END`, nothing else. Editing it and re-running the game is the
|
||||||
|
whole development loop; the engine never rebuilds.
|
||||||
|
|
||||||
|
## What you will do
|
||||||
|
|
||||||
|
- **[Step 1](#step-1-declare-the-enemy-once-in-c)** — declare the enemy record
|
||||||
|
once, in C, and register it as a BASIC type
|
||||||
|
- **[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 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
|
||||||
|
rules that shape every enemy function
|
||||||
|
- **[Step 6](#step-6-the-shared-maneuvers)** — write the shared maneuvers:
|
||||||
|
glide home, dive, decide to fire
|
||||||
|
- **[Step 7](#step-7-the-three-kinds)** — write the bee, the butterfly and the
|
||||||
|
boss
|
||||||
|
- **[Step 8](#step-8-the-formation-c-or-basic)** — decide who owns the
|
||||||
|
formation, and lay it out
|
||||||
|
- **[Step 9](#step-9-when-a-script-dies)** — decide what a script error does to
|
||||||
|
the game, and make it do that
|
||||||
|
- **[Step 10](#step-10-prove-it)** — prove the boundary with a test that links
|
||||||
|
the real files
|
||||||
|
- **[Step 11](#step-11-the-cost-measured)** — measure what thinking in BASIC
|
||||||
|
costs, against the same logic in C
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1: Declare the enemy once, in C
|
||||||
|
|
||||||
|
**Goal: one struct that both languages read and write, with one source of truth.**
|
||||||
|
|
||||||
|
An enemy is what the state machine needs to remember between frames, plus one
|
||||||
|
inbox and one outbox:
|
||||||
|
|
||||||
|
```c wrap=galagatypes requires=akgl
|
||||||
|
#define GALAGA_ENEMY_BEE 0
|
||||||
|
#define GALAGA_ENEMY_BUTTERFLY 1
|
||||||
|
#define GALAGA_ENEMY_BOSS 2
|
||||||
|
|
||||||
|
/*
|
||||||
|
* galaga_Enemy.state bits. The script owns these transitions; the engine only
|
||||||
|
* writes the word at spawn.
|
||||||
|
*
|
||||||
|
* 8 0
|
||||||
|
* 0 0 0 0 0 1 1 1
|
||||||
|
* | | `-- ENTERING: flying its entry path toward the formation slot
|
||||||
|
* | `---- FORMATION: holding (and breathing around) homex/homey
|
||||||
|
* `------ DIVING: attacking, off the grid until it leaves the screen
|
||||||
|
*/
|
||||||
|
#define GALAGA_ES_ENTERING (1 << 0)
|
||||||
|
#define GALAGA_ES_FORMATION (1 << 1)
|
||||||
|
#define GALAGA_ES_DIVING (1 << 2)
|
||||||
|
|
||||||
|
typedef struct galaga_Enemy
|
||||||
|
{
|
||||||
|
int32_t kind; /* GALAGA_ENEMY_BEE / BUTTERFLY / BOSS */
|
||||||
|
int32_t state; /* GALAGA_ES_* bit flags */
|
||||||
|
float homex; /* formation slot, in map pixels */
|
||||||
|
float homey;
|
||||||
|
float t; /* parametric clock for the current maneuver */
|
||||||
|
int32_t hp;
|
||||||
|
int32_t fire; /* outbox: script sets 1, engine consumes */
|
||||||
|
float rnd; /* inbox: fresh 0..1 each call; ROLL% in BASIC */
|
||||||
|
} galaga_Enemy;
|
||||||
|
```
|
||||||
|
|
||||||
|
The C struct *is* the BASIC type. `akbasic_host_register_type()` takes a table
|
||||||
|
of field descriptors — the BASIC name with its suffix, the C representation,
|
||||||
|
and where the member sits — and after that the language's own machinery works
|
||||||
|
across the boundary with no second set of rules
|
||||||
|
([Chapter 16](16-structures.md)):
|
||||||
|
|
||||||
|
```c wrap=galagatypes requires=akgl
|
||||||
|
typedef struct galaga_Enemy
|
||||||
|
{
|
||||||
|
int32_t kind;
|
||||||
|
int32_t state;
|
||||||
|
float homex;
|
||||||
|
float homey;
|
||||||
|
float t;
|
||||||
|
int32_t hp;
|
||||||
|
int32_t fire;
|
||||||
|
float rnd;
|
||||||
|
} galaga_Enemy;
|
||||||
|
|
||||||
|
static const akbasic_HostField ENEMY_FIELDS[] = {
|
||||||
|
/* struct member BASIC name C representation */
|
||||||
|
AKBASIC_HOST_FIELD( galaga_Enemy, kind, "KIND#", AKBASIC_HOSTFIELD_INT32 ),
|
||||||
|
AKBASIC_HOST_FIELD( galaga_Enemy, state, "STATE#", AKBASIC_HOSTFIELD_INT32 ),
|
||||||
|
AKBASIC_HOST_FIELD( galaga_Enemy, homex, "HOMEX%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||||
|
AKBASIC_HOST_FIELD( galaga_Enemy, homey, "HOMEY%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||||
|
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, "ROLL%", AKBASIC_HOSTFIELD_FLOAT )
|
||||||
|
};
|
||||||
|
static const akbasic_HostType ENEMY_TYPE = {
|
||||||
|
"ENEMY", sizeof(galaga_Enemy), ENEMY_FIELDS, 8
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Three decisions are load-bearing here:
|
||||||
|
|
||||||
|
- **`AKBASIC_HOST_FIELD` takes the offset and the width from the member
|
||||||
|
itself**, via `offsetof` — so the two sides cannot drift. Writing them out by
|
||||||
|
hand is two chances to name the wrong member and no way to notice.
|
||||||
|
- **The script never declares a `TYPE`.** A host type and a script `TYPE` share
|
||||||
|
one namespace, and a script that tries to redeclare `ENEMY` is refused. The
|
||||||
|
"structure definitions" half of the boundary lives here, once.
|
||||||
|
- **The suffixes are the dialect's**: `#` is integer, `%` is float
|
||||||
|
([Chapter 3](03-the-language.md)). `HOMEX%` because a formation slot is a
|
||||||
|
pixel coordinate the glide arithmetic must not truncate.
|
||||||
|
|
||||||
|
The limits that shape the struct: a type may carry 16 fields and the runtime 16
|
||||||
|
types ([Chapter 16](16-structures.md)). `ENEMY` spends 8 fields; the game
|
||||||
|
spends 3 types.
|
||||||
|
|
||||||
|
## Step 2: Bind the engine's own actor
|
||||||
|
|
||||||
|
**Goal: the script writes the same bytes the renderer reads.**
|
||||||
|
|
||||||
|
The enemy record is the game's own invention. The second type is not — it is
|
||||||
|
libakgl's `akgl_Actor`, registered field-for-field over the engine's real
|
||||||
|
struct:
|
||||||
|
|
||||||
|
```c wrap=galagatypes requires=akgl
|
||||||
|
static const akbasic_HostField ACTOR_FIELDS[] = {
|
||||||
|
AKBASIC_HOST_FIELD( akgl_Actor, x, "X%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||||
|
AKBASIC_HOST_FIELD( akgl_Actor, y, "Y%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||||
|
AKBASIC_HOST_FIELD( akgl_Actor, state, "STATE#", AKBASIC_HOSTFIELD_INT32 ),
|
||||||
|
AKBASIC_HOST_FIELD( akgl_Actor, visible, "VISIBLE#", AKBASIC_HOSTFIELD_BOOL )
|
||||||
|
};
|
||||||
|
static const akbasic_HostType ACTOR_TYPE = {
|
||||||
|
"ACTOR", sizeof(akgl_Actor), ACTOR_FIELDS, 4
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the demonstrative point of the whole exercise. When the script writes
|
||||||
|
`ACTOR@.X%`, it writes `akgl_Actor.x` — the same memory the renderer reads on
|
||||||
|
the same frame. There is no copy going in, no copy coming out, and no code
|
||||||
|
between the script's decision and the engine's pixel. Null physics
|
||||||
|
(Chapter 20, Step 1) is what makes that safe: nothing else is trying to move
|
||||||
|
the actor.
|
||||||
|
|
||||||
|
Registration and the first binding happen at boot, before the script loads —
|
||||||
|
between `akbasic_runtime_init()` and `akbasic_runtime_load()` in Chapter 20's
|
||||||
|
boot sequence. A binding is **borrowed, never copied**, so the placeholders it
|
||||||
|
points at must be static storage:
|
||||||
|
|
||||||
|
```c wrap=galagacalls requires=akgl
|
||||||
|
CATCH(errctx, akbasic_host_register_type(&SCRIPT, &ENEMY_TYPE));
|
||||||
|
CATCH(errctx, akbasic_host_register_type(&SCRIPT, &ACTOR_TYPE));
|
||||||
|
CATCH(errctx, akbasic_host_register_type(&SCRIPT, &GAME_TYPE));
|
||||||
|
|
||||||
|
CATCH(errctx, akbasic_host_bind(&SCRIPT, "SELF@", "ENEMY", &SCRATCH_ENEMY));
|
||||||
|
CATCH(errctx, akbasic_host_bind(&SCRIPT, "ACTOR@", "ACTOR", &SCRATCH_ACTOR));
|
||||||
|
CATCH(errctx, akbasic_host_bind(&SCRIPT, "GAME@", "GAME", &galaga_shared));
|
||||||
|
```
|
||||||
|
|
||||||
|
`akbasic_host_bind()` takes the script name, the registered type's name, and
|
||||||
|
the instance; after that, `SELF@` and `ACTOR@` are only ever *re*bound.
|
||||||
|
|
||||||
|
The per-frame call binds both names to *this* enemy before dispatching — one
|
||||||
|
binding per name, pointed at forty enemies in turn, which is what
|
||||||
|
`akbasic_host_rebind()` is for:
|
||||||
|
|
||||||
|
```c wrap=galagacalls requires=akgl
|
||||||
|
CATCH(errctx, akbasic_host_rebind(&SCRIPT, "SELF@", enemy));
|
||||||
|
CATCH(errctx, akbasic_host_rebind(&SCRIPT, "ACTOR@", actor));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Share the frame, and the dice
|
||||||
|
|
||||||
|
**Goal: everything a diving enemy needs to know about the world, in one record.**
|
||||||
|
|
||||||
|
```c wrap=galagatypes requires=akgl
|
||||||
|
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; ROLL% to the script */
|
||||||
|
} galaga_Shared;
|
||||||
|
```
|
||||||
|
|
||||||
|
`GAME@` is bound once at boot to this one global instance and never rebound;
|
||||||
|
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 — 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
|
||||||
|
|
||||||
|
**Goal: know why `SELF@` is a bound global rather than a parameter.**
|
||||||
|
|
||||||
|
The language can pass structures to functions — by value with `E@ AS ENEMY`,
|
||||||
|
by reference with `E@ AS PTR TO ENEMY` ([Chapter 16](16-structures.md)) — and
|
||||||
|
a host can construct those argument values, so the obvious alternative
|
||||||
|
interface is honest functions:
|
||||||
|
|
||||||
|
```basic norun
|
||||||
|
DEF UPDATEBEE(E@ AS PTR TO ENEMY, A@ AS PTR TO ACTOR, G@ AS PTR TO GAME, DT%)
|
||||||
|
```
|
||||||
|
|
||||||
|
It was measured before this chapter chose. Pointer arguments work — writes
|
||||||
|
through `E@->X%` land in the host struct, the type check refuses a wrong type,
|
||||||
|
by-value copies exactly as documented. What rules them out is the pool math:
|
||||||
|
|
||||||
|
| | bound globals | pointer arguments |
|
||||||
|
|---|---|---|
|
||||||
|
| value-pool slots per call | 0 | 1 per structure parameter, never returned |
|
||||||
|
| calls before exhaustion | unbounded | 1,015 measured (2,048-slot pool, 2 pointer args) |
|
||||||
|
| at 40 enemies per frame | unbounded | 25 frames |
|
||||||
|
| per-call cost | 148 us | 251 us |
|
||||||
|
|
||||||
|
A `@`-suffixed name always takes value-pool storage, and that pool never
|
||||||
|
reclaims — a documented property of structures, because a pointer may outlive
|
||||||
|
the scope that `DIM`med it. A *parameter* is a local that dies with the call,
|
||||||
|
but it pays the storage price of a `DIM` that must survive one; the pool
|
||||||
|
drains, and the wave stops thinking mid-flight. Issue #36 tracks it, with the
|
||||||
|
reduction for whoever fixes it. Until then: **bind and rebind for per-frame
|
||||||
|
host calls; pass structures only to functions called a bounded number of
|
||||||
|
times.**
|
||||||
|
|
||||||
|
## Step 5: The shape of the script
|
||||||
|
|
||||||
|
**Goal: the three rules every enemy function is written under.**
|
||||||
|
|
||||||
|
`galaga.bas` is definitions and an `END` — no top-level code, no line numbers,
|
||||||
|
no `LABEL`s. Three rules of the dialect shape every body in it.
|
||||||
|
|
||||||
|
**Rule 1: the left operand decides integer or float arithmetic**
|
||||||
|
([Chapter 3](03-the-language.md)). This will bite every enemy script exactly
|
||||||
|
once, so meet it now. The natural spelling of "move by speed times dt" moves
|
||||||
|
nothing:
|
||||||
|
|
||||||
|
```basic norun
|
||||||
|
ACTOR@.Y% = ACTOR@.Y% + 260 * DT%
|
||||||
|
```
|
||||||
|
|
||||||
|
`260` is an integer, it is on the left of `*`, so `DT%` — a float around
|
||||||
|
0.016 — is converted to integer **zero** before the multiply. Nothing fails;
|
||||||
|
the enemy simply does not move. The working spelling puts the float first:
|
||||||
|
|
||||||
|
```basic norun
|
||||||
|
ACTOR@.Y% = ACTOR@.Y% + SPD% * DT%
|
||||||
|
SPD% = SELF@.T% * 150 + 260
|
||||||
|
```
|
||||||
|
|
||||||
|
Every expression in the finished script is written float-first. When an enemy
|
||||||
|
of yours will not move, this is the first thing to check.
|
||||||
|
|
||||||
|
**Rule 2: only the last `RETURN` may start a line.** A multi-line `DEF` body
|
||||||
|
runs until `RETURN` — and the *definition* is scanned the same way, ending at
|
||||||
|
the first line that begins with one. An early return therefore always rides an
|
||||||
|
`IF ... THEN RETURN 0` on one line, and exactly one line-leading `RETURN` ends
|
||||||
|
each function. The stagger guard at the top of every update function is the
|
||||||
|
idiom:
|
||||||
|
|
||||||
|
```basic norun
|
||||||
|
DEF UPDATEBEE(DT%)
|
||||||
|
SELF@.T% = SELF@.T% + DT%
|
||||||
|
IF SELF@.T% < 0 THEN RETURN 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rule 3: the budgets are small and named.** Eight function slots exist
|
||||||
|
(`AKBASIC_MAX_FUNCTIONS`), each a measured 36 KiB of the runtime's 2.40 MiB.
|
||||||
|
This game defines six: three update functions, two shared maneuvers, one fire
|
||||||
|
decision. Nesting draws from the twelve-slot environment pool exactly as
|
||||||
|
`GOSUB` does; the deepest chain here is three (update → maneuver → nothing).
|
||||||
|
If a design needs a ninth function, raising the limit is one `#define` and
|
||||||
|
+36 KiB per slot — weighed, not assumed.
|
||||||
|
|
||||||
|
## Step 6: The shared maneuvers
|
||||||
|
|
||||||
|
**Goal: three helpers that make the three kinds one page each.**
|
||||||
|
|
||||||
|
Ease toward the formation slot, with a little entry swirl. Answers 1 once the
|
||||||
|
slot is reached — the caller flips the state on that answer:
|
||||||
|
|
||||||
|
```basic
|
||||||
|
DEF GLIDEHOME(DT%)
|
||||||
|
DX% = SELF@.HOMEX% - ACTOR@.X%
|
||||||
|
DY% = SELF@.HOMEY% - ACTOR@.Y%
|
||||||
|
K% = DT% * 4.5
|
||||||
|
IF K% > 1 THEN K% = 1
|
||||||
|
ACTOR@.X% = ACTOR@.X% + DX% * K% + SIN(SELF@.T% * 6) * 90 * DT%
|
||||||
|
ACTOR@.Y% = ACTOR@.Y% + DY% * K%
|
||||||
|
IF ABS(DX%) < 3 AND ABS(DY%) < 3 THEN RETURN 1
|
||||||
|
RETURN 0
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
One frame of a dive: accelerate downward, weave, lean toward the player's
|
||||||
|
column, and glide back in from the top after falling out the bottom. The
|
||||||
|
weave and the lean are parameters, which is what makes three kinds out of one
|
||||||
|
maneuver:
|
||||||
|
|
||||||
|
```basic
|
||||||
|
DEF DIVESTEP(DT%, WEAVE%, LEAD%)
|
||||||
|
SPD% = SELF@.T% * 150 + 260
|
||||||
|
ACTOR@.Y% = ACTOR@.Y% + SPD% * DT%
|
||||||
|
ACTOR@.X% = ACTOR@.X% + SIN(SELF@.T% * 4) * WEAVE% * DT%
|
||||||
|
DX% = GAME@.PLAYERX% - ACTOR@.X%
|
||||||
|
IF DX% > 220 THEN DX% = 220
|
||||||
|
IF DX% < -220 THEN DX% = -220
|
||||||
|
ACTOR@.X% = ACTOR@.X% + DX% * LEAD% * DT%
|
||||||
|
IF ACTOR@.Y% > 1040 THEN BEGIN
|
||||||
|
ACTOR@.Y% = 0.0 - 90
|
||||||
|
SELF@.STATE# = 1
|
||||||
|
SELF@.T% = 0
|
||||||
|
BEND
|
||||||
|
RETURN 0
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
Note the off-screen exit: state back to `1` (ENTERING), clock to zero, and the
|
||||||
|
glide brings it home — a dive that misses rejoins the formation, which is the
|
||||||
|
classic loop. `0.0 - 90` rather than `0 - 90` is Rule 1 again: the float goes
|
||||||
|
first even to make a negative.
|
||||||
|
|
||||||
|
The fire decision raises the flag when diving roughly above the player. The
|
||||||
|
engine consumes `FIRE#` and does the spawning — the script only wishes,
|
||||||
|
because spawning takes an actor from a bounded pool and pool exhaustion must
|
||||||
|
be a C-side refusal with the house error context, not a script mystery:
|
||||||
|
|
||||||
|
```basic
|
||||||
|
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@.ROLL% < DT% * 1.5 THEN SELF@.FIRE# = 1
|
||||||
|
RETURN 0
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 7: The three kinds
|
||||||
|
|
||||||
|
**Goal: bee, butterfly, boss — one state machine, three characters.**
|
||||||
|
|
||||||
|
Every kind is the same three-state machine, dispatched by the bits of
|
||||||
|
`SELF@.STATE#`. The bee is the reference implementation:
|
||||||
|
|
||||||
|
```basic
|
||||||
|
DEF GLIDEHOME(DT%)
|
||||||
|
ACTOR@.X% = SELF@.HOMEX%
|
||||||
|
ACTOR@.Y% = SELF@.HOMEY%
|
||||||
|
RETURN 1
|
||||||
|
|
||||||
|
DEF DIVESTEP(DT%, WEAVE%, LEAD%)
|
||||||
|
RETURN 0
|
||||||
|
|
||||||
|
DEF DECIDEFIRE(DT%)
|
||||||
|
RETURN 0
|
||||||
|
|
||||||
|
DEF UPDATEBEE(DT%)
|
||||||
|
SELF@.T% = SELF@.T% + DT%
|
||||||
|
IF SELF@.T% < 0 THEN RETURN 0
|
||||||
|
S# = SELF@.STATE#
|
||||||
|
IF (S# AND 1) > 0 THEN BEGIN
|
||||||
|
R# = GLIDEHOME(DT%)
|
||||||
|
IF R# = 1 THEN SELF@.STATE# = 2 : SELF@.T% = 0
|
||||||
|
BEND
|
||||||
|
IF (S# AND 2) > 0 THEN BEGIN
|
||||||
|
ACTOR@.X% = SELF@.HOMEX% + SIN(SELF@.T% * 1.7) * 16
|
||||||
|
ACTOR@.Y% = SELF@.HOMEY%
|
||||||
|
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)
|
||||||
|
R# = DECIDEFIRE(DT%)
|
||||||
|
BEND
|
||||||
|
RETURN 0
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
(The three helpers above are stubs so this listing runs alone; the real ones
|
||||||
|
are Step 6's. The listing in `galaga.bas` is this function verbatim.)
|
||||||
|
|
||||||
|
The shape to notice: `S#` is read **once**, so a state flipped this frame does
|
||||||
|
not also run its new state's block this frame — transitions are frame-atomic.
|
||||||
|
Each block is one `IF ... BEGIN`/`BEND`, never nested. The formation block
|
||||||
|
computes position *relative to home* every frame — `HOMEX% + SIN(...)` — so
|
||||||
|
the grid's idle breathing belongs to the script even though C placed the grid.
|
||||||
|
|
||||||
|
The butterfly is the bee with a wide lateral weave — `DIVESTEP(DT%, 260, 0.1)`
|
||||||
|
— and a slightly itchier trigger. The boss differs three ways: two hit points
|
||||||
|
(C fills `HP#` at spawn), a dive that leads the player —
|
||||||
|
`DIVESTEP(DT%, 60, 0.9)` — and one line that crosses the boundary in the other
|
||||||
|
direction:
|
||||||
|
|
||||||
|
```basic norun
|
||||||
|
IF SELF@.HP# = 1 THEN ACTOR@.STATE# = ACTOR@.STATE# OR 8192
|
||||||
|
```
|
||||||
|
|
||||||
|
8192 is `AKGL_ACTOR_STATE_UNDEFINED_13`, one of the actor state bits libakgl
|
||||||
|
reserves for the game. The boss's character file maps the state word
|
||||||
|
`ALIVE` to the green sprite and `ALIVE`+bit-13 to the drained one — so when
|
||||||
|
the script raises the bit, the engine's own character machinery swaps the
|
||||||
|
sprite. BASIC decides *that* the boss looks hurt; C never hears about it.
|
||||||
|
|
||||||
|
## Step 8: The formation: C or BASIC?
|
||||||
|
|
||||||
|
**Goal: decide who owns the grid, from the trade-offs rather than taste.**
|
||||||
|
|
||||||
|
Both can lay out the formation. The choice is argued, not asserted:
|
||||||
|
|
||||||
|
| | C lays out the grid | BASIC lays out the grid |
|
||||||
|
|---|---|---|
|
||||||
|
| actor pool safety | refusal at spawn, house error path | script can ask for more than 64 exist |
|
||||||
|
| tuning without rebuild | no | yes |
|
||||||
|
| call budget | zero calls | one call per spawn |
|
||||||
|
| who knows the screen size | the engine owns it anyway | needs it exported through `GAME@` |
|
||||||
|
|
||||||
|
**Decision: C owns the grid, the wave table and the spawn timing; BASIC owns
|
||||||
|
everything an enemy does after it exists.** The slot arrives in
|
||||||
|
`SELF@.HOMEX%`/`HOMEY%`, so the breathing stays the script's (Step 7), and the
|
||||||
|
pool stays behind a C-side refusal. The wave is the aligned table house style
|
||||||
|
already prescribes for tabular data — one row per formation row:
|
||||||
|
|
||||||
|
```c wrap=galagagame requires=akgl
|
||||||
|
static const struct
|
||||||
|
{
|
||||||
|
int32_t kind; /* GALAGA_ENEMY_* */
|
||||||
|
int row; /* formation row */
|
||||||
|
int first; /* first column filled */
|
||||||
|
int count; /* columns filled */
|
||||||
|
int32_t hp;
|
||||||
|
}
|
||||||
|
WAVE_ROWS[] = {
|
||||||
|
/* kind row first count hp */
|
||||||
|
{ GALAGA_ENEMY_BOSS, 0, 3, 4, 2 },
|
||||||
|
{ GALAGA_ENEMY_BUTTERFLY, 1, 1, 8, 1 },
|
||||||
|
{ GALAGA_ENEMY_BUTTERFLY, 2, 1, 8, 1 },
|
||||||
|
{ GALAGA_ENEMY_BEE, 3, 0, 10, 1 },
|
||||||
|
{ GALAGA_ENEMY_BEE, 4, 0, 10, 1 }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Forty enemies: 4 bosses, 16 butterflies, 20 bees. The actor heap holds 64:
|
||||||
|
|
||||||
|
```text
|
||||||
|
player 1
|
||||||
|
player shots 2 /* the classic two-on-screen rule */
|
||||||
|
enemies 40 /* 20 bees, 16 butterflies, 4 bosses */
|
||||||
|
enemy shots 8
|
||||||
|
explosions 8 /* short-lived actors, released on a timer */
|
||||||
|
---
|
||||||
|
59 of 64
|
||||||
|
```
|
||||||
|
|
||||||
|
The spawn walks the table, fills each `galaga_Enemy`, and staggers the entry
|
||||||
|
clocks — `t = -0.08 * index`, so each enemy holds still until its own clock
|
||||||
|
crosses zero and the wave pours in as a stream rather than a wall. The full
|
||||||
|
loop is `examples/galaga/enemies.c`.
|
||||||
|
|
||||||
|
## Step 9: When a script dies
|
||||||
|
|
||||||
|
**Goal: a script error costs one enemy's wits, never the frame.**
|
||||||
|
|
||||||
|
A BASIC-level error in an enemy's function — a misspelled field, arithmetic on
|
||||||
|
the wrong type — reports through the sink and stops the script. The engine's
|
||||||
|
policy, implemented around the call in `script.c`:
|
||||||
|
|
||||||
|
- **The enemy goes dumb**: state cleared to a formation hold it will never
|
||||||
|
leave, outbox cleared. The other thirty-nine keep thinking.
|
||||||
|
- **The runtime is revived**: a run's first error latches, and while it stands
|
||||||
|
every later call answers a stale value after doing nothing. Revival is two
|
||||||
|
calls — `akbasic_runtime_clear_error()`, then the same
|
||||||
|
`akbasic_runtime_set_mode(RUN)` the boot needed (issue #8's mechanics).
|
||||||
|
- **The first failure is logged, the rest are counted.** Sixty a second of the
|
||||||
|
same message is how a log stops being read; the count lands in the closing
|
||||||
|
readout as `script errors N`, where a headless run cannot miss it.
|
||||||
|
|
||||||
|
The same detection runs at boot: every function in the dispatch table is
|
||||||
|
called once against a zeroed scratch enemy, so a script that cannot run fails
|
||||||
|
at startup with the function's name in the message — not on frame one of the
|
||||||
|
first wave.
|
||||||
|
|
||||||
|
## Step 10: Prove it
|
||||||
|
|
||||||
|
**Goal: a test that fails the moment the two sides disagree.**
|
||||||
|
|
||||||
|
`examples/galaga/interop_test.c` links the real `script.c` and loads the real
|
||||||
|
`galaga.bas` — not copies — and pins the four claims this chapter made:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ok: a formation bee's sway is written into akgl_Actor.x/y by the script
|
||||||
|
ok: a diving bee above the player raises FIRE# for the engine to consume
|
||||||
|
ok: a boss at one hit point raises actor state bit 13 from BASIC
|
||||||
|
ok: 24000 calls survive the per-call akbasic_environment_zero() regime
|
||||||
|
```
|
||||||
|
|
||||||
|
That last claim is the per-frame contract from Chapter 20 Step 6 under a full
|
||||||
|
game's load — forty enemies at sixty frames a second for ten seconds. CTest
|
||||||
|
runs it as `example_galaga_interop` beside the headless game itself.
|
||||||
|
|
||||||
|
And because the script is data, the proof extends to scripts nobody planned:
|
||||||
|
run the game with `--script` pointing at a variant — enemies that never dive,
|
||||||
|
enemies that always dive — and the engine neither knows nor cares. That
|
||||||
|
swap-a-brain-without-rebuilding property is what the two chapters were about;
|
||||||
|
the readout tells you how each brain did:
|
||||||
|
|
||||||
|
```text
|
||||||
|
galaga: 3000 frames, screen 2, score 2350, alive 0, kills bee 20 bfly 15 boss 1, shots bee 1 bfly 1 boss 1, script errors 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 11: The cost, measured
|
||||||
|
|
||||||
|
**Goal: the real price of the boundary, in numbers, next to the same logic in C.**
|
||||||
|
|
||||||
|
The interop test binary ends with a benchmark: 24,000 formation-hold updates —
|
||||||
|
forty enemies at sixty frames a second for ten seconds — once through
|
||||||
|
`galaga_script_update_enemy()` and once through a line-for-line C translation
|
||||||
|
of `UPDATEBEE` with its helpers inlined. Same guard, same branches, same
|
||||||
|
arithmetic; the difference is the interpreter. On this repository's build
|
||||||
|
machine (a two-core VM, the interpreter built `-O2`):
|
||||||
|
|
||||||
|
```text
|
||||||
|
benchmark: 24000 formation-hold updates, dt 0.016
|
||||||
|
BASIC through the boundary: 21.147 s 881.11 us/call 35.245 ms per 40-enemy frame
|
||||||
|
the same logic in C: 0.000 s 0.01 us/call 0.001 ms per 40-enemy frame
|
||||||
|
ratio: 61022x
|
||||||
|
```
|
||||||
|
|
||||||
|
The facts, without decoration:
|
||||||
|
|
||||||
|
- **A BASIC-driven update costs about four orders of magnitude more than the
|
||||||
|
same logic compiled.** The C translation of the whole state machine costs
|
||||||
|
tens of *nano*seconds; the scripted call costs high hundreds of
|
||||||
|
*micro*seconds.
|
||||||
|
- **The cost is per line executed, not per call.** The interpreter scans and
|
||||||
|
parses each body line from source text on every call; a 3-line body measured
|
||||||
|
~148 us on this class of machine, and this ~15-line body measures ~881 us.
|
||||||
|
Body length is the knob.
|
||||||
|
- **At this cost, forty thinking enemies spend ~35 ms per frame on this
|
||||||
|
hardware** — more than two 60 Hz frames. The shipped example visibly runs
|
||||||
|
below 60 fps on this machine while the whole wave is alive, and exactly at
|
||||||
|
its frame pace once the wave thins. A faster machine moves the numbers, not
|
||||||
|
the shape.
|
||||||
|
|
||||||
|
This is the measured version of decisions the chapters already made on
|
||||||
|
architectural grounds. Bullets, collision and the starfield are C
|
||||||
|
([Chapter 20](20-tutorial-galaga.md), Steps 2 and 4) — at two shots and forty
|
||||||
|
tests a frame, scripting them would multiply the call count for things that
|
||||||
|
decide nothing. The fire decision is one flag rather than a per-bullet
|
||||||
|
callback (Step 6): the script's call budget is bounded by the enemy count and
|
||||||
|
nothing else. C owns the formation and the spawn timing (Step 8), so zero
|
||||||
|
calls happen for enemies that do not exist yet. And the 36 KiB function slots
|
||||||
|
and 2.40 MiB runtime (Step 5) are the memory half of the same bill.
|
||||||
|
|
||||||
|
What the cost buys is the previous ten steps: behavior as data, edited and
|
||||||
|
swapped without a compiler. Whether ~900 us per thinking entity per frame is
|
||||||
|
acceptable is a per-project decision — fewer thinkers, shorter bodies, or a
|
||||||
|
lower think rate (every Nth frame) are the standard levers, and all three are
|
||||||
|
host-side choices this architecture leaves open.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Where to go from here: more waves are rows in the table; a new enemy kind is
|
||||||
|
one table row, one character file and one `DEF`; a smarter boss is edits to a
|
||||||
|
text file while the game is closed — or a different file handed to
|
||||||
|
`--script`. The engine is done. That is the point.
|
||||||
@@ -16,7 +16,10 @@ embedding it, debugging it or changing it.
|
|||||||
**[Chapters 17](17-tutorial-breakout.md)** and **[18](18-tutorial-breakout-artwork.md)**
|
**[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
|
are tutorials rather than reference: they build one complete game twice, two different
|
||||||
ways, in numbered steps you can type in one at a time. Start with 17 — it needs nothing
|
ways, in numbered steps you can type in one at a time. Start with 17 — it needs nothing
|
||||||
but the earlier chapters, and 18 assumes it.
|
but the earlier chapters, and 18 assumes it. **[Chapters 20](20-tutorial-galaga.md)** and
|
||||||
|
**[21](21-tutorial-galaga-enemies.md)** are the third tutorial, from the other side of
|
||||||
|
the boundary: a C game on libakgl that embeds the interpreter as its enemy-behavior
|
||||||
|
engine, for anyone whose question is "how do I put this in *my* game".
|
||||||
|
|
||||||
## Chapters
|
## Chapters
|
||||||
|
|
||||||
@@ -41,6 +44,8 @@ but the earlier chapters, and 18 assumes it.
|
|||||||
| **[17. Tutorial: Breakout](17-tutorial-breakout.md)** | Build a whole game out of the text grid and two `DATA` sprites, in sixteen steps |
|
| **[17. Tutorial: Breakout](17-tutorial-breakout.md)** | Build a whole game out of the text grid and two `DATA` sprites, in sixteen steps |
|
||||||
| **[18. Tutorial: Breakout with artwork](18-tutorial-breakout-artwork.md)** | Build it again out of loaded artwork, powerups and a drawn colour HUD, in thirteen |
|
| **[18. Tutorial: Breakout with artwork](18-tutorial-breakout-artwork.md)** | Build it again out of loaded artwork, powerups and a drawn colour HUD, in thirteen |
|
||||||
| **[19. Menus and dialogs](19-user-interface.md)** | `MENU`, `DIALOG`, `HUD` and `UISTYLE` — the widgets, and who owns the keyboard |
|
| **[19. Menus and dialogs](19-user-interface.md)** | `MENU`, `DIALOG`, `HUD` and `UISTYLE` — the widgets, and who owns the keyboard |
|
||||||
|
| **[20. Tutorial: GALAGA](20-tutorial-galaga.md)** | Build a C engine on libakgl that embeds the interpreter, boots a script and hands it an actor |
|
||||||
|
| **[21. Tutorial: GALAGA enemies](21-tutorial-galaga-enemies.md)** | Share three C structs with the script, then write the wave's whole brain in BASIC |
|
||||||
|
|
||||||
## The shortest possible start
|
## The shortest possible start
|
||||||
|
|
||||||
|
|||||||
BIN
docs/images/galaga-title.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
BIN
docs/images/galaga-wave.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
@@ -145,7 +145,7 @@ RX# = 0
|
|||||||
N# = 0
|
N# = 0
|
||||||
MROW# = 0
|
MROW# = 0
|
||||||
RMAX# = 2
|
RMAX# = 2
|
||||||
RND# = 0
|
ROLL# = 0
|
||||||
SND# = 0
|
SND# = 0
|
||||||
P$ = ""
|
P$ = ""
|
||||||
H$ = ""
|
H$ = ""
|
||||||
@@ -172,7 +172,8 @@ BSG$(4) = "[--]"
|
|||||||
BSG$(5) = "[--]"
|
BSG$(5) = "[--]"
|
||||||
|
|
||||||
REM --- the seed -------------------------------------------------------
|
REM --- the seed -------------------------------------------------------
|
||||||
REM There is no RND in this dialect. TI# is jiffies off the host's clock
|
REM RND exists now, but this program keeps its own LCG so a headless run
|
||||||
|
REM is the same game every time. TI# is jiffies off the host's clock
|
||||||
REM and is host uptime rather than zero-based, which makes it a fine seed.
|
REM and is host uptime rather than zero-based, which makes it a fine seed.
|
||||||
SEED# = TI#
|
SEED# = TI#
|
||||||
|
|
||||||
@@ -389,17 +390,18 @@ BY# = PY# - 10
|
|||||||
RMAX# = 2
|
RMAX# = 2
|
||||||
GOSUB RANDOM
|
GOSUB RANDOM
|
||||||
BVX# = BSPD#
|
BVX# = BSPD#
|
||||||
IF RND# = 0 THEN BVX# = 0 - BSPD#
|
IF ROLL# = 0 THEN BVX# = 0 - BSPD#
|
||||||
BVY# = 0 - BSPD#
|
BVY# = 0 - BSPD#
|
||||||
PDEC# = 0
|
PDEC# = 0
|
||||||
GOSUB SHOWSPR
|
GOSUB SHOWSPR
|
||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
REM A linear congruential generator, because this dialect has no RND.
|
REM A linear congruential generator. RND(n) would do this in one token
|
||||||
|
REM now; the LCG stays because its sequence is reproducible.
|
||||||
REM The multiply stays inside int64 for any seed under 2^31.
|
REM The multiply stays inside int64 for any seed under 2^31.
|
||||||
LABEL RANDOM
|
LABEL RANDOM
|
||||||
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
|
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
|
||||||
RND# = MOD((SEED# / 65536), RMAX#)
|
ROLL# = MOD((SEED# / 65536), RMAX#)
|
||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
REM ####################################################################
|
REM ####################################################################
|
||||||
@@ -632,7 +634,7 @@ NUDGE# = 0
|
|||||||
STALL# = 0
|
STALL# = 0
|
||||||
RMAX# = 4
|
RMAX# = 4
|
||||||
GOSUB RANDOM
|
GOSUB RANDOM
|
||||||
BVX# = (RND# * 3) - 6
|
BVX# = (ROLL# * 3) - 6
|
||||||
IF BVX# = 0 THEN BVX# = 3
|
IF BVX# = 0 THEN BVX# = 3
|
||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
@@ -641,7 +643,7 @@ REM like something with a hand on the paddle rather than a mirror.
|
|||||||
LABEL DEMOAIM
|
LABEL DEMOAIM
|
||||||
RMAX# = 81
|
RMAX# = 81
|
||||||
GOSUB RANDOM
|
GOSUB RANDOM
|
||||||
DOFF# = RND# - 40
|
DOFF# = ROLL# - 40
|
||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
REM ####################################################################
|
REM ####################################################################
|
||||||
|
|||||||
@@ -241,7 +241,8 @@ SLX# = 0
|
|||||||
SLY# = 0
|
SLY# = 0
|
||||||
SLI# = 0
|
SLI# = 0
|
||||||
REM The two eraser stamps. Declared here for exactly the same reason -- built
|
REM The two eraser stamps. Declared here for exactly the same reason -- built
|
||||||
REM inside DRAWPROTOS and left undeclared, they were SHAPE:6 and SHAPE:7 inside
|
REM inside DRAWPROTOS and left undeclared, they were SHAPE:6 and SHAPE:7
|
||||||
|
REM inside
|
||||||
REM it and empty everywhere else.
|
REM it and empty everywhere else.
|
||||||
BL$ = ""
|
BL$ = ""
|
||||||
HBL$ = ""
|
HBL$ = ""
|
||||||
@@ -254,10 +255,13 @@ REM cursor through every DATA item in the program in the order they appear
|
|||||||
REM in the file, so whichever loader runs first gets the DATA that is
|
REM in the file, so whichever loader runs first gets the DATA that is
|
||||||
REM written first. The tables are written first.
|
REM written first. The tables are written first.
|
||||||
REM The text layer repaints every row it owns, opaque, so it has to be moved
|
REM The text layer repaints every row it owns, opaque, so it has to be moved
|
||||||
REM out of the way before anything drawn can be seen. Two rows at the bottom is
|
REM out of the way before anything drawn can be seen. Two rows at the bottom
|
||||||
|
REM is
|
||||||
REM enough for the final score, and hands the other thirty-five to the drawing
|
REM enough for the final score, and hands the other thirty-five to the drawing
|
||||||
REM verbs. Everything this game draws then simply stays there -- a drawing goes
|
REM verbs. Everything this game draws then simply stays there -- a drawing
|
||||||
REM into a layer the frame composites, so nothing here is captured into a sprite
|
REM goes
|
||||||
|
REM into a layer the frame composites, so nothing here is captured into a
|
||||||
|
REM sprite
|
||||||
REM and nothing is redrawn every frame.
|
REM and nothing is redrawn every frame.
|
||||||
WINDOW 0, 35, 49, 36
|
WINDOW 0, 35, 49, 36
|
||||||
|
|
||||||
@@ -293,7 +297,8 @@ ENVELOPE 0, 0, 6, 0, 4
|
|||||||
TEMPO 12
|
TEMPO 12
|
||||||
|
|
||||||
COLLISION 2, BRICKHIT
|
COLLISION 2, BRICKHIT
|
||||||
REM The stamps, once. They used to be rebuilt whenever the SSHAPE pool ran dry,
|
REM The stamps, once. They used to be rebuilt whenever the SSHAPE pool ran
|
||||||
|
REM dry,
|
||||||
REM because every frame's capture spent another slot; nothing captures now, so
|
REM because every frame's capture spent another slot; nothing captures now, so
|
||||||
REM eight slots are spent here and never again.
|
REM eight slots are spent here and never again.
|
||||||
GOSUB DRAWPROTOS
|
GOSUB DRAWPROTOS
|
||||||
@@ -374,7 +379,8 @@ COLOR 1, 1
|
|||||||
FOR K# = 0 TO 15
|
FOR K# = 0 TO 15
|
||||||
DRAW 1, 0, 130 + K# TO 67, 130 + K#
|
DRAW 1, 0, 130 + K# TO 67, 130 + K#
|
||||||
NEXT K#
|
NEXT K#
|
||||||
REM And an eighth the width of the HUD strip, for the same reason: the strip is
|
REM And an eighth the width of the HUD strip, for the same reason: the strip
|
||||||
|
REM is
|
||||||
REM rewritten whenever a number in it changes, and the old digits have to go
|
REM rewritten whenever a number in it changes, and the old digits have to go
|
||||||
REM somewhere first.
|
REM somewhere first.
|
||||||
FOR K# = 0 TO 59
|
FOR K# = 0 TO 59
|
||||||
@@ -393,7 +399,8 @@ DPLAY# = 1
|
|||||||
DHUD# = 1
|
DHUD# = 1
|
||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
REM Take one brick off the screen: stamp the blank over it. Called when a brick
|
REM Take one brick off the screen: stamp the blank over it. Called when a
|
||||||
|
REM brick
|
||||||
REM breaks, so the field is never redrawn as a whole during play -- which is
|
REM breaks, so the field is never redrawn as a whole during play -- which is
|
||||||
REM what lets the whole live-list machinery go.
|
REM what lets the whole live-list machinery go.
|
||||||
LABEL ERASEBRICK
|
LABEL ERASEBRICK
|
||||||
@@ -444,7 +451,8 @@ WIDTH 1
|
|||||||
COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5
|
COLOR 0, 1 : COLOR 1, 4 : COLOR 2, 8 : COLOR 3, 5
|
||||||
COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6
|
COLOR 4, 11 : COLOR 5, 16 : COLOR 6, 6
|
||||||
REM The old strip goes first. Nothing here clears the screen -- a drawing
|
REM The old strip goes first. Nothing here clears the screen -- a drawing
|
||||||
REM stays, which is the whole point -- so the digits that were there have to be
|
REM stays, which is the whole point -- so the digits that were there have to
|
||||||
|
REM be
|
||||||
REM stamped over before the new ones are drawn.
|
REM stamped over before the new ones are drawn.
|
||||||
Z$ = HBL$
|
Z$ = HBL$
|
||||||
GSHAPE Z$, 0, 0
|
GSHAPE Z$, 0, 0
|
||||||
@@ -495,7 +503,10 @@ IF SNDON# = 0 THEN VOL 0
|
|||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
LABEL PRESSPAUSE
|
LABEL PRESSPAUSE
|
||||||
IF STATE# = 2 THEN STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED" : GOSUB SETBANNER : RETURN
|
IF STATE# <> 2 THEN GOTO PRESSPAUSE2
|
||||||
|
STATE# = 6 : GMTYP# = 0 : BAN$ = "PAUSED" : GOSUB SETBANNER
|
||||||
|
RETURN
|
||||||
|
LABEL PRESSPAUSE2
|
||||||
IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER
|
IF STATE# = 6 THEN STATE# = 2 : BAN$ = "" : GOSUB SETBANNER
|
||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
|
|||||||
63
examples/galaga/README.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# GALAGA — a C engine with akbasic embedded as its enemy brain
|
||||||
|
|
||||||
|
A GALAGA-style fixed shooter whose core engine is C on libakgl, with akbasic
|
||||||
|
linked in as the scripting engine that owns every enemy's behavior. One BASIC
|
||||||
|
script — `galaga.bas`, nothing but `DEF` functions and an `END` — is called
|
||||||
|
once per enemy per frame through a custom `akgl_Actor` update hook. Bullets,
|
||||||
|
collision, scoring and screens are C forever; everything an enemy *decides* is
|
||||||
|
BASIC.
|
||||||
|
|
||||||
|
This is the checked-in example behind two tutorial chapters, and the chapters
|
||||||
|
are the intended way in:
|
||||||
|
|
||||||
|
* **[Chapter 20](../../docs/20-tutorial-galaga.md)** — the engine and the
|
||||||
|
boundary: from an empty file to a C game that boots a script and hands one
|
||||||
|
actor to BASIC.
|
||||||
|
* **[Chapter 21](../../docs/21-tutorial-galaga-enemies.md)** — the data
|
||||||
|
structures and the AI: from `SELF@` to a full attacking wave.
|
||||||
|
|
||||||
|
It is an academic exercise demonstrating *how* such an embed is done, not a
|
||||||
|
claim that it is the best way to write a GALAGA.
|
||||||
|
|
||||||
|
## Building and running
|
||||||
|
|
||||||
|
The example builds when both example and graphics builds are on:
|
||||||
|
|
||||||
|
```
|
||||||
|
cmake -S . -B build-akgl -DAKBASIC_WITH_AKGL=ON
|
||||||
|
cmake --build build-akgl --target akbasic_example_galaga
|
||||||
|
build-akgl/akbasic_example_galaga
|
||||||
|
```
|
||||||
|
|
||||||
|
| Key | Does |
|
||||||
|
|---|---|
|
||||||
|
| Left / Right | move the ship |
|
||||||
|
| Space | fire (two shots on screen, the classic rule) |
|
||||||
|
| Return | choose a menu entry |
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
|
```
|
||||||
|
akbasic_example_galaga [--assets DIR] [--script PATH] [--frames N]
|
||||||
|
[--autoplay] [--screenshot PATH] [--screenshot-frame N]
|
||||||
|
```
|
||||||
|
|
||||||
|
`--script` points at a different enemy script, which is the whole point of the
|
||||||
|
architecture: edit `galaga.bas`, run again, no rebuild. `--frames N` with
|
||||||
|
`--autoplay` is the headless smoke test CI runs under the dummy SDL drivers;
|
||||||
|
the final log line reports frames, score, kills and shots per kind, and the
|
||||||
|
script-error count — a wave of dumb enemies still exits 0, and that count is
|
||||||
|
how you notice.
|
||||||
|
|
||||||
|
## The files
|
||||||
|
|
||||||
|
| File | Owns |
|
||||||
|
|---|---|
|
||||||
|
| `galaga.h` | the shared structs — the whole boundary in one header |
|
||||||
|
| `main.c` | startup order, the frame loop, screens, the starfield |
|
||||||
|
| `script.c` | everything that touches the interpreter |
|
||||||
|
| `enemies.c` | wave table, formation grid, the enemy update hook |
|
||||||
|
| `player.c` | the ship, both bullet kinds, every collision |
|
||||||
|
| `galaga.bas` | every decision an enemy makes |
|
||||||
|
| `interop_test.c` | round-trip proof the boundary works, run by CTest |
|
||||||
|
| `assets/` | sprite/character JSON, and Kenney CC0 art under `assets/art/` |
|
||||||
14
examples/galaga/assets/art/License.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
Space Shooter (Remastered, plus fonts and sounds) by Kenney Vleugels (www.kenney.nl)
|
||||||
|
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
License (CC0)
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/
|
||||||
|
|
||||||
|
You may use these graphics in personal and commercial projects.
|
||||||
|
Credit (Kenney or www.kenney.nl) would be nice but is not mandatory.
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
39
examples/galaga/assets/art/PROVENANCE.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Where this art came from
|
||||||
|
|
||||||
|
Every PNG in this directory is from **Kenney's Space Shooter (Remastered)**, 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/space-shooter-remastered>
|
||||||
|
* Downloaded: 2026-08-04, `kenney_space-shooter-remastered.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/` versions, byte for byte — nothing is resized,
|
||||||
|
recoloured or re-encoded, so the checksum of any of them still matches the
|
||||||
|
distributed archive. `playerShip1_blue.png` sits at the top of `PNG/`; the enemies
|
||||||
|
are from `PNG/Enemies/` and the lasers from `PNG/Lasers/`.
|
||||||
|
|
||||||
|
| File | Size | Used for |
|
||||||
|
|---|---|---|
|
||||||
|
| `playerShip1_blue.png` | 99x75 | the player's ship |
|
||||||
|
| `enemyBlue1.png` | 93x84 | the bee |
|
||||||
|
| `enemyRed2.png` | 104x84 | the butterfly |
|
||||||
|
| `enemyGreen3.png` | 103x84 | the boss, at full health |
|
||||||
|
| `enemyBlack3.png` | 103x84 | the boss at one hit point — same silhouette, drained colour |
|
||||||
|
| `laserBlue01.png` | 9x54 | the player's shot |
|
||||||
|
| `laserRed01.png` | 9x54 | an enemy's shot |
|
||||||
|
| `laserBlue08.png` | 48x46 | the explosion burst |
|
||||||
|
|
||||||
|
The sprites are used at their distributed size: libakgl draws a sprite at the
|
||||||
|
sprite's own dimensions (`akgl_Actor.scale` is overwritten every frame — libakgl
|
||||||
|
docs/12-actors.md), so there is no way to draw these smaller, and the game's
|
||||||
|
1280x960 view is sized to fit a ten-column formation of them instead. The boss's
|
||||||
|
damage state is the same shape in a different colour deliberately: the swap has to
|
||||||
|
read at a glance from the top of the screen.
|
||||||
|
|
||||||
|
Everything else on screen — the starfield and the HUD — is drawn by the program
|
||||||
|
with `akgl_draw_point()` and the UI layer. See `../../README.md` for the run
|
||||||
|
instructions and the two tutorial chapters (docs/20, docs/21) for why only the
|
||||||
|
things that move are artwork.
|
||||||
BIN
examples/galaga/assets/art/enemyBlack3.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
examples/galaga/assets/art/enemyBlue1.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
examples/galaga/assets/art/enemyGreen3.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
examples/galaga/assets/art/enemyRed2.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
examples/galaga/assets/art/laserBlue01.png
Normal file
|
After Width: | Height: | Size: 744 B |
BIN
examples/galaga/assets/art/laserBlue08.png
Normal file
|
After Width: | Height: | Size: 882 B |
BIN
examples/galaga/assets/art/laserRed01.png
Normal file
|
After Width: | Height: | Size: 735 B |
BIN
examples/galaga/assets/art/playerShip1_blue.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
16
examples/galaga/assets/character_galaga_bee.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "galaga_bee",
|
||||||
|
"speedtime": 200,
|
||||||
|
"speed_x": 0.0,
|
||||||
|
"speed_y": 0.0,
|
||||||
|
"acceleration_x": 0.0,
|
||||||
|
"acceleration_y": 0.0,
|
||||||
|
"sprite_mappings": [
|
||||||
|
{
|
||||||
|
"state": [
|
||||||
|
"AKGL_ACTOR_STATE_ALIVE"
|
||||||
|
],
|
||||||
|
"sprite": "galaga_bee"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
examples/galaga/assets/character_galaga_boom.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "galaga_boom",
|
||||||
|
"speedtime": 200,
|
||||||
|
"speed_x": 0.0,
|
||||||
|
"speed_y": 0.0,
|
||||||
|
"acceleration_x": 0.0,
|
||||||
|
"acceleration_y": 0.0,
|
||||||
|
"sprite_mappings": [
|
||||||
|
{
|
||||||
|
"state": [
|
||||||
|
"AKGL_ACTOR_STATE_ALIVE"
|
||||||
|
],
|
||||||
|
"sprite": "galaga_boom"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
23
examples/galaga/assets/character_galaga_boss.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "galaga_boss",
|
||||||
|
"speedtime": 200,
|
||||||
|
"speed_x": 0.0,
|
||||||
|
"speed_y": 0.0,
|
||||||
|
"acceleration_x": 0.0,
|
||||||
|
"acceleration_y": 0.0,
|
||||||
|
"sprite_mappings": [
|
||||||
|
{
|
||||||
|
"state": [
|
||||||
|
"AKGL_ACTOR_STATE_ALIVE"
|
||||||
|
],
|
||||||
|
"sprite": "galaga_boss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"state": [
|
||||||
|
"AKGL_ACTOR_STATE_ALIVE",
|
||||||
|
"AKGL_ACTOR_STATE_UNDEFINED_13"
|
||||||
|
],
|
||||||
|
"sprite": "galaga_boss_hurt"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
examples/galaga/assets/character_galaga_butterfly.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "galaga_butterfly",
|
||||||
|
"speedtime": 200,
|
||||||
|
"speed_x": 0.0,
|
||||||
|
"speed_y": 0.0,
|
||||||
|
"acceleration_x": 0.0,
|
||||||
|
"acceleration_y": 0.0,
|
||||||
|
"sprite_mappings": [
|
||||||
|
{
|
||||||
|
"state": [
|
||||||
|
"AKGL_ACTOR_STATE_ALIVE"
|
||||||
|
],
|
||||||
|
"sprite": "galaga_butterfly"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
examples/galaga/assets/character_galaga_enemyshot.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "galaga_enemyshot",
|
||||||
|
"speedtime": 200,
|
||||||
|
"speed_x": 0.0,
|
||||||
|
"speed_y": 0.0,
|
||||||
|
"acceleration_x": 0.0,
|
||||||
|
"acceleration_y": 0.0,
|
||||||
|
"sprite_mappings": [
|
||||||
|
{
|
||||||
|
"state": [
|
||||||
|
"AKGL_ACTOR_STATE_ALIVE"
|
||||||
|
],
|
||||||
|
"sprite": "galaga_enemyshot"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
examples/galaga/assets/character_galaga_player.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "galaga_player",
|
||||||
|
"speedtime": 200,
|
||||||
|
"speed_x": 0.0,
|
||||||
|
"speed_y": 0.0,
|
||||||
|
"acceleration_x": 0.0,
|
||||||
|
"acceleration_y": 0.0,
|
||||||
|
"sprite_mappings": [
|
||||||
|
{
|
||||||
|
"state": [
|
||||||
|
"AKGL_ACTOR_STATE_ALIVE"
|
||||||
|
],
|
||||||
|
"sprite": "galaga_player"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
examples/galaga/assets/character_galaga_playershot.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "galaga_playershot",
|
||||||
|
"speedtime": 200,
|
||||||
|
"speed_x": 0.0,
|
||||||
|
"speed_y": 0.0,
|
||||||
|
"acceleration_x": 0.0,
|
||||||
|
"acceleration_y": 0.0,
|
||||||
|
"sprite_mappings": [
|
||||||
|
{
|
||||||
|
"state": [
|
||||||
|
"AKGL_ACTOR_STATE_ALIVE"
|
||||||
|
],
|
||||||
|
"sprite": "galaga_playershot"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
16
examples/galaga/assets/sprite_galaga_bee.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"spritesheet": {
|
||||||
|
"filename": "art/enemyBlue1.png",
|
||||||
|
"frame_width": 93,
|
||||||
|
"frame_height": 84
|
||||||
|
},
|
||||||
|
"name": "galaga_bee",
|
||||||
|
"width": 93,
|
||||||
|
"height": 84,
|
||||||
|
"speed": 200,
|
||||||
|
"loop": false,
|
||||||
|
"loopReverse": false,
|
||||||
|
"frames": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
16
examples/galaga/assets/sprite_galaga_boom.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"spritesheet": {
|
||||||
|
"filename": "art/laserBlue08.png",
|
||||||
|
"frame_width": 48,
|
||||||
|
"frame_height": 46
|
||||||
|
},
|
||||||
|
"name": "galaga_boom",
|
||||||
|
"width": 48,
|
||||||
|
"height": 46,
|
||||||
|
"speed": 200,
|
||||||
|
"loop": false,
|
||||||
|
"loopReverse": false,
|
||||||
|
"frames": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
16
examples/galaga/assets/sprite_galaga_boss.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"spritesheet": {
|
||||||
|
"filename": "art/enemyGreen3.png",
|
||||||
|
"frame_width": 103,
|
||||||
|
"frame_height": 84
|
||||||
|
},
|
||||||
|
"name": "galaga_boss",
|
||||||
|
"width": 103,
|
||||||
|
"height": 84,
|
||||||
|
"speed": 200,
|
||||||
|
"loop": false,
|
||||||
|
"loopReverse": false,
|
||||||
|
"frames": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
16
examples/galaga/assets/sprite_galaga_boss_hurt.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"spritesheet": {
|
||||||
|
"filename": "art/enemyBlack3.png",
|
||||||
|
"frame_width": 103,
|
||||||
|
"frame_height": 84
|
||||||
|
},
|
||||||
|
"name": "galaga_boss_hurt",
|
||||||
|
"width": 103,
|
||||||
|
"height": 84,
|
||||||
|
"speed": 200,
|
||||||
|
"loop": false,
|
||||||
|
"loopReverse": false,
|
||||||
|
"frames": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
16
examples/galaga/assets/sprite_galaga_butterfly.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"spritesheet": {
|
||||||
|
"filename": "art/enemyRed2.png",
|
||||||
|
"frame_width": 104,
|
||||||
|
"frame_height": 84
|
||||||
|
},
|
||||||
|
"name": "galaga_butterfly",
|
||||||
|
"width": 104,
|
||||||
|
"height": 84,
|
||||||
|
"speed": 200,
|
||||||
|
"loop": false,
|
||||||
|
"loopReverse": false,
|
||||||
|
"frames": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
16
examples/galaga/assets/sprite_galaga_enemyshot.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"spritesheet": {
|
||||||
|
"filename": "art/laserRed01.png",
|
||||||
|
"frame_width": 9,
|
||||||
|
"frame_height": 54
|
||||||
|
},
|
||||||
|
"name": "galaga_enemyshot",
|
||||||
|
"width": 9,
|
||||||
|
"height": 54,
|
||||||
|
"speed": 200,
|
||||||
|
"loop": false,
|
||||||
|
"loopReverse": false,
|
||||||
|
"frames": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
16
examples/galaga/assets/sprite_galaga_player.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"spritesheet": {
|
||||||
|
"filename": "art/playerShip1_blue.png",
|
||||||
|
"frame_width": 99,
|
||||||
|
"frame_height": 75
|
||||||
|
},
|
||||||
|
"name": "galaga_player",
|
||||||
|
"width": 99,
|
||||||
|
"height": 75,
|
||||||
|
"speed": 200,
|
||||||
|
"loop": false,
|
||||||
|
"loopReverse": false,
|
||||||
|
"frames": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
16
examples/galaga/assets/sprite_galaga_playershot.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"spritesheet": {
|
||||||
|
"filename": "art/laserBlue01.png",
|
||||||
|
"frame_width": 9,
|
||||||
|
"frame_height": 54
|
||||||
|
},
|
||||||
|
"name": "galaga_playershot",
|
||||||
|
"width": 9,
|
||||||
|
"height": 54,
|
||||||
|
"speed": 200,
|
||||||
|
"loop": false,
|
||||||
|
"loopReverse": false,
|
||||||
|
"frames": [
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
319
examples/galaga/enemies.c
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
/**
|
||||||
|
* @file enemies.c
|
||||||
|
* @brief The formation, the wave, and the hook that hands each enemy to BASIC.
|
||||||
|
*
|
||||||
|
* C owns the grid, the wave table and the spawn timing; BASIC owns everything
|
||||||
|
* an enemy does after it exists. The formation slot arrives in SELF@.HOMEX% /
|
||||||
|
* HOMEY%, so even the idle breathing of the grid is the script's, computed
|
||||||
|
* relative to home.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <akgl/actor.h>
|
||||||
|
#include <akgl/error.h>
|
||||||
|
#include <akgl/game.h>
|
||||||
|
#include <akgl/heap.h>
|
||||||
|
|
||||||
|
#include "galaga.h"
|
||||||
|
|
||||||
|
galaga_Enemy galaga_enemies[GALAGA_MAX_ENEMIES];
|
||||||
|
akgl_Actor *galaga_enemy_actors[GALAGA_MAX_ENEMIES];
|
||||||
|
|
||||||
|
/* Explosion lifetimes, indexed by heap slot. An explosion is an actor with
|
||||||
|
* nothing to decide, so its whole state is one countdown. */
|
||||||
|
static float BOOM_TTL[AKGL_MAX_HEAP_ACTOR];
|
||||||
|
|
||||||
|
/* A spawn serial per shot so registry names never collide while two shots
|
||||||
|
* with the same slot number are briefly both alive. */
|
||||||
|
static uint32_t SHOT_SERIAL = 0;
|
||||||
|
static uint32_t BOOM_SERIAL = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The wave, one row per formation row. Columns are 0..9 at GALAGA_COL_PITCH;
|
||||||
|
* `first` and `count` say which columns the row fills. 40 enemies: 4 bosses,
|
||||||
|
* 16 butterflies, 20 bees -- 59 of the 64 actor heap slots at peak, counting
|
||||||
|
* the player, two player shots, eight enemy shots and eight explosions.
|
||||||
|
*/
|
||||||
|
static const struct
|
||||||
|
{
|
||||||
|
int32_t kind; /* GALAGA_ENEMY_* */
|
||||||
|
int row; /* formation row */
|
||||||
|
int first; /* first column filled */
|
||||||
|
int count; /* columns filled */
|
||||||
|
int32_t hp;
|
||||||
|
}
|
||||||
|
WAVE_ROWS[] = {
|
||||||
|
/* kind row first count hp */
|
||||||
|
{ GALAGA_ENEMY_BOSS, 0, 3, 4, 2 },
|
||||||
|
{ GALAGA_ENEMY_BUTTERFLY, 1, 1, 8, 1 },
|
||||||
|
{ GALAGA_ENEMY_BUTTERFLY, 2, 1, 8, 1 },
|
||||||
|
{ GALAGA_ENEMY_BEE, 3, 0, 10, 1 },
|
||||||
|
{ GALAGA_ENEMY_BEE, 4, 0, 10, 1 }
|
||||||
|
};
|
||||||
|
#define WAVE_ROW_COUNT ((int)(sizeof(WAVE_ROWS) / sizeof(WAVE_ROWS[0])))
|
||||||
|
|
||||||
|
/* Enemy kind -> character name, the render half of the dispatch table. */
|
||||||
|
static char *ENEMY_CHARACTER[GALAGA_ENEMY_KINDS] = {
|
||||||
|
"galaga_bee",
|
||||||
|
"galaga_butterfly",
|
||||||
|
"galaga_boss"
|
||||||
|
};
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------- random --- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The engine is this script's source of randomness: it refreshes GAME@.ROLL%
|
||||||
|
* each frame and SELF@.ROLL% each call from this PRNG. A hand-rolled LCG
|
||||||
|
* rather than rand() so a headless run is the same game on every libc, which
|
||||||
|
* is what lets interop_test.c assert exact counts. The dialect gained a native
|
||||||
|
* RND (issue #16) after this example was written; the field stays because RND
|
||||||
|
* would reintroduce exactly the per-machine variation this avoids.
|
||||||
|
*
|
||||||
|
* The BASIC-visible name is ROLL%, not RND%: host field names share a
|
||||||
|
* namespace with verbs and functions, so RND stopped being available as one.
|
||||||
|
*/
|
||||||
|
static uint32_t PRNG_STATE = 0x12345678u;
|
||||||
|
|
||||||
|
float galaga_random(void)
|
||||||
|
{
|
||||||
|
PRNG_STATE = PRNG_STATE * 1664525u + 1013904223u;
|
||||||
|
return (float)(PRNG_STATE >> 8) / (float)0x01000000u;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------- helpers --- */
|
||||||
|
|
||||||
|
static akerr_ErrorContext *release_actor(akgl_Actor *actor)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
|
||||||
|
PASS(errctx, akgl_heap_release_actor(actor));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- shots --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Move an enemy shot; release it once it has left the screen.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *enemy_shot_update(akgl_Actor *obj)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
|
||||||
|
obj->y += 380.0f * galaga_game.dt;
|
||||||
|
if ( obj->y > (float)GALAGA_VIEW_HEIGHT + 60.0f ) {
|
||||||
|
galaga_game.enemy_shots_live -= 1;
|
||||||
|
PASS(errctx, release_actor(obj));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Consume an enemy's fire flag: take an actor and aim it downward.
|
||||||
|
*
|
||||||
|
* The script only raises a flag. Spawning takes a slot from the actor heap,
|
||||||
|
* and pool exhaustion must be a C-side refusal with the house error context --
|
||||||
|
* so C consumes the flag and does the spawn. The engine also enforces the
|
||||||
|
* eight-shot cap by simply not consuming the flag's wish.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *enemy_fire(galaga_Enemy *enemy, akgl_Actor *from)
|
||||||
|
{
|
||||||
|
akgl_Actor *shot = NULL;
|
||||||
|
char name[32];
|
||||||
|
int count = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, enemy, AKERR_NULLPOINTER, "enemy");
|
||||||
|
FAIL_ZERO_RETURN(errctx, from, AKERR_NULLPOINTER, "from");
|
||||||
|
|
||||||
|
enemy->fire = 0;
|
||||||
|
if ( galaga_game.enemy_shots_live >= GALAGA_MAX_ENEMY_SHOTS ) {
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
SHOT_SERIAL += 1;
|
||||||
|
PASS(errctx, aksl_snprintf(&count, name, sizeof(name), "eshot%u", SHOT_SERIAL));
|
||||||
|
PASS(errctx, akgl_heap_next_actor(&shot));
|
||||||
|
PASS(errctx, akgl_actor_initialize(shot, name));
|
||||||
|
PASS(errctx, akgl_actor_set_character(shot, "galaga_enemyshot"));
|
||||||
|
/* AFTER initialize: it resets all seven hooks. */
|
||||||
|
shot->updatefunc = &enemy_shot_update;
|
||||||
|
shot->movement_controls_face = false;
|
||||||
|
shot->state = AKGL_ACTOR_STATE_ALIVE;
|
||||||
|
/* akgl_actor_initialize() does not raise `visible`; a hand-spawned actor
|
||||||
|
* that skips this line exists, moves and collides -- invisibly. */
|
||||||
|
shot->visible = true;
|
||||||
|
/* Actor x/y is a sprite's top-left corner; the shot leaves the enemy's
|
||||||
|
* midline. Enemy sprites run 93..104 wide, the shot is 9. */
|
||||||
|
shot->x = from->x + 46.0f;
|
||||||
|
shot->y = from->y + 60.0f;
|
||||||
|
|
||||||
|
galaga_game.enemy_shots_live += 1;
|
||||||
|
galaga_game.shots[enemy->kind] += 1;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------- explosions --- */
|
||||||
|
|
||||||
|
static akerr_ErrorContext *boom_update(akgl_Actor *obj)
|
||||||
|
{
|
||||||
|
ptrdiff_t slot = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
|
||||||
|
slot = obj - akgl_heap_actors;
|
||||||
|
BOOM_TTL[slot] -= galaga_game.dt;
|
||||||
|
if ( BOOM_TTL[slot] <= 0.0f ) {
|
||||||
|
PASS(errctx, release_actor(obj));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *galaga_boom_spawn(float x, float y)
|
||||||
|
{
|
||||||
|
akgl_Actor *boom = NULL;
|
||||||
|
char name[32];
|
||||||
|
int count = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akgl_heap_next_actor(&boom));
|
||||||
|
BOOM_SERIAL += 1;
|
||||||
|
CATCH(errctx, aksl_snprintf(&count, name, sizeof(name), "boom%u", BOOM_SERIAL));
|
||||||
|
CATCH(errctx, akgl_actor_initialize(boom, name));
|
||||||
|
CATCH(errctx, akgl_actor_set_character(boom, "galaga_boom"));
|
||||||
|
boom->updatefunc = &boom_update;
|
||||||
|
boom->movement_controls_face = false;
|
||||||
|
boom->state = AKGL_ACTOR_STATE_ALIVE;
|
||||||
|
boom->visible = true;
|
||||||
|
boom->x = x;
|
||||||
|
boom->y = y;
|
||||||
|
BOOM_TTL[boom - akgl_heap_actors] = 0.25f;
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE(errctx, AKGL_ERR_HEAP) {
|
||||||
|
/* Explosions are decoration. When the heap is momentarily full the
|
||||||
|
* right outcome is no explosion, not a dead frame -- this is the one
|
||||||
|
* spawn that absorbs exhaustion. */
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------- enemies --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The custom update hook: one enemy, once per frame, thought in BASIC.
|
||||||
|
*
|
||||||
|
* The whole body is the protocol from docs/20: refresh the inbox, hand the
|
||||||
|
* pair to the script, consume the outbox. akgl_game_update() calls this in
|
||||||
|
* place of akgl_actor_update() because spawn replaced the hook.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *enemy_update(akgl_Actor *obj)
|
||||||
|
{
|
||||||
|
galaga_Enemy *enemy = NULL;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
|
||||||
|
enemy = (galaga_Enemy *)obj->actorData;
|
||||||
|
FAIL_ZERO_RETURN(errctx, enemy, AKERR_NULLPOINTER, "an enemy actor with no galaga_Enemy attached");
|
||||||
|
|
||||||
|
enemy->rnd = galaga_random();
|
||||||
|
PASS(errctx, galaga_script_update_enemy(enemy, obj, galaga_game.dt));
|
||||||
|
if ( enemy->fire != 0 ) {
|
||||||
|
PASS(errctx, enemy_fire(enemy, obj));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *galaga_wave_spawn(void)
|
||||||
|
{
|
||||||
|
akgl_Actor *actor = NULL;
|
||||||
|
galaga_Enemy *enemy = NULL;
|
||||||
|
char name[32];
|
||||||
|
int row = 0;
|
||||||
|
int col = 0;
|
||||||
|
int index = 0;
|
||||||
|
int count = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
for ( row = 0; row < WAVE_ROW_COUNT; row++ ) {
|
||||||
|
for ( col = 0; col < WAVE_ROWS[row].count; col++ ) {
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (index >= GALAGA_MAX_ENEMIES), AKERR_OUTOFBOUNDS,
|
||||||
|
"The wave table places more than %d enemies", GALAGA_MAX_ENEMIES);
|
||||||
|
enemy = &galaga_enemies[index];
|
||||||
|
memset(enemy, 0, sizeof(*enemy));
|
||||||
|
enemy->kind = WAVE_ROWS[row].kind;
|
||||||
|
enemy->state = GALAGA_ES_ENTERING;
|
||||||
|
enemy->homex = (float)(GALAGA_FORM_LEFT
|
||||||
|
+ (WAVE_ROWS[row].first + col) * GALAGA_COL_PITCH);
|
||||||
|
enemy->homey = (float)(GALAGA_FORM_TOP + WAVE_ROWS[row].row * GALAGA_ROW_PITCH);
|
||||||
|
enemy->hp = WAVE_ROWS[row].hp;
|
||||||
|
/* Stagger the entries: each enemy's clock starts in the past, and
|
||||||
|
* the script holds still until its own t crosses zero. */
|
||||||
|
enemy->t = -0.08f * (float)index;
|
||||||
|
|
||||||
|
PASS(errctx, aksl_snprintf(&count, name, sizeof(name), "enemy%02d", index));
|
||||||
|
PASS(errctx, akgl_heap_next_actor(&actor));
|
||||||
|
PASS(errctx, akgl_actor_initialize(actor, name));
|
||||||
|
PASS(errctx, akgl_actor_set_character(actor, ENEMY_CHARACTER[enemy->kind]));
|
||||||
|
/* AFTER initialize: it resets all seven hooks. */
|
||||||
|
actor->updatefunc = &enemy_update;
|
||||||
|
actor->actorData = enemy;
|
||||||
|
/* Nothing here moves by state bits, and an actor whose state word
|
||||||
|
* matches no character mapping is silently not drawn -- so facing
|
||||||
|
* stays entirely out of the state word. */
|
||||||
|
actor->movement_controls_face = false;
|
||||||
|
actor->state = AKGL_ACTOR_STATE_ALIVE;
|
||||||
|
/* akgl_actor_initialize() does not raise `visible` -- the map
|
||||||
|
* loader copies it from map data, and there is no map here. Skip
|
||||||
|
* this and the whole wave exists, moves, fires and dies without
|
||||||
|
* ever being drawn. */
|
||||||
|
actor->visible = true;
|
||||||
|
/* Off screen above, pouring in from whichever side is closer. */
|
||||||
|
actor->x = (enemy->homex < (float)GALAGA_VIEW_WIDTH / 2.0f)
|
||||||
|
? -80.0f : (float)GALAGA_VIEW_WIDTH + 80.0f;
|
||||||
|
actor->y = -80.0f;
|
||||||
|
|
||||||
|
galaga_enemy_actors[index] = actor;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *galaga_wave_release(void)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
|
||||||
|
if ( galaga_enemy_actors[i] != NULL ) {
|
||||||
|
PASS(errctx, release_actor(galaga_enemy_actors[i]));
|
||||||
|
galaga_enemy_actors[i] = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
int galaga_enemies_alive(void)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
int alive = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
|
||||||
|
if ( galaga_enemy_actors[i] != NULL ) {
|
||||||
|
alive += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return alive;
|
||||||
|
}
|
||||||
119
examples/galaga/galaga.bas
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
REM GALAGA enemy behavior. The C engine loads this file, runs it once so the
|
||||||
|
REM definitions exist, and then calls one UPDATE function per enemy per frame.
|
||||||
|
REM There is no top-level code: definitions, then END.
|
||||||
|
REM
|
||||||
|
REM Names the engine binds before every call:
|
||||||
|
REM SELF@ - this enemy's record (ENEMY): the state machine's memory
|
||||||
|
REM ACTOR@ - the engine's live actor (ACTOR): position is the real thing
|
||||||
|
REM GAME@ - shared frame state (GAME): player position, wave, randomness
|
||||||
|
REM
|
||||||
|
REM SELF@.STATE# bits: 1 = entering 2 = in formation 4 = diving
|
||||||
|
REM
|
||||||
|
REM Two rules of this dialect that bite here, both from docs/03:
|
||||||
|
REM - the LEFT operand decides integer or float arithmetic, so a float
|
||||||
|
REM always goes first: SELF@.T% * 150 + 260, never 260 + 150 * SELF@.T%
|
||||||
|
REM - RETURN at the start of a line ends the DEF body, so every early
|
||||||
|
REM return rides an IF ... THEN, and only the last RETURN starts a line
|
||||||
|
|
||||||
|
REM Ease toward the formation slot, with a little entry swirl.
|
||||||
|
REM Answers 1 once the slot is reached, else 0.
|
||||||
|
DEF GLIDEHOME(DT%)
|
||||||
|
DX% = SELF@.HOMEX% - ACTOR@.X%
|
||||||
|
DY% = SELF@.HOMEY% - ACTOR@.Y%
|
||||||
|
K% = DT% * 4.5
|
||||||
|
IF K% > 1 THEN K% = 1
|
||||||
|
ACTOR@.X% = ACTOR@.X% + DX% * K% + SIN(SELF@.T% * 6) * 90 * DT%
|
||||||
|
ACTOR@.Y% = ACTOR@.Y% + DY% * K%
|
||||||
|
IF ABS(DX%) < 3 AND ABS(DY%) < 3 THEN RETURN 1
|
||||||
|
RETURN 0
|
||||||
|
|
||||||
|
REM One frame of a dive: accelerate downward, weave, lean toward the
|
||||||
|
REM player's column, and glide back in from the top after falling out.
|
||||||
|
DEF DIVESTEP(DT%, WEAVE%, LEAD%)
|
||||||
|
SPD% = SELF@.T% * 150 + 260
|
||||||
|
ACTOR@.Y% = ACTOR@.Y% + SPD% * DT%
|
||||||
|
ACTOR@.X% = ACTOR@.X% + SIN(SELF@.T% * 4) * WEAVE% * DT%
|
||||||
|
DX% = GAME@.PLAYERX% - ACTOR@.X%
|
||||||
|
IF DX% > 220 THEN DX% = 220
|
||||||
|
IF DX% < -220 THEN DX% = -220
|
||||||
|
ACTOR@.X% = ACTOR@.X% + DX% * LEAD% * DT%
|
||||||
|
IF ACTOR@.Y% > 1040 THEN BEGIN
|
||||||
|
ACTOR@.Y% = 0.0 - 90
|
||||||
|
SELF@.STATE# = 1
|
||||||
|
SELF@.T% = 0
|
||||||
|
BEND
|
||||||
|
RETURN 0
|
||||||
|
|
||||||
|
REM Raise the fire flag when diving roughly above the player. The engine
|
||||||
|
REM consumes FIRE# and does the spawning; the script only wishes.
|
||||||
|
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@.ROLL% < DT% * 1.5 THEN SELF@.FIRE# = 1
|
||||||
|
RETURN 0
|
||||||
|
|
||||||
|
REM Bee: enter, breathe in formation, occasionally dive nearly straight.
|
||||||
|
DEF UPDATEBEE(DT%)
|
||||||
|
SELF@.T% = SELF@.T% + DT%
|
||||||
|
IF SELF@.T% < 0 THEN RETURN 0
|
||||||
|
S# = SELF@.STATE#
|
||||||
|
IF (S# AND 1) > 0 THEN BEGIN
|
||||||
|
R# = GLIDEHOME(DT%)
|
||||||
|
IF R# = 1 THEN SELF@.STATE# = 2 : SELF@.T% = 0
|
||||||
|
BEND
|
||||||
|
IF (S# AND 2) > 0 THEN BEGIN
|
||||||
|
ACTOR@.X% = SELF@.HOMEX% + SIN(SELF@.T% * 1.7) * 16
|
||||||
|
ACTOR@.Y% = SELF@.HOMEY%
|
||||||
|
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)
|
||||||
|
R# = DECIDEFIRE(DT%)
|
||||||
|
BEND
|
||||||
|
RETURN 0
|
||||||
|
|
||||||
|
REM Butterfly: the same machine with a wide lateral weave on the dive.
|
||||||
|
DEF UPDATEBFLY(DT%)
|
||||||
|
SELF@.T% = SELF@.T% + DT%
|
||||||
|
IF SELF@.T% < 0 THEN RETURN 0
|
||||||
|
S# = SELF@.STATE#
|
||||||
|
IF (S# AND 1) > 0 THEN BEGIN
|
||||||
|
R# = GLIDEHOME(DT%)
|
||||||
|
IF R# = 1 THEN SELF@.STATE# = 2 : SELF@.T% = 0
|
||||||
|
BEND
|
||||||
|
IF (S# AND 2) > 0 THEN BEGIN
|
||||||
|
ACTOR@.X% = SELF@.HOMEX% + SIN(SELF@.T% * 2.1) * 24
|
||||||
|
ACTOR@.Y% = SELF@.HOMEY%
|
||||||
|
IF SELF@.ROLL% < DT% * 0.05 THEN SELF@.STATE# = 4 : SELF@.T% = 0
|
||||||
|
BEND
|
||||||
|
IF (S# AND 4) > 0 THEN BEGIN
|
||||||
|
R# = DIVESTEP(DT%, 260, 0.1)
|
||||||
|
R# = DECIDEFIRE(DT%)
|
||||||
|
BEND
|
||||||
|
RETURN 0
|
||||||
|
|
||||||
|
REM Boss: two hit points, a slow sway, and a dive that leads the player.
|
||||||
|
REM At one hit point it raises actor state bit 13 (8192), and the engine's
|
||||||
|
REM character mapping swaps the sprite - the boundary crossed the other way.
|
||||||
|
DEF UPDATEBOSS(DT%)
|
||||||
|
SELF@.T% = SELF@.T% + DT%
|
||||||
|
IF SELF@.T% < 0 THEN RETURN 0
|
||||||
|
IF SELF@.HP# = 1 THEN ACTOR@.STATE# = ACTOR@.STATE# OR 8192
|
||||||
|
S# = SELF@.STATE#
|
||||||
|
IF (S# AND 1) > 0 THEN BEGIN
|
||||||
|
R# = GLIDEHOME(DT%)
|
||||||
|
IF R# = 1 THEN SELF@.STATE# = 2 : SELF@.T% = 0
|
||||||
|
BEND
|
||||||
|
IF (S# AND 2) > 0 THEN BEGIN
|
||||||
|
ACTOR@.X% = SELF@.HOMEX% + SIN(SELF@.T% * 1.1) * 10
|
||||||
|
ACTOR@.Y% = SELF@.HOMEY%
|
||||||
|
IF SELF@.ROLL% < DT% * 0.03 THEN SELF@.STATE# = 4 : SELF@.T% = 0
|
||||||
|
BEND
|
||||||
|
IF (S# AND 4) > 0 THEN BEGIN
|
||||||
|
R# = DIVESTEP(DT%, 60, 0.9)
|
||||||
|
R# = DECIDEFIRE(DT%)
|
||||||
|
BEND
|
||||||
|
RETURN 0
|
||||||
|
|
||||||
|
END
|
||||||
155
examples/galaga/galaga.h
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/**
|
||||||
|
* @file galaga.h
|
||||||
|
* @brief Shared declarations for the GALAGA embedding example.
|
||||||
|
*
|
||||||
|
* The engine is C on libakgl; the enemies think in BASIC. Everything the two
|
||||||
|
* sides share crosses in exactly one place: the three structures below, which
|
||||||
|
* script.c registers as host types so a script reads and writes them directly.
|
||||||
|
* docs/20-tutorial-galaga.md and docs/21-tutorial-galaga-enemies.md build this
|
||||||
|
* program from an empty file; the split between files follows the split
|
||||||
|
* between chapters.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _GALAGA_H_
|
||||||
|
#define _GALAGA_H_
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akgl/actor.h>
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------- geometry --- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The view is sized to the artwork rather than the other way round: the Kenney
|
||||||
|
* sprites are ~100 pixels wide, libakgl has no way to draw a sprite smaller
|
||||||
|
* than it is (akgl_Actor.scale is overwritten every frame -- libakgl
|
||||||
|
* docs/12-actors.md), and a ten-column formation of them needs 1120 pixels.
|
||||||
|
*/
|
||||||
|
#define GALAGA_VIEW_WIDTH 1280
|
||||||
|
#define GALAGA_VIEW_HEIGHT 960
|
||||||
|
|
||||||
|
#define GALAGA_FORM_COLUMNS 10 /* formation width, in slots */
|
||||||
|
#define GALAGA_FORM_LEFT 136 /* x of column 0, map pixels */
|
||||||
|
#define GALAGA_FORM_TOP 120 /* y of row 0, map pixels */
|
||||||
|
#define GALAGA_COL_PITCH 112
|
||||||
|
#define GALAGA_ROW_PITCH 100
|
||||||
|
|
||||||
|
#define GALAGA_PLAYER_Y 860.0f
|
||||||
|
#define GALAGA_PLAYER_SPEED 420.0f /* map pixels per second */
|
||||||
|
#define GALAGA_PLAYER_MARGIN 60.0f /* how close to the edge it may go */
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------- entities --- */
|
||||||
|
|
||||||
|
#define GALAGA_ENEMY_BEE 0
|
||||||
|
#define GALAGA_ENEMY_BUTTERFLY 1
|
||||||
|
#define GALAGA_ENEMY_BOSS 2
|
||||||
|
#define GALAGA_ENEMY_KINDS 3
|
||||||
|
|
||||||
|
#define GALAGA_MAX_ENEMIES 40
|
||||||
|
#define GALAGA_MAX_PLAYER_SHOTS 2 /* the classic two-on-screen rule */
|
||||||
|
#define GALAGA_MAX_ENEMY_SHOTS 8
|
||||||
|
|
||||||
|
/*
|
||||||
|
* galaga_Enemy.state bits. The script owns these transitions; the engine only
|
||||||
|
* writes the word at spawn and when a script error forces an enemy dumb.
|
||||||
|
* galaga.bas spells the same three values as literals, with a REM naming them.
|
||||||
|
*
|
||||||
|
* 8 0
|
||||||
|
* 0 0 0 0 0 1 1 1
|
||||||
|
* | | `-- ENTERING: flying its entry path toward the formation slot
|
||||||
|
* | `---- FORMATION: holding (and breathing around) homex/homey
|
||||||
|
* `------ DIVING: attacking, off the grid until it leaves the screen
|
||||||
|
*/
|
||||||
|
#define GALAGA_ES_ENTERING (1 << 0)
|
||||||
|
#define GALAGA_ES_FORMATION (1 << 1)
|
||||||
|
#define GALAGA_ES_DIVING (1 << 2)
|
||||||
|
|
||||||
|
/** @brief One enemy, as both sides see it. Hangs off akgl_Actor.actorData. */
|
||||||
|
typedef struct galaga_Enemy
|
||||||
|
{
|
||||||
|
int32_t kind; /* GALAGA_ENEMY_BEE / BUTTERFLY / BOSS */
|
||||||
|
int32_t state; /* GALAGA_ES_* bit flags */
|
||||||
|
float homex; /* formation slot, in map pixels */
|
||||||
|
float homey;
|
||||||
|
float t; /* parametric clock for the current maneuver */
|
||||||
|
int32_t hp;
|
||||||
|
int32_t fire; /* outbox: script sets 1, engine consumes */
|
||||||
|
float rnd; /* inbox: fresh 0..1 each call; ROLL% in BASIC */
|
||||||
|
} galaga_Enemy;
|
||||||
|
|
||||||
|
/** @brief Frame state every enemy may read. Bound once as GAME@. */
|
||||||
|
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; ROLL% to the script */
|
||||||
|
} galaga_Shared;
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------- screens --- */
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
GALAGA_SCREEN_TITLE = 0,
|
||||||
|
GALAGA_SCREEN_PLAY,
|
||||||
|
GALAGA_SCREEN_GAMEOVER,
|
||||||
|
GALAGA_SCREEN_VICTORY
|
||||||
|
} galaga_Screen;
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ game state --- */
|
||||||
|
|
||||||
|
typedef struct galaga_Game
|
||||||
|
{
|
||||||
|
galaga_Screen screen;
|
||||||
|
int frame;
|
||||||
|
float dt; /* seconds, clamped; see main.c */
|
||||||
|
bool autoplay;
|
||||||
|
|
||||||
|
int score;
|
||||||
|
int lives;
|
||||||
|
int kills[GALAGA_ENEMY_KINDS];
|
||||||
|
int shots[GALAGA_ENEMY_KINDS]; /* shots each kind fired */
|
||||||
|
int script_errors;
|
||||||
|
|
||||||
|
akgl_Actor *player;
|
||||||
|
float fire_cooldown;
|
||||||
|
float respawn_timer; /* > 0 while the player is invulnerable */
|
||||||
|
bool firing;
|
||||||
|
bool moveleft;
|
||||||
|
bool moveright;
|
||||||
|
|
||||||
|
int player_shots_live;
|
||||||
|
int enemy_shots_live;
|
||||||
|
} galaga_Game;
|
||||||
|
|
||||||
|
extern galaga_Game galaga_game;
|
||||||
|
extern galaga_Shared galaga_shared;
|
||||||
|
|
||||||
|
extern galaga_Enemy galaga_enemies[GALAGA_MAX_ENEMIES];
|
||||||
|
extern akgl_Actor *galaga_enemy_actors[GALAGA_MAX_ENEMIES];
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- script --- */
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_script_boot(char *path);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_script_update_enemy(galaga_Enemy *enemy, akgl_Actor *actor, float dt);
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------- enemies --- */
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_wave_spawn(void);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_wave_release(void);
|
||||||
|
int galaga_enemies_alive(void);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_boom_spawn(float x, float y);
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- player --- */
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_player_spawn(void);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_player_controls(void);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_player_autoplay(int frame);
|
||||||
|
|
||||||
|
/** @brief A 0..1 random draw from the engine's own PRNG (see enemies.c). */
|
||||||
|
float galaga_random(void);
|
||||||
|
|
||||||
|
#endif // _GALAGA_H_
|
||||||
265
examples/galaga/interop_test.c
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
/**
|
||||||
|
* @file interop_test.c
|
||||||
|
* @brief Round-trip test for the galaga boundary, hoststruct.c-style.
|
||||||
|
*
|
||||||
|
* Links the real script.c and the real galaga.bas -- not copies -- so this
|
||||||
|
* fails the moment the boundary and the script disagree. The four claims it
|
||||||
|
* pins:
|
||||||
|
*
|
||||||
|
* 1. The script writes the engine's actor memory: a formation enemy's sway
|
||||||
|
* lands in akgl_Actor.x with no marshalling step.
|
||||||
|
* 2. The outbox works: a diving enemy above the player raises FIRE# and the
|
||||||
|
* C side reads it.
|
||||||
|
* 3. The boss flips actor state bit 13 at one hit point -- the boundary
|
||||||
|
* crossed engine-ward.
|
||||||
|
* 4. Sustained calling holds: 24000 calls through the per-call
|
||||||
|
* akbasic_environment_zero() regime, the load a 40-enemy wave puts on
|
||||||
|
* the runtime in ten seconds.
|
||||||
|
*
|
||||||
|
* Exit status equals the number of failed claims.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akgl/actor.h>
|
||||||
|
|
||||||
|
#include "galaga.h"
|
||||||
|
|
||||||
|
#ifndef GALAGA_SCRIPT_PATH
|
||||||
|
#define GALAGA_SCRIPT_PATH "galaga.bas"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* script.c reads these; main.c usually defines them. This test is the host. */
|
||||||
|
galaga_Game galaga_game;
|
||||||
|
galaga_Shared galaga_shared;
|
||||||
|
|
||||||
|
static int FAILURES = 0;
|
||||||
|
|
||||||
|
#define CLAIM(__cond, __text) \
|
||||||
|
if ( !(__cond) ) { \
|
||||||
|
fprintf(stderr, "FAILED: %s\n", __text); \
|
||||||
|
FAILURES += 1; \
|
||||||
|
} else { \
|
||||||
|
printf("ok: %s\n", __text); \
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *run_claims(void)
|
||||||
|
{
|
||||||
|
galaga_Enemy enemy;
|
||||||
|
akgl_Actor actor;
|
||||||
|
int i = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, galaga_script_boot((char *)GALAGA_SCRIPT_PATH));
|
||||||
|
|
||||||
|
/* --- 1: formation sway lands in the actor ---------------------------- */
|
||||||
|
memset(&enemy, 0, sizeof(enemy));
|
||||||
|
memset(&actor, 0, sizeof(actor));
|
||||||
|
enemy.kind = GALAGA_ENEMY_BEE;
|
||||||
|
enemy.state = GALAGA_ES_FORMATION;
|
||||||
|
enemy.homex = 400.0f;
|
||||||
|
enemy.homey = 300.0f;
|
||||||
|
enemy.hp = 1;
|
||||||
|
actor.x = 0.0f;
|
||||||
|
actor.y = 0.0f;
|
||||||
|
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
|
||||||
|
CLAIM((fabsf(actor.x - enemy.homex) <= 16.5f) && (actor.y == enemy.homey),
|
||||||
|
"a formation bee's sway is written into akgl_Actor.x/y by the script");
|
||||||
|
|
||||||
|
/* --- 2: the fire outbox ---------------------------------------------- */
|
||||||
|
memset(&enemy, 0, sizeof(enemy));
|
||||||
|
memset(&actor, 0, sizeof(actor));
|
||||||
|
enemy.kind = GALAGA_ENEMY_BEE;
|
||||||
|
enemy.state = GALAGA_ES_DIVING;
|
||||||
|
enemy.rnd = 0.0f; /* 0.0 < DT% * 1.5: always willing */
|
||||||
|
actor.x = 600.0f;
|
||||||
|
actor.y = 200.0f;
|
||||||
|
galaga_shared.playerx = 610.0f; /* just off the shot's column */
|
||||||
|
galaga_shared.playery = 860.0f; /* well below */
|
||||||
|
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
|
||||||
|
CLAIM(enemy.fire == 1,
|
||||||
|
"a diving bee above the player raises FIRE# for the engine to consume");
|
||||||
|
|
||||||
|
/* --- 3: the boss's hurt bit ------------------------------------------ */
|
||||||
|
memset(&enemy, 0, sizeof(enemy));
|
||||||
|
memset(&actor, 0, sizeof(actor));
|
||||||
|
enemy.kind = GALAGA_ENEMY_BOSS;
|
||||||
|
enemy.state = GALAGA_ES_FORMATION;
|
||||||
|
enemy.homex = 500.0f;
|
||||||
|
enemy.homey = 120.0f;
|
||||||
|
enemy.hp = 1;
|
||||||
|
actor.state = AKGL_ACTOR_STATE_ALIVE;
|
||||||
|
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
|
||||||
|
CLAIM((actor.state & AKGL_ACTOR_STATE_UNDEFINED_13) != 0,
|
||||||
|
"a boss at one hit point raises actor state bit 13 from BASIC");
|
||||||
|
|
||||||
|
/* --- 4: sustained calling --------------------------------------------- */
|
||||||
|
memset(&enemy, 0, sizeof(enemy));
|
||||||
|
memset(&actor, 0, sizeof(actor));
|
||||||
|
enemy.kind = GALAGA_ENEMY_BEE;
|
||||||
|
enemy.state = GALAGA_ES_FORMATION;
|
||||||
|
enemy.homex = 400.0f;
|
||||||
|
enemy.homey = 300.0f;
|
||||||
|
for ( i = 0; i < 24000; i++ ) {
|
||||||
|
enemy.rnd = 0.9f; /* never dive: keep the state put */
|
||||||
|
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
|
||||||
|
}
|
||||||
|
CLAIM(galaga_game.script_errors == 0,
|
||||||
|
"24000 calls survive the per-call akbasic_environment_zero() regime");
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ benchmark --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief UPDATEBEE's state machine, translated line for line into C.
|
||||||
|
*
|
||||||
|
* The native comparator for the benchmark below: the same guard, the same
|
||||||
|
* three branches, the same arithmetic as galaga.bas's UPDATEBEE with its
|
||||||
|
* helpers inlined. Nothing is simplified, so the timing difference is the
|
||||||
|
* interpreter's, not the algorithm's.
|
||||||
|
*/
|
||||||
|
static void native_updatebee(galaga_Enemy *enemy, akgl_Actor *actor, float dt)
|
||||||
|
{
|
||||||
|
float dx = 0.0f;
|
||||||
|
float dy = 0.0f;
|
||||||
|
float k = 0.0f;
|
||||||
|
int32_t s = 0;
|
||||||
|
|
||||||
|
enemy->t += dt;
|
||||||
|
if ( enemy->t < 0.0f ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s = enemy->state;
|
||||||
|
if ( (s & GALAGA_ES_ENTERING) != 0 ) {
|
||||||
|
dx = enemy->homex - actor->x;
|
||||||
|
dy = enemy->homey - actor->y;
|
||||||
|
k = dt * 4.5f;
|
||||||
|
if ( k > 1.0f ) {
|
||||||
|
k = 1.0f;
|
||||||
|
}
|
||||||
|
actor->x += dx * k + sinf(enemy->t * 6.0f) * 90.0f * dt;
|
||||||
|
actor->y += dy * k;
|
||||||
|
if ( fabsf(dx) < 3.0f && fabsf(dy) < 3.0f ) {
|
||||||
|
enemy->state = GALAGA_ES_FORMATION;
|
||||||
|
enemy->t = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( (s & GALAGA_ES_FORMATION) != 0 ) {
|
||||||
|
actor->x = enemy->homex + sinf(enemy->t * 1.7f) * 16.0f;
|
||||||
|
actor->y = enemy->homey;
|
||||||
|
if ( enemy->rnd < dt * 0.04f ) {
|
||||||
|
enemy->state = GALAGA_ES_DIVING;
|
||||||
|
enemy->t = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( (s & GALAGA_ES_DIVING) != 0 ) {
|
||||||
|
actor->y += (enemy->t * 150.0f + 260.0f) * dt;
|
||||||
|
actor->x += sinf(enemy->t * 4.0f) * 130.0f * dt;
|
||||||
|
dx = galaga_shared.playerx - actor->x;
|
||||||
|
if ( dx > 220.0f ) {
|
||||||
|
dx = 220.0f;
|
||||||
|
}
|
||||||
|
if ( dx < -220.0f ) {
|
||||||
|
dx = -220.0f;
|
||||||
|
}
|
||||||
|
actor->x += dx * 0.2f * dt;
|
||||||
|
if ( actor->y > 1040.0f ) {
|
||||||
|
actor->y = -90.0f;
|
||||||
|
enemy->state = GALAGA_ES_ENTERING;
|
||||||
|
enemy->t = 0.0f;
|
||||||
|
}
|
||||||
|
dx = galaga_shared.playerx - actor->x;
|
||||||
|
if ( fabsf(dx) <= 140.0f && actor->y <= galaga_shared.playery
|
||||||
|
&& enemy->rnd < dt * 1.5f ) {
|
||||||
|
enemy->fire = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static double seconds_since(const struct timespec *t0)
|
||||||
|
{
|
||||||
|
struct timespec t1;
|
||||||
|
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||||
|
return (double)(t1.tv_sec - t0->tv_sec) + (double)(t1.tv_nsec - t0->tv_nsec) / 1e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The cost of thinking in BASIC, measured against the same logic in C.
|
||||||
|
*
|
||||||
|
* Both loops run the identical formation-hold workload, 24,000 calls -- forty
|
||||||
|
* enemies at sixty frames a second for ten seconds. Informational: nothing
|
||||||
|
* asserts on the timing, because CI machines vary; the numbers print so the
|
||||||
|
* tutorial can quote a real measurement.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *run_benchmark(void)
|
||||||
|
{
|
||||||
|
galaga_Enemy enemy;
|
||||||
|
akgl_Actor actor;
|
||||||
|
struct timespec t0;
|
||||||
|
double basic_s = 0.0;
|
||||||
|
double native_s = 0.0;
|
||||||
|
int i = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
memset(&enemy, 0, sizeof(enemy));
|
||||||
|
memset(&actor, 0, sizeof(actor));
|
||||||
|
enemy.kind = GALAGA_ENEMY_BEE;
|
||||||
|
enemy.state = GALAGA_ES_FORMATION;
|
||||||
|
enemy.homex = 400.0f;
|
||||||
|
enemy.homey = 300.0f;
|
||||||
|
enemy.rnd = 0.9f;
|
||||||
|
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||||
|
for ( i = 0; i < 24000; i++ ) {
|
||||||
|
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
|
||||||
|
}
|
||||||
|
basic_s = seconds_since(&t0);
|
||||||
|
|
||||||
|
memset(&enemy, 0, sizeof(enemy));
|
||||||
|
memset(&actor, 0, sizeof(actor));
|
||||||
|
enemy.kind = GALAGA_ENEMY_BEE;
|
||||||
|
enemy.state = GALAGA_ES_FORMATION;
|
||||||
|
enemy.homex = 400.0f;
|
||||||
|
enemy.homey = 300.0f;
|
||||||
|
enemy.rnd = 0.9f;
|
||||||
|
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||||
|
for ( i = 0; i < 24000; i++ ) {
|
||||||
|
native_updatebee(&enemy, &actor, 0.016f);
|
||||||
|
}
|
||||||
|
native_s = seconds_since(&t0);
|
||||||
|
|
||||||
|
printf("benchmark: 24000 formation-hold updates, dt 0.016\n");
|
||||||
|
printf(" BASIC through the boundary: %8.3f s %7.2f us/call %6.3f ms per 40-enemy frame\n",
|
||||||
|
basic_s, basic_s / 24000.0 * 1e6, basic_s / 24000.0 * 40.0 * 1e3);
|
||||||
|
printf(" the same logic in C: %8.3f s %7.2f us/call %6.3f ms per 40-enemy frame\n",
|
||||||
|
native_s, native_s / 24000.0 * 1e6, native_s / 24000.0 * 40.0 * 1e3);
|
||||||
|
printf(" ratio: %.0fx\n", basic_s / native_s);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, run_claims());
|
||||||
|
CATCH(errctx, run_benchmark());
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE_DEFAULT(errctx) {
|
||||||
|
LOG_ERROR_WITH_MESSAGE(errctx, "the interop test could not run");
|
||||||
|
FAILURES += 1;
|
||||||
|
} FINISH_NORETURN(errctx);
|
||||||
|
|
||||||
|
return FAILURES;
|
||||||
|
}
|
||||||
699
examples/galaga/main.c
Normal file
@@ -0,0 +1,699 @@
|
|||||||
|
/**
|
||||||
|
* @file main.c
|
||||||
|
* @brief Startup, the frame loop, the screens, and teardown.
|
||||||
|
*
|
||||||
|
* The startup order is libakgl's one sequence that works (deps/libakgl
|
||||||
|
* include/akgl/game.h): metadata, akgl_game_init(), screen properties,
|
||||||
|
* akgl_render_2d_init(), a physics backend -- and then, new in this example,
|
||||||
|
* the interpreter. The scripts compute, the engine draws; the interpreter is
|
||||||
|
* lent no devices at all, so a script that tries SPRITE is refused by name.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3_image/SDL_image.h>
|
||||||
|
#include <SDL3_ttf/SDL_ttf.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <akgl/actor.h>
|
||||||
|
#include <akgl/character.h>
|
||||||
|
#include <akgl/controller.h>
|
||||||
|
#include <akgl/draw.h>
|
||||||
|
#include <akgl/game.h>
|
||||||
|
#include <akgl/heap.h>
|
||||||
|
#include <akgl/physics.h>
|
||||||
|
#include <akgl/registry.h>
|
||||||
|
#include <akgl/renderer.h>
|
||||||
|
#include <akgl/sprite.h>
|
||||||
|
#include <akgl/text.h>
|
||||||
|
#include <akgl/ui.h>
|
||||||
|
|
||||||
|
#include "galaga.h"
|
||||||
|
|
||||||
|
/** @brief Where the example's assets live. CMake defines it; `--assets` overrides. */
|
||||||
|
#ifndef GALAGA_ASSET_DIR
|
||||||
|
#define GALAGA_ASSET_DIR "."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** @brief The enemy script. CMake defines it; `--script` overrides. */
|
||||||
|
#ifndef GALAGA_SCRIPT_PATH
|
||||||
|
#define GALAGA_SCRIPT_PATH "galaga.bas"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** @brief The HUD font. CMake defines it; headless runs still need it for the UI. */
|
||||||
|
#ifndef GALAGA_FONT_PATH
|
||||||
|
#define GALAGA_FONT_PATH "font.ttf"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define GALAGA_PATH_MAX 1024
|
||||||
|
|
||||||
|
galaga_Game galaga_game;
|
||||||
|
galaga_Shared galaga_shared;
|
||||||
|
|
||||||
|
/** @brief Where `--screenshot` writes, and on which frame. NULL means never. */
|
||||||
|
static char *SHOTPATH = NULL;
|
||||||
|
static int SHOTFRAME = 0;
|
||||||
|
|
||||||
|
/** @brief Set in HANDLE_DEFAULT and read after FINISH; see the note in main. */
|
||||||
|
static int FAILED = 0;
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------- starfield --- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* No parallax facility exists in libakgl and none is needed: a fixed array of
|
||||||
|
* stars advanced per frame and drawn with akgl_draw_point() between
|
||||||
|
* frame_start and akgl_game_update(). Two speed bands give the depth for
|
||||||
|
* free -- the slow band reads as far away.
|
||||||
|
*/
|
||||||
|
#define GALAGA_STARS 96
|
||||||
|
|
||||||
|
static struct
|
||||||
|
{
|
||||||
|
float x;
|
||||||
|
float y;
|
||||||
|
float speed;
|
||||||
|
Uint8 bright;
|
||||||
|
} STARS[GALAGA_STARS];
|
||||||
|
|
||||||
|
static void starfield_seed(void)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for ( i = 0; i < GALAGA_STARS; i++ ) {
|
||||||
|
STARS[i].x = galaga_random() * (float)GALAGA_VIEW_WIDTH;
|
||||||
|
STARS[i].y = galaga_random() * (float)GALAGA_VIEW_HEIGHT;
|
||||||
|
if ( (i % 2) == 0 ) {
|
||||||
|
STARS[i].speed = 40.0f; /* the far band */
|
||||||
|
STARS[i].bright = 110;
|
||||||
|
} else {
|
||||||
|
STARS[i].speed = 110.0f; /* the near band */
|
||||||
|
STARS[i].bright = 220;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *starfield_draw(void)
|
||||||
|
{
|
||||||
|
SDL_Color color = { 255, 255, 255, 255 };
|
||||||
|
int i = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
for ( i = 0; i < GALAGA_STARS; i++ ) {
|
||||||
|
STARS[i].y += STARS[i].speed * galaga_game.dt;
|
||||||
|
if ( STARS[i].y > (float)GALAGA_VIEW_HEIGHT ) {
|
||||||
|
STARS[i].y -= (float)GALAGA_VIEW_HEIGHT;
|
||||||
|
STARS[i].x = galaga_random() * (float)GALAGA_VIEW_WIDTH;
|
||||||
|
}
|
||||||
|
color.r = STARS[i].bright;
|
||||||
|
color.g = STARS[i].bright;
|
||||||
|
color.b = STARS[i].bright;
|
||||||
|
PASS(errctx, akgl_draw_point(akgl_renderer, STARS[i].x, STARS[i].y, color));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ screenshots --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Read the render target back and write it out as a PNG.
|
||||||
|
*
|
||||||
|
* Called after everything has drawn and before the frame is presented,
|
||||||
|
* because SDL_RenderPresent is where the target stops being readable. The
|
||||||
|
* figures in docs/20 and docs/21 are output from this program rather than
|
||||||
|
* pictures somebody took once, so they cannot show a game that no longer
|
||||||
|
* exists.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *save_screenshot(char *path)
|
||||||
|
{
|
||||||
|
SDL_Surface *shot = NULL;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "path");
|
||||||
|
shot = SDL_RenderReadPixels(akgl_renderer->sdl_renderer, NULL);
|
||||||
|
FAIL_ZERO_RETURN(errctx, shot, AKGL_ERR_SDL, "SDL_RenderReadPixels: %s", SDL_GetError());
|
||||||
|
ATTEMPT {
|
||||||
|
FAIL_ZERO_BREAK(errctx, IMG_SavePNG(shot, path), AKGL_ERR_SDL,
|
||||||
|
"IMG_SavePNG(%s): %s", path, SDL_GetError());
|
||||||
|
} CLEANUP {
|
||||||
|
SDL_DestroySurface(shot);
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SDL_Log("Wrote %s", path);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- assets --- */
|
||||||
|
|
||||||
|
static char *SPRITE_FILES[] = {
|
||||||
|
"sprite_galaga_player.json",
|
||||||
|
"sprite_galaga_bee.json",
|
||||||
|
"sprite_galaga_butterfly.json",
|
||||||
|
"sprite_galaga_boss.json",
|
||||||
|
"sprite_galaga_boss_hurt.json",
|
||||||
|
"sprite_galaga_playershot.json",
|
||||||
|
"sprite_galaga_enemyshot.json",
|
||||||
|
"sprite_galaga_boom.json",
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
static char *CHARACTER_FILES[] = {
|
||||||
|
"character_galaga_player.json",
|
||||||
|
"character_galaga_bee.json",
|
||||||
|
"character_galaga_butterfly.json",
|
||||||
|
"character_galaga_boss.json",
|
||||||
|
"character_galaga_playershot.json",
|
||||||
|
"character_galaga_enemyshot.json",
|
||||||
|
"character_galaga_boom.json",
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
static akerr_ErrorContext *asset_path(char *dir, char *name, char *dest, size_t size)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, dir, AKERR_NULLPOINTER, "dir");
|
||||||
|
FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name");
|
||||||
|
FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest");
|
||||||
|
PASS(errctx, aksl_snprintf(&count, dest, size, "%s/%s", dir, name));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sprites first, characters second. Not a preference: a character's
|
||||||
|
* JSON names its sprites by registry name, so a character loaded first fails
|
||||||
|
* on the first sprite it cannot find.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *load_assets(char *assetdir)
|
||||||
|
{
|
||||||
|
char path[GALAGA_PATH_MAX];
|
||||||
|
int i = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, assetdir, AKERR_NULLPOINTER, "assetdir");
|
||||||
|
for ( i = 0; SPRITE_FILES[i] != NULL; i++ ) {
|
||||||
|
PASS(errctx, asset_path(assetdir, SPRITE_FILES[i], (char *)&path, sizeof(path)));
|
||||||
|
PASS(errctx, akgl_sprite_load_json((char *)&path));
|
||||||
|
}
|
||||||
|
for ( i = 0; CHARACTER_FILES[i] != NULL; i++ ) {
|
||||||
|
PASS(errctx, asset_path(assetdir, CHARACTER_FILES[i], (char *)&path, sizeof(path)));
|
||||||
|
PASS(errctx, akgl_character_load_json((char *)&path));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------------------- startup --- */
|
||||||
|
|
||||||
|
/** @brief Replacement for akgl_game.lowfpsfunc, which logs a line per frame. */
|
||||||
|
static void galaga_lowfps(void)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *startup(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, aksl_strncpy((char *)&akgl_game.name, sizeof(akgl_game.name),
|
||||||
|
"akbasic galaga tutorial", sizeof(akgl_game.name) - 1));
|
||||||
|
PASS(errctx, aksl_strncpy((char *)&akgl_game.version, sizeof(akgl_game.version),
|
||||||
|
"1.0.0", sizeof(akgl_game.version) - 1));
|
||||||
|
PASS(errctx, aksl_strncpy((char *)&akgl_game.uri, sizeof(akgl_game.uri),
|
||||||
|
"net.aklabs.akbasic.galaga", sizeof(akgl_game.uri) - 1));
|
||||||
|
|
||||||
|
PASS(errctx, akgl_game_init());
|
||||||
|
akgl_game.lowfpsfunc = &galaga_lowfps;
|
||||||
|
|
||||||
|
/* Properties before the renderer: akgl_render_2d_init reads both, and an
|
||||||
|
* unset one defaults to the string "0" -- a zero-sized window. */
|
||||||
|
PASS(errctx, akgl_set_property("game.screenwidth", "1280"));
|
||||||
|
PASS(errctx, akgl_set_property("game.screenheight", "960"));
|
||||||
|
PASS(errctx, akgl_render_2d_init(akgl_renderer));
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(
|
||||||
|
errctx,
|
||||||
|
SDL_SetRenderLogicalPresentation(
|
||||||
|
akgl_renderer->sdl_renderer,
|
||||||
|
GALAGA_VIEW_WIDTH,
|
||||||
|
GALAGA_VIEW_HEIGHT,
|
||||||
|
SDL_LOGICAL_PRESENTATION_INTEGER_SCALE),
|
||||||
|
AKGL_ERR_SDL,
|
||||||
|
"%s",
|
||||||
|
SDL_GetError()
|
||||||
|
);
|
||||||
|
|
||||||
|
/* The view is what the camera looks through, so it says the same thing. */
|
||||||
|
akgl_camera->x = 0.0f;
|
||||||
|
akgl_camera->y = 0.0f;
|
||||||
|
akgl_camera->w = (float)GALAGA_VIEW_WIDTH;
|
||||||
|
akgl_camera->h = (float)GALAGA_VIEW_HEIGHT;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* akgl_game_init does NOT install a physics backend, whatever physics.h's
|
||||||
|
* file comment says (libakgl docs/14-physics.md). Null physics accepts
|
||||||
|
* every call and moves nothing: whatever writes x and y directly is the
|
||||||
|
* mover, and in this game that is BASIC writing through ACTOR@.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akgl_physics_init_null(akgl_physics));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- the UI --- */
|
||||||
|
|
||||||
|
static akgl_UiMenu TITLE_MENU = {
|
||||||
|
"titlemenu", { "START", "QUIT" }, 2, 0, false, NULL
|
||||||
|
};
|
||||||
|
static akgl_UiMenu AGAIN_MENU = {
|
||||||
|
"againmenu", { "PLAY AGAIN", "QUIT" }, 2, 0, false, NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Clay borrows label text until frame_end, so these cannot be locals. */
|
||||||
|
static char HUD_SCORE[64];
|
||||||
|
static char HUD_LIVES[64];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Draw a headline centred above the menu, in the banner font.
|
||||||
|
*
|
||||||
|
* Direct text rather than a ui label: the menu owns AKGL_UI_ANCHOR_CENTER,
|
||||||
|
* and a label anchored there disappears behind it -- there is no
|
||||||
|
* top-centre anchor to reach for. Drawn before the UI bracket, so the menu
|
||||||
|
* still paints over it if the two ever meet.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *draw_banner(char *text)
|
||||||
|
{
|
||||||
|
SDL_Color ink = { 235, 235, 235, 255 };
|
||||||
|
TTF_Font *font = NULL;
|
||||||
|
int w = 0;
|
||||||
|
int h = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, text, AKERR_NULLPOINTER, "text");
|
||||||
|
font = SDL_GetPointerProperty(AKGL_REGISTRY_FONT, "banner", NULL);
|
||||||
|
FAIL_ZERO_RETURN(errctx, font, AKERR_KEY, "the banner font is not loaded");
|
||||||
|
PASS(errctx, akgl_text_measure(font, text, &w, &h));
|
||||||
|
PASS(errctx, akgl_text_rendertextat(font, text, ink, 0,
|
||||||
|
(GALAGA_VIEW_WIDTH - w) / 2, 280));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *declare_title(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, akgl_ui_menu(&TITLE_MENU));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *declare_play(void)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, aksl_snprintf(&count, HUD_SCORE, sizeof(HUD_SCORE),
|
||||||
|
"SCORE %06d", galaga_game.score));
|
||||||
|
PASS(errctx, aksl_snprintf(&count, HUD_LIVES, sizeof(HUD_LIVES),
|
||||||
|
"LIVES %d WAVE %d", galaga_game.lives, galaga_shared.wave));
|
||||||
|
PASS(errctx, akgl_ui_label("score", HUD_SCORE, AKGL_UI_ANCHOR_TOP_LEFT, NULL));
|
||||||
|
PASS(errctx, akgl_ui_label("lives", HUD_LIVES, AKGL_UI_ANCHOR_TOP_RIGHT, NULL));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *declare_end(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, akgl_ui_menu(&AGAIN_MENU));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ transitions --- */
|
||||||
|
|
||||||
|
static akerr_ErrorContext *start_game(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
galaga_game.score = 0;
|
||||||
|
galaga_game.lives = 3;
|
||||||
|
memset(galaga_game.kills, 0, sizeof(galaga_game.kills));
|
||||||
|
memset(galaga_game.shots, 0, sizeof(galaga_game.shots));
|
||||||
|
galaga_game.respawn_timer = 0.0f;
|
||||||
|
galaga_shared.wave = 1;
|
||||||
|
|
||||||
|
galaga_game.player->x = (float)GALAGA_VIEW_WIDTH / 2.0f - 50.0f;
|
||||||
|
PASS(errctx, galaga_wave_spawn());
|
||||||
|
galaga_game.screen = GALAGA_SCREEN_PLAY;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief End-of-round bookkeeping: notice a cleared wave or a spent ship.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *check_transitions(bool *running)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, running, AKERR_NULLPOINTER, "running");
|
||||||
|
(void)running;
|
||||||
|
if ( galaga_game.screen != GALAGA_SCREEN_PLAY ) {
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
if ( galaga_game.lives <= 0 ) {
|
||||||
|
PASS(errctx, galaga_wave_release());
|
||||||
|
galaga_game.screen = GALAGA_SCREEN_GAMEOVER;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
if ( galaga_enemies_alive() == 0 ) {
|
||||||
|
galaga_game.screen = GALAGA_SCREEN_VICTORY;
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Consume a menu activation. The menu never clears `activated`; the
|
||||||
|
* state machine that acts on it does.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *consume_menus(bool *running)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, running, AKERR_NULLPOINTER, "running");
|
||||||
|
if ( galaga_game.screen == GALAGA_SCREEN_TITLE && TITLE_MENU.activated ) {
|
||||||
|
TITLE_MENU.activated = false;
|
||||||
|
if ( TITLE_MENU.selected == 0 ) {
|
||||||
|
PASS(errctx, start_game());
|
||||||
|
} else {
|
||||||
|
*running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( (galaga_game.screen == GALAGA_SCREEN_GAMEOVER
|
||||||
|
|| galaga_game.screen == GALAGA_SCREEN_VICTORY)
|
||||||
|
&& AGAIN_MENU.activated ) {
|
||||||
|
AGAIN_MENU.activated = false;
|
||||||
|
if ( AGAIN_MENU.selected == 0 ) {
|
||||||
|
PASS(errctx, galaga_wave_release());
|
||||||
|
PASS(errctx, start_game());
|
||||||
|
} else {
|
||||||
|
*running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------- the frame --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Route one event: the UI gets first refusal, then the menus, then
|
||||||
|
* the controller. A consumed event goes no further.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *route_event(SDL_Event *event, bool *running)
|
||||||
|
{
|
||||||
|
bool consumed = false;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "event");
|
||||||
|
FAIL_ZERO_RETURN(errctx, running, AKERR_NULLPOINTER, "running");
|
||||||
|
|
||||||
|
if ( event->type == SDL_EVENT_QUIT ) {
|
||||||
|
*running = false;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
PASS(errctx, akgl_ui_handle_event((void *)&akgl_game.state, event, &consumed));
|
||||||
|
if ( consumed ) {
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
if ( galaga_game.screen == GALAGA_SCREEN_TITLE ) {
|
||||||
|
PASS(errctx, akgl_ui_menu_handle_event(&TITLE_MENU, event, &consumed));
|
||||||
|
} else if ( galaga_game.screen == GALAGA_SCREEN_GAMEOVER
|
||||||
|
|| galaga_game.screen == GALAGA_SCREEN_VICTORY ) {
|
||||||
|
PASS(errctx, akgl_ui_menu_handle_event(&AGAIN_MENU, event, &consumed));
|
||||||
|
}
|
||||||
|
if ( consumed ) {
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
/* Every event, unconditionally: one that no control map binds is not an
|
||||||
|
* error, it is a call that did nothing. */
|
||||||
|
PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, event));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief The previous frame's timestamp, for dt. Stamped once at loop start. */
|
||||||
|
static uint64_t LAST_NS = 0;
|
||||||
|
|
||||||
|
static akerr_ErrorContext *frame(bool *running)
|
||||||
|
{
|
||||||
|
SDL_Event event;
|
||||||
|
uint64_t now = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, running, AKERR_NULLPOINTER, "running");
|
||||||
|
|
||||||
|
while ( SDL_PollEvent(&event) == true ) {
|
||||||
|
PASS(errctx, route_event(&event, running));
|
||||||
|
}
|
||||||
|
|
||||||
|
galaga_game.frame += 1;
|
||||||
|
if ( galaga_game.autoplay ) {
|
||||||
|
if ( galaga_game.screen == GALAGA_SCREEN_TITLE && galaga_game.frame >= 8 ) {
|
||||||
|
PASS(errctx, start_game());
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* On an end screen the pilot presses Return, which drives the real
|
||||||
|
* menu path -- declare, handle, activate, restart -- so a headless
|
||||||
|
* run that dies keeps exercising the game instead of idling.
|
||||||
|
*/
|
||||||
|
if ( galaga_game.screen == GALAGA_SCREEN_GAMEOVER
|
||||||
|
|| galaga_game.screen == GALAGA_SCREEN_VICTORY ) {
|
||||||
|
if ( (galaga_game.frame % 30) == 0 ) {
|
||||||
|
SDL_Event press;
|
||||||
|
memset(&press, 0, sizeof(press));
|
||||||
|
press.type = SDL_EVENT_KEY_DOWN;
|
||||||
|
press.key.key = SDLK_RETURN;
|
||||||
|
PASS(errctx, route_event(&press, running));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PASS(errctx, galaga_player_autoplay(galaga_game.frame));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* dt from the wall clock, clamped: a debugger pause or a stalled runner
|
||||||
|
* must not become one frame of teleporting enemies. The clamp is a 30 Hz
|
||||||
|
* frame, the slowest game this is still worth playing at.
|
||||||
|
*/
|
||||||
|
now = SDL_GetTicksNS();
|
||||||
|
galaga_game.dt = (float)(now - LAST_NS) / 1e9f;
|
||||||
|
LAST_NS = now;
|
||||||
|
if ( galaga_game.dt > (1.0f / 30.0f) ) {
|
||||||
|
galaga_game.dt = 1.0f / 30.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The shared frame state, refreshed before any enemy thinks. The engine
|
||||||
|
* fills GAME@.ROLL% from its own PRNG rather than letting the script call
|
||||||
|
* the native RND, so a headless run is the same game on every machine. */
|
||||||
|
galaga_shared.playerx = galaga_game.player->x + 50.0f;
|
||||||
|
galaga_shared.playery = galaga_game.player->y;
|
||||||
|
galaga_shared.rnd = galaga_random();
|
||||||
|
|
||||||
|
PASS(errctx, akgl_renderer->frame_start(akgl_renderer));
|
||||||
|
PASS(errctx, starfield_draw());
|
||||||
|
|
||||||
|
/*
|
||||||
|
* akgl_game_update is update-every-actor, step-the-physics, draw-the-
|
||||||
|
* world. Updating every actor is where the forty scripts run: each
|
||||||
|
* enemy's updatefunc is the hook in enemies.c, and that hook is a BASIC
|
||||||
|
* call. Held back on the menu screens so the world stands still there.
|
||||||
|
*/
|
||||||
|
if ( galaga_game.screen == GALAGA_SCREEN_PLAY ) {
|
||||||
|
PASS(errctx, akgl_game_update(NULL));
|
||||||
|
PASS(errctx, check_transitions(running));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The banner is direct text, drawn before the UI bracket. */
|
||||||
|
if ( galaga_game.screen == GALAGA_SCREEN_TITLE ) {
|
||||||
|
PASS(errctx, draw_banner("GALAGA"));
|
||||||
|
} else if ( galaga_game.screen == GALAGA_SCREEN_GAMEOVER ) {
|
||||||
|
PASS(errctx, draw_banner("GAME OVER"));
|
||||||
|
} else if ( galaga_game.screen == GALAGA_SCREEN_VICTORY ) {
|
||||||
|
PASS(errctx, draw_banner("VICTORY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The UI bracket sits between akgl_game_update and frame_end, exactly as
|
||||||
|
* libakgl docs/22-ui.md draws it. */
|
||||||
|
PASS(errctx, akgl_ui_frame_begin());
|
||||||
|
switch ( galaga_game.screen ) {
|
||||||
|
case GALAGA_SCREEN_TITLE:
|
||||||
|
PASS(errctx, declare_title());
|
||||||
|
break;
|
||||||
|
case GALAGA_SCREEN_PLAY:
|
||||||
|
PASS(errctx, declare_play());
|
||||||
|
break;
|
||||||
|
case GALAGA_SCREEN_GAMEOVER:
|
||||||
|
case GALAGA_SCREEN_VICTORY:
|
||||||
|
PASS(errctx, declare_end());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
PASS(errctx, akgl_ui_frame_end(akgl_renderer));
|
||||||
|
|
||||||
|
PASS(errctx, consume_menus(running));
|
||||||
|
|
||||||
|
if ( (SHOTPATH != NULL) && (galaga_game.frame == SHOTFRAME) ) {
|
||||||
|
PASS(errctx, save_screenshot(SHOTPATH));
|
||||||
|
}
|
||||||
|
PASS(errctx, akgl_renderer->frame_end(akgl_renderer));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *run(int frames)
|
||||||
|
{
|
||||||
|
bool running = true;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
LAST_NS = SDL_GetTicksNS();
|
||||||
|
while ( running == true ) {
|
||||||
|
PASS(errctx, frame(&running));
|
||||||
|
if ( (frames > 0) && (galaga_game.frame >= frames) ) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
/* A crude frame limiter. A game on a real display should ask SDL for
|
||||||
|
* vsync; this one has to work under the dummy video driver, where
|
||||||
|
* there is nothing to sync to. */
|
||||||
|
SDL_Delay(16);
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------- teardown --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Give back what the process is holding.
|
||||||
|
*
|
||||||
|
* There is no akgl_game_shutdown; teardown is the application's. Fonts have
|
||||||
|
* to unload before TTF_Quit destroys them underneath the registry. IGNORE()
|
||||||
|
* on every call: a teardown failure must not mask whatever error is already
|
||||||
|
* being reported.
|
||||||
|
*/
|
||||||
|
static void shutdown_game(void)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
IGNORE(akgl_ui_shutdown());
|
||||||
|
IGNORE(akgl_text_unloadallfonts());
|
||||||
|
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||||||
|
if ( akgl_heap_actors[i].refcount > 0 ) {
|
||||||
|
IGNORE(akgl_heap_release_actor(&akgl_heap_actors[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TTF_Quit();
|
||||||
|
SDL_Quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ args --- */
|
||||||
|
|
||||||
|
static akerr_ErrorContext *parse_args(int argc, char *argv[], char **assetdir,
|
||||||
|
char **script, int *frames)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, assetdir, AKERR_NULLPOINTER, "assetdir");
|
||||||
|
FAIL_ZERO_RETURN(errctx, script, AKERR_NULLPOINTER, "script");
|
||||||
|
FAIL_ZERO_RETURN(errctx, frames, AKERR_NULLPOINTER, "frames");
|
||||||
|
|
||||||
|
for ( i = 1; i < argc; i++ ) {
|
||||||
|
if ( strcmp(argv[i], "--autoplay") == 0 ) {
|
||||||
|
galaga_game.autoplay = true;
|
||||||
|
} else if ( strcmp(argv[i], "--frames") == 0 ) {
|
||||||
|
i += 1;
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--frames needs a count");
|
||||||
|
PASS(errctx, aksl_atoi(argv[i], frames));
|
||||||
|
} else if ( strcmp(argv[i], "--assets") == 0 ) {
|
||||||
|
i += 1;
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--assets needs a directory");
|
||||||
|
*assetdir = argv[i];
|
||||||
|
} else if ( strcmp(argv[i], "--script") == 0 ) {
|
||||||
|
i += 1;
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--script needs a path");
|
||||||
|
*script = argv[i];
|
||||||
|
} else if ( strcmp(argv[i], "--screenshot") == 0 ) {
|
||||||
|
i += 1;
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE, "--screenshot needs a path");
|
||||||
|
SHOTPATH = argv[i];
|
||||||
|
} else if ( strcmp(argv[i], "--screenshot-frame") == 0 ) {
|
||||||
|
i += 1;
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (i >= argc), AKERR_VALUE,
|
||||||
|
"--screenshot-frame needs a number");
|
||||||
|
PASS(errctx, aksl_atoi(argv[i], &SHOTFRAME));
|
||||||
|
} else {
|
||||||
|
FAIL_RETURN(
|
||||||
|
errctx,
|
||||||
|
AKERR_VALUE,
|
||||||
|
"usage: galaga [--assets DIR] [--script PATH] [--frames N]"
|
||||||
|
" [--autoplay] [--screenshot PATH] [--screenshot-frame N]"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
char *assetdir = GALAGA_ASSET_DIR;
|
||||||
|
char *script = GALAGA_SCRIPT_PATH;
|
||||||
|
int frames = 0;
|
||||||
|
uint16_t fontid = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, parse_args(argc, argv, &assetdir, &script, &frames));
|
||||||
|
CATCH(errctx, startup());
|
||||||
|
CATCH(errctx, load_assets(assetdir));
|
||||||
|
/* The engine refuses to start when the script will not boot: a game
|
||||||
|
* whose enemies cannot think is not a game missing a feature. */
|
||||||
|
CATCH(errctx, galaga_script_boot(script));
|
||||||
|
CATCH(errctx, akgl_ui_init(GALAGA_VIEW_WIDTH, GALAGA_VIEW_HEIGHT));
|
||||||
|
CATCH(errctx, akgl_text_loadfont("hud", GALAGA_FONT_PATH, 28));
|
||||||
|
CATCH(errctx, akgl_text_loadfont("banner", GALAGA_FONT_PATH, 84));
|
||||||
|
CATCH(errctx, akgl_ui_font_register("hud", &fontid));
|
||||||
|
CATCH(errctx, galaga_player_spawn());
|
||||||
|
CATCH(errctx, galaga_player_controls());
|
||||||
|
starfield_seed();
|
||||||
|
galaga_game.screen = GALAGA_SCREEN_TITLE;
|
||||||
|
galaga_game.lives = 3;
|
||||||
|
CATCH(errctx, run(frames));
|
||||||
|
} CLEANUP {
|
||||||
|
shutdown_game();
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE_DEFAULT(errctx) {
|
||||||
|
LOG_ERROR_WITH_MESSAGE(errctx, "galaga could not run");
|
||||||
|
/* Set a flag rather than returning: leaving a HANDLE block early
|
||||||
|
* skips FINISH's RELEASE_ERROR and leaks the context's pool slot. */
|
||||||
|
FAILED = 1;
|
||||||
|
/* FINISH_NORETURN rather than FINISH: FINISH expands a return that an
|
||||||
|
* int-returning function cannot compile. */
|
||||||
|
} FINISH_NORETURN(errctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The readout is the evidence: exiting 0 is not proof the wave flew. A
|
||||||
|
* headless CI log gets the same line a reader's terminal does, and the
|
||||||
|
* script-error count is the line's whole reason to exist -- a wave of
|
||||||
|
* dumb enemies still exits 0.
|
||||||
|
*/
|
||||||
|
SDL_Log(
|
||||||
|
"galaga: %d frames, screen %d, score %d, alive %d, kills bee %d bfly %d boss %d,"
|
||||||
|
" shots bee %d bfly %d boss %d, script errors %d",
|
||||||
|
galaga_game.frame,
|
||||||
|
(int)galaga_game.screen,
|
||||||
|
galaga_game.score,
|
||||||
|
galaga_enemies_alive(),
|
||||||
|
galaga_game.kills[GALAGA_ENEMY_BEE],
|
||||||
|
galaga_game.kills[GALAGA_ENEMY_BUTTERFLY],
|
||||||
|
galaga_game.kills[GALAGA_ENEMY_BOSS],
|
||||||
|
galaga_game.shots[GALAGA_ENEMY_BEE],
|
||||||
|
galaga_game.shots[GALAGA_ENEMY_BUTTERFLY],
|
||||||
|
galaga_game.shots[GALAGA_ENEMY_BOSS],
|
||||||
|
galaga_game.script_errors);
|
||||||
|
return FAILED;
|
||||||
|
}
|
||||||
407
examples/galaga/player.c
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
/**
|
||||||
|
* @file player.c
|
||||||
|
* @brief The player's ship, its shots, and every collision in the game.
|
||||||
|
*
|
||||||
|
* Bullets and collision are C forever -- they are engine, not behavior. The
|
||||||
|
* per-frame budget for the script is spent on the forty things that think;
|
||||||
|
* nothing here thinks, it just moves and intersects.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <akgl/actor.h>
|
||||||
|
#include <akgl/controller.h>
|
||||||
|
#include <akgl/game.h>
|
||||||
|
#include <akgl/heap.h>
|
||||||
|
#include <akgl/registry.h>
|
||||||
|
#include <akgl/util.h>
|
||||||
|
|
||||||
|
#include "galaga.h"
|
||||||
|
|
||||||
|
/* Points per kill, indexed by enemy kind. */
|
||||||
|
static const int KILL_SCORE[GALAGA_ENEMY_KINDS] = {
|
||||||
|
/* bee butterfly boss */
|
||||||
|
50, 80, 150
|
||||||
|
};
|
||||||
|
|
||||||
|
static uint32_t PSHOT_SERIAL = 0;
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------- hitboxes --- */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Actor x/y is a sprite's top-left corner. Every box is inset from the
|
||||||
|
* artwork's rectangle, because the PNGs carry transparent margin and wing
|
||||||
|
* tips that should not kill anybody.
|
||||||
|
*/
|
||||||
|
static void player_box(akgl_Actor *actor, SDL_FRect *dest)
|
||||||
|
{
|
||||||
|
dest->x = actor->x + 12.0f;
|
||||||
|
dest->y = actor->y + 8.0f;
|
||||||
|
dest->w = 75.0f;
|
||||||
|
dest->h = 60.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void enemy_box(akgl_Actor *actor, SDL_FRect *dest)
|
||||||
|
{
|
||||||
|
dest->x = actor->x + 8.0f;
|
||||||
|
dest->y = actor->y + 8.0f;
|
||||||
|
dest->w = 78.0f;
|
||||||
|
dest->h = 68.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void shot_box(akgl_Actor *actor, SDL_FRect *dest)
|
||||||
|
{
|
||||||
|
dest->x = actor->x;
|
||||||
|
dest->y = actor->y;
|
||||||
|
dest->w = 9.0f;
|
||||||
|
dest->h = 54.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------- player shots --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Kill one enemy: score it, blow it up, free its slot.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *kill_enemy(int index)
|
||||||
|
{
|
||||||
|
akgl_Actor *actor = NULL;
|
||||||
|
galaga_Enemy *enemy = NULL;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (index < 0 || index >= GALAGA_MAX_ENEMIES),
|
||||||
|
AKERR_OUTOFBOUNDS, "enemy index %d", index);
|
||||||
|
actor = galaga_enemy_actors[index];
|
||||||
|
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "enemy %d is already gone", index);
|
||||||
|
enemy = &galaga_enemies[index];
|
||||||
|
|
||||||
|
galaga_game.score += KILL_SCORE[enemy->kind];
|
||||||
|
galaga_game.kills[enemy->kind] += 1;
|
||||||
|
PASS(errctx, galaga_boom_spawn(actor->x + 20.0f, actor->y + 20.0f));
|
||||||
|
PASS(errctx, akgl_heap_release_actor(actor));
|
||||||
|
galaga_enemy_actors[index] = NULL;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Move a player shot and test it against every live enemy.
|
||||||
|
*
|
||||||
|
* The classic O(shots x enemies) sweep: at most 2 x 40 rectangle tests a
|
||||||
|
* frame, which is noise. A hit costs the enemy a point of hp; the boss's
|
||||||
|
* second point is the script's business to survive, not this file's.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *player_shot_update(akgl_Actor *obj)
|
||||||
|
{
|
||||||
|
SDL_FRect mine;
|
||||||
|
SDL_FRect theirs;
|
||||||
|
bool hit = false;
|
||||||
|
int i = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
|
||||||
|
obj->y -= 900.0f * galaga_game.dt;
|
||||||
|
if ( obj->y < -60.0f ) {
|
||||||
|
galaga_game.player_shots_live -= 1;
|
||||||
|
PASS(errctx, akgl_heap_release_actor(obj));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
shot_box(obj, &mine);
|
||||||
|
for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
|
||||||
|
if ( galaga_enemy_actors[i] == NULL ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
enemy_box(galaga_enemy_actors[i], &theirs);
|
||||||
|
PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
|
||||||
|
if ( !hit ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
galaga_enemies[i].hp -= 1;
|
||||||
|
if ( galaga_enemies[i].hp <= 0 ) {
|
||||||
|
PASS(errctx, kill_enemy(i));
|
||||||
|
}
|
||||||
|
galaga_game.player_shots_live -= 1;
|
||||||
|
PASS(errctx, akgl_heap_release_actor(obj));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *player_fire(akgl_Actor *player)
|
||||||
|
{
|
||||||
|
akgl_Actor *shot = NULL;
|
||||||
|
char name[32];
|
||||||
|
int count = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "player");
|
||||||
|
|
||||||
|
PSHOT_SERIAL += 1;
|
||||||
|
PASS(errctx, aksl_snprintf(&count, name, sizeof(name), "pshot%u", PSHOT_SERIAL));
|
||||||
|
PASS(errctx, akgl_heap_next_actor(&shot));
|
||||||
|
PASS(errctx, akgl_actor_initialize(shot, name));
|
||||||
|
PASS(errctx, akgl_actor_set_character(shot, "galaga_playershot"));
|
||||||
|
/* AFTER initialize: it resets all seven hooks. */
|
||||||
|
shot->updatefunc = &player_shot_update;
|
||||||
|
shot->movement_controls_face = false;
|
||||||
|
shot->state = AKGL_ACTOR_STATE_ALIVE;
|
||||||
|
shot->visible = true;
|
||||||
|
shot->x = player->x + 45.0f;
|
||||||
|
shot->y = player->y - 44.0f;
|
||||||
|
|
||||||
|
galaga_game.player_shots_live += 1;
|
||||||
|
galaga_game.fire_cooldown = 0.22f;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------- player hit --- */
|
||||||
|
|
||||||
|
static akerr_ErrorContext *player_hit(akgl_Actor *player)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "player");
|
||||||
|
galaga_game.lives -= 1;
|
||||||
|
galaga_game.respawn_timer = 2.0f;
|
||||||
|
PASS(errctx, galaga_boom_spawn(player->x + 25.0f, player->y + 10.0f));
|
||||||
|
player->x = (float)GALAGA_VIEW_WIDTH / 2.0f - 50.0f;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The player's own update hook: motion, fire, and what can kill it.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *player_update(akgl_Actor *obj)
|
||||||
|
{
|
||||||
|
SDL_FRect mine;
|
||||||
|
SDL_FRect theirs;
|
||||||
|
bool hit = false;
|
||||||
|
float dx = 0.0f;
|
||||||
|
int i = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
|
||||||
|
|
||||||
|
dx = 0.0f;
|
||||||
|
if ( galaga_game.moveleft ) {
|
||||||
|
dx -= GALAGA_PLAYER_SPEED;
|
||||||
|
}
|
||||||
|
if ( galaga_game.moveright ) {
|
||||||
|
dx += GALAGA_PLAYER_SPEED;
|
||||||
|
}
|
||||||
|
obj->x += dx * galaga_game.dt;
|
||||||
|
if ( obj->x < GALAGA_PLAYER_MARGIN ) {
|
||||||
|
obj->x = GALAGA_PLAYER_MARGIN;
|
||||||
|
}
|
||||||
|
if ( obj->x > (float)GALAGA_VIEW_WIDTH - GALAGA_PLAYER_MARGIN - 99.0f ) {
|
||||||
|
obj->x = (float)GALAGA_VIEW_WIDTH - GALAGA_PLAYER_MARGIN - 99.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
galaga_game.fire_cooldown -= galaga_game.dt;
|
||||||
|
if ( galaga_game.firing
|
||||||
|
&& galaga_game.fire_cooldown <= 0.0f
|
||||||
|
&& galaga_game.player_shots_live < GALAGA_MAX_PLAYER_SHOTS
|
||||||
|
&& galaga_game.screen == GALAGA_SCREEN_PLAY ) {
|
||||||
|
PASS(errctx, player_fire(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Respawn grace: two seconds of blinking invulnerability. The blink is
|
||||||
|
* the `visible` flag, which is deliberate hiding -- the actor still
|
||||||
|
* updates, it just is not drawn on the off frames.
|
||||||
|
*/
|
||||||
|
if ( galaga_game.respawn_timer > 0.0f ) {
|
||||||
|
galaga_game.respawn_timer -= galaga_game.dt;
|
||||||
|
obj->visible = ((galaga_game.frame / 6) % 2) == 0;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
obj->visible = true;
|
||||||
|
|
||||||
|
player_box(obj, &mine);
|
||||||
|
|
||||||
|
/* Enemy shots. */
|
||||||
|
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
|
||||||
|
if ( akgl_heap_actors[i].refcount == 0 ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ( strncmp(akgl_heap_actors[i].name, "eshot", 5) != 0 ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
shot_box(&akgl_heap_actors[i], &theirs);
|
||||||
|
PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
|
||||||
|
if ( hit ) {
|
||||||
|
galaga_game.enemy_shots_live -= 1;
|
||||||
|
PASS(errctx, akgl_heap_release_actor(&akgl_heap_actors[i]));
|
||||||
|
PASS(errctx, player_hit(obj));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Diving enemies. The formation never reaches this low, so testing all
|
||||||
|
* forty is the same answer as testing the divers, without a state read. */
|
||||||
|
for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
|
||||||
|
if ( galaga_enemy_actors[i] == NULL ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
enemy_box(galaga_enemy_actors[i], &theirs);
|
||||||
|
PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
|
||||||
|
if ( hit ) {
|
||||||
|
PASS(errctx, kill_enemy(i));
|
||||||
|
PASS(errctx, player_hit(obj));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------- controls --- */
|
||||||
|
|
||||||
|
static akerr_ErrorContext *left_on(akgl_Actor *obj, SDL_Event *event)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)obj; (void)event;
|
||||||
|
galaga_game.moveleft = true;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *left_off(akgl_Actor *obj, SDL_Event *event)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)obj; (void)event;
|
||||||
|
galaga_game.moveleft = false;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *right_on(akgl_Actor *obj, SDL_Event *event)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)obj; (void)event;
|
||||||
|
galaga_game.moveright = true;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *right_off(akgl_Actor *obj, SDL_Event *event)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)obj; (void)event;
|
||||||
|
galaga_game.moveright = false;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *fire_on(akgl_Actor *obj, SDL_Event *event)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)obj; (void)event;
|
||||||
|
galaga_game.firing = true;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *fire_off(akgl_Actor *obj, SDL_Event *event)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
(void)obj; (void)event;
|
||||||
|
galaga_game.firing = false;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *galaga_player_controls(void)
|
||||||
|
{
|
||||||
|
akgl_Control control;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
memset(&control, 0, sizeof(control));
|
||||||
|
control.event_on = SDL_EVENT_KEY_DOWN;
|
||||||
|
control.event_off = SDL_EVENT_KEY_UP;
|
||||||
|
|
||||||
|
control.key = SDLK_LEFT;
|
||||||
|
control.handler_on = &left_on;
|
||||||
|
control.handler_off = &left_off;
|
||||||
|
PASS(errctx, akgl_controller_pushmap(0, &control));
|
||||||
|
|
||||||
|
control.key = SDLK_RIGHT;
|
||||||
|
control.handler_on = &right_on;
|
||||||
|
control.handler_off = &right_off;
|
||||||
|
PASS(errctx, akgl_controller_pushmap(0, &control));
|
||||||
|
|
||||||
|
control.key = SDLK_SPACE;
|
||||||
|
control.handler_on = &fire_on;
|
||||||
|
control.handler_off = &fire_off;
|
||||||
|
PASS(errctx, akgl_controller_pushmap(0, &control));
|
||||||
|
|
||||||
|
akgl_controlmaps[0].target = galaga_game.player;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------------------------------------------------- spawn --- */
|
||||||
|
|
||||||
|
akerr_ErrorContext *galaga_player_spawn(void)
|
||||||
|
{
|
||||||
|
akgl_Actor *player = NULL;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, akgl_heap_next_actor(&player));
|
||||||
|
PASS(errctx, akgl_actor_initialize(player, "player"));
|
||||||
|
PASS(errctx, akgl_actor_set_character(player, "galaga_player"));
|
||||||
|
/* AFTER initialize: it resets all seven hooks. */
|
||||||
|
player->updatefunc = &player_update;
|
||||||
|
player->movement_controls_face = false;
|
||||||
|
player->state = AKGL_ACTOR_STATE_ALIVE;
|
||||||
|
player->visible = true;
|
||||||
|
player->x = (float)GALAGA_VIEW_WIDTH / 2.0f - 50.0f;
|
||||||
|
player->y = GALAGA_PLAYER_Y;
|
||||||
|
|
||||||
|
galaga_game.player = player;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------- autoplay --- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send one synthetic key event through the controller.
|
||||||
|
*
|
||||||
|
* Through akgl_controller_handle_event(), never the handlers directly: the
|
||||||
|
* point of autoplay is to exercise the same path a keyboard does.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *synth_key(SDL_Keycode key, bool down)
|
||||||
|
{
|
||||||
|
SDL_Event event;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
memset(&event, 0, sizeof(event));
|
||||||
|
event.type = (down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP);
|
||||||
|
event.key.key = key;
|
||||||
|
PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The scripted pilot for headless runs: hold fire, sweep the floor.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext *galaga_player_autoplay(int frame)
|
||||||
|
{
|
||||||
|
int phase = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
/* Hold fire until the wave has mostly assembled: shooting the entry
|
||||||
|
* stream point-blank empties the formation before it exists, which makes
|
||||||
|
* both the game and its figure worse. */
|
||||||
|
if ( frame == 300 ) {
|
||||||
|
PASS(errctx, synth_key(SDLK_SPACE, true));
|
||||||
|
}
|
||||||
|
phase = frame % 240;
|
||||||
|
if ( phase == 30 ) {
|
||||||
|
PASS(errctx, synth_key(SDLK_LEFT, true));
|
||||||
|
} else if ( phase == 90 ) {
|
||||||
|
PASS(errctx, synth_key(SDLK_LEFT, false));
|
||||||
|
PASS(errctx, synth_key(SDLK_RIGHT, true));
|
||||||
|
} else if ( phase == 210 ) {
|
||||||
|
PASS(errctx, synth_key(SDLK_RIGHT, false));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
281
examples/galaga/script.c
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
/**
|
||||||
|
* @file script.c
|
||||||
|
* @brief The boundary: everything that touches the interpreter lives here.
|
||||||
|
*
|
||||||
|
* One runtime, one script, three host types, three bindings. The engine calls
|
||||||
|
* exactly one thing per enemy per frame -- galaga_script_update_enemy() -- and
|
||||||
|
* that function is the whole protocol: rebind, call, recover, reset.
|
||||||
|
*
|
||||||
|
* The structure types are declared once, in C, right below. The script never
|
||||||
|
* declares a TYPE of its own; akbasic_host_register_type() makes these structs
|
||||||
|
* *be* the BASIC types, offsets taken from offsetof() so the two sides cannot
|
||||||
|
* drift (include/akbasic/host.h).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
|
||||||
|
#include <akbasic/environment.h>
|
||||||
|
#include <akbasic/error.h>
|
||||||
|
#include <akbasic/host.h>
|
||||||
|
#include <akbasic/runtime.h>
|
||||||
|
#include <akbasic/sink.h>
|
||||||
|
|
||||||
|
#include "galaga.h"
|
||||||
|
|
||||||
|
/* The interpreter. Static because an akbasic_Runtime is far too big for a
|
||||||
|
* stack frame -- 2.40 MiB on this branch. */
|
||||||
|
static akbasic_Runtime SCRIPT;
|
||||||
|
static akbasic_TextSink SINK;
|
||||||
|
static akbasic_StdioSink SINKSTATE;
|
||||||
|
|
||||||
|
/** @brief Longest galaga.bas this loader will accept. */
|
||||||
|
#define GALAGA_MAX_SCRIPT_BYTES 16384
|
||||||
|
|
||||||
|
static char SOURCE[GALAGA_MAX_SCRIPT_BYTES];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enemy kind -> BASIC function name. Dispatch is a table, not a conditional:
|
||||||
|
* adding a kind is one row here and one DEF in galaga.bas.
|
||||||
|
*/
|
||||||
|
static const char *UPDATE_FUNCTION[GALAGA_ENEMY_KINDS] = {
|
||||||
|
"UPDATEBEE", /* GALAGA_ENEMY_BEE */
|
||||||
|
"UPDATEBFLY", /* GALAGA_ENEMY_BUTTERFLY */
|
||||||
|
"UPDATEBOSS" /* GALAGA_ENEMY_BOSS */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------- host types --- */
|
||||||
|
|
||||||
|
static const akbasic_HostField ENEMY_FIELDS[] = {
|
||||||
|
/* struct member BASIC name C representation */
|
||||||
|
AKBASIC_HOST_FIELD( galaga_Enemy, kind, "KIND#", AKBASIC_HOSTFIELD_INT32 ),
|
||||||
|
AKBASIC_HOST_FIELD( galaga_Enemy, state, "STATE#", AKBASIC_HOSTFIELD_INT32 ),
|
||||||
|
AKBASIC_HOST_FIELD( galaga_Enemy, homex, "HOMEX%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||||
|
AKBASIC_HOST_FIELD( galaga_Enemy, homey, "HOMEY%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||||
|
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, "ROLL%", AKBASIC_HOSTFIELD_FLOAT )
|
||||||
|
};
|
||||||
|
static const akbasic_HostType ENEMY_TYPE = {
|
||||||
|
"ENEMY", sizeof(galaga_Enemy), ENEMY_FIELDS, 8
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The live actor itself. This is the demonstrative point of the whole
|
||||||
|
* example: the script writes the engine's *real* actor memory -- the same x
|
||||||
|
* the renderer reads -- with no copy in either direction.
|
||||||
|
*/
|
||||||
|
static const akbasic_HostField ACTOR_FIELDS[] = {
|
||||||
|
AKBASIC_HOST_FIELD( akgl_Actor, x, "X%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||||
|
AKBASIC_HOST_FIELD( akgl_Actor, y, "Y%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||||
|
AKBASIC_HOST_FIELD( akgl_Actor, state, "STATE#", AKBASIC_HOSTFIELD_INT32 ),
|
||||||
|
AKBASIC_HOST_FIELD( akgl_Actor, visible, "VISIBLE#", AKBASIC_HOSTFIELD_BOOL )
|
||||||
|
};
|
||||||
|
static const akbasic_HostType ACTOR_TYPE = {
|
||||||
|
"ACTOR", sizeof(akgl_Actor), ACTOR_FIELDS, 4
|
||||||
|
};
|
||||||
|
|
||||||
|
static const akbasic_HostField GAME_FIELDS[] = {
|
||||||
|
AKBASIC_HOST_FIELD( galaga_Shared, playerx, "PLAYERX%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||||
|
AKBASIC_HOST_FIELD( galaga_Shared, playery, "PLAYERY%", AKBASIC_HOSTFIELD_FLOAT ),
|
||||||
|
AKBASIC_HOST_FIELD( galaga_Shared, wave, "WAVE#", AKBASIC_HOSTFIELD_INT32 ),
|
||||||
|
AKBASIC_HOST_FIELD( galaga_Shared, rnd, "ROLL%", AKBASIC_HOSTFIELD_FLOAT )
|
||||||
|
};
|
||||||
|
static const akbasic_HostType GAME_TYPE = {
|
||||||
|
"GAME", sizeof(galaga_Shared), GAME_FIELDS, 4
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Placeholders the boot bindings point at until the first real rebind. A
|
||||||
|
* binding is borrowed, never copied, so these must be static storage. */
|
||||||
|
static galaga_Enemy SCRATCH_ENEMY;
|
||||||
|
static akgl_Actor SCRATCH_ACTOR;
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------- boot --- */
|
||||||
|
|
||||||
|
static akerr_ErrorContext *read_script(char *path)
|
||||||
|
{
|
||||||
|
FILE *fp = NULL;
|
||||||
|
size_t got = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "path");
|
||||||
|
fp = fopen(path, "rb");
|
||||||
|
FAIL_ZERO_RETURN(errctx, fp, AKERR_IO, "Cannot open the enemy script %s", path);
|
||||||
|
ATTEMPT {
|
||||||
|
got = fread(SOURCE, 1, sizeof(SOURCE) - 1, fp);
|
||||||
|
SOURCE[got] = '\0';
|
||||||
|
FAIL_NONZERO_BREAK(errctx, (got >= sizeof(SOURCE) - 1), AKERR_OUTOFBOUNDS,
|
||||||
|
"%s does not fit in the %d byte script buffer",
|
||||||
|
path, GALAGA_MAX_SCRIPT_BYTES);
|
||||||
|
} CLEANUP {
|
||||||
|
fclose(fp);
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief One dry call of every dispatch-table function, at boot.
|
||||||
|
*
|
||||||
|
* A missing or misspelled DEF fails here, at startup, with the function's name
|
||||||
|
* in the message -- not on frame one of the first wave. The scratch enemy's
|
||||||
|
* state word is zero, so no maneuver block runs and nothing moves.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *dry_run(void)
|
||||||
|
{
|
||||||
|
akbasic_Value dt;
|
||||||
|
akbasic_Value *argp[1];
|
||||||
|
akbasic_Value *result = NULL;
|
||||||
|
int i = 0;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
memset(&SCRATCH_ENEMY, 0, sizeof(SCRATCH_ENEMY));
|
||||||
|
memset(&dt, 0, sizeof(dt));
|
||||||
|
dt.valuetype = AKBASIC_TYPE_FLOAT;
|
||||||
|
dt.floatval = 0.0;
|
||||||
|
argp[0] = &dt;
|
||||||
|
|
||||||
|
for ( i = 0; i < GALAGA_ENEMY_KINDS; i++ ) {
|
||||||
|
PASS(errctx, akbasic_host_rebind(&SCRIPT, "SELF@", &SCRATCH_ENEMY));
|
||||||
|
PASS(errctx, akbasic_host_rebind(&SCRIPT, "ACTOR@", &SCRATCH_ACTOR));
|
||||||
|
PASS(errctx, akbasic_runtime_call_function(&SCRIPT, (char *)UPDATE_FUNCTION[i],
|
||||||
|
argp, 1, &result));
|
||||||
|
/*
|
||||||
|
* A body that died reports through the sink and answers zero; the
|
||||||
|
* dropped mode is the only signal C gets. At boot that must be fatal
|
||||||
|
* and must say which function -- not frame one of the first wave.
|
||||||
|
*/
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (SCRIPT.mode != AKBASIC_MODE_RUN), AKERR_VALUE,
|
||||||
|
"%s died during the boot dry run; the interpreter's report"
|
||||||
|
" is above", UPDATE_FUNCTION[i]);
|
||||||
|
PASS(errctx, akbasic_environment_zero(SCRIPT.environment));
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *galaga_script_boot(char *path)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, path, AKERR_NULLPOINTER, "path");
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_error_register());
|
||||||
|
PASS(errctx, akbasic_sink_init_stdio(&SINK, &SINKSTATE, stdout, NULL));
|
||||||
|
PASS(errctx, akbasic_runtime_init(&SCRIPT, &SINK));
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_host_register_type(&SCRIPT, &ENEMY_TYPE));
|
||||||
|
PASS(errctx, akbasic_host_register_type(&SCRIPT, &ACTOR_TYPE));
|
||||||
|
PASS(errctx, akbasic_host_register_type(&SCRIPT, &GAME_TYPE));
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_host_bind(&SCRIPT, "SELF@", "ENEMY", &SCRATCH_ENEMY));
|
||||||
|
PASS(errctx, akbasic_host_bind(&SCRIPT, "ACTOR@", "ACTOR", &SCRATCH_ACTOR));
|
||||||
|
PASS(errctx, akbasic_host_bind(&SCRIPT, "GAME@", "GAME", &galaga_shared));
|
||||||
|
|
||||||
|
PASS(errctx, read_script(path));
|
||||||
|
PASS(errctx, akbasic_runtime_load(&SCRIPT, SOURCE));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A "no top level code" script still has to run once: executing the DEF
|
||||||
|
* statements is what files the functions. The run is bounded because a
|
||||||
|
* script that is all definitions has no business taking more than a step
|
||||||
|
* per line, and an accidental loop at boot should be a diagnosis, not a
|
||||||
|
* hang.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_runtime_start(&SCRIPT, AKBASIC_MODE_RUN));
|
||||||
|
PASS(errctx, akbasic_runtime_run(&SCRIPT, 4 * AKBASIC_MAX_SOURCE_LINES));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The program has now ended and the runtime sits in QUIT mode, where a
|
||||||
|
* multi-line DEF called from the host returns a silent zero. Forcing the
|
||||||
|
* mode back makes the bodies run, and it stays put because nothing here
|
||||||
|
* ever steps the runtime again. Issue #8 tracks making this unnecessary.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_runtime_set_mode(&SCRIPT, AKBASIC_MODE_RUN));
|
||||||
|
|
||||||
|
PASS(errctx, dry_run());
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------ per frame --- */
|
||||||
|
|
||||||
|
akerr_ErrorContext *galaga_script_update_enemy(galaga_Enemy *enemy, akgl_Actor *actor, float dt)
|
||||||
|
{
|
||||||
|
akbasic_Value dtval;
|
||||||
|
akbasic_Value *argp[1];
|
||||||
|
akbasic_Value *result = NULL;
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, enemy, AKERR_NULLPOINTER, "enemy");
|
||||||
|
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor");
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (enemy->kind < 0 || enemy->kind >= GALAGA_ENEMY_KINDS),
|
||||||
|
AKERR_VALUE, "Enemy kind %d has no update function", enemy->kind);
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_host_rebind(&SCRIPT, "SELF@", enemy));
|
||||||
|
PASS(errctx, akbasic_host_rebind(&SCRIPT, "ACTOR@", actor));
|
||||||
|
|
||||||
|
memset(&dtval, 0, sizeof(dtval));
|
||||||
|
dtval.valuetype = AKBASIC_TYPE_FLOAT;
|
||||||
|
dtval.floatval = (double)dt;
|
||||||
|
argp[0] = &dtval;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* An error in an enemy's function is that script's problem, not the
|
||||||
|
* engine's: the enemy goes dumb -- cleared to a formation hold it will
|
||||||
|
* never leave -- and the frame lives. HANDLE_DEFAULT absorbs whatever the
|
||||||
|
* interpreter raised; the first failure is logged with the function's
|
||||||
|
* name, the rest are counted, because sixty a second of the same message
|
||||||
|
* is how a log stops being read.
|
||||||
|
*/
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, akbasic_runtime_call_function(&SCRIPT, (char *)UPDATE_FUNCTION[enemy->kind],
|
||||||
|
argp, 1, &result));
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE_DEFAULT(errctx) {
|
||||||
|
if ( galaga_game.script_errors == 0 ) {
|
||||||
|
LOG_ERROR_WITH_MESSAGE(errctx, "first script error; this enemy is now dumb");
|
||||||
|
}
|
||||||
|
galaga_game.script_errors += 1;
|
||||||
|
enemy->state = GALAGA_ES_FORMATION;
|
||||||
|
enemy->fire = 0;
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A BASIC-level error in the body is quieter than an interpreter error:
|
||||||
|
* it reports through the sink, the call answers a stale value, and the
|
||||||
|
* runtime falls out of RUN mode -- after which every later call is a
|
||||||
|
* silent no-op. The mode is the tell. Revival is two calls:
|
||||||
|
* clear_error(), because a run's first error latches and every line is
|
||||||
|
* skipped while it stands, and the same set_mode(RUN) the boot needed
|
||||||
|
* (issue #8's mechanics). The enemy is treated exactly like the
|
||||||
|
* interpreter-error case above.
|
||||||
|
*/
|
||||||
|
if ( SCRIPT.mode != AKBASIC_MODE_RUN ) {
|
||||||
|
if ( galaga_game.script_errors == 0 ) {
|
||||||
|
SDL_Log("first script error (reported by the interpreter above);"
|
||||||
|
" enemy %s is now dumb", UPDATE_FUNCTION[enemy->kind]);
|
||||||
|
}
|
||||||
|
galaga_game.script_errors += 1;
|
||||||
|
enemy->state = GALAGA_ES_FORMATION;
|
||||||
|
enemy->fire = 0;
|
||||||
|
PASS(errctx, akbasic_runtime_clear_error(&SCRIPT));
|
||||||
|
PASS(errctx, akbasic_runtime_set_mode(&SCRIPT, AKBASIC_MODE_RUN));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load-bearing: akbasic_runtime_call_function() parks every result in the
|
||||||
|
* caller environment's per-line value scratch, and a host calling in a
|
||||||
|
* loop never crosses the line boundary that would reset it. Without this
|
||||||
|
* the pool drains in under two frames of a 40-enemy wave.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_environment_zero(SCRIPT.environment));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ REM A demoscene production for the akbasic interpreter, written as if
|
|||||||
REM it were 1985 and this dialect were the machine under the tree.
|
REM it were 1985 and this dialect were the machine under the tree.
|
||||||
REM It leans on every corner of the interpreter on purpose:
|
REM It leans on every corner of the interpreter on purpose:
|
||||||
REM
|
REM
|
||||||
REM - there is no RND, so it carries chapter 17's LCG and seeds it
|
REM - it carries chapter 17's LCG rather than the native RND, and seeds it
|
||||||
REM from the jiffy clock
|
REM from the jiffy clock
|
||||||
REM - the palette cannot be rewritten, so every "colour cycle" is an
|
REM - the palette cannot be rewritten, so every "colour cycle" is an
|
||||||
REM honest redraw of the same strokes in the next colour
|
REM honest redraw of the same strokes in the next colour
|
||||||
@@ -81,7 +81,7 @@ REM machine and a predeclared scratch variable occupies one forever,
|
|||||||
REM where a scoped one gives its slot back. The first cut of this
|
REM where a scoped one gives its slot back. The first cut of this
|
||||||
REM program predeclared everything and ran the pool dry.
|
REM program predeclared everything and ran the pool dry.
|
||||||
RMAX# = 0
|
RMAX# = 0
|
||||||
RND# = 0
|
ROLL# = 0
|
||||||
TX% = 0
|
TX% = 0
|
||||||
TB# = 0
|
TB# = 0
|
||||||
|
|
||||||
@@ -816,11 +816,12 @@ REM =====================================================================
|
|||||||
REM Subroutines.
|
REM Subroutines.
|
||||||
REM =====================================================================
|
REM =====================================================================
|
||||||
|
|
||||||
REM Chapter 17's generator, verbatim: there is no RND in this dialect.
|
REM Chapter 17's historical generator, verbatim. RND(n) is built in now;
|
||||||
REM Answers 0 to RMAX#-1 in RND#, from the middle bits of the seed.
|
REM this stays so the demo runs identically on every machine.
|
||||||
|
REM Answers 0 to RMAX#-1 in ROLL#, from the middle bits of the seed.
|
||||||
LABEL RANDOM
|
LABEL RANDOM
|
||||||
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
|
SEED# = MOD(((SEED# * 1103515245) + 12345), 2147483648)
|
||||||
RND# = MOD((SEED# / 65536), RMAX#)
|
ROLL# = MOD((SEED# / 65536), RMAX#)
|
||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
REM NS# stars in four brightnesses. Two LCG pulls a star, and the
|
REM NS# stars in four brightnesses. Two LCG pulls a star, and the
|
||||||
@@ -830,10 +831,10 @@ SI# = 0
|
|||||||
DO WHILE SI# < NS#
|
DO WHILE SI# < NS#
|
||||||
RMAX# = W#
|
RMAX# = W#
|
||||||
GOSUB RANDOM
|
GOSUB RANDOM
|
||||||
X# = RND#
|
X# = ROLL#
|
||||||
RMAX# = H#
|
RMAX# = H#
|
||||||
GOSUB RANDOM
|
GOSUB RANDOM
|
||||||
Y# = RND#
|
Y# = ROLL#
|
||||||
B# = MOD(SEED#, 4)
|
B# = MOD(SEED#, 4)
|
||||||
COLOR 1, STC#(B#)
|
COLOR 1, STC#(B#)
|
||||||
DRAW 1, X#, Y#
|
DRAW 1, X#, Y#
|
||||||
@@ -1291,7 +1292,7 @@ LABEL REHOME
|
|||||||
MOVSPR L2#, 0.5 * W#, 0.5 * H#
|
MOVSPR L2#, 0.5 * W#, 0.5 * H#
|
||||||
RMAX# = 360
|
RMAX# = 360
|
||||||
GOSUB RANDOM
|
GOSUB RANDOM
|
||||||
SBG#(L2#) = RND#
|
SBG#(L2#) = ROLL#
|
||||||
MOVSPR L2#, SBG#(L2#) # 5
|
MOVSPR L2#, SBG#(L2#) # 5
|
||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ typedef struct
|
|||||||
*
|
*
|
||||||
* Claimed up front rather than per scan for two reasons. The pool is shared
|
* Claimed up front rather than per scan for two reasons. The pool is shared
|
||||||
* with whatever host this interpreter is embedded in -- a game with its own
|
* with whatever host this interpreter is embedded in -- a game with its own
|
||||||
* shaped actors draws from the same #AKGL_MAX_HEAP_COLLISION_PROXY -- so
|
* shaped actors draws from the same `AKGL_MAX_HEAP_COLLISION_PROXY` -- so
|
||||||
* running out is a real possibility, and it should be an init-time failure
|
* running out is a real possibility, and it should be an init-time failure
|
||||||
* naming the pool rather than a collision scan that starts refusing halfway
|
* naming the pool rather than a collision scan that starts refusing halfway
|
||||||
* through a game. And a proxy carries a *copy* of its shape, so there is
|
* through a game. And a proxy carries a *copy* of its shape, so there is
|
||||||
|
|||||||
@@ -72,6 +72,37 @@ typedef struct akbasic_Environment
|
|||||||
*/
|
*/
|
||||||
bool exiting;
|
bool exiting;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Generator state (GEN / EMIT / FOR EACH / DO EACH).
|
||||||
|
*
|
||||||
|
* `isGenerator` is set on the environment a GEN call pushes -- the one
|
||||||
|
* whose body is actually running the GEN's lines, as opposed to the loop's
|
||||||
|
* own environment. `generatorFn` records *which* GEN it is running, so a
|
||||||
|
* FOR EACH/DO EACH that would invoke a GEN currently running higher up the
|
||||||
|
* parent chain (self-recursion) can be told apart from one invoking it
|
||||||
|
* fresh, or invoking a sibling instance of the same GEN sitting detached in
|
||||||
|
* someone else's `forGeneratorEnv`. It carries an akbasic_FunctionDef *, kept
|
||||||
|
* as void * for the same reason akbasic_environment_get_function() does:
|
||||||
|
* runtime.h includes this header, not the other way around.
|
||||||
|
*/
|
||||||
|
bool isGenerator;
|
||||||
|
void *generatorFn;
|
||||||
|
/**
|
||||||
|
* Set on a FOR EACH or DO EACH loop's own environment, distinguishing it
|
||||||
|
* from a plain FOR/DO for verbs that need to know which kind of loop this
|
||||||
|
* is -- EXIT, NEXT and LOOP all read it.
|
||||||
|
*/
|
||||||
|
bool isEachLoop;
|
||||||
|
/**
|
||||||
|
* The generator environment a FOR EACH/DO EACH loop is suspended on
|
||||||
|
* between iterations -- alive, detached from the step loop, but not
|
||||||
|
* released, so its own `nextline` still says where to resume. NULL means
|
||||||
|
* either "not an EACH loop" or "the generator is exhausted": both leave
|
||||||
|
* nothing to resume, and by the time either becomes true the loop
|
||||||
|
* environment itself is on its way out too.
|
||||||
|
*/
|
||||||
|
struct akbasic_Environment *forGeneratorEnv;
|
||||||
|
|
||||||
int64_t gosubReturnLine;
|
int64_t gosubReturnLine;
|
||||||
|
|
||||||
/* READ state. The identifier leaves are deep copies, so they need storage. */
|
/* READ state. The identifier leaves are deep copies, so they need storage. */
|
||||||
|
|||||||
@@ -12,9 +12,12 @@
|
|||||||
* libakerror 2.0.0 is the floor, raised from 1.0.0 because 2.0.0 is an ABI break
|
* libakerror 2.0.0 is the floor, raised from 1.0.0 because 2.0.0 is an ABI break
|
||||||
* that a compile against the wrong header cannot survive quietly:
|
* that a compile against the wrong header cannot survive quietly:
|
||||||
*
|
*
|
||||||
* - `__akerr_last_ignored` became thread-local. `IGNORE` expands at *our* call
|
* - The context behind `IGNORE` became thread-local. `IGNORE` expands at *our*
|
||||||
* site, so our objects reference that symbol under whichever storage model
|
* call site, so our objects reference that storage under whichever model the
|
||||||
* the header on the include path declared.
|
* header on the include path declared. 2.0.2 went further and made it a
|
||||||
|
* per-translation-unit `static` snapshot named `akerr_last_ignored`, copied
|
||||||
|
* from the pool slot so the slot can be released; the old spelling
|
||||||
|
* `__akerr_last_ignored` was an `extern` pointer and no longer exists.
|
||||||
* - `akerr_next_error()` now returns a context that already holds a reference,
|
* - `akerr_next_error()` now returns a context that already holds a reference,
|
||||||
* and `ENSURE_ERROR_READY` no longer increments. Objects compiled against a
|
* and `ENSURE_ERROR_READY` no longer increments. Objects compiled against a
|
||||||
* 1.x header count every reference twice and never give a slot back.
|
* 1.x header count every reference twice and never give a slot back.
|
||||||
|
|||||||
@@ -102,6 +102,15 @@ typedef struct
|
|||||||
akbasic_ASTLeaf *arglist;
|
akbasic_ASTLeaf *arglist;
|
||||||
akbasic_ASTLeaf *expression;
|
akbasic_ASTLeaf *expression;
|
||||||
int64_t lineno;
|
int64_t lineno;
|
||||||
|
/*
|
||||||
|
* Set by akbasic_parse_gen(), left false by akbasic_parse_def(). GEN and
|
||||||
|
* DEF share this table (TODO.md's namespace decision for generators), so
|
||||||
|
* this is what lets akbasic_runtime_call_function() refuse to run a GEN
|
||||||
|
* called like an ordinary function -- cleanly, before anything is pushed,
|
||||||
|
* rather than relying on EMIT to fail deep inside a call whose BASIC-level
|
||||||
|
* error a caller driving its own step loop would not see raised.
|
||||||
|
*/
|
||||||
|
bool isGenerator;
|
||||||
/*
|
/*
|
||||||
* There is deliberately no environment here. It used to be owned by the
|
* There is deliberately no environment here. It used to be owned by the
|
||||||
* funcdef and reset on every call, which made a function not re-entrant --
|
* funcdef and reset on every call, which made a function not re-entrant --
|
||||||
@@ -252,6 +261,11 @@ typedef struct akbasic_Runtime
|
|||||||
*/
|
*/
|
||||||
int64_t timems;
|
int64_t timems;
|
||||||
|
|
||||||
|
/* RND's lazy seed state. The flag distinguishes an unseeded run from a
|
||||||
|
* legitimate LCG state of zero. */
|
||||||
|
int64_t rndseed;
|
||||||
|
bool rndseeded;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set by a branch that has decided the remaining statements on its line
|
* Set by a branch that has decided the remaining statements on its line
|
||||||
* belong to the arm it did not take, and cleared at the top of every line.
|
* belong to the arm it did not take, and cleared at the top of every line.
|
||||||
@@ -590,6 +604,115 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_environment(akbasic_Runti
|
|||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_prev_environment(akbasic_Runtime *obj);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_prev_environment(akbasic_Runtime *obj);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Return control to the active scope's parent, without releasing it.
|
||||||
|
*
|
||||||
|
* The half of akbasic_runtime_prev_environment() that pops; the other half,
|
||||||
|
* akbasic_runtime_release_environment(), gives the scope's variables and its
|
||||||
|
* pool slot back. Split for EMIT: a generator suspended between iterations
|
||||||
|
* has to keep existing -- its own `nextline` is where NEXT resumes it -- so
|
||||||
|
* detaching without releasing is what lets `obj->environment` move on to the
|
||||||
|
* loop while the generator's scope stays alive, reachable through
|
||||||
|
* `forGeneratorEnv`.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKBASIC_ERR_ENVIRONMENT When the active scope is the root.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_detach_environment(akbasic_Runtime *obj);
|
||||||
|
/**
|
||||||
|
* @brief Give a scope's variables and its pool slot back.
|
||||||
|
*
|
||||||
|
* The other half of akbasic_runtime_prev_environment(): unlike that function,
|
||||||
|
* @p env need not be `obj->environment` -- a generator environment sitting
|
||||||
|
* detached in some loop's `forGeneratorEnv` is released this way once it is
|
||||||
|
* exhausted or abandoned, without disturbing whatever scope is active now.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param env The scope to release; must not be NULL and must not be the root.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When `env` is NULL.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_release_environment(akbasic_Runtime *obj, akbasic_Environment *env);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Pop and release every scope from the active one up to @p target.
|
||||||
|
*
|
||||||
|
* The shared teardown for every path that abandons part of the environment
|
||||||
|
* chain at once instead of popping it a verb at a time: the error unwinds in
|
||||||
|
* akbasic_runtime_pump_generator() and akbasic_runtime_call_function(). Each
|
||||||
|
* popped scope's suspended generator (`forGeneratorEnv`), if it still holds
|
||||||
|
* one, is released through akbasic_runtime_release_generator() -- a suspended
|
||||||
|
* generator is a *child* of its loop scope, so no walk up the parent chain
|
||||||
|
* would ever reach it.
|
||||||
|
*
|
||||||
|
* Stops without error at the root if @p target is not on the chain: callers
|
||||||
|
* are already cleaning up after a failure, and releasing everything is the
|
||||||
|
* least-wrong answer to a target that has gone missing.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param target The scope to stop at; it is left active and untouched.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When @p target is NULL.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_unwind_to_environment(akbasic_Runtime *obj, akbasic_Environment *target);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Push a GEN's environment, bind its parameters and run it to its first EMIT.
|
||||||
|
*
|
||||||
|
* Shared by the EACH branches of `FOR` and `DO`: both look up the same kind of
|
||||||
|
* call expression (an `AKBASIC_LEAF_FUNCTION` leaf naming a GEN), guard
|
||||||
|
* against invoking a GEN that is already running higher up this same parent
|
||||||
|
* chain, evaluate the call's arguments in the caller's scope, bind them into a
|
||||||
|
* fresh environment the way a function call does, and run that environment
|
||||||
|
* until EMIT detaches it or END GEN ends it with nothing emitted.
|
||||||
|
*
|
||||||
|
* @p loopenv is left in the state a caller checks afterwards:
|
||||||
|
* `loopenv->forGeneratorEnv` is the live generator environment when
|
||||||
|
* something was emitted, or NULL when the GEN produced nothing at all.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param loopenv The FOR EACH/DO EACH loop's own environment; becomes the new
|
||||||
|
* generator environment's parent.
|
||||||
|
* @param callexpr The generator call, e.g. `ROOMOBJECTS(CURROOM%)`.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKBASIC_ERR_STATE When the named GEN is already running higher up
|
||||||
|
* this same parent chain.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_generator_invoke(akbasic_Runtime *obj, akbasic_Environment *loopenv, akbasic_ASTLeaf *callexpr);
|
||||||
|
/**
|
||||||
|
* @brief Run a suspended generator until it next detaches.
|
||||||
|
*
|
||||||
|
* Shared by the EACH branches of `NEXT` and `LOOP`: `obj->environment` is
|
||||||
|
* expected to already be the generator environment to resume (a caller sets
|
||||||
|
* that from `loopenv->forGeneratorEnv` before calling), and this drives the
|
||||||
|
* step loop until either EMIT detaches it back to @p loopenv with another
|
||||||
|
* value, or END GEN really ends it -- in which case it releases the
|
||||||
|
* generator environment itself and clears `loopenv->forGeneratorEnv`.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param loopenv The FOR EACH/DO EACH loop's own environment, and the pump's
|
||||||
|
* stopping point.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_pump_generator(akbasic_Runtime *obj, akbasic_Environment *loopenv);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Release a live, abandoned generator, wherever EMIT left it suspended.
|
||||||
|
*
|
||||||
|
* `env` is expected to be a loop's `forGeneratorEnv` -- the resume point, not
|
||||||
|
* necessarily the GEN's own call frame, since EMIT may have run several
|
||||||
|
* levels below it inside a FOR/DO/GOSUB the body wrote. Walks up from there,
|
||||||
|
* releasing every environment through the call frame itself inclusive, so
|
||||||
|
* nothing above the resume point is left behind.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param env The suspended resume point to release.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When `env` is NULL.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_release_generator(akbasic_Runtime *obj, akbasic_Environment *env);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Report a BASIC error on the current line, in the reference's format.
|
* @brief Report a BASIC error on the current line, in the reference's format.
|
||||||
*
|
*
|
||||||
@@ -632,6 +755,27 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_println(akbasic_Runtime *obj,
|
|||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_mode(akbasic_Runtime *obj, int mode);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_set_mode(akbasic_Runtime *obj, int mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Forgive the last BASIC-level error, so a host may call again.
|
||||||
|
*
|
||||||
|
* A program run latches its first runtime error and ends -- deliberately, and
|
||||||
|
* a host cannot un-decide that with akbasic_runtime_set_mode() alone: the
|
||||||
|
* latch survives the mode change, every later line is skipped, and every
|
||||||
|
* later akbasic_runtime_call_function() answers a stale value after walking
|
||||||
|
* the whole source table doing nothing.
|
||||||
|
*
|
||||||
|
* A host that absorbed a script error -- reported through the sink, actor
|
||||||
|
* marked dumb, frame preserved -- calls this beside
|
||||||
|
* `akbasic_runtime_set_mode(obj, AKBASIC_MODE_RUN)` to put the runtime back
|
||||||
|
* in service. It is for hosts between calls, not for verbs during a run: a
|
||||||
|
* running program's first error still ends it, exactly once, with one line.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_clear_error(akbasic_Runtime *obj);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Evaluate one AST leaf, drawing scratch values from the environment.
|
* @brief Evaluate one AST leaf, drawing scratch values from the environment.
|
||||||
* @param obj Object to initialize, inspect, or modify.
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
@@ -806,13 +950,6 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_reserve_globals(akbasic_Runti
|
|||||||
* @throws AKBASIC_ERR_BOUNDS When every variable slot is in use.
|
* @throws AKBASIC_ERR_BOUNDS When every variable slot is in use.
|
||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_variable(akbasic_Runtime *obj, akbasic_Variable **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_variable(akbasic_Runtime *obj, akbasic_Variable **dest);
|
||||||
/**
|
|
||||||
* @brief Take an unused function definition from the runtime's pool.
|
|
||||||
* @param obj Object to initialize, inspect, or modify.
|
|
||||||
* @param dest Output destination populated by the function.
|
|
||||||
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
||||||
* @throws AKBASIC_ERR_BOUNDS When every function slot is in use.
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* @brief Call a user-defined function with values a caller already has.
|
* @brief Call a user-defined function with values a caller already has.
|
||||||
*
|
*
|
||||||
@@ -839,6 +976,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_variable(akbasic_Runtime
|
|||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_call_function(struct akbasic_Runtime *obj, const char *name, akbasic_Value **args, int nargs, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_call_function(struct akbasic_Runtime *obj, const char *name, akbasic_Value **args, int nargs, akbasic_Value **dest);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Take an unused function definition from the runtime's pool.
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param dest Output destination populated by the function.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
* @throws AKBASIC_ERR_BOUNDS When every function slot is in use.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_function(akbasic_Runtime *obj, akbasic_FunctionDef **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_runtime_new_function(akbasic_Runtime *obj, akbasic_FunctionDef **dest);
|
||||||
/**
|
/**
|
||||||
* @brief File one already-scanned source line under its line number.
|
* @brief File one already-scanned source line under its line number.
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
* with headroom. The ceiling matters because these come out of libakgl's
|
* with headroom. The ceiling matters because these come out of libakgl's
|
||||||
* collision proxy pool, which is shared with whatever host this interpreter is
|
* collision proxy pool, which is shared with whatever host this interpreter is
|
||||||
* embedded in: eight sprites plus sixty-four solids is seventy-two of
|
* embedded in: eight sprites plus sixty-four solids is seventy-two of
|
||||||
* #AKGL_MAX_HEAP_COLLISION_PROXY, and the rest is the host's.
|
* `AKGL_MAX_HEAP_COLLISION_PROXY`, and the rest is the host's.
|
||||||
*/
|
*/
|
||||||
#ifndef AKBASIC_MAX_SOLIDS
|
#ifndef AKBASIC_MAX_SOLIDS
|
||||||
#define AKBASIC_MAX_SOLIDS 64
|
#define AKBASIC_MAX_SOLIDS 64
|
||||||
@@ -117,7 +117,7 @@ typedef struct
|
|||||||
int speed; /* clockwise from vertical, and 0-15 */
|
int speed; /* clockwise from vertical, and 0-15 */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SPRHIT's collision shape: one of the #AKBASIC_SHAPE_* kinds, and a
|
* SPRHIT's collision shape: one of the `AKBASIC_SHAPE_*` kinds, and a
|
||||||
* rectangle measured from the sprite's top-left corner.
|
* rectangle measured from the sprite's top-left corner.
|
||||||
*
|
*
|
||||||
* `shapeexplicit` is what separates "the program asked for the whole frame"
|
* `shapeexplicit` is what separates "the program asked for the whole frame"
|
||||||
|
|||||||
@@ -282,9 +282,8 @@ def copy_tree(src, dst):
|
|||||||
# and a coverage tree drags along every .gcno/.gcda as well.
|
# and a coverage tree drags along every .gcno/.gcda as well.
|
||||||
#
|
#
|
||||||
# deps/ is copied wholesale and has to be: the build pulls libakerror and
|
# deps/ is copied wholesale and has to be: the build pulls libakerror and
|
||||||
# libakstdlib in with add_subdirectory. The golden corpus used to be the
|
# libakstdlib in with add_subdirectory. The language corpus is the other
|
||||||
# other reason -- it was driven in place out of deps/basicinterpret -- and
|
# reason it comes along with the rest of the tree.
|
||||||
# now lives in tests/reference/, which comes along with the rest of the tree.
|
|
||||||
# Only vendored Windows DLLs are dead weight, hence "*.dll".
|
# Only vendored Windows DLLs are dead weight, hence "*.dll".
|
||||||
#
|
#
|
||||||
# **PNGs are ours or theirs, and the difference matters.** This used to drop
|
# **PNGs are ours or theirs, and the difference matters.** This used to drop
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ akerr_ErrorContext *akbasic_environment_init(akbasic_Environment *obj, akbasic_R
|
|||||||
obj->doConditionLeaf = NULL;
|
obj->doConditionLeaf = NULL;
|
||||||
obj->doConditionKind = AKBASIC_LOOPCOND_NONE;
|
obj->doConditionKind = AKBASIC_LOOPCOND_NONE;
|
||||||
obj->isDoLoop = false;
|
obj->isDoLoop = false;
|
||||||
|
obj->isGenerator = false;
|
||||||
|
obj->generatorFn = NULL;
|
||||||
|
obj->isEachLoop = false;
|
||||||
|
obj->forGeneratorEnv = NULL;
|
||||||
obj->gosubReturnLine = 0;
|
obj->gosubReturnLine = 0;
|
||||||
obj->readReturnLine = 0;
|
obj->readReturnLine = 0;
|
||||||
obj->readIdentifierIdx = 0;
|
obj->readIdentifierIdx = 0;
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
|
|
||||||
#include "verbs.h"
|
#include "verbs.h"
|
||||||
|
|
||||||
|
/* Shared by akbasic_parse_for() and akbasic_parse_do(); defined after akbasic_parse_def(). */
|
||||||
|
static akerr_ErrorContext *parse_each_clause(akbasic_Parser *parser, akbasic_ASTLeaf **var, akbasic_ASTLeaf **callexpr);
|
||||||
|
|
||||||
akerr_ErrorContext *akbasic_parse_arglist(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
akerr_ErrorContext *akbasic_parse_arglist(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -309,8 +312,58 @@ akerr_ErrorContext *akbasic_parse_do(akbasic_Parser *parser, akbasic_ASTLeaf **d
|
|||||||
akbasic_Environment *newenv = NULL;
|
akbasic_Environment *newenv = NULL;
|
||||||
akbasic_ASTLeaf *expr = NULL;
|
akbasic_ASTLeaf *expr = NULL;
|
||||||
akbasic_ASTLeaf *condition = NULL;
|
akbasic_ASTLeaf *condition = NULL;
|
||||||
|
akbasic_Token *peeked = NULL;
|
||||||
int kind = AKBASIC_LOOPCOND_NONE;
|
int kind = AKBASIC_LOOPCOND_NONE;
|
||||||
int64_t firstline = parent->lineno + 1;
|
int64_t firstline = parent->lineno + 1;
|
||||||
|
int cmp = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* DO EACH <variable> IN <generator call> ... LOOP. Mutually exclusive with
|
||||||
|
* DO WHILE/UNTIL on the same DO, so this is checked first and returns
|
||||||
|
* before any of the WHILE/UNTIL machinery runs.
|
||||||
|
*/
|
||||||
|
peeked = akbasic_parser_peek(parser);
|
||||||
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
|
||||||
|
PASS(errctx, aksl_strcmp(peeked->lexeme, "EACH", &cmp));
|
||||||
|
if ( cmp == 0 ) {
|
||||||
|
akbasic_ASTLeaf *var = NULL;
|
||||||
|
akbasic_ASTLeaf *callexpr = NULL;
|
||||||
|
|
||||||
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
||||||
|
newenv = runtime->environment;
|
||||||
|
runtime->environment = parent;
|
||||||
|
|
||||||
|
PASS(errctx, parse_each_clause(parser, &var, &callexpr));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Same guard as akbasic_parse_for()'s EACH branch, with the likely
|
||||||
|
* mistake named: a condition belongs on the LOOP, where it is
|
||||||
|
* checked against each emitted value, not here on the DO.
|
||||||
|
*/
|
||||||
|
peeked = akbasic_parser_peek(parser);
|
||||||
|
FAIL_NONZERO_RETURN(errctx,
|
||||||
|
(peeked != NULL &&
|
||||||
|
peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
|
||||||
|
peeked->tokentype != AKBASIC_TOK_COLON),
|
||||||
|
AKBASIC_ERR_SYNTAX,
|
||||||
|
"DO EACH takes its WHILE/UNTIL on the LOOP, and nothing else here");
|
||||||
|
|
||||||
|
newenv->isDoLoop = true;
|
||||||
|
newenv->isEachLoop = true;
|
||||||
|
newenv->loopFirstLine = firstline;
|
||||||
|
/* See akbasic_parse_for()'s EACH branch for why this is not cloned. */
|
||||||
|
newenv->forToLeaf = callexpr;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
||||||
|
PASS(errctx, akbasic_leaf_new_command(expr, "DO", var));
|
||||||
|
|
||||||
|
runtime->environment = newenv;
|
||||||
|
*dest = expr;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
||||||
newenv = runtime->environment;
|
newenv = runtime->environment;
|
||||||
@@ -883,6 +936,134 @@ akerr_ErrorContext *akbasic_parse_def(akbasic_Parser *parser, akbasic_ASTLeaf **
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GEN NAME(parameters) ... END GEN
|
||||||
|
*
|
||||||
|
* A DEF that yields more than once, in the same shape a multi-line DEF is:
|
||||||
|
* the header is parsed, the parameter list is read with parse_def_parameters()
|
||||||
|
* (GEN and DEF share the functions table -- TODO.md's namespace decision for
|
||||||
|
* this feature -- so a name cannot be both), and the body is skipped on this,
|
||||||
|
* the definitional pass, by arming akbasic_environment_wait_for_command() for
|
||||||
|
* "END GEN" instead of "RETURN". It only really executes when a FOR EACH/DO
|
||||||
|
* EACH invokes it through akbasic_runtime_generator_invoke(), which sets
|
||||||
|
* `nextline` to `fndef->lineno` directly and never runs this line again.
|
||||||
|
*
|
||||||
|
* There is no single-expression form: a GEN with nothing to loop over is just
|
||||||
|
* a DEF, and EMIT already needs a body to sit in.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext *akbasic_parse_gen(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Runtime *runtime = parser->runtime;
|
||||||
|
akbasic_ASTLeaf *identifier = NULL;
|
||||||
|
akbasic_ASTLeaf *arglist = NULL;
|
||||||
|
akbasic_ASTLeaf *command = NULL;
|
||||||
|
akbasic_FunctionDef *fndef = NULL;
|
||||||
|
size_t namelen = 0;
|
||||||
|
size_t i = 0;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_parser_primary(parser, &identifier));
|
||||||
|
FAIL_ZERO_RETURN(errctx, (identifier->leaftype == AKBASIC_LEAF_IDENTIFIER),
|
||||||
|
AKBASIC_ERR_SYNTAX, "Expected identifier");
|
||||||
|
|
||||||
|
PASS(errctx, parse_def_parameters(parser, &arglist));
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_runtime_new_function(runtime, &fndef));
|
||||||
|
|
||||||
|
/* Uppercase the name: verbs, functions and generators are all case-insensitive. */
|
||||||
|
PASS(errctx, aksl_strlen(identifier->identifier, &namelen));
|
||||||
|
FAIL_ZERO_RETURN(errctx, (namelen < sizeof(fndef->name)),
|
||||||
|
AKBASIC_ERR_BOUNDS, "Function name '%s' is too long", identifier->identifier);
|
||||||
|
for ( i = 0; i < namelen; i++ ) {
|
||||||
|
char c = identifier->identifier[i];
|
||||||
|
fndef->name[i] = (char)((c >= 'a' && c <= 'z') ? (c - 'a' + 'A') : c);
|
||||||
|
}
|
||||||
|
fndef->name[namelen] = '\0';
|
||||||
|
|
||||||
|
fndef->expression = NULL;
|
||||||
|
fndef->isGenerator = true;
|
||||||
|
PASS(errctx, akbasic_environment_wait_for_command(runtime->environment, "END GEN"));
|
||||||
|
PASS(errctx, akbasic_leaf_clone(arglist, &fndef->leafpool, &fndef->arglist));
|
||||||
|
fndef->lineno = runtime->environment->lineno + 1;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_symtab_set(&runtime->environment->functions, fndef->name, fndef, 0));
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_parser_new_leaf(parser, &command));
|
||||||
|
PASS(errctx, akbasic_leaf_new_command(command, "GEN", NULL));
|
||||||
|
*dest = command;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* END [GEN]
|
||||||
|
*
|
||||||
|
* Bare END finishes the program, the default command path's job before this
|
||||||
|
* handler existed. `END GEN` is a GEN body's own closing verb, and it is
|
||||||
|
* never scanned as one token -- GEN follows END as an ordinary COMMAND token
|
||||||
|
* on the same line -- so this is what tells the two apart and builds the
|
||||||
|
* compound leaf akbasic_cmd_end_gen dispatches on, the same trick
|
||||||
|
* akbasic_parse_print() uses for `PRINT #`.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext *akbasic_parse_end(akbasic_Parser *parser, akbasic_ASTLeaf **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_ASTLeaf *expr = NULL;
|
||||||
|
akbasic_ASTLeaf *right = NULL;
|
||||||
|
akbasic_Token *peeked = NULL;
|
||||||
|
int cmp = 0;
|
||||||
|
|
||||||
|
peeked = akbasic_parser_peek(parser);
|
||||||
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
|
||||||
|
PASS(errctx, aksl_strcmp(peeked->lexeme, "GEN", &cmp));
|
||||||
|
if ( cmp == 0 ) {
|
||||||
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
||||||
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
||||||
|
PASS(errctx, akbasic_leaf_new_command(expr, "END GEN", NULL));
|
||||||
|
*dest = expr;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A plain END, matching what the default command path used to do. */
|
||||||
|
if ( peeked != NULL && peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
|
||||||
|
peeked->tokentype != AKBASIC_TOK_COLON ) {
|
||||||
|
PASS(errctx, akbasic_parser_expression(parser, &right));
|
||||||
|
}
|
||||||
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
||||||
|
PASS(errctx, akbasic_leaf_new_command(expr, "END", right));
|
||||||
|
*dest = expr;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* EACH <variable> IN <generator call>
|
||||||
|
*
|
||||||
|
* Shared by akbasic_parse_for() and akbasic_parse_do(), both of which consume
|
||||||
|
* the leading EACH themselves (it is what tells them this is an EACH loop
|
||||||
|
* rather than their ordinary form) before calling this for the rest.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *parse_each_clause(akbasic_Parser *parser, akbasic_ASTLeaf **var, akbasic_ASTLeaf **callexpr)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Token *word = NULL;
|
||||||
|
int cmp = 0;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_parser_expression(parser, var));
|
||||||
|
FAIL_ZERO_RETURN(errctx, (*var != NULL && akbasic_leaf_is_identifier(*var)), AKBASIC_ERR_SYNTAX,
|
||||||
|
"Expected EACH (variable) IN (generator call)");
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND), AKBASIC_ERR_SYNTAX,
|
||||||
|
"Expected IN after EACH (variable)");
|
||||||
|
PASS(errctx, akbasic_parser_previous(parser, &word));
|
||||||
|
PASS(errctx, aksl_strcmp(word->lexeme, "IN", &cmp));
|
||||||
|
FAIL_NONZERO_RETURN(errctx, cmp, AKBASIC_ERR_SYNTAX, "Expected IN after EACH (variable)");
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_parser_expression(parser, callexpr));
|
||||||
|
FAIL_ZERO_RETURN(errctx, (*callexpr != NULL && (*callexpr)->leaftype == AKBASIC_LEAF_FUNCTION),
|
||||||
|
AKBASIC_ERR_SYNTAX, "Expected a generator call after IN");
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* FOR ... TO .... [STEP ...]
|
* FOR ... TO .... [STEP ...]
|
||||||
* COMMAND ASSIGNMENT EXPRESSION [COMMAND EXPRESSION]
|
* COMMAND ASSIGNMENT EXPRESSION [COMMAND EXPRESSION]
|
||||||
@@ -898,11 +1079,72 @@ akerr_ErrorContext *akbasic_parse_for(akbasic_Parser *parser, akbasic_ASTLeaf **
|
|||||||
akbasic_ASTLeaf *assignment = NULL;
|
akbasic_ASTLeaf *assignment = NULL;
|
||||||
akbasic_ASTLeaf *expr = NULL;
|
akbasic_ASTLeaf *expr = NULL;
|
||||||
akbasic_Token *operator_ = NULL;
|
akbasic_Token *operator_ = NULL;
|
||||||
|
akbasic_Token *peeked = NULL;
|
||||||
akbasic_Environment *parent = runtime->environment;
|
akbasic_Environment *parent = runtime->environment;
|
||||||
akbasic_Environment *newenv = NULL;
|
akbasic_Environment *newenv = NULL;
|
||||||
int64_t firstline = 0;
|
int64_t firstline = 0;
|
||||||
int cmp = 0;
|
int cmp = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FOR EACH <variable> IN <generator call>. Checked before the leaf right of
|
||||||
|
* FOR is required to be an assignment, because EACH is the one other thing
|
||||||
|
* that leaf is allowed to be.
|
||||||
|
*/
|
||||||
|
peeked = akbasic_parser_peek(parser);
|
||||||
|
if ( peeked != NULL && peeked->tokentype == AKBASIC_TOK_COMMAND ) {
|
||||||
|
PASS(errctx, aksl_strcmp(peeked->lexeme, "EACH", &cmp));
|
||||||
|
if ( cmp == 0 ) {
|
||||||
|
akbasic_ASTLeaf *var = NULL;
|
||||||
|
akbasic_ASTLeaf *callexpr = NULL;
|
||||||
|
|
||||||
|
(void)akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND);
|
||||||
|
firstline = parent->lineno + 1;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Pushed the same way a plain FOR's environment is: while the
|
||||||
|
* parent is still active, so the call expression's identifiers
|
||||||
|
* resolve in the caller's scope rather than the loop's own.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_runtime_new_environment(runtime));
|
||||||
|
newenv = runtime->environment;
|
||||||
|
runtime->environment = parent;
|
||||||
|
|
||||||
|
PASS(errctx, parse_each_clause(parser, &var, &callexpr));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Nothing may follow the generator call but another statement.
|
||||||
|
* Without this, a stray clause sits unparsed on the line and only
|
||||||
|
* blows up after the whole loop has run, when the parent scope
|
||||||
|
* resumes the line mid-statement -- an error at the loop's end
|
||||||
|
* pointing at its beginning.
|
||||||
|
*/
|
||||||
|
peeked = akbasic_parser_peek(parser);
|
||||||
|
FAIL_NONZERO_RETURN(errctx,
|
||||||
|
(peeked != NULL &&
|
||||||
|
peeked->tokentype != AKBASIC_TOK_UNDEFINED &&
|
||||||
|
peeked->tokentype != AKBASIC_TOK_COLON),
|
||||||
|
AKBASIC_ERR_SYNTAX,
|
||||||
|
"FOR EACH takes nothing after the generator call");
|
||||||
|
|
||||||
|
newenv->isEachLoop = true;
|
||||||
|
newenv->loopFirstLine = firstline;
|
||||||
|
/*
|
||||||
|
* Stashed on forToLeaf rather than cloned into a leaf pool: unlike
|
||||||
|
* DO's condition, this expression is evaluated exactly once, by
|
||||||
|
* akbasic_cmd_for() on this same pass before the per-line leaf
|
||||||
|
* storage it lives in is reused -- see akbasic_runtime_generator_invoke().
|
||||||
|
*/
|
||||||
|
newenv->forToLeaf = callexpr;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_parser_new_leaf(parser, &expr));
|
||||||
|
PASS(errctx, akbasic_leaf_new_command(expr, "FOR", var));
|
||||||
|
|
||||||
|
runtime->environment = newenv;
|
||||||
|
*dest = expr;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PASS(errctx, akbasic_parser_assignment(parser, &assignment));
|
PASS(errctx, akbasic_parser_assignment(parser, &assignment));
|
||||||
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
FAIL_ZERO_RETURN(errctx, akbasic_parser_match1(parser, AKBASIC_TOK_COMMAND),
|
||||||
AKBASIC_ERR_SYNTAX,
|
AKBASIC_ERR_SYNTAX,
|
||||||
|
|||||||
127
src/runtime.c
@@ -143,17 +143,24 @@ akerr_ErrorContext *akbasic_runtime_new_environment(akbasic_Runtime *obj)
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
akerr_ErrorContext *akbasic_runtime_detach_environment(akbasic_Runtime *obj)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
akbasic_Environment *popped = NULL;
|
|
||||||
int i = 0;
|
|
||||||
|
|
||||||
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in prev_environment");
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in detach_environment");
|
||||||
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||||
"No previous environment to return to");
|
"No previous environment to return to");
|
||||||
popped = obj->environment;
|
obj->environment = obj->environment->parent;
|
||||||
obj->environment = popped->parent;
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_runtime_release_environment(akbasic_Runtime *obj, akbasic_Environment *env)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && env != NULL), AKERR_NULLPOINTER,
|
||||||
|
"NULL argument in release_environment");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Give back the variables this scope created, as well as the scope.
|
* Give back the variables this scope created, as well as the scope.
|
||||||
@@ -173,9 +180,9 @@ akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
|||||||
* times exhausted the 128-slot pool and reported "Maximum runtime variables
|
* times exhausted the 128-slot pool and reported "Maximum runtime variables
|
||||||
* reached" on a four-line program.
|
* reached" on a four-line program.
|
||||||
*/
|
*/
|
||||||
for ( i = 0; i < popped->variables.capacity; i++ ) {
|
for ( i = 0; i < env->variables.capacity; i++ ) {
|
||||||
akbasic_Variable *variable = (akbasic_Variable *)popped->variables.slots[i].value;
|
akbasic_Variable *variable = (akbasic_Variable *)env->variables.slots[i].value;
|
||||||
if ( popped->variables.slots[i].used && variable != NULL ) {
|
if ( env->variables.slots[i].used && variable != NULL ) {
|
||||||
variable->used = false;
|
variable->used = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,7 +192,51 @@ akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
|||||||
* here the pool is finite, so an unreleased environment is a bug that shows
|
* here the pool is finite, so an unreleased environment is a bug that shows
|
||||||
* up as exhaustion a few thousand GOSUBs later.
|
* up as exhaustion a few thousand GOSUBs later.
|
||||||
*/
|
*/
|
||||||
popped->used = false;
|
env->used = false;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_runtime_prev_environment(akbasic_Runtime *obj)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Environment *popped = NULL;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in prev_environment");
|
||||||
|
popped = obj->environment;
|
||||||
|
PASS(errctx, akbasic_runtime_detach_environment(obj));
|
||||||
|
PASS(errctx, akbasic_runtime_release_environment(obj, popped));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_runtime_unwind_to_environment(akbasic_Runtime *obj, akbasic_Environment *target)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Environment *popped = NULL;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && target != NULL), AKERR_NULLPOINTER,
|
||||||
|
"NULL argument in unwind_to_environment");
|
||||||
|
/*
|
||||||
|
* Stops early at the root rather than failing on it: every caller is an
|
||||||
|
* error-unwind path, where "release whatever there is" beats raising a
|
||||||
|
* second failure on top of the one being cleaned up after.
|
||||||
|
*/
|
||||||
|
while ( obj->environment != target && obj->environment->parent != NULL ) {
|
||||||
|
popped = obj->environment;
|
||||||
|
obj->environment = popped->parent;
|
||||||
|
/*
|
||||||
|
* An EACH loop scope on its way out takes its suspended generator with
|
||||||
|
* it -- the generator is a *child* of the scope, off the parent chain,
|
||||||
|
* and this walk is the only thing that will ever see it again. Guarded
|
||||||
|
* on `used` because a generator that was being pumped when the failure
|
||||||
|
* hit is *on* the chain being unwound, already released by the time
|
||||||
|
* the walk reaches the loop scope that references it.
|
||||||
|
*/
|
||||||
|
if ( popped->forGeneratorEnv != NULL && popped->forGeneratorEnv->used ) {
|
||||||
|
PASS(errctx, akbasic_runtime_release_generator(obj, popped->forGeneratorEnv));
|
||||||
|
}
|
||||||
|
popped->forGeneratorEnv = NULL;
|
||||||
|
PASS(errctx, akbasic_runtime_release_environment(obj, popped));
|
||||||
|
}
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,7 +944,7 @@ akerr_ErrorContext *akbasic_runtime_interpret(akbasic_Runtime *obj, akbasic_ASTL
|
|||||||
*
|
*
|
||||||
* **Only a block skip**, which is what the `BEND` test is for. A
|
* **Only a block skip**, which is what the `BEND` test is for. A
|
||||||
* zero-iteration `FOR` skips its body the same way, and there the
|
* zero-iteration `FOR` skips its body the same way, and there the
|
||||||
* orphan is load-bearing: `tests/reference/.../nestedforloopwaiting
|
* orphan is load-bearing: `tests/language/.../nestedforloopwaiting
|
||||||
* forcommand.bas` nests a loop inside one that runs zero times, and
|
* forcommand.bas` nests a loop inside one that runs zero times, and
|
||||||
* the inner scope is what absorbs the inner `NEXT` so the outer `NEXT`
|
* the inner scope is what absorbs the inner `NEXT` so the outer `NEXT`
|
||||||
* still finds its `FOR`. Popping it there turns that case into "NEXT
|
* still finds its `FOR`. Popping it there turns that case into "NEXT
|
||||||
@@ -1028,6 +1079,18 @@ akerr_ErrorContext *akbasic_runtime_call_function(akbasic_Runtime *obj, const ch
|
|||||||
|
|
||||||
PASS(errctx, akbasic_environment_get_function(obj->environment, name, &fnptr));
|
PASS(errctx, akbasic_environment_get_function(obj->environment, name, &fnptr));
|
||||||
fndef = (akbasic_FunctionDef *)fnptr;
|
fndef = (akbasic_FunctionDef *)fnptr;
|
||||||
|
/*
|
||||||
|
* A GEN is not a function: it yields more than once, through EMIT, and
|
||||||
|
* nothing about an ordinary call ever resumes it for a second value.
|
||||||
|
* Refused here, before anything is pushed, rather than left to fail
|
||||||
|
* inside the call -- a BASIC-level error down in EMIT is swallowed by
|
||||||
|
* process_line_run() the same way any statement's is, so a caller
|
||||||
|
* driving its own step loop would see this "succeed" with whatever
|
||||||
|
* garbage was left in the return slot instead of failing at all.
|
||||||
|
*/
|
||||||
|
FAIL_NONZERO_RETURN(errctx, fndef->isGenerator, AKBASIC_ERR_STATE,
|
||||||
|
"%s is a GEN; call it with FOR EACH or DO EACH, not as a function",
|
||||||
|
fndef->name);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* **One environment per call, from the pool -- exactly as GOSUB does.**
|
* **One environment per call, from the pool -- exactly as GOSUB does.**
|
||||||
@@ -1114,9 +1177,34 @@ akerr_ErrorContext *akbasic_runtime_call_function(akbasic_Runtime *obj, const ch
|
|||||||
* answering wrongly -- which is worse. The fix wants the REPL's own line
|
* answering wrongly -- which is worse. The fix wants the REPL's own line
|
||||||
* cycle, and that is a larger change than a condition.
|
* cycle, and that is a larger change than a condition.
|
||||||
*/
|
*/
|
||||||
while ( obj->environment != targetenv && obj->mode == AKBASIC_MODE_RUN ) {
|
ATTEMPT {
|
||||||
PASS(errctx, akbasic_runtime_process_line_run(obj));
|
while ( obj->environment != targetenv && obj->mode == AKBASIC_MODE_RUN ) {
|
||||||
}
|
/*
|
||||||
|
* The same per-line prologue akbasic_runtime_step() runs. Without
|
||||||
|
* it the call environment's value scratch accumulates across the
|
||||||
|
* whole body, and a body of ten real lines dies with "Maximum
|
||||||
|
* values per line reached" -- a limit that is supposed to be per
|
||||||
|
* line, not per call. step() cannot do this for us: this loop
|
||||||
|
* drives process_line_run() directly.
|
||||||
|
*/
|
||||||
|
CATCH(errctx, akbasic_runtime_zero(obj));
|
||||||
|
CATCH(errctx, akbasic_scanner_zero(obj));
|
||||||
|
CATCH(errctx, akbasic_runtime_process_line_run(obj));
|
||||||
|
}
|
||||||
|
} CLEANUP {
|
||||||
|
/*
|
||||||
|
* A body that died mid-line -- a runtime error set run_finished_mode,
|
||||||
|
* or a scanner error escaped (issue #4) -- left its scopes active.
|
||||||
|
* Give them back, or a host absorbing script errors drains the
|
||||||
|
* twelve-slot environment pool after twelve dead calls and every
|
||||||
|
* call after that fails for a reason nobody can see in the script.
|
||||||
|
* The unwind, not a bare prev_environment() loop, because a body that
|
||||||
|
* died inside a FOR EACH leaves a suspended generator hanging off the
|
||||||
|
* loop scope, and only the unwind knows to take it down too.
|
||||||
|
*/
|
||||||
|
IGNORE(akbasic_runtime_unwind_to_environment(obj, targetenv));
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
PASS(errctx, akbasic_environment_new_value(targetenv, &out));
|
PASS(errctx, akbasic_environment_new_value(targetenv, &out));
|
||||||
PASS(errctx, akbasic_value_clone(&targetenv->returnValue, out));
|
PASS(errctx, akbasic_value_clone(&targetenv->returnValue, out));
|
||||||
*dest = out;
|
*dest = out;
|
||||||
@@ -1262,7 +1350,7 @@ akerr_ErrorContext *akbasic_runtime_process_line_runstream(akbasic_Runtime *obj)
|
|||||||
* This mode used to file it like any other, under the cursor -- which for a
|
* This mode used to file it like any other, under the cursor -- which for a
|
||||||
* blank line is the number of the line *before* it. A file ending in a blank
|
* blank line is the number of the line *before* it. A file ending in a blank
|
||||||
* line therefore had its last line erased before it ever ran, silently. The
|
* line therefore had its last line erased before it ever ran, silently. The
|
||||||
* reference did the same, and `tests/reference/language/arithmetic/integer.bas`
|
* reference did the same, and `tests/language/arithmetic/integer.bas`
|
||||||
* has an expectation with three values for four PRINT statements to prove it.
|
* has an expectation with three values for four PRINT statements to prove it.
|
||||||
*/
|
*/
|
||||||
if ( buffer[0] == '\0' ) {
|
if ( buffer[0] == '\0' ) {
|
||||||
@@ -1918,6 +2006,15 @@ akerr_ErrorContext *akbasic_runtime_step(akbasic_Runtime *obj)
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_runtime_clear_error(akbasic_Runtime *obj)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER, "NULL runtime in clear_error");
|
||||||
|
obj->errclass = AKBASIC_ERRCLASS_NONE;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *akbasic_runtime_run(akbasic_Runtime *obj, int maxsteps)
|
akerr_ErrorContext *akbasic_runtime_run(akbasic_Runtime *obj, int maxsteps)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|||||||
@@ -161,8 +161,22 @@ akerr_ErrorContext *akbasic_cmd_return(akbasic_Runtime *obj, akbasic_ASTLeaf *ex
|
|||||||
SUCCEED_TRUE(obj, dest);
|
SUCCEED_TRUE(obj, dest);
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* A GEN is a function at heart, and RETURN ends it the way it ends a DEF
|
||||||
|
* or a GOSUB: early, cleanly, from its own frame. What a generator's
|
||||||
|
* RETURN cannot do is carry a value -- values leave a GEN one at a time,
|
||||||
|
* through EMIT, and there is no caller waiting on a return slot.
|
||||||
|
*/
|
||||||
|
if ( obj->environment->isGenerator ) {
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_STATE,
|
||||||
|
"A GEN yields values through EMIT; RETURN here takes none");
|
||||||
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||||
|
obj->environment->forGeneratorEnv = NULL;
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
FAIL_ZERO_RETURN(errctx, (obj->environment->gosubReturnLine != 0), AKBASIC_ERR_STATE,
|
FAIL_ZERO_RETURN(errctx, (obj->environment->gosubReturnLine != 0), AKBASIC_ERR_STATE,
|
||||||
"RETURN outside the context of GOSUB");
|
"RETURN outside the context of GOSUB, DEF, or GEN");
|
||||||
|
|
||||||
if ( expr != NULL && expr->right != NULL ) {
|
if ( expr != NULL && expr->right != NULL ) {
|
||||||
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &result));
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &result));
|
||||||
@@ -840,6 +854,31 @@ akerr_ErrorContext *akbasic_cmd_for(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
|||||||
bool met = false;
|
bool met = false;
|
||||||
|
|
||||||
(void)lval; (void)rval;
|
(void)lval; (void)rval;
|
||||||
|
if ( obj->environment->isEachLoop ) {
|
||||||
|
akbasic_Environment *loopenv = obj->environment;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && akbasic_leaf_is_identifier(expr->right)),
|
||||||
|
AKBASIC_ERR_SYNTAX, "Expected FOR EACH (variable) IN (generator call)");
|
||||||
|
FAIL_ZERO_RETURN(errctx, (loopenv->forToLeaf != NULL), AKBASIC_ERR_STATE,
|
||||||
|
"Expected FOR EACH (variable) IN (generator call)");
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_environment_get(loopenv, expr->right->identifier,
|
||||||
|
&loopenv->forNextVariable));
|
||||||
|
FAIL_ZERO_RETURN(errctx, (loopenv->forNextVariable != NULL), AKBASIC_ERR_UNDEFINED,
|
||||||
|
"Unable to get loop variable %s", expr->right->identifier);
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_runtime_generator_invoke(obj, loopenv, loopenv->forToLeaf));
|
||||||
|
loopenv->forToLeaf = NULL;
|
||||||
|
|
||||||
|
if ( loopenv->forGeneratorEnv == NULL ) {
|
||||||
|
/* The generator produced nothing: skip the body by waiting for NEXT,
|
||||||
|
exactly as a zero-iteration plain FOR does. */
|
||||||
|
PASS(errctx, akbasic_environment_wait_for_command(loopenv, "NEXT"));
|
||||||
|
}
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
FAIL_ZERO_RETURN(errctx, (obj->environment->forToLeaf != NULL && expr != NULL && expr->right != NULL),
|
FAIL_ZERO_RETURN(errctx, (obj->environment->forToLeaf != NULL && expr != NULL && expr->right != NULL),
|
||||||
AKBASIC_ERR_STATE, "Expected FOR ... TO [STEP ...]");
|
AKBASIC_ERR_STATE, "Expected FOR ... TO [STEP ...]");
|
||||||
FAIL_ZERO_RETURN(errctx,
|
FAIL_ZERO_RETURN(errctx,
|
||||||
@@ -891,10 +930,16 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
|||||||
"NEXT outside the context of FOR");
|
"NEXT outside the context of FOR");
|
||||||
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX,
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX,
|
||||||
"Expected NEXT IDENTIFIER");
|
"Expected NEXT IDENTIFIER");
|
||||||
FAIL_ZERO_RETURN(errctx,
|
if ( obj->environment->isEachLoop ) {
|
||||||
(expr->right->leaftype == AKBASIC_LEAF_IDENTIFIER_INT ||
|
/* EACH accepts any emitted type; the numeric-only check is for plain FOR. */
|
||||||
expr->right->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT),
|
FAIL_ZERO_RETURN(errctx, akbasic_leaf_is_identifier(expr->right), AKBASIC_ERR_SYNTAX,
|
||||||
AKBASIC_ERR_TYPE, "FOR ... NEXT only valid over INT and FLOAT types");
|
"Expected NEXT IDENTIFIER");
|
||||||
|
} else {
|
||||||
|
FAIL_ZERO_RETURN(errctx,
|
||||||
|
(expr->right->leaftype == AKBASIC_LEAF_IDENTIFIER_INT ||
|
||||||
|
expr->right->leaftype == AKBASIC_LEAF_IDENTIFIER_FLOAT),
|
||||||
|
AKBASIC_ERR_TYPE, "FOR ... NEXT only valid over INT and FLOAT types");
|
||||||
|
}
|
||||||
|
|
||||||
obj->environment->loopExitLine = obj->environment->lineno + 1;
|
obj->environment->loopExitLine = obj->environment->lineno + 1;
|
||||||
|
|
||||||
@@ -909,6 +954,14 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
|||||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "NEXT"));
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "NEXT"));
|
||||||
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||||
"NEXT in an orphaned environment");
|
"NEXT in an orphaned environment");
|
||||||
|
/*
|
||||||
|
* A live generator abandoned mid-run: release it too, or it never comes
|
||||||
|
* back to the pool. See MAINTENANCE.md's note on abandoned generators.
|
||||||
|
*/
|
||||||
|
if ( obj->environment->forGeneratorEnv != NULL ) {
|
||||||
|
PASS(errctx, akbasic_runtime_release_generator(obj, obj->environment->forGeneratorEnv));
|
||||||
|
obj->environment->forGeneratorEnv = NULL;
|
||||||
|
}
|
||||||
obj->environment->parent->nextline = obj->environment->loopExitLine;
|
obj->environment->parent->nextline = obj->environment->loopExitLine;
|
||||||
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||||
*dest = &obj->staticFalseValue;
|
*dest = &obj->staticFalseValue;
|
||||||
@@ -924,12 +977,37 @@ akerr_ErrorContext *akbasic_cmd_next(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
|||||||
if ( cmp != 0 ) {
|
if ( cmp != 0 ) {
|
||||||
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||||
"NEXT in an orphaned environment");
|
"NEXT in an orphaned environment");
|
||||||
|
if ( obj->environment->forGeneratorEnv != NULL ) {
|
||||||
|
PASS(errctx, akbasic_runtime_release_generator(obj, obj->environment->forGeneratorEnv));
|
||||||
|
obj->environment->forGeneratorEnv = NULL;
|
||||||
|
}
|
||||||
obj->environment->parent->nextline = obj->environment->nextline;
|
obj->environment->parent->nextline = obj->environment->nextline;
|
||||||
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||||
*dest = &obj->staticFalseValue;
|
*dest = &obj->staticFalseValue;
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( obj->environment->isEachLoop ) {
|
||||||
|
akbasic_Environment *loopenv = obj->environment;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "NEXT"));
|
||||||
|
if ( loopenv->forGeneratorEnv != NULL ) {
|
||||||
|
obj->environment = loopenv->forGeneratorEnv;
|
||||||
|
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
||||||
|
}
|
||||||
|
if ( loopenv->forGeneratorEnv == NULL ) {
|
||||||
|
/* Exhausted: pop the loop, same landing NEXT always uses when done. */
|
||||||
|
FAIL_ZERO_RETURN(errctx, (loopenv->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||||
|
"NEXT in an orphaned environment");
|
||||||
|
loopenv->parent->nextline = loopenv->loopExitLine;
|
||||||
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||||
|
*dest = &obj->staticFalseValue;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
PASS(errctx, akbasic_environment_get(obj->environment, expr->right->identifier, &nextvar));
|
PASS(errctx, akbasic_environment_get(obj->environment, expr->right->identifier, &nextvar));
|
||||||
FAIL_ZERO_RETURN(errctx, (nextvar != NULL), AKBASIC_ERR_UNDEFINED,
|
FAIL_ZERO_RETURN(errctx, (nextvar != NULL), AKBASIC_ERR_UNDEFINED,
|
||||||
"Unable to get loop variable %s", expr->right->identifier);
|
"Unable to get loop variable %s", expr->right->identifier);
|
||||||
@@ -972,7 +1050,7 @@ akerr_ErrorContext *akbasic_cmd_exit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
|||||||
*/
|
*/
|
||||||
FAIL_NONZERO_RETURN(errctx,
|
FAIL_NONZERO_RETURN(errctx,
|
||||||
(obj->environment->forToValue.valuetype == AKBASIC_TYPE_UNDEFINED &&
|
(obj->environment->forToValue.valuetype == AKBASIC_TYPE_UNDEFINED &&
|
||||||
!obj->environment->isDoLoop),
|
!obj->environment->isDoLoop && !obj->environment->isEachLoop),
|
||||||
AKBASIC_ERR_STATE, "EXIT outside the context of FOR or DO");
|
AKBASIC_ERR_STATE, "EXIT outside the context of FOR or DO");
|
||||||
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
FAIL_ZERO_RETURN(errctx, (obj->environment->parent != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||||
"EXIT in an orphaned environment");
|
"EXIT in an orphaned environment");
|
||||||
|
|||||||
@@ -407,17 +407,22 @@ akerr_ErrorContext *akbasic_cmd_directory(akbasic_Runtime *obj, akbasic_ASTLeaf
|
|||||||
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
|
||||||
"NULL argument in DIRECTORY");
|
"NULL argument in DIRECTORY");
|
||||||
/*
|
/*
|
||||||
* Refused rather than half-built. Listing a directory needs opendir/readdir,
|
* Refused rather than half-built. This was blocked upstream: listing a
|
||||||
* which `libakstdlib` does not wrap -- and this project's rule is that a
|
* directory needs opendir/readdir, `libakstdlib` did not wrap them, and
|
||||||
* missing capability gets filed upstream rather than worked around here
|
* this project's rule is that a missing capability gets filed upstream
|
||||||
* (MAINTENANCE.md). Filed as libakstdlib issue #10.
|
* rather than worked around here (MAINTENANCE.md). That was libakstdlib
|
||||||
|
* issue #10, and it landed -- aksl_opendir, aksl_readdir, aksl_closedir
|
||||||
|
* and aksl_rewinddir all exist as of the revision this tree pins.
|
||||||
*
|
*
|
||||||
* The alternative was shelling out to `ls`, which a library has no business
|
* So the blocker is gone and only the work is left. Writing the verb needs
|
||||||
* doing, or calling readdir directly and stepping outside the error
|
* decisions this commit is not the place for: what a listing looks like on
|
||||||
* convention every other call in this file follows.
|
* a filesystem with no disk-image block counts, which of the Commodore
|
||||||
|
* wildcard forms to honour, and where the entries go. Tracked as akbasic
|
||||||
|
* issue #55; the refusal stays honest until then rather than growing a
|
||||||
|
* half-listing nobody specified.
|
||||||
*/
|
*/
|
||||||
FAIL_RETURN(errctx, AKBASIC_ERR_DEVICE,
|
FAIL_RETURN(errctx, AKBASIC_ERR_DEVICE,
|
||||||
"DIRECTORY is not implemented: libakstdlib has no directory-reading wrapper yet");
|
"DIRECTORY is not implemented yet");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------ BSAVE/BLOAD -- */
|
/* ------------------------------------------------------------ BSAVE/BLOAD -- */
|
||||||
|
|||||||
@@ -183,6 +183,69 @@ akerr_ErrorContext *akbasic_fn_chr(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
|||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_fn_asc(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Value *arg = NULL;
|
||||||
|
akbasic_Value *out = NULL;
|
||||||
|
const unsigned char *text = NULL;
|
||||||
|
int64_t codepoint = 0;
|
||||||
|
|
||||||
|
(void)lval; (void)rval;
|
||||||
|
PASS(errctx, first_arg(obj, expr, "ASC", NULL, &arg, &out));
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_STRING), AKBASIC_ERR_TYPE,
|
||||||
|
"ASC expected a string");
|
||||||
|
FAIL_ZERO_RETURN(errctx, (arg->stringval[0] != '\0'), AKBASIC_ERR_BOUNDS,
|
||||||
|
"ASC expected a non-empty string");
|
||||||
|
|
||||||
|
/* Decode the first UTF-8 code point, the inverse of CHR's encoder. */
|
||||||
|
text = (const unsigned char *)arg->stringval;
|
||||||
|
if ( text[0] < 0x80 ) {
|
||||||
|
codepoint = text[0];
|
||||||
|
} else if ( (text[0] & 0xE0) == 0xC0 ) {
|
||||||
|
codepoint = ((int64_t)(text[0] & 0x1F) << 6) |
|
||||||
|
(text[1] & 0x3F);
|
||||||
|
} else if ( (text[0] & 0xF0) == 0xE0 ) {
|
||||||
|
codepoint = ((int64_t)(text[0] & 0x0F) << 12) |
|
||||||
|
((int64_t)(text[1] & 0x3F) << 6) |
|
||||||
|
(text[2] & 0x3F);
|
||||||
|
} else {
|
||||||
|
codepoint = ((int64_t)(text[0] & 0x07) << 18) |
|
||||||
|
((int64_t)(text[1] & 0x3F) << 12) |
|
||||||
|
((int64_t)(text[2] & 0x3F) << 6) |
|
||||||
|
(text[3] & 0x3F);
|
||||||
|
}
|
||||||
|
out->valuetype = AKBASIC_TYPE_INTEGER;
|
||||||
|
out->intval = codepoint;
|
||||||
|
*dest = out;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_fn_rnd(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Value *arg = NULL;
|
||||||
|
akbasic_Value *out = NULL;
|
||||||
|
const int64_t modulus = 2147483648;
|
||||||
|
|
||||||
|
(void)lval; (void)rval;
|
||||||
|
PASS(errctx, first_arg(obj, expr, "RND", NULL, &arg, &out));
|
||||||
|
FAIL_NONZERO_RETURN(errctx, (arg->valuetype != AKBASIC_TYPE_INTEGER), AKBASIC_ERR_TYPE,
|
||||||
|
"RND expected an integer");
|
||||||
|
FAIL_ZERO_RETURN(errctx, (arg->intval > 0), AKBASIC_ERR_VALUE,
|
||||||
|
"RND count %" PRId64 " must be positive", arg->intval);
|
||||||
|
|
||||||
|
if ( !obj->rndseeded ) {
|
||||||
|
obj->rndseed = obj->timems % modulus;
|
||||||
|
obj->rndseeded = true;
|
||||||
|
}
|
||||||
|
obj->rndseed = (obj->rndseed * 1103515245 + 12345) % modulus;
|
||||||
|
out->valuetype = AKBASIC_TYPE_INTEGER;
|
||||||
|
out->intval = (obj->rndseed / 65536) % arg->intval;
|
||||||
|
*dest = out;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
akerr_ErrorContext *akbasic_fn_hex(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
akerr_ErrorContext *akbasic_fn_hex(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
|
|||||||
307
src/runtime_generator.c
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
/**
|
||||||
|
* @file runtime_generator.c
|
||||||
|
* @brief GEN, EMIT and END GEN, plus the machinery FOR EACH/DO EACH share.
|
||||||
|
*
|
||||||
|
* A GEN is a DEF that yields more than once. Its body is skipped on the
|
||||||
|
* definitional pass exactly as a multi-line DEF's is -- `akbasic_parse_gen()`
|
||||||
|
* arms `akbasic_environment_wait_for_command(env, "END GEN")` the same way
|
||||||
|
* `akbasic_parse_def()` arms one for `RETURN` -- and it only ever really runs
|
||||||
|
* when a `FOR EACH`/`DO EACH` invokes it.
|
||||||
|
*
|
||||||
|
* That invocation pushes one environment for the whole lifetime of the loop,
|
||||||
|
* exactly as a GOSUB or a function call does, except that `EMIT` does not pop
|
||||||
|
* it: it hands control back to the loop without releasing anything, so the
|
||||||
|
* environment EMIT actually ran in -- which may be nested several levels
|
||||||
|
* below the GEN's own call frame, inside a FOR/DO/GOSUB the body wrote --
|
||||||
|
* still says exactly where to resume when `NEXT`/`LOOP` calls back into
|
||||||
|
* akbasic_runtime_pump_generator(). Only `END GEN` reached for real --
|
||||||
|
* meaning `obj->environment->isGenerator` is true and nothing is skipping
|
||||||
|
* forward to it -- actually releases the call frame, the way `RETURN`
|
||||||
|
* releases a DEF's call environment.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <akbasic/error.h>
|
||||||
|
#include <akbasic/runtime.h>
|
||||||
|
#include <akbasic/scanner.h>
|
||||||
|
|
||||||
|
#include "verbs.h"
|
||||||
|
|
||||||
|
/* Most verbs answer "did something happen"; this is that answer. */
|
||||||
|
#define SUCCEED_TRUE(__obj, __dest) \
|
||||||
|
do { \
|
||||||
|
*(__dest) = &(__obj)->staticTrueValue; \
|
||||||
|
} while ( 0 )
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_runtime_pump_generator(akbasic_Runtime *obj, akbasic_Environment *loopenv)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && loopenv != NULL), AKERR_NULLPOINTER,
|
||||||
|
"NULL argument in pump_generator");
|
||||||
|
/*
|
||||||
|
* The same per-line prologue akbasic_runtime_step() and
|
||||||
|
* akbasic_runtime_call_function() run, driving process_line_run() directly
|
||||||
|
* rather than through the ordinary step loop: a generator body is not the
|
||||||
|
* top-level program and nothing else is going to advance it.
|
||||||
|
*/
|
||||||
|
ATTEMPT {
|
||||||
|
while ( obj->environment != loopenv && obj->mode == AKBASIC_MODE_RUN ) {
|
||||||
|
CATCH(errctx, akbasic_runtime_zero(obj));
|
||||||
|
CATCH(errctx, akbasic_scanner_zero(obj));
|
||||||
|
CATCH(errctx, akbasic_runtime_process_line_run(obj));
|
||||||
|
}
|
||||||
|
} CLEANUP {
|
||||||
|
/*
|
||||||
|
* CLEANUP runs unconditionally -- it is not a `catch` -- so it is
|
||||||
|
* guarded on the one thing that tells success and failure apart here:
|
||||||
|
* whether `obj->environment` is still `loopenv`. On the ordinary
|
||||||
|
* success path it already is (that is the ATTEMPT loop's own exit
|
||||||
|
* condition), so this is a no-op there, exactly as it is meant to be.
|
||||||
|
* Only a genuine C-level failure -- the scanner or parser raised, or a
|
||||||
|
* runtime error escaped the swallow process_line_run() ordinarily does
|
||||||
|
* for a BASIC-level one -- leaves scopes active between here and
|
||||||
|
* loopenv, and only then does this force them back, taking
|
||||||
|
* `forGeneratorEnv` down with them since whatever it pointed at is
|
||||||
|
* among the scopes just released.
|
||||||
|
*/
|
||||||
|
if ( obj->environment != loopenv ) {
|
||||||
|
IGNORE(akbasic_runtime_unwind_to_environment(obj, loopenv));
|
||||||
|
loopenv->forGeneratorEnv = NULL;
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Release a live generator, however deep EMIT left it suspended.
|
||||||
|
*
|
||||||
|
* `loopenv->forGeneratorEnv` is the *resume point*, not necessarily the GEN's
|
||||||
|
* own call frame -- EMIT may have run several levels down, inside a FOR/DO/
|
||||||
|
* GOSUB the body wrote around it. Abandoning it (EXIT) has to give back every
|
||||||
|
* environment from there up through the call frame itself, or everything
|
||||||
|
* above the resume point leaks.
|
||||||
|
*
|
||||||
|
* @param obj Object to initialize, inspect, or modify.
|
||||||
|
* @param env The suspended resume point; walks up through its own parents.
|
||||||
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
||||||
|
*/
|
||||||
|
akerr_ErrorContext *akbasic_runtime_release_generator(akbasic_Runtime *obj, akbasic_Environment *env)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Environment *walk = env;
|
||||||
|
akbasic_Environment *next = NULL;
|
||||||
|
bool isgen = false;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && env != NULL), AKERR_NULLPOINTER,
|
||||||
|
"NULL argument in release_generator");
|
||||||
|
while ( walk != NULL ) {
|
||||||
|
isgen = walk->isGenerator;
|
||||||
|
next = walk->parent;
|
||||||
|
/*
|
||||||
|
* A scope between the resume point and the call frame may be an EACH
|
||||||
|
* loop with its *own* generator suspended off to the side. Releasing
|
||||||
|
* the loop scope without releasing that generator strands it in the
|
||||||
|
* pool -- the walk goes through parents and a suspended generator is a
|
||||||
|
* child. Guarded on `used` so a generator already released as part of
|
||||||
|
* some enclosing teardown is not released twice.
|
||||||
|
*/
|
||||||
|
if ( walk->forGeneratorEnv != NULL && walk->forGeneratorEnv->used ) {
|
||||||
|
PASS(errctx, akbasic_runtime_release_generator(obj, walk->forGeneratorEnv));
|
||||||
|
}
|
||||||
|
walk->forGeneratorEnv = NULL;
|
||||||
|
PASS(errctx, akbasic_runtime_release_environment(obj, walk));
|
||||||
|
if ( isgen ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
walk = next;
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_runtime_generator_invoke(akbasic_Runtime *obj, akbasic_Environment *loopenv, akbasic_ASTLeaf *callexpr)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Environment *callenv = NULL;
|
||||||
|
akbasic_Environment *walk = NULL;
|
||||||
|
akbasic_FunctionDef *fndef = NULL;
|
||||||
|
akbasic_ASTLeaf *fnarg = NULL;
|
||||||
|
akbasic_ASTLeaf *paramleaf = NULL;
|
||||||
|
akbasic_Value *argvals[AKBASIC_MAX_CALL_ARGUMENTS];
|
||||||
|
akbasic_Value *unused = NULL;
|
||||||
|
void *fnptr = NULL;
|
||||||
|
int nargs = 0;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj != NULL && loopenv != NULL && callexpr != NULL), AKERR_NULLPOINTER,
|
||||||
|
"NULL argument in generator_invoke");
|
||||||
|
FAIL_ZERO_RETURN(errctx, (callexpr->leaftype == AKBASIC_LEAF_FUNCTION), AKBASIC_ERR_SYNTAX,
|
||||||
|
"Expected a generator call after IN");
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GEN and DEF share the functions table (TODO.md's namespace decision for
|
||||||
|
* this feature), so this is the same lookup akbasic_runtime_call_function()
|
||||||
|
* does. The parser already proved the name resolves and the arity matches
|
||||||
|
* when it parsed `callexpr` -- akbasic_parser_expression() would not have
|
||||||
|
* produced an AKBASIC_LEAF_FUNCTION leaf otherwise -- so a miss here would
|
||||||
|
* mean the function table changed out from under a leaf built against it,
|
||||||
|
* which is not a case this needs its own message for.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_environment_get_function(loopenv, callexpr->identifier, &fnptr));
|
||||||
|
fndef = (akbasic_FunctionDef *)fnptr;
|
||||||
|
FAIL_ZERO_RETURN(errctx, fndef->isGenerator, AKBASIC_ERR_STATE,
|
||||||
|
"%s is a DEF, not a GEN -- FOR EACH/DO EACH needs a generator",
|
||||||
|
fndef->name);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Self-recursion: walk the *parent* chain, not the pool. An environment
|
||||||
|
* reachable only through some other loop's `forGeneratorEnv` is a sibling
|
||||||
|
* invocation sitting detached between its own iterations, not an ancestor
|
||||||
|
* of this call -- nothing points from here to it via `parent`, so it never
|
||||||
|
* matches and independent or nested FOR EACH/DO EACH over the same GEN
|
||||||
|
* (even the same GEN with different arguments) is unaffected.
|
||||||
|
*/
|
||||||
|
for ( walk = loopenv; walk != NULL; walk = walk->parent ) {
|
||||||
|
if ( walk->isGenerator && walk->generatorFn == (void *)fndef ) {
|
||||||
|
FAIL_RETURN(errctx, AKBASIC_ERR_STATE,
|
||||||
|
"GEN %s cannot FOR EACH/DO EACH over itself from its own body",
|
||||||
|
fndef->name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Evaluated in the caller's own scope, before anything is pushed -- the
|
||||||
|
* same reason akbasic_runtime_user_function() evaluates every argument
|
||||||
|
* before binding the first one: a later argument must not see an earlier
|
||||||
|
* one already sitting in the callee's scope.
|
||||||
|
*/
|
||||||
|
fnarg = akbasic_leaf_first_argument(callexpr);
|
||||||
|
for ( ; fnarg != NULL; fnarg = fnarg->next ) {
|
||||||
|
FAIL_ZERO_RETURN(errctx, (nargs < AKBASIC_MAX_CALL_ARGUMENTS), AKBASIC_ERR_BOUNDS,
|
||||||
|
"%s was called with more than %d arguments",
|
||||||
|
callexpr->identifier, AKBASIC_MAX_CALL_ARGUMENTS);
|
||||||
|
PASS(errctx, akbasic_runtime_evaluate(obj, fnarg, &argvals[nargs]));
|
||||||
|
nargs += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* One environment for the whole lifetime of the loop, exactly as GOSUB and
|
||||||
|
* a function call take one from the pool -- and unlike either, this one
|
||||||
|
* survives past the verb that pushed it, held alive by `loopenv`'s own
|
||||||
|
* reference until NEXT/LOOP exhausts or EXIT abandons it.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_runtime_new_environment(obj));
|
||||||
|
callenv = obj->environment;
|
||||||
|
callenv->isGenerator = true;
|
||||||
|
callenv->generatorFn = (void *)fndef;
|
||||||
|
callenv->nextline = fndef->lineno;
|
||||||
|
loopenv->forGeneratorEnv = callenv;
|
||||||
|
|
||||||
|
paramleaf = (fndef->arglist != NULL ? fndef->arglist->right : NULL);
|
||||||
|
for ( i = 0; i < nargs && paramleaf != NULL; i++ ) {
|
||||||
|
PASS(errctx, akbasic_environment_assign(callenv, paramleaf, argvals[i], &unused));
|
||||||
|
paramleaf = paramleaf->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_cmd_gen(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
(void)expr; (void)lval; (void)rval;
|
||||||
|
/* The parse handler already installed the generator, exactly as DEF's does. */
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_cmd_emit(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Environment *genenv = NULL;
|
||||||
|
akbasic_Environment *loopenv = NULL;
|
||||||
|
akbasic_Value *value = NULL;
|
||||||
|
int64_t zerosubscript[1] = { 0 };
|
||||||
|
|
||||||
|
(void)lval; (void)rval;
|
||||||
|
FAIL_ZERO_RETURN(errctx, (expr != NULL && expr->right != NULL), AKBASIC_ERR_SYNTAX,
|
||||||
|
"Expected EMIT (expression)");
|
||||||
|
/*
|
||||||
|
* EMIT is not necessarily standing directly in the environment
|
||||||
|
* akbasic_runtime_generator_invoke() pushed: a GEN body is ordinary BASIC
|
||||||
|
* and may nest its own FOR, DO or GOSUB around an EMIT, each of which
|
||||||
|
* pushes an environment of its own -- exactly what the issue's own
|
||||||
|
* ROOMOBJECTS example does. Walk up to the nearest one that really is a
|
||||||
|
* GEN's own call frame.
|
||||||
|
*/
|
||||||
|
for ( genenv = obj->environment; genenv != NULL && !genenv->isGenerator; genenv = genenv->parent ) {
|
||||||
|
}
|
||||||
|
FAIL_ZERO_RETURN(errctx, (genenv != NULL), AKBASIC_ERR_STATE,
|
||||||
|
"EMIT outside the context of a GEN body");
|
||||||
|
loopenv = genenv->parent;
|
||||||
|
FAIL_ZERO_RETURN(errctx, (loopenv != NULL), AKBASIC_ERR_ENVIRONMENT,
|
||||||
|
"EMIT from an orphaned environment");
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_runtime_evaluate(obj, expr->right, &value));
|
||||||
|
/*
|
||||||
|
* Straight into the loop variable's storage, bypassing the arithmetic
|
||||||
|
* akbasic_environment_assign() and evaluate_for_condition() carry for a
|
||||||
|
* plain FOR: an EACH variable takes whatever type the GEN emits, string or
|
||||||
|
* structure element included, and there is no TO/STEP to compare it
|
||||||
|
* against.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_variable_set_subscript(loopenv->forNextVariable, value, zerosubscript, 1));
|
||||||
|
loopenv->nextline = loopenv->loopFirstLine;
|
||||||
|
/*
|
||||||
|
* The resume point, which may be several levels below `genenv` -- whatever
|
||||||
|
* nested FOR/DO/GOSUB environment this EMIT actually ran in. NEXT/LOOP
|
||||||
|
* reactivates exactly this one, so the nested structure picks up exactly
|
||||||
|
* where it left off rather than restarting at the top of the GEN body.
|
||||||
|
*/
|
||||||
|
loopenv->forGeneratorEnv = obj->environment;
|
||||||
|
/*
|
||||||
|
* Not a pop, and not a single-level detach either: everything between here
|
||||||
|
* and `loopenv` -- `genenv` and any of its own descendants -- has to
|
||||||
|
* survive untouched to be resumed, so control moves to `loopenv` directly
|
||||||
|
* rather than walking the chain one release at a time.
|
||||||
|
*/
|
||||||
|
obj->environment = loopenv;
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_ErrorContext *akbasic_cmd_end_gen(akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
bool waiting = false;
|
||||||
|
|
||||||
|
(void)expr; (void)lval; (void)rval;
|
||||||
|
/*
|
||||||
|
* A END GEN reached while skipping forward to one is the end of a GEN
|
||||||
|
* body's *definition*, not the end of a call -- the same distinction
|
||||||
|
* RETURN draws for DEF.
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_environment_is_waiting_for(obj->environment, "END GEN", &waiting));
|
||||||
|
if ( waiting ) {
|
||||||
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "END GEN"));
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
FAIL_ZERO_RETURN(errctx, (obj->environment->isGenerator), AKBASIC_ERR_STATE,
|
||||||
|
"END GEN outside the context of a generator invocation");
|
||||||
|
/*
|
||||||
|
* Real exhaustion: release this environment and detach in the same
|
||||||
|
* motion prev_environment() always does, then clear the parent's
|
||||||
|
* reference to it so a caller pumping this loop can tell "still alive"
|
||||||
|
* apart from "nothing left to resume".
|
||||||
|
*/
|
||||||
|
PASS(errctx, akbasic_runtime_prev_environment(obj));
|
||||||
|
obj->environment->forGeneratorEnv = NULL;
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
@@ -80,6 +80,31 @@ akerr_ErrorContext *akbasic_cmd_do(akbasic_Runtime *obj, akbasic_ASTLeaf *expr,
|
|||||||
FAIL_ZERO_RETURN(errctx, obj->environment->isDoLoop, AKBASIC_ERR_STATE,
|
FAIL_ZERO_RETURN(errctx, obj->environment->isDoLoop, AKBASIC_ERR_STATE,
|
||||||
"DO did not establish its own scope");
|
"DO did not establish its own scope");
|
||||||
|
|
||||||
|
if ( obj->environment->isEachLoop ) {
|
||||||
|
akbasic_Environment *loopenv = obj->environment;
|
||||||
|
akbasic_ASTLeaf *var = (expr != NULL ? expr->right : NULL);
|
||||||
|
|
||||||
|
FAIL_ZERO_RETURN(errctx, (var != NULL && akbasic_leaf_is_identifier(var)),
|
||||||
|
AKBASIC_ERR_SYNTAX, "Expected DO EACH (variable) IN (generator call)");
|
||||||
|
FAIL_ZERO_RETURN(errctx, (loopenv->forToLeaf != NULL), AKBASIC_ERR_STATE,
|
||||||
|
"Expected DO EACH (variable) IN (generator call)");
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_environment_get(loopenv, var->identifier, &loopenv->forNextVariable));
|
||||||
|
FAIL_ZERO_RETURN(errctx, (loopenv->forNextVariable != NULL), AKBASIC_ERR_UNDEFINED,
|
||||||
|
"Unable to get loop variable %s", var->identifier);
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_runtime_generator_invoke(obj, loopenv, loopenv->forToLeaf));
|
||||||
|
loopenv->forToLeaf = NULL;
|
||||||
|
|
||||||
|
if ( loopenv->forGeneratorEnv == NULL ) {
|
||||||
|
/* The generator produced nothing: skip the body, same as DO WHILE
|
||||||
|
false does. */
|
||||||
|
PASS(errctx, akbasic_environment_wait_for_command(loopenv, "LOOP"));
|
||||||
|
}
|
||||||
|
SUCCEED_TRUE(obj, dest);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
PASS(errctx, loop_continues(obj, obj->environment->doConditionLeaf,
|
PASS(errctx, loop_continues(obj, obj->environment->doConditionLeaf,
|
||||||
obj->environment->doConditionKind, &enter));
|
obj->environment->doConditionKind, &enter));
|
||||||
if ( !enter ) {
|
if ( !enter ) {
|
||||||
@@ -114,7 +139,37 @@ akerr_ErrorContext *akbasic_cmd_loop(akbasic_Runtime *obj, akbasic_ASTLeaf *expr
|
|||||||
if ( obj->environment->exiting ) {
|
if ( obj->environment->exiting ) {
|
||||||
obj->environment->exiting = false;
|
obj->environment->exiting = false;
|
||||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
||||||
|
/* A live generator abandoned mid-run: release it too. */
|
||||||
|
if ( obj->environment->forGeneratorEnv != NULL ) {
|
||||||
|
PASS(errctx, akbasic_runtime_release_generator(obj, obj->environment->forGeneratorEnv));
|
||||||
|
obj->environment->forGeneratorEnv = NULL;
|
||||||
|
}
|
||||||
again = false;
|
again = false;
|
||||||
|
} else if ( obj->environment->isEachLoop ) {
|
||||||
|
akbasic_Environment *loopenv = obj->environment;
|
||||||
|
|
||||||
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
||||||
|
/*
|
||||||
|
* A condition on the LOOP composes with EACH: it is checked after each
|
||||||
|
* trip through the body, with the loop variable still holding that
|
||||||
|
* trip's value, before the generator is pumped for the next one. A
|
||||||
|
* condition that says stop abandons the generator exactly as EXIT does.
|
||||||
|
*/
|
||||||
|
again = true;
|
||||||
|
arg = (expr != NULL ? expr->right : NULL);
|
||||||
|
if ( arg != NULL ) {
|
||||||
|
kind = (int)arg->literal_int;
|
||||||
|
PASS(errctx, loop_continues(obj, arg->left, kind, &again));
|
||||||
|
}
|
||||||
|
if ( !again && loopenv->forGeneratorEnv != NULL ) {
|
||||||
|
PASS(errctx, akbasic_runtime_release_generator(obj, loopenv->forGeneratorEnv));
|
||||||
|
loopenv->forGeneratorEnv = NULL;
|
||||||
|
}
|
||||||
|
if ( again && loopenv->forGeneratorEnv != NULL ) {
|
||||||
|
obj->environment = loopenv->forGeneratorEnv;
|
||||||
|
PASS(errctx, akbasic_runtime_pump_generator(obj, loopenv));
|
||||||
|
}
|
||||||
|
again = (again && loopenv->forGeneratorEnv != NULL);
|
||||||
} else {
|
} else {
|
||||||
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
PASS(errctx, akbasic_environment_stop_waiting(obj->environment, "LOOP"));
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ akerr_ErrorContext *akbasic_scanner_zero(akbasic_Runtime *obj)
|
|||||||
obj->current = 0;
|
obj->current = 0;
|
||||||
obj->start = 0;
|
obj->start = 0;
|
||||||
obj->hasError = false;
|
obj->hasError = false;
|
||||||
|
obj->tokentype = AKBASIC_TOK_UNDEFINED;
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ static akerr_ErrorContext *is_at_end(akbasic_Runtime *obj, bool *dest)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The character under the cursor.
|
* @brief The character under the cursor.
|
||||||
|
* @param obj The runtime whose scan cursor is being read.
|
||||||
* @param[out] dest The character. Untouched when there is none.
|
* @param[out] dest The character. Untouched when there is none.
|
||||||
* @param[out] got Whether there was one. The old `bool` return.
|
* @param[out] got Whether there was one. The old `bool` return.
|
||||||
*/
|
*/
|
||||||
@@ -69,6 +71,7 @@ static akerr_ErrorContext *peek(akbasic_Runtime *obj, char *dest, bool *got)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The character one past the cursor.
|
* @brief The character one past the cursor.
|
||||||
|
* @param obj The runtime whose scan cursor is being read.
|
||||||
* @param[out] dest The character. Untouched when there is none.
|
* @param[out] dest The character. Untouched when there is none.
|
||||||
* @param[out] got Whether there was one. The old `bool` return.
|
* @param[out] got Whether there was one. The old `bool` return.
|
||||||
*/
|
*/
|
||||||
@@ -144,6 +147,10 @@ static akerr_ErrorContext *add_token(akbasic_Runtime *obj, akbasic_TokenType tok
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Consume one more character when it matches, choosing between two token types.
|
* @brief Consume one more character when it matches, choosing between two token types.
|
||||||
|
* @param obj The runtime whose scan cursor is being advanced.
|
||||||
|
* @param cm The character that must be next for the match to succeed.
|
||||||
|
* @param truetype The token type to report when @p cm matches.
|
||||||
|
* @param falsetype The token type to report when it does not.
|
||||||
* @param[out] matched Whether the character was consumed. The old `bool` return.
|
* @param[out] matched Whether the character was consumed. The old `bool` return.
|
||||||
*
|
*
|
||||||
* On the chain below `peek`, so it reports the same way. See libakstdlib #38.
|
* On the chain below `peek`, so it reports the same way. See libakstdlib #38.
|
||||||
@@ -409,6 +416,16 @@ akerr_ErrorContext *akbasic_scanner_scan(akbasic_Runtime *obj, const char *line,
|
|||||||
obj->current = 0;
|
obj->current = 0;
|
||||||
obj->start = 0;
|
obj->start = 0;
|
||||||
obj->hasError = false;
|
obj->hasError = false;
|
||||||
|
/*
|
||||||
|
* The `REM` early-exit below leaves `tokentype` holding AKBASIC_TOK_REM,
|
||||||
|
* and the loop's post-switch check reads it before the first character of
|
||||||
|
* the *next* line has assigned anything. A line whose first character
|
||||||
|
* carries no token of its own -- leading whitespace -- then re-triggered
|
||||||
|
* the REM break and scanned to nothing: every indented line after a REM
|
||||||
|
* was silently skipped. A numbered program never saw it, because the line
|
||||||
|
* number is the first token and overwrites the leftover.
|
||||||
|
*/
|
||||||
|
obj->tokentype = AKBASIC_TOK_UNDEFINED;
|
||||||
/*
|
/*
|
||||||
* Cleared here rather than by each caller, so the flag always describes the
|
* Cleared here rather than by each caller, so the flag always describes the
|
||||||
* line this call just scanned. It used to be cleared only in
|
* line this call just scanned. It used to be cleared only in
|
||||||
|
|||||||
@@ -875,7 +875,7 @@ static akerr_ErrorContext AKERR_NOIGNORE *run_scan(akbasic_AkglSprites *state)
|
|||||||
* proxy carries the owner only so a resolver can push something. Nothing here
|
* proxy carries the owner only so a resolver can push something. Nothing here
|
||||||
* resolves anything, so the field stays empty and the shape is what matters.
|
* resolves anything, so the field stays empty and the shape is what matters.
|
||||||
*
|
*
|
||||||
* Layers are the other half. A wall sits on #AKGL_COLLISION_LAYER_STATIC and
|
* Layers are the other half. A wall sits on `AKGL_COLLISION_LAYER_STATIC` and
|
||||||
* responds to nothing, which is the asymmetry libakgl's masks exist for: the
|
* responds to nothing, which is the asymmetry libakgl's masks exist for: the
|
||||||
* sprite's own `collidemask` includes STATIC, so a sprite finds a wall and two
|
* sprite's own `collidemask` includes STATIC, so a sprite finds a wall and two
|
||||||
* walls never test against each other. Sixty-four motionless rectangles
|
* walls never test against each other. Sixty-four motionless rectangles
|
||||||
|
|||||||
25
src/verbs.c
@@ -37,6 +37,7 @@ static const akbasic_Verb VERBS[] = {
|
|||||||
{ "ABS", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_abs },
|
{ "ABS", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_abs },
|
||||||
{ "AND", AKBASIC_TOK_AND, -1, NULL, NULL },
|
{ "AND", AKBASIC_TOK_AND, -1, NULL, NULL },
|
||||||
{ "APPEND", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_append },
|
{ "APPEND", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_append },
|
||||||
|
{ "ASC", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_asc },
|
||||||
{ "ATN", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_atn },
|
{ "ATN", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_atn },
|
||||||
{ "AUTO", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_auto },
|
{ "AUTO", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_auto },
|
||||||
{ "BACKUP", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_backup },
|
{ "BACKUP", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_backup },
|
||||||
@@ -73,14 +74,32 @@ static const akbasic_Verb VERBS[] = {
|
|||||||
{ "DRAW", AKBASIC_TOK_COMMAND, -1, akbasic_parse_draw, akbasic_cmd_draw },
|
{ "DRAW", AKBASIC_TOK_COMMAND, -1, akbasic_parse_draw, akbasic_cmd_draw },
|
||||||
{ "DSAVE", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_dsave },
|
{ "DSAVE", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, NULL, akbasic_cmd_dsave },
|
||||||
{ "DVERIFY", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, akbasic_parse_arglist, akbasic_cmd_dverify },
|
{ "DVERIFY", AKBASIC_TOK_COMMAND_IMMEDIATE, -1, akbasic_parse_arglist, akbasic_cmd_dverify },
|
||||||
|
/*
|
||||||
|
* EACH is never dispatched on its own -- akbasic_parse_for() and
|
||||||
|
* akbasic_parse_do() consume it directly, the same way TO, STEP, WHILE and
|
||||||
|
* UNTIL are. It exists here only so the scanner gives it a COMMAND token
|
||||||
|
* rather than letting it scan as a plain identifier.
|
||||||
|
*/
|
||||||
|
{ "EACH", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
||||||
{ "ELSE", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
{ "ELSE", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
||||||
{ "END", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_end },
|
{ "EMIT", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_emit },
|
||||||
|
{ "END", AKBASIC_TOK_COMMAND, -1, akbasic_parse_end, akbasic_cmd_end },
|
||||||
|
/*
|
||||||
|
* `END GEN` is never scanned as one token -- END and GEN are ordinary
|
||||||
|
* COMMAND tokens on the same line -- so this row is reached only from
|
||||||
|
* akbasic_parse_end(), which builds a leaf carrying this exact name after
|
||||||
|
* it sees GEN follow END. It still has to be here, and in order, because
|
||||||
|
* dispatch is a bsearch on the leaf's name; the same reason INPUT# and
|
||||||
|
* PRINT# are.
|
||||||
|
*/
|
||||||
|
{ "END GEN", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_end_gen },
|
||||||
{ "ENVELOPE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_envelope },
|
{ "ENVELOPE", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_envelope },
|
||||||
{ "ERR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_err },
|
{ "ERR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_err },
|
||||||
{ "EXIT", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_exit },
|
{ "EXIT", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_exit },
|
||||||
{ "FETCH", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_fetch },
|
{ "FETCH", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_fetch },
|
||||||
{ "FILTER", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_filter },
|
{ "FILTER", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_filter },
|
||||||
{ "FOR", AKBASIC_TOK_COMMAND, -1, akbasic_parse_for, akbasic_cmd_for },
|
{ "FOR", AKBASIC_TOK_COMMAND, -1, akbasic_parse_for, akbasic_cmd_for },
|
||||||
|
{ "GEN", AKBASIC_TOK_COMMAND, -1, akbasic_parse_gen, akbasic_cmd_gen },
|
||||||
{ "GET", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_get },
|
{ "GET", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_get },
|
||||||
{ "GETKEY", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_getkey },
|
{ "GETKEY", AKBASIC_TOK_COMMAND, -1, NULL, akbasic_cmd_getkey },
|
||||||
{ "GETMENU", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_getmenu },
|
{ "GETMENU", AKBASIC_TOK_COMMAND, -1, akbasic_parse_arglist, akbasic_cmd_getmenu },
|
||||||
@@ -93,6 +112,9 @@ static const akbasic_Verb VERBS[] = {
|
|||||||
{ "HEX", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_hex },
|
{ "HEX", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_hex },
|
||||||
{ "HUD", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_hud },
|
{ "HUD", AKBASIC_TOK_COMMAND, -1, akbasic_parse_optional_arglist, akbasic_cmd_hud },
|
||||||
{ "IF", AKBASIC_TOK_COMMAND, -1, akbasic_parse_if, akbasic_cmd_if },
|
{ "IF", AKBASIC_TOK_COMMAND, -1, akbasic_parse_if, akbasic_cmd_if },
|
||||||
|
/* IN is consumed directly by akbasic_parse_for()/akbasic_parse_do()'s EACH
|
||||||
|
clause, the same way EACH itself is. */
|
||||||
|
{ "IN", AKBASIC_TOK_COMMAND, -1, NULL, NULL },
|
||||||
{ "INPUT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_input, akbasic_cmd_input },
|
{ "INPUT", AKBASIC_TOK_COMMAND, -1, akbasic_parse_input, akbasic_cmd_input },
|
||||||
/*
|
/*
|
||||||
* `INPUT#` and `PRINT#` are never scanned as verb names -- the scanner reads
|
* `INPUT#` and `PRINT#` are never scanned as verb names -- the scanner reads
|
||||||
@@ -146,6 +168,7 @@ static const akbasic_Verb VERBS[] = {
|
|||||||
{ "RGR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rgr },
|
{ "RGR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rgr },
|
||||||
{ "RIGHT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_right },
|
{ "RIGHT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_right },
|
||||||
{ "RMENU", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rmenu },
|
{ "RMENU", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rmenu },
|
||||||
|
{ "RND", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rnd },
|
||||||
{ "RSPCOLOR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rspcolor },
|
{ "RSPCOLOR", AKBASIC_TOK_FUNCTION, 1, NULL, akbasic_fn_rspcolor },
|
||||||
{ "RSPHIT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsphit },
|
{ "RSPHIT", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsphit },
|
||||||
{ "RSPPOS", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsppos },
|
{ "RSPPOS", AKBASIC_TOK_FUNCTION, 2, NULL, akbasic_fn_rsppos },
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_data(struct akbasic_Parser *par
|
|||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_graphic(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_graphic(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_draw(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_draw(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_def(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_def(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_gen(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_end(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_dim(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_dim(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_for(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_for(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_if(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_parse_if(struct akbasic_Parser *parser, akbasic_ASTLeaf **dest);
|
||||||
@@ -111,6 +113,11 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_swap(struct akbasic_Runtime *obj,
|
|||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_troff(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_troff(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_tron(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_tron(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
|
|
||||||
|
/* Group L generator verbs -- src/runtime_generator.c */
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_gen(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_emit(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_end_gen(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
|
|
||||||
/* Verb handlers -- src/runtime_commands.c */
|
/* Verb handlers -- src/runtime_commands.c */
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_auto(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_auto(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_data(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_data(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
@@ -140,6 +147,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_cmd_stop(struct akbasic_Runtime *obj,
|
|||||||
|
|
||||||
/* Function handlers -- src/runtime_functions.c */
|
/* Function handlers -- src/runtime_functions.c */
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_abs(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_abs(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_asc(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_atn(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_atn(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_chr(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_chr(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_cos(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_cos(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
@@ -154,6 +162,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_peek(struct akbasic_Runtime *obj,
|
|||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_pointer(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_pointer(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_pointervar(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_pointervar(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_rad(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_rad(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_rnd(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_right(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_right(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_sgn(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_sgn(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_shl(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_fn_shl(struct akbasic_Runtime *obj, akbasic_ASTLeaf *expr, akbasic_Value *lval, akbasic_Value *rval, akbasic_Value **dest);
|
||||||
|
|||||||
@@ -244,10 +244,16 @@ static void test_no_drive_verbs(void)
|
|||||||
harness_stop();
|
harness_stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* DIRECTORY is refused for a different reason, and says which. */
|
/*
|
||||||
|
* DIRECTORY is refused for a different reason than the five above: not for
|
||||||
|
* want of a drive, but because it is unwritten. It used to name the missing
|
||||||
|
* libakstdlib wrapper; that wrapper landed, so naming it would be a lie.
|
||||||
|
*/
|
||||||
TEST_REQUIRE_OK(run_program("10 DIRECTORY\n"));
|
TEST_REQUIRE_OK(run_program("10 DIRECTORY\n"));
|
||||||
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "libakstdlib") != NULL,
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "not implemented") != NULL,
|
||||||
"DIRECTORY should name the missing wrapper, got \"%s\"", HARNESS_OUTPUT);
|
"DIRECTORY should say it is unwritten, got \"%s\"", HARNESS_OUTPUT);
|
||||||
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "libakstdlib") == NULL,
|
||||||
|
"DIRECTORY must not still blame libakstdlib, got \"%s\"", HARNESS_OUTPUT);
|
||||||
harness_stop();
|
harness_stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
tests/docs_preludes/galagacalls.post
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
(void)SCRIPT; (void)SINK; (void)SINKSTATE; (void)SOURCE;
|
||||||
|
(void)args; (void)argp; (void)dtval; (void)result;
|
||||||
|
(void)enemy; (void)actor; (void)dt;
|
||||||
|
(void)SCRATCH_ENEMY; (void)SCRATCH_ACTOR; (void)galaga_shared;
|
||||||
|
(void)ENEMY_TYPE; (void)ACTOR_TYPE; (void)GAME_TYPE;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
84
tests/docs_preludes/galagacalls.pre
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* Prelude for the interpreter-facing fragments in docs/20: the boot sequence,
|
||||||
|
* the ADDEM proof and the rebind-call-reset protocol, shown as runs of CATCH
|
||||||
|
* calls. The statics are the ones examples/galaga/script.c keeps; the locals
|
||||||
|
* are the superset every fragment draws from, void-cast in the postlude so an
|
||||||
|
* unused one is not a warning.
|
||||||
|
*/
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <akgl/actor.h>
|
||||||
|
|
||||||
|
#include <akbasic/environment.h>
|
||||||
|
#include <akbasic/error.h>
|
||||||
|
#include <akbasic/host.h>
|
||||||
|
#include <akbasic/runtime.h>
|
||||||
|
#include <akbasic/sink.h>
|
||||||
|
|
||||||
|
typedef struct galaga_docs_Enemy
|
||||||
|
{
|
||||||
|
int32_t kind;
|
||||||
|
int32_t state;
|
||||||
|
float homex;
|
||||||
|
float homey;
|
||||||
|
float t;
|
||||||
|
int32_t hp;
|
||||||
|
int32_t fire;
|
||||||
|
float rnd;
|
||||||
|
} galaga_docs_Enemy;
|
||||||
|
|
||||||
|
typedef struct galaga_docs_Shared
|
||||||
|
{
|
||||||
|
float playerx;
|
||||||
|
float playery;
|
||||||
|
int32_t wave;
|
||||||
|
float rnd;
|
||||||
|
} galaga_docs_Shared;
|
||||||
|
|
||||||
|
static akbasic_Runtime SCRIPT;
|
||||||
|
static akbasic_TextSink SINK;
|
||||||
|
static akbasic_StdioSink SINKSTATE;
|
||||||
|
static char SOURCE[16384];
|
||||||
|
|
||||||
|
static galaga_docs_Enemy SCRATCH_ENEMY;
|
||||||
|
static akgl_Actor SCRATCH_ACTOR;
|
||||||
|
static galaga_docs_Shared galaga_shared;
|
||||||
|
|
||||||
|
static const akbasic_HostField ENEMY_FIELDS[] = {
|
||||||
|
AKBASIC_HOST_FIELD( galaga_docs_Enemy, kind, "KIND#", AKBASIC_HOSTFIELD_INT32 )
|
||||||
|
};
|
||||||
|
static const akbasic_HostType ENEMY_TYPE = {
|
||||||
|
"ENEMY", sizeof(galaga_docs_Enemy), ENEMY_FIELDS, 1
|
||||||
|
};
|
||||||
|
static const akbasic_HostField ACTOR_FIELDS[] = {
|
||||||
|
AKBASIC_HOST_FIELD( akgl_Actor, x, "X%", AKBASIC_HOSTFIELD_FLOAT )
|
||||||
|
};
|
||||||
|
static const akbasic_HostType ACTOR_TYPE = {
|
||||||
|
"ACTOR", sizeof(akgl_Actor), ACTOR_FIELDS, 1
|
||||||
|
};
|
||||||
|
static const akbasic_HostField GAME_FIELDS[] = {
|
||||||
|
AKBASIC_HOST_FIELD( galaga_docs_Shared, wave, "WAVE#", AKBASIC_HOSTFIELD_INT32 )
|
||||||
|
};
|
||||||
|
static const akbasic_HostType GAME_TYPE = {
|
||||||
|
"GAME", sizeof(galaga_docs_Shared), GAME_FIELDS, 1
|
||||||
|
};
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_docs_fragment(galaga_docs_Enemy *enemy, akgl_Actor *actor, float dt);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_docs_fragment(galaga_docs_Enemy *enemy, akgl_Actor *actor, float dt)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
akbasic_Value args[2];
|
||||||
|
akbasic_Value *argp[2];
|
||||||
|
akbasic_Value dtval;
|
||||||
|
akbasic_Value *result = NULL;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
121
tests/docs_preludes/galagagame.pre
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* Prelude for file-scope fragments in docs/20 and docs/21 that assume the
|
||||||
|
* galaga example's own declarations already exist -- the shared structures
|
||||||
|
* from examples/galaga/galaga.h and the helpers a fragment calls but does not
|
||||||
|
* define. The types are copied rather than included so a fragment compiles
|
||||||
|
* against exactly what the chapter has shown so far; the helper declarations
|
||||||
|
* are invented prototypes, per the prelude policy in MAINTENANCE.md.
|
||||||
|
*/
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3_ttf/SDL_ttf.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <akgl/actor.h>
|
||||||
|
#include <akgl/character.h>
|
||||||
|
#include <akgl/controller.h>
|
||||||
|
#include <akgl/draw.h>
|
||||||
|
#include <akgl/error.h>
|
||||||
|
#include <akgl/game.h>
|
||||||
|
#include <akgl/heap.h>
|
||||||
|
#include <akgl/physics.h>
|
||||||
|
#include <akgl/registry.h>
|
||||||
|
#include <akgl/renderer.h>
|
||||||
|
#include <akgl/sprite.h>
|
||||||
|
#include <akgl/text.h>
|
||||||
|
#include <akgl/ui.h>
|
||||||
|
#include <akgl/util.h>
|
||||||
|
|
||||||
|
#include <akbasic/environment.h>
|
||||||
|
#include <akbasic/error.h>
|
||||||
|
#include <akbasic/host.h>
|
||||||
|
#include <akbasic/runtime.h>
|
||||||
|
#include <akbasic/sink.h>
|
||||||
|
|
||||||
|
#define GALAGA_ENEMY_BEE 0
|
||||||
|
#define GALAGA_ENEMY_BUTTERFLY 1
|
||||||
|
#define GALAGA_ENEMY_BOSS 2
|
||||||
|
#define GALAGA_ENEMY_KINDS 3
|
||||||
|
#define GALAGA_MAX_ENEMIES 40
|
||||||
|
#define GALAGA_MAX_PLAYER_SHOTS 2
|
||||||
|
#define GALAGA_MAX_ENEMY_SHOTS 8
|
||||||
|
#define GALAGA_ES_ENTERING (1 << 0)
|
||||||
|
#define GALAGA_ES_FORMATION (1 << 1)
|
||||||
|
#define GALAGA_ES_DIVING (1 << 2)
|
||||||
|
|
||||||
|
typedef struct galaga_Enemy
|
||||||
|
{
|
||||||
|
int32_t kind;
|
||||||
|
int32_t state;
|
||||||
|
float homex;
|
||||||
|
float homey;
|
||||||
|
float t;
|
||||||
|
int32_t hp;
|
||||||
|
int32_t fire;
|
||||||
|
float rnd;
|
||||||
|
} galaga_Enemy;
|
||||||
|
|
||||||
|
typedef struct galaga_Shared
|
||||||
|
{
|
||||||
|
float playerx;
|
||||||
|
float playery;
|
||||||
|
int32_t wave;
|
||||||
|
float rnd;
|
||||||
|
} galaga_Shared;
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
GALAGA_SCREEN_TITLE = 0,
|
||||||
|
GALAGA_SCREEN_PLAY,
|
||||||
|
GALAGA_SCREEN_GAMEOVER,
|
||||||
|
GALAGA_SCREEN_VICTORY
|
||||||
|
} galaga_Screen;
|
||||||
|
|
||||||
|
typedef struct galaga_Game
|
||||||
|
{
|
||||||
|
galaga_Screen screen;
|
||||||
|
int frame;
|
||||||
|
float dt;
|
||||||
|
bool autoplay;
|
||||||
|
int score;
|
||||||
|
int lives;
|
||||||
|
int kills[GALAGA_ENEMY_KINDS];
|
||||||
|
int shots[GALAGA_ENEMY_KINDS];
|
||||||
|
int script_errors;
|
||||||
|
akgl_Actor *player;
|
||||||
|
float fire_cooldown;
|
||||||
|
float respawn_timer;
|
||||||
|
bool firing;
|
||||||
|
bool moveleft;
|
||||||
|
bool moveright;
|
||||||
|
int player_shots_live;
|
||||||
|
int enemy_shots_live;
|
||||||
|
} galaga_Game;
|
||||||
|
|
||||||
|
extern galaga_Game galaga_game;
|
||||||
|
extern galaga_Shared galaga_shared;
|
||||||
|
extern galaga_Enemy galaga_enemies[GALAGA_MAX_ENEMIES];
|
||||||
|
extern akgl_Actor *galaga_enemy_actors[GALAGA_MAX_ENEMIES];
|
||||||
|
|
||||||
|
float galaga_random(void);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_script_update_enemy(galaga_Enemy *enemy, akgl_Actor *actor, float dt);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_boom_spawn(float x, float y);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *enemy_fire(galaga_Enemy *enemy, akgl_Actor *from);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *kill_enemy(int index);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *player_update(akgl_Actor *obj);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *left_on(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *left_off(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *right_on(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *right_off(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *fire_on(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *fire_off(akgl_Actor *obj, SDL_Event *event);
|
||||||
|
void shot_box(akgl_Actor *actor, SDL_FRect *dest);
|
||||||
|
void enemy_box(akgl_Actor *actor, SDL_FRect *dest);
|
||||||
|
void player_box(akgl_Actor *actor, SDL_FRect *dest);
|
||||||
6
tests/docs_preludes/galagahost.post
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
(void)event;
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
55
tests/docs_preludes/galagahost.pre
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/*
|
||||||
|
* Prelude for statement-context fragments in docs/20: runs of CATCH calls
|
||||||
|
* from the galaga frame loop, shown without their scaffolding because the
|
||||||
|
* ATTEMPT protocol is the scaffolding. Same policy as hostcalls.pre.
|
||||||
|
*/
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3_ttf/SDL_ttf.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <akgl/actor.h>
|
||||||
|
#include <akgl/controller.h>
|
||||||
|
#include <akgl/draw.h>
|
||||||
|
#include <akgl/game.h>
|
||||||
|
#include <akgl/heap.h>
|
||||||
|
#include <akgl/renderer.h>
|
||||||
|
#include <akgl/ui.h>
|
||||||
|
|
||||||
|
#include <akbasic/error.h>
|
||||||
|
#include <akbasic/runtime.h>
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
GALAGA_SCREEN_TITLE = 0,
|
||||||
|
GALAGA_SCREEN_PLAY,
|
||||||
|
GALAGA_SCREEN_GAMEOVER,
|
||||||
|
GALAGA_SCREEN_VICTORY
|
||||||
|
} galaga_Screen;
|
||||||
|
|
||||||
|
struct galaga_docs_Game
|
||||||
|
{
|
||||||
|
galaga_Screen screen;
|
||||||
|
float dt;
|
||||||
|
akgl_Actor *player;
|
||||||
|
};
|
||||||
|
extern struct galaga_docs_Game galaga_game;
|
||||||
|
extern akgl_Actor *galaga_enemy_actors[40];
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *declare_title(void);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *declare_play(void);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *declare_end(void);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_docs_fragment(void);
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *galaga_docs_fragment(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
SDL_Event event;
|
||||||
|
|
||||||
|
ATTEMPT {
|
||||||
37
tests/docs_preludes/galagatypes.pre
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Prelude for the self-contained file-scope fragments in docs/20 and docs/21:
|
||||||
|
* blocks that define a struct, a table or a whole function from scratch need
|
||||||
|
* only the includes. Only compiled in the AKBASIC_WITH_AKGL build.
|
||||||
|
*/
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3_ttf/SDL_ttf.h>
|
||||||
|
|
||||||
|
#include <akerror.h>
|
||||||
|
#include <akstdlib.h>
|
||||||
|
|
||||||
|
#include <akgl/actor.h>
|
||||||
|
#include <akgl/character.h>
|
||||||
|
#include <akgl/controller.h>
|
||||||
|
#include <akgl/draw.h>
|
||||||
|
#include <akgl/error.h>
|
||||||
|
#include <akgl/game.h>
|
||||||
|
#include <akgl/heap.h>
|
||||||
|
#include <akgl/physics.h>
|
||||||
|
#include <akgl/registry.h>
|
||||||
|
#include <akgl/renderer.h>
|
||||||
|
#include <akgl/sprite.h>
|
||||||
|
#include <akgl/text.h>
|
||||||
|
#include <akgl/ui.h>
|
||||||
|
#include <akgl/util.h>
|
||||||
|
|
||||||
|
#include <akbasic/environment.h>
|
||||||
|
#include <akbasic/error.h>
|
||||||
|
#include <akbasic/host.h>
|
||||||
|
#include <akbasic/runtime.h>
|
||||||
|
#include <akbasic/sink.h>
|
||||||
@@ -2,5 +2,7 @@
|
|||||||
} PROCESS(errctx) {
|
} PROCESS(errctx) {
|
||||||
} FINISH(errctx, true);
|
} FINISH(errctx, true);
|
||||||
(void)score;
|
(void)score;
|
||||||
|
(void)argp;
|
||||||
|
(void)result;
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
* surrounding prose says exists but does not print.
|
* surrounding prose says exists but does not print.
|
||||||
*/
|
*/
|
||||||
#include <akerror.h>
|
#include <akerror.h>
|
||||||
|
#include <akbasic/environment.h>
|
||||||
#include <akbasic/error.h>
|
#include <akbasic/error.h>
|
||||||
#include <akbasic/runtime.h>
|
#include <akbasic/runtime.h>
|
||||||
#include <akbasic/variable.h>
|
#include <akbasic/variable.h>
|
||||||
@@ -23,5 +24,7 @@ akerr_ErrorContext AKERR_NOIGNORE *akbasic_docs_fragment(void)
|
|||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
int64_t score = 0;
|
int64_t score = 0;
|
||||||
|
akbasic_Value *argp[4];
|
||||||
|
akbasic_Value *result = NULL;
|
||||||
|
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ static void test_counter_survives_the_loop(void)
|
|||||||
* `FOR I = 1 TO 1` executes the body one time on every BASIC there has ever
|
* `FOR I = 1 TO 1` executes the body one time on every BASIC there has ever
|
||||||
* been. Here the entry test treats "the counter has reached the limit" as
|
* been. Here the entry test treats "the counter has reached the limit" as
|
||||||
* "do not enter", so the body is skipped entirely -- and
|
* "do not enter", so the body is skipped entirely -- and
|
||||||
* tests/reference/language/flowcontrol/forloopwaitingforcommand.bas pins that,
|
* tests/language/flowcontrol/forloopwaitingforcommand.bas pins that,
|
||||||
* which is why this cannot simply be corrected.
|
* which is why this cannot simply be corrected.
|
||||||
*/
|
*/
|
||||||
static void test_single_iteration_loop(void)
|
static void test_single_iteration_loop(void)
|
||||||
|
|||||||
360
tests/generators.c
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
/**
|
||||||
|
* @file generators.c
|
||||||
|
* @brief Generators: GEN/EMIT/END GEN and the FOR EACH/DO EACH loops over them.
|
||||||
|
*
|
||||||
|
* The golden corpus (tests/language/flowcontrol/generators_*.bas) covers the
|
||||||
|
* ordinary shapes: the issue's own ROOMOBJECTS example in both loop forms, an
|
||||||
|
* empty generator, a non-numeric EMIT and nested/interleaved invocations. This
|
||||||
|
* file covers what a byte-compared program cannot: that abandoning a
|
||||||
|
* generator with EXIT gives its environment back to the pool rather than
|
||||||
|
* leaking it, and that misusing a GEN fails cleanly rather than corrupting the
|
||||||
|
* environment stack.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <akbasic/error.h>
|
||||||
|
#include <akbasic/runtime.h>
|
||||||
|
|
||||||
|
#include "harness.h"
|
||||||
|
#include "testutil.h"
|
||||||
|
|
||||||
|
/** @brief Run a program to completion under an explicit step budget. */
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *run_program_bounded(const char *source, int64_t steps)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, harness_start(NULL));
|
||||||
|
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
|
||||||
|
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||||
|
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, steps));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Run a program to completion, bounded so a hang fails rather than waits. */
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
PASS(errctx, run_program_bounded(source, 20000));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief The issue's own example, as a sanity check independent of the golden corpus. */
|
||||||
|
static void test_room_objects_smoke(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 DIM OBJ#(3)\n"
|
||||||
|
"20 OBJ#(0) = 100\n"
|
||||||
|
"30 OBJ#(1) = 200\n"
|
||||||
|
"40 OBJ#(2) = 300\n"
|
||||||
|
"50 GEN ROOMOBJECTS(R#)\n"
|
||||||
|
"60 FOR I# = 0 TO 2\n"
|
||||||
|
"70 IF I# <> 1 THEN EMIT OBJ#(I#)\n"
|
||||||
|
"80 NEXT I#\n"
|
||||||
|
"90 END GEN\n"
|
||||||
|
"100 FOR EACH O# IN ROOMOBJECTS(0)\n"
|
||||||
|
"110 PRINT O#\n"
|
||||||
|
"120 NEXT O#\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "100\n300\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief EXIT out of a FOR EACH loop releases its generator, not just the loop.
|
||||||
|
*
|
||||||
|
* Run more FOR EACH/EXIT constructs than AKBASIC_MAX_ENVIRONMENTS -- each one
|
||||||
|
* takes two environments (the loop's own and the generator's) -- in a single
|
||||||
|
* program. If EXIT abandoned the generator environment instead of releasing
|
||||||
|
* it, this exhausts the pool partway through and the run reports "Environment
|
||||||
|
* pool exhausted" instead of finishing.
|
||||||
|
*/
|
||||||
|
static void test_exit_releases_generator_for_each(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN ONE(X#)\n"
|
||||||
|
"20 EMIT 1\n"
|
||||||
|
"30 END GEN\n"
|
||||||
|
"40 FOR K# = 1 TO 13\n"
|
||||||
|
"50 FOR EACH V# IN ONE(0)\n"
|
||||||
|
"60 EXIT\n"
|
||||||
|
"70 NEXT V#\n"
|
||||||
|
"80 NEXT K#\n"
|
||||||
|
"90 PRINT \"DONE\"\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief The same leak check for DO EACH/EXIT. */
|
||||||
|
static void test_exit_releases_generator_do_each(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN ONE(X#)\n"
|
||||||
|
"20 EMIT 1\n"
|
||||||
|
"30 END GEN\n"
|
||||||
|
"40 FOR K# = 1 TO 13\n"
|
||||||
|
"50 DO EACH V# IN ONE(0)\n"
|
||||||
|
"60 EXIT\n"
|
||||||
|
"70 LOOP\n"
|
||||||
|
"80 NEXT K#\n"
|
||||||
|
"90 PRINT \"DONE\"\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief EXIT partway through, with more of the generator left to run, still
|
||||||
|
* frees the environment for the next construct that needs one.
|
||||||
|
*/
|
||||||
|
static void test_exit_partway_through(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
||||||
|
"20 FOR I# = 1 TO N#\n"
|
||||||
|
"30 EMIT I#\n"
|
||||||
|
"40 NEXT I#\n"
|
||||||
|
"50 END GEN\n"
|
||||||
|
"60 FOR EACH V# IN COUNTUP(10)\n"
|
||||||
|
"70 PRINT V#\n"
|
||||||
|
"80 IF V# = 2 THEN EXIT\n"
|
||||||
|
"90 NEXT V#\n"
|
||||||
|
"100 PRINT \"AFTER\"\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n2\nAFTER\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Abandoning a generator that is itself suspended inside a FOR EACH
|
||||||
|
* over another generator releases the inner generator too.
|
||||||
|
*
|
||||||
|
* The inner generator's environment hangs off the *loop* scope inside OUTER's
|
||||||
|
* body as a child, off the parent chain -- the one place a bare parent walk
|
||||||
|
* never looks. Before akbasic_runtime_release_generator() recursed into
|
||||||
|
* `forGeneratorEnv`, every trip through this loop stranded one pool slot and
|
||||||
|
* the 13th trip died with "Environment pool exhausted".
|
||||||
|
*/
|
||||||
|
static void test_exit_releases_nested_generators(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN INNER(N#)\n"
|
||||||
|
"20 EMIT 1\n"
|
||||||
|
"30 EMIT 2\n"
|
||||||
|
"40 END GEN\n"
|
||||||
|
"50 GEN OUTER(N#)\n"
|
||||||
|
"60 FOR EACH I# IN INNER(0)\n"
|
||||||
|
"70 EMIT I#\n"
|
||||||
|
"80 NEXT I#\n"
|
||||||
|
"90 END GEN\n"
|
||||||
|
"100 FOR K# = 1 TO 40\n"
|
||||||
|
"110 FOR EACH V# IN OUTER(0)\n"
|
||||||
|
"120 EXIT\n"
|
||||||
|
"130 NEXT V#\n"
|
||||||
|
"140 NEXT K#\n"
|
||||||
|
"150 PRINT \"DONE\"\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A LOOP UNTIL that stops a DO EACH early releases the generator it
|
||||||
|
* abandons, every time.
|
||||||
|
*/
|
||||||
|
static void test_loop_condition_releases_generator(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
||||||
|
"20 FOR I# = 1 TO N#\n"
|
||||||
|
"30 EMIT I#\n"
|
||||||
|
"40 NEXT I#\n"
|
||||||
|
"50 END GEN\n"
|
||||||
|
"60 FOR K# = 1 TO 40\n"
|
||||||
|
"70 DO EACH V# IN COUNTUP(10)\n"
|
||||||
|
"80 LOOP UNTIL V# = 2\n"
|
||||||
|
"90 NEXT K#\n"
|
||||||
|
"100 PRINT \"DONE\"\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "DONE\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief RETURN standing in a GEN's own frame ends the generator early,
|
||||||
|
* exactly as END GEN would -- a GEN is a function at heart.
|
||||||
|
*/
|
||||||
|
static void test_return_ends_generator(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN G(N#)\n"
|
||||||
|
"20 EMIT 1\n"
|
||||||
|
"30 RETURN\n"
|
||||||
|
"40 EMIT 2\n"
|
||||||
|
"50 END GEN\n"
|
||||||
|
"60 FOR EACH V# IN G(0)\n"
|
||||||
|
"70 PRINT V#\n"
|
||||||
|
"80 NEXT V#\n"
|
||||||
|
"90 PRINT \"DONE\"\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\nDONE\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief RETURN with a value inside a GEN is refused: values leave a GEN one
|
||||||
|
* at a time, through EMIT, and there is no return slot waiting.
|
||||||
|
*/
|
||||||
|
static void test_return_value_in_generator_refused(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program_bounded("10 GEN G(N#)\n"
|
||||||
|
"20 EMIT 1\n"
|
||||||
|
"30 RETURN 99\n"
|
||||||
|
"40 END GEN\n"
|
||||||
|
"50 FOR EACH V# IN G(0)\n"
|
||||||
|
"60 PRINT V#\n"
|
||||||
|
"70 NEXT V#\n"
|
||||||
|
"80 PRINT \"UNREACHABLE\"\n", 2000));
|
||||||
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "1\n") != NULL,
|
||||||
|
"expected the first EMIT in \"%s\"", HARNESS_OUTPUT);
|
||||||
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHABLE") == NULL,
|
||||||
|
"RETURN with a value must stop the run, got \"%s\"", HARNESS_OUTPUT);
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The unwind primitive releases a popped scope's suspended generator.
|
||||||
|
*
|
||||||
|
* Built by hand rather than through BASIC because the paths that need this --
|
||||||
|
* the error unwinds in pump_generator() and call_function() -- only trigger
|
||||||
|
* on C-level failures a program cannot politely ask for. The shape is the
|
||||||
|
* one EMIT leaves behind: a loop scope holding a detached generator child,
|
||||||
|
* with a further scope active above it.
|
||||||
|
*/
|
||||||
|
static void test_unwind_releases_suspended_generators(void)
|
||||||
|
{
|
||||||
|
akbasic_Environment *root = NULL;
|
||||||
|
akbasic_Environment *loopenv = NULL;
|
||||||
|
akbasic_Environment *genenv = NULL;
|
||||||
|
akbasic_Environment *forenv = NULL;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||||||
|
root = HARNESS_RUNTIME.environment;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
||||||
|
loopenv = HARNESS_RUNTIME.environment;
|
||||||
|
loopenv->isEachLoop = true;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
||||||
|
genenv = HARNESS_RUNTIME.environment;
|
||||||
|
genenv->isGenerator = true;
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_detach_environment(&HARNESS_RUNTIME));
|
||||||
|
loopenv->forGeneratorEnv = genenv;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_new_environment(&HARNESS_RUNTIME));
|
||||||
|
forenv = HARNESS_RUNTIME.environment;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_unwind_to_environment(&HARNESS_RUNTIME, root));
|
||||||
|
TEST_REQUIRE(HARNESS_RUNTIME.environment == root,
|
||||||
|
"unwind must land on the target scope");
|
||||||
|
TEST_REQUIRE(!forenv->used && !loopenv->used && !genenv->used,
|
||||||
|
"unwind must release the chain and the suspended generator");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A GEN invoked like an ordinary function, rather than through FOR
|
||||||
|
* EACH/DO EACH, fails cleanly.
|
||||||
|
*
|
||||||
|
* EMIT requires `isGenerator`, which only a FOR EACH/DO EACH invocation sets
|
||||||
|
* -- an ordinary call pushes a plain environment, exactly as a DEF's does --
|
||||||
|
* so the first EMIT the call reaches is where this is refused.
|
||||||
|
*/
|
||||||
|
static void test_called_like_a_function(void)
|
||||||
|
{
|
||||||
|
akbasic_Value *args[1];
|
||||||
|
akbasic_Value argvalue;
|
||||||
|
akbasic_Value *out = NULL;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
||||||
|
"10 GEN ONE(N#)\n"
|
||||||
|
"20 EMIT N#\n"
|
||||||
|
"30 END GEN\n"));
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||||||
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 100));
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(akbasic_value_zero(&argvalue));
|
||||||
|
argvalue.valuetype = AKBASIC_TYPE_INTEGER;
|
||||||
|
argvalue.intval = 5;
|
||||||
|
args[0] = &argvalue;
|
||||||
|
TEST_REQUIRE_ANY_ERROR(akbasic_runtime_call_function(&HARNESS_RUNTIME, "ONE", args, 1, &out));
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief EMIT reached with no enclosing GEN invocation is refused. */
|
||||||
|
static void test_emit_outside_gen(void)
|
||||||
|
{
|
||||||
|
akbasic_ASTLeaf *leaf = NULL;
|
||||||
|
akbasic_Value *out = NULL;
|
||||||
|
|
||||||
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||||||
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||||||
|
TEST_REQUIRE_OK(harness_parse("EMIT 5", &leaf));
|
||||||
|
TEST_REQUIRE_ANY_ERROR(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A GEN invoking itself, directly, is refused rather than recursing.
|
||||||
|
*
|
||||||
|
* Bounded tightly: a program that recursed forever would hang the whole
|
||||||
|
* suite, and this is exactly the case that must not.
|
||||||
|
*/
|
||||||
|
static void test_self_recursion_refused(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program_bounded("10 GEN RECURSIVE(N#)\n"
|
||||||
|
"20 FOR EACH X# IN RECURSIVE(N# + 1)\n"
|
||||||
|
"30 EMIT X#\n"
|
||||||
|
"40 NEXT X#\n"
|
||||||
|
"50 END GEN\n"
|
||||||
|
"60 PRINT \"BEFORE\"\n"
|
||||||
|
"70 FOR EACH R# IN RECURSIVE(1)\n"
|
||||||
|
"80 PRINT R#\n"
|
||||||
|
"90 NEXT R#\n"
|
||||||
|
"100 PRINT \"UNREACHABLE\"\n", 2000));
|
||||||
|
/* Whatever else happened, the line before the recursive call ran and the
|
||||||
|
one two lines after invoking it -- which would only print after the
|
||||||
|
loop completed successfully -- did not. */
|
||||||
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "BEFORE\n") != NULL,
|
||||||
|
"expected \"BEFORE\" in \"%s\"", HARNESS_OUTPUT);
|
||||||
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "UNREACHABLE") == NULL,
|
||||||
|
"self-recursion must not reach \"UNREACHABLE\", got \"%s\"", HARNESS_OUTPUT);
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Independent (sibling) FOR EACH invocations of the same GEN are not
|
||||||
|
* self-recursion, even nested.
|
||||||
|
*/
|
||||||
|
static void test_sibling_invocations_allowed(void)
|
||||||
|
{
|
||||||
|
TEST_REQUIRE_OK(run_program("10 GEN COUNTUP(N#)\n"
|
||||||
|
"20 FOR I# = 1 TO N#\n"
|
||||||
|
"30 EMIT I#\n"
|
||||||
|
"40 NEXT I#\n"
|
||||||
|
"50 END GEN\n"
|
||||||
|
"60 FOR EACH A# IN COUNTUP(2)\n"
|
||||||
|
"70 FOR EACH B# IN COUNTUP(2)\n"
|
||||||
|
"80 PRINT A# * 10 + B#\n"
|
||||||
|
"90 NEXT B#\n"
|
||||||
|
"100 NEXT A#\n"));
|
||||||
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "11\n12\n21\n22\n");
|
||||||
|
harness_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
test_room_objects_smoke();
|
||||||
|
test_exit_releases_generator_for_each();
|
||||||
|
test_exit_releases_generator_do_each();
|
||||||
|
test_exit_partway_through();
|
||||||
|
test_exit_releases_nested_generators();
|
||||||
|
test_loop_condition_releases_generator();
|
||||||
|
test_return_ends_generator();
|
||||||
|
test_return_value_in_generator_refused();
|
||||||
|
test_unwind_releases_suspended_generators();
|
||||||
|
test_called_like_a_function();
|
||||||
|
test_emit_outside_gen();
|
||||||
|
test_self_recursion_refused();
|
||||||
|
test_sibling_invocations_allowed();
|
||||||
|
return akbasic_test_failures;
|
||||||
|
}
|
||||||
16
tests/language/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# The language test corpus
|
||||||
|
|
||||||
|
The `.bas` files in this directory and their sibling `.txt` files are the editable language
|
||||||
|
corpus. CMake registers every program as an individual `local_*` CTest case and compares its
|
||||||
|
output with the sibling expectation.
|
||||||
|
|
||||||
|
The corpus includes cases carried over from the deprecated Go implementation as well as cases
|
||||||
|
written for this interpreter. Their provenance is useful when investigating a regression, but it
|
||||||
|
does not make any case immutable or turn the old implementation into a specification.
|
||||||
|
|
||||||
|
**Change a program and its expectation deliberately and in the same commit.** If the changed
|
||||||
|
output represents an intentional language decision, record the reason in `TODO.md` or the
|
||||||
|
relevant documentation. If it is an accidental change, fix the interpreter instead.
|
||||||
|
|
||||||
|
The `examples/` subdirectory contains complete BASIC programs used as worked examples. It is
|
||||||
|
part of the same corpus and follows the same `.bas`/`.txt` pairing rule.
|
||||||