c64-kb 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2082 -0
- package/LICENSE +29 -0
- package/README.md +559 -0
- package/VERSION +25 -0
- package/dist/cli/setup.js +66 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli-memorize-audit.js +97 -0
- package/dist/cli-memorize-audit.js.map +1 -0
- package/dist/cli.js +335 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +83 -0
- package/dist/config.js.map +1 -0
- package/dist/context.js +53 -0
- package/dist/context.js.map +1 -0
- package/dist/domain/budget-notes.js +104 -0
- package/dist/domain/budget-notes.js.map +1 -0
- package/dist/domain/budget.js +417 -0
- package/dist/domain/budget.js.map +1 -0
- package/dist/domain/game-design.js +77 -0
- package/dist/domain/game-design.js.map +1 -0
- package/dist/domain/timing.js +39 -0
- package/dist/domain/timing.js.map +1 -0
- package/dist/graph/apply.js +132 -0
- package/dist/graph/apply.js.map +1 -0
- package/dist/graph/claims.js +231 -0
- package/dist/graph/claims.js.map +1 -0
- package/dist/graph/extract/archetype.js +118 -0
- package/dist/graph/extract/archetype.js.map +1 -0
- package/dist/graph/extract/common.js +69 -0
- package/dist/graph/extract/common.js.map +1 -0
- package/dist/graph/extract/failure.js +37 -0
- package/dist/graph/extract/failure.js.map +1 -0
- package/dist/graph/extract/game-design.js +189 -0
- package/dist/graph/extract/game-design.js.map +1 -0
- package/dist/graph/extract/hardware.js +124 -0
- package/dist/graph/extract/hardware.js.map +1 -0
- package/dist/graph/extract/pitfall.js +62 -0
- package/dist/graph/extract/pitfall.js.map +1 -0
- package/dist/graph/extract/raster-band.js +56 -0
- package/dist/graph/extract/raster-band.js.map +1 -0
- package/dist/graph/extract/recipe.js +71 -0
- package/dist/graph/extract/recipe.js.map +1 -0
- package/dist/graph/extract/technique-entities.js +180 -0
- package/dist/graph/extract/technique-entities.js.map +1 -0
- package/dist/graph/extract/technique.js +179 -0
- package/dist/graph/extract/technique.js.map +1 -0
- package/dist/graph/extract/toolchain.js +67 -0
- package/dist/graph/extract/toolchain.js.map +1 -0
- package/dist/graph/extract/types.js +3 -0
- package/dist/graph/extract/types.js.map +1 -0
- package/dist/graph/extract/vocabulary.js +65 -0
- package/dist/graph/extract/vocabulary.js.map +1 -0
- package/dist/graph/extract.js +43 -0
- package/dist/graph/extract.js.map +1 -0
- package/dist/graph/kernal-clobbers.js +64 -0
- package/dist/graph/kernal-clobbers.js.map +1 -0
- package/dist/graph/machine-variants.js +208 -0
- package/dist/graph/machine-variants.js.map +1 -0
- package/dist/ingest/files.js +99 -0
- package/dist/ingest/files.js.map +1 -0
- package/dist/ingest/passes.js +104 -0
- package/dist/ingest/passes.js.map +1 -0
- package/dist/ingest/points.js +50 -0
- package/dist/ingest/points.js.map +1 -0
- package/dist/ingest/report.js +140 -0
- package/dist/ingest/report.js.map +1 -0
- package/dist/ingest/tally.js +81 -0
- package/dist/ingest/tally.js.map +1 -0
- package/dist/ingest/verified-on.js +43 -0
- package/dist/ingest/verified-on.js.map +1 -0
- package/dist/ingest.js +135 -0
- package/dist/ingest.js.map +1 -0
- package/dist/schemas/cost-basis.js +5 -0
- package/dist/schemas/cost-basis.js.map +1 -0
- package/dist/schemas/machine-variant.js +25 -0
- package/dist/schemas/machine-variant.js.map +1 -0
- package/dist/schemas/plan-budget.js +121 -0
- package/dist/schemas/plan-budget.js.map +1 -0
- package/dist/schemas/tool-outputs.js +526 -0
- package/dist/schemas/tool-outputs.js.map +1 -0
- package/dist/server/define-tool.js +52 -0
- package/dist/server/define-tool.js.map +1 -0
- package/dist/server/defined-only.js +9 -0
- package/dist/server/defined-only.js.map +1 -0
- package/dist/server/tools-briefings.js +70 -0
- package/dist/server/tools-briefings.js.map +1 -0
- package/dist/server/tools-lookup.js +180 -0
- package/dist/server/tools-lookup.js.map +1 -0
- package/dist/server/tools-maintenance.js +116 -0
- package/dist/server/tools-maintenance.js.map +1 -0
- package/dist/server/tools-pitfalls.js +89 -0
- package/dist/server/tools-pitfalls.js.map +1 -0
- package/dist/server/tools-recipes.js +296 -0
- package/dist/server/tools-recipes.js.map +1 -0
- package/dist/server/tools-runtime.js +48 -0
- package/dist/server/tools-runtime.js.map +1 -0
- package/dist/server/tools.js +43 -0
- package/dist/server/tools.js.map +1 -0
- package/dist/server.js +95 -0
- package/dist/server.js.map +1 -0
- package/dist/services/analytics/schema.js +78 -0
- package/dist/services/analytics/schema.js.map +1 -0
- package/dist/services/analytics/statements.js +99 -0
- package/dist/services/analytics/statements.js.map +1 -0
- package/dist/services/analytics.js +195 -0
- package/dist/services/analytics.js.map +1 -0
- package/dist/services/bm25.js +158 -0
- package/dist/services/bm25.js.map +1 -0
- package/dist/services/chunker.js +81 -0
- package/dist/services/chunker.js.map +1 -0
- package/dist/services/embeddings.js +58 -0
- package/dist/services/embeddings.js.map +1 -0
- package/dist/services/falkor/base.js +85 -0
- package/dist/services/falkor/base.js.map +1 -0
- package/dist/services/falkor/links.js +361 -0
- package/dist/services/falkor/links.js.map +1 -0
- package/dist/services/falkor/nodes.js +183 -0
- package/dist/services/falkor/nodes.js.map +1 -0
- package/dist/services/falkor/params.js +45 -0
- package/dist/services/falkor/params.js.map +1 -0
- package/dist/services/falkor/schema.js +77 -0
- package/dist/services/falkor/schema.js.map +1 -0
- package/dist/services/falkor.js +100 -0
- package/dist/services/falkor.js.map +1 -0
- package/dist/services/qdrant.js +238 -0
- package/dist/services/qdrant.js.map +1 -0
- package/dist/services/versions.js +73 -0
- package/dist/services/versions.js.map +1 -0
- package/dist/services/vice-bin.js +51 -0
- package/dist/services/vice-bin.js.map +1 -0
- package/dist/tools/briefings/archetype.js +174 -0
- package/dist/tools/briefings/archetype.js.map +1 -0
- package/dist/tools/briefings/budget.js +177 -0
- package/dist/tools/briefings/budget.js.map +1 -0
- package/dist/tools/briefings/build-order.js +135 -0
- package/dist/tools/briefings/build-order.js.map +1 -0
- package/dist/tools/briefings/build.js +185 -0
- package/dist/tools/briefings/build.js.map +1 -0
- package/dist/tools/briefings/discovery.js +290 -0
- package/dist/tools/briefings/discovery.js.map +1 -0
- package/dist/tools/briefings/plan-pitfalls.js +69 -0
- package/dist/tools/briefings/plan-pitfalls.js.map +1 -0
- package/dist/tools/briefings/render.js +151 -0
- package/dist/tools/briefings/render.js.map +1 -0
- package/dist/tools/briefings/rows.js +41 -0
- package/dist/tools/briefings/rows.js.map +1 -0
- package/dist/tools/briefings/toolchain.js +63 -0
- package/dist/tools/briefings/toolchain.js.map +1 -0
- package/dist/tools/briefings/why-proposed.js +69 -0
- package/dist/tools/briefings/why-proposed.js.map +1 -0
- package/dist/tools/briefings.js +44 -0
- package/dist/tools/briefings.js.map +1 -0
- package/dist/tools/feedback.js +97 -0
- package/dist/tools/feedback.js.map +1 -0
- package/dist/tools/hydrate.js +75 -0
- package/dist/tools/hydrate.js.map +1 -0
- package/dist/tools/intelligence.js +67 -0
- package/dist/tools/intelligence.js.map +1 -0
- package/dist/tools/lint/asm-decimal.js +86 -0
- package/dist/tools/lint/asm-decimal.js.map +1 -0
- package/dist/tools/lint/asm-rules.js +221 -0
- package/dist/tools/lint/asm-rules.js.map +1 -0
- package/dist/tools/lint/asm-shared.js +3 -0
- package/dist/tools/lint/asm-shared.js.map +1 -0
- package/dist/tools/lint/c-rules.js +215 -0
- package/dist/tools/lint/c-rules.js.map +1 -0
- package/dist/tools/lint/text.js +122 -0
- package/dist/tools/lint/text.js.map +1 -0
- package/dist/tools/lint/types.js +40 -0
- package/dist/tools/lint/types.js.map +1 -0
- package/dist/tools/lint.js +76 -0
- package/dist/tools/lint.js.map +1 -0
- package/dist/tools/memorization-mcp.js +111 -0
- package/dist/tools/memorization-mcp.js.map +1 -0
- package/dist/tools/pitfalls/bm25.js +53 -0
- package/dist/tools/pitfalls/bm25.js.map +1 -0
- package/dist/tools/pitfalls/format.js +58 -0
- package/dist/tools/pitfalls/format.js.map +1 -0
- package/dist/tools/pitfalls/graph.js +124 -0
- package/dist/tools/pitfalls/graph.js.map +1 -0
- package/dist/tools/pitfalls.js +134 -0
- package/dist/tools/pitfalls.js.map +1 -0
- package/dist/tools/prompts.js +81 -0
- package/dist/tools/prompts.js.map +1 -0
- package/dist/tools/query/compatibility/closure.js +62 -0
- package/dist/tools/query/compatibility/closure.js.map +1 -0
- package/dist/tools/query/compatibility/facts.js +28 -0
- package/dist/tools/query/compatibility/facts.js.map +1 -0
- package/dist/tools/query/compatibility/fetch.js +164 -0
- package/dist/tools/query/compatibility/fetch.js.map +1 -0
- package/dist/tools/query/compatibility/hard-rules.js +159 -0
- package/dist/tools/query/compatibility/hard-rules.js.map +1 -0
- package/dist/tools/query/compatibility/index.js +27 -0
- package/dist/tools/query/compatibility/index.js.map +1 -0
- package/dist/tools/query/compatibility/kernal-zp-rule.js +90 -0
- package/dist/tools/query/compatibility/kernal-zp-rule.js.map +1 -0
- package/dist/tools/query/compatibility/render.js +106 -0
- package/dist/tools/query/compatibility/render.js.map +1 -0
- package/dist/tools/query/compatibility/rules.js +337 -0
- package/dist/tools/query/compatibility/rules.js.map +1 -0
- package/dist/tools/query/compatibility/unit-rules.js +253 -0
- package/dist/tools/query/compatibility/unit-rules.js.map +1 -0
- package/dist/tools/query/game-design.js +83 -0
- package/dist/tools/query/game-design.js.map +1 -0
- package/dist/tools/query/lookups.js +220 -0
- package/dist/tools/query/lookups.js.map +1 -0
- package/dist/tools/query/plan-budget.js +266 -0
- package/dist/tools/query/plan-budget.js.map +1 -0
- package/dist/tools/query/recipes.js +214 -0
- package/dist/tools/query/recipes.js.map +1 -0
- package/dist/tools/query/retrieval.js +128 -0
- package/dist/tools/query/retrieval.js.map +1 -0
- package/dist/tools/query/shared.js +121 -0
- package/dist/tools/query/shared.js.map +1 -0
- package/dist/tools/query/techniques.js +331 -0
- package/dist/tools/query/techniques.js.map +1 -0
- package/dist/tools/query/timing.js +106 -0
- package/dist/tools/query/timing.js.map +1 -0
- package/dist/tools/query/types.js +11 -0
- package/dist/tools/query/types.js.map +1 -0
- package/dist/tools/query.js +21 -0
- package/dist/tools/query.js.map +1 -0
- package/dist/tools/resources.js +186 -0
- package/dist/tools/resources.js.map +1 -0
- package/dist/tools/run-game.js +249 -0
- package/dist/tools/run-game.js.map +1 -0
- package/dist/tools/selfimprovement.js +367 -0
- package/dist/tools/selfimprovement.js.map +1 -0
- package/docker-compose.yml +35 -0
- package/docs/ARCHITECTURE.md +164 -0
- package/docs/CONVENTIONS-archetypes.md +158 -0
- package/docs/CONVENTIONS-failures.md +46 -0
- package/docs/CONVENTIONS-game-designs.md +75 -0
- package/docs/CONVENTIONS-hardware-reference.md +184 -0
- package/docs/CONVENTIONS-pitfalls.md +91 -0
- package/docs/CONVENTIONS-recipes.md +106 -0
- package/docs/CONVENTIONS-techniques.md +309 -0
- package/docs/CONVENTIONS-toolchain-reference.md +89 -0
- package/docs/ONTOLOGY.md +655 -0
- package/docs/art/art-production-reference.md +157 -0
- package/docs/art/asset-pipelines.md +586 -0
- package/docs/c64-failure-patterns.md +693 -0
- package/docs/demo-design/demo-composition.md +349 -0
- package/docs/demo-design/demo-design-philosophy.md +320 -0
- package/docs/demo-design/intro-cracktro-patterns.md +435 -0
- package/docs/figures/ctm-spd-embed-probe.png +0 -0
- package/docs/figures/dot-flag-mid-5000000.png +0 -0
- package/docs/figures/dot-flag-no-erase-12000000.png +0 -0
- package/docs/figures/edges.json +1 -0
- package/docs/figures/fig6-architecture.png +0 -0
- package/docs/figures/fig7-ontology.png +0 -0
- package/docs/figures/screen-dissolve-mid-frame25.png +0 -0
- package/docs/figures/screen-dissolve-wipe-frame25.png +0 -0
- package/docs/figures/spindle-two-parts-part1.png +0 -0
- package/docs/figures/spindle-two-parts-part2.png +0 -0
- package/docs/figures/starters/action-puzzle.png +0 -0
- package/docs/figures/starters/adventure.png +0 -0
- package/docs/figures/starters/beat-em-up.png +0 -0
- package/docs/figures/starters/demo.png +0 -0
- package/docs/figures/starters/platformer.png +0 -0
- package/docs/figures/starters/shmup-vertical.png +0 -0
- package/docs/figures/tape-kernal-master-ntsc.png +0 -0
- package/docs/figures/tape-kernal-master-pal.png +0 -0
- package/docs/figures/tape-turbo-loader-ntsc.png +0 -0
- package/docs/figures/tape-turbo-loader-pal.png +0 -0
- package/docs/figures/tape-turbo-master-ntsc.png +0 -0
- package/docs/figures/tape-turbo-master-pal.png +0 -0
- package/docs/figures/tech-tech-pad2-12000000.png +0 -0
- package/docs/figures/tech-tech-pad4-12000000.png +0 -0
- package/docs/figures/tech-tech-seven-12000000.png +0 -0
- package/docs/figures/twister-mid-5000000.png +0 -0
- package/docs/figures/twister-straight-12000000.png +0 -0
- package/docs/figures/vector-balls-nosort-ntsc.png +0 -0
- package/docs/figures/vector-balls-nosort.png +0 -0
- package/docs/formats/c64-file-formats.md +1212 -0
- package/docs/formats/iec-disk-reference.md +754 -0
- package/docs/game-design/c64-game-archetypes.md +273 -0
- package/docs/game-design/designs/falling-blocks.md +54 -0
- package/docs/game-design/designs/platformer-scaffold.md +48 -0
- package/docs/game-design/designs/simple-shmup.md +27 -0
- package/docs/game-design/enemy-behaviour-and-difficulty.md +349 -0
- package/docs/game-design/game-design-patterns.md +1189 -0
- package/docs/game-design/game-structure.md +354 -0
- package/docs/game-design/production-planning.md +349 -0
- package/docs/game-design/reference-game-sources.md +90 -0
- package/docs/hardware/6502-illegal-opcodes.md +1353 -0
- package/docs/hardware/6510-cpu-reference.md +2112 -0
- package/docs/hardware/c64-memory-map.md +3184 -0
- package/docs/hardware/c64-registers-reference.md +996 -0
- package/docs/hardware/cia-reference.md +1281 -0
- package/docs/hardware/kernal-routines-reference.md +1836 -0
- package/docs/hardware/pal-ntsc-reference.md +813 -0
- package/docs/hardware/sid-reference.md +1137 -0
- package/docs/hardware/vic-ii-reference.md +1714 -0
- package/docs/music/music-production-reference.md +403 -0
- package/docs/pitfalls/banking.md +1067 -0
- package/docs/pitfalls/cia.md +1147 -0
- package/docs/pitfalls/cpu.md +874 -0
- package/docs/pitfalls/input.md +282 -0
- package/docs/pitfalls/kernal-and-io.md +1615 -0
- package/docs/pitfalls/loader.md +1191 -0
- package/docs/pitfalls/logic.md +94 -0
- package/docs/pitfalls/maths.md +187 -0
- package/docs/pitfalls/raster-and-badline.md +721 -0
- package/docs/pitfalls/region-timing.md +660 -0
- package/docs/pitfalls/scroll.md +207 -0
- package/docs/pitfalls/sid.md +738 -0
- package/docs/pitfalls/sprite.md +979 -0
- package/docs/pitfalls/text-mode-render.md +1172 -0
- package/docs/recipes/cc65/cartridge-8k.md +398 -0
- package/docs/recipes/cc65/hello-world-conio.md +76 -0
- package/docs/recipes/cc65/memory-layout.md +312 -0
- package/docs/recipes/cc65/screenshots/README.md +14 -0
- package/docs/recipes/cc65/screenshots/cartridge-8k-ntsc.png +0 -0
- package/docs/recipes/cc65/screenshots/cartridge-8k.png +0 -0
- package/docs/recipes/cc65/screenshots/hello-world-conio.png +0 -0
- package/docs/recipes/cc65/screenshots/memory-layout-ntsc.png +0 -0
- package/docs/recipes/cc65/screenshots/memory-layout.png +0 -0
- package/docs/recipes/kickassembler/basic-float-calls.md +578 -0
- package/docs/recipes/kickassembler/basic-wedge.md +398 -0
- package/docs/recipes/kickassembler/big-font-scroller.md +620 -0
- package/docs/recipes/kickassembler/charset-copy-rom-to-ram.md +623 -0
- package/docs/recipes/kickassembler/cia-revision-detect.md +366 -0
- package/docs/recipes/kickassembler/colour-cycling.md +343 -0
- package/docs/recipes/kickassembler/colour-fade.md +303 -0
- package/docs/recipes/kickassembler/compare-16bit-signed.md +1007 -0
- package/docs/recipes/kickassembler/cracktro-template.md +526 -0
- package/docs/recipes/kickassembler/crt-banked.md +276 -0
- package/docs/recipes/kickassembler/dos-error-codes.md +466 -0
- package/docs/recipes/kickassembler/dot-flag.md +576 -0
- package/docs/recipes/kickassembler/drive-job-queue.md +667 -0
- package/docs/recipes/kickassembler/drive-via-probe.md +690 -0
- package/docs/recipes/kickassembler/dycp-scroller.md +568 -0
- package/docs/recipes/kickassembler/dypp-sprite-scroller.md +681 -0
- package/docs/recipes/kickassembler/dysp.md +670 -0
- package/docs/recipes/kickassembler/easyflash-save.md +800 -0
- package/docs/recipes/kickassembler/eight-way-scroll.md +1484 -0
- package/docs/recipes/kickassembler/file-io-roundtrip.md +546 -0
- package/docs/recipes/kickassembler/fire-effect.md +420 -0
- package/docs/recipes/kickassembler/fld.md +414 -0
- package/docs/recipes/kickassembler/fli-image.md +336 -0
- package/docs/recipes/kickassembler/four-player-read.md +672 -0
- package/docs/recipes/kickassembler/headless-verify.md +228 -0
- package/docs/recipes/kickassembler/hello-world.md +98 -0
- package/docs/recipes/kickassembler/high-score-insert.md +660 -0
- package/docs/recipes/kickassembler/hires-plot-line.md +723 -0
- package/docs/recipes/kickassembler/irq-chain.md +316 -0
- package/docs/recipes/kickassembler/irq-owns-port.md +668 -0
- package/docs/recipes/kickassembler/isometric-room.md +1145 -0
- package/docs/recipes/kickassembler/light-pen-read.md +926 -0
- package/docs/recipes/kickassembler/logic-rate-decoupling.md +670 -0
- package/docs/recipes/kickassembler/mci-interlace.md +543 -0
- package/docs/recipes/kickassembler/memory-layout.md +277 -0
- package/docs/recipes/kickassembler/mouse-1351-read.md +801 -0
- package/docs/recipes/kickassembler/nmi-timer-tick.md +562 -0
- package/docs/recipes/kickassembler/own-keyscan.md +881 -0
- package/docs/recipes/kickassembler/paddle-read.md +751 -0
- package/docs/recipes/kickassembler/palette-cells.md +219 -0
- package/docs/recipes/kickassembler/pseudo-3d-road.md +930 -0
- package/docs/recipes/kickassembler/pwm-digi.md +463 -0
- package/docs/recipes/kickassembler/raster-bars.md +264 -0
- package/docs/recipes/kickassembler/reu-dma.md +1019 -0
- package/docs/recipes/kickassembler/runtime-relocation.md +743 -0
- package/docs/recipes/kickassembler/screen-dissolve.md +545 -0
- package/docs/recipes/kickassembler/screen-wipe.md +408 -0
- package/docs/recipes/kickassembler/screenshots/README.md +72 -0
- package/docs/recipes/kickassembler/screenshots/basic-float-calls-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/basic-float-calls.png +0 -0
- package/docs/recipes/kickassembler/screenshots/basic-wedge-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/basic-wedge.png +0 -0
- package/docs/recipes/kickassembler/screenshots/big-font-scroller-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/big-font-scroller.png +0 -0
- package/docs/recipes/kickassembler/screenshots/charset-copy-rom-to-ram-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/charset-copy-rom-to-ram.png +0 -0
- package/docs/recipes/kickassembler/screenshots/cia-revision-detect-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/cia-revision-detect.png +0 -0
- package/docs/recipes/kickassembler/screenshots/colour-cycling-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/colour-cycling.png +0 -0
- package/docs/recipes/kickassembler/screenshots/colour-fade-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/colour-fade.png +0 -0
- package/docs/recipes/kickassembler/screenshots/compare-16bit-signed-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/compare-16bit-signed.png +0 -0
- package/docs/recipes/kickassembler/screenshots/cracktro-template.png +0 -0
- package/docs/recipes/kickassembler/screenshots/crt-banked-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/crt-banked.png +0 -0
- package/docs/recipes/kickassembler/screenshots/dos-error-codes-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/dos-error-codes.png +0 -0
- package/docs/recipes/kickassembler/screenshots/dot-flag-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/dot-flag.png +0 -0
- package/docs/recipes/kickassembler/screenshots/drive-job-queue-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/drive-job-queue.png +0 -0
- package/docs/recipes/kickassembler/screenshots/drive-via-probe-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/drive-via-probe.png +0 -0
- package/docs/recipes/kickassembler/screenshots/dycp-scroller-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/dycp-scroller.png +0 -0
- package/docs/recipes/kickassembler/screenshots/dypp-sprite-scroller-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/dypp-sprite-scroller.png +0 -0
- package/docs/recipes/kickassembler/screenshots/dysp-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/dysp.png +0 -0
- package/docs/recipes/kickassembler/screenshots/easyflash-save-ntsc-run2.png +0 -0
- package/docs/recipes/kickassembler/screenshots/easyflash-save-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/easyflash-save-run2.png +0 -0
- package/docs/recipes/kickassembler/screenshots/easyflash-save.png +0 -0
- package/docs/recipes/kickassembler/screenshots/eight-way-scroll-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/eight-way-scroll.png +0 -0
- package/docs/recipes/kickassembler/screenshots/file-io-roundtrip-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/file-io-roundtrip.png +0 -0
- package/docs/recipes/kickassembler/screenshots/fire-effect-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/fire-effect.png +0 -0
- package/docs/recipes/kickassembler/screenshots/fld-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/fld.png +0 -0
- package/docs/recipes/kickassembler/screenshots/fli-image.png +0 -0
- package/docs/recipes/kickassembler/screenshots/four-player-read-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/four-player-read.png +0 -0
- package/docs/recipes/kickassembler/screenshots/headless-verify-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/headless-verify.png +0 -0
- package/docs/recipes/kickassembler/screenshots/hello-world.png +0 -0
- package/docs/recipes/kickassembler/screenshots/high-score-insert-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/high-score-insert.png +0 -0
- package/docs/recipes/kickassembler/screenshots/hires-plot-line-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/hires-plot-line.png +0 -0
- package/docs/recipes/kickassembler/screenshots/irq-chain-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/irq-chain.png +0 -0
- package/docs/recipes/kickassembler/screenshots/irq-owns-port-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/irq-owns-port.png +0 -0
- package/docs/recipes/kickassembler/screenshots/isometric-room-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/isometric-room.png +0 -0
- package/docs/recipes/kickassembler/screenshots/light-pen-read-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/light-pen-read.png +0 -0
- package/docs/recipes/kickassembler/screenshots/logic-rate-decoupling-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/logic-rate-decoupling.png +0 -0
- package/docs/recipes/kickassembler/screenshots/mci-interlace-average.png +0 -0
- package/docs/recipes/kickassembler/screenshots/mci-interlace-frame-b.png +0 -0
- package/docs/recipes/kickassembler/screenshots/mci-interlace-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/mci-interlace.png +0 -0
- package/docs/recipes/kickassembler/screenshots/memory-layout-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/memory-layout.png +0 -0
- package/docs/recipes/kickassembler/screenshots/mouse-1351-read-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/mouse-1351-read.png +0 -0
- package/docs/recipes/kickassembler/screenshots/nmi-timer-tick-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/nmi-timer-tick.png +0 -0
- package/docs/recipes/kickassembler/screenshots/own-keyscan-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/own-keyscan.png +0 -0
- package/docs/recipes/kickassembler/screenshots/paddle-read-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/paddle-read.png +0 -0
- package/docs/recipes/kickassembler/screenshots/palette-cells-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/palette-cells.png +0 -0
- package/docs/recipes/kickassembler/screenshots/pseudo-3d-road-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/pseudo-3d-road.png +0 -0
- package/docs/recipes/kickassembler/screenshots/pwm-digi-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/pwm-digi.png +0 -0
- package/docs/recipes/kickassembler/screenshots/raster-bars.png +0 -0
- package/docs/recipes/kickassembler/screenshots/reu-dma-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/reu-dma.png +0 -0
- package/docs/recipes/kickassembler/screenshots/runtime-relocation-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/runtime-relocation.png +0 -0
- package/docs/recipes/kickassembler/screenshots/screen-dissolve-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/screen-dissolve.png +0 -0
- package/docs/recipes/kickassembler/screenshots/screen-wipe-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/screen-wipe.png +0 -0
- package/docs/recipes/kickassembler/screenshots/scroll-panel-split-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/scroll-panel-split.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sfx-in-player-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sfx-in-player.png +0 -0
- package/docs/recipes/kickassembler/screenshots/shadebobs-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/shadebobs.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sideborder-open.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sine-scroller.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sine-table-runtime-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sine-table-runtime.png +0 -0
- package/docs/recipes/kickassembler/screenshots/software-sprite-preshifted-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/software-sprite-preshifted.png +0 -0
- package/docs/recipes/kickassembler/screenshots/speedcode-generator-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/speedcode-generator.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprite-border-scroller-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprite-border-scroller.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprite-cache-flip-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprite-cache-flip.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprite-multiplex-24.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprite-multiplex-game-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprite-multiplex-game.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprite-priority-classes-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprite-priority-classes.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprite-sine-chain-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprite-sine-chain.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprite-stretcher-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprite-stretcher.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprites-only-screen-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sprites-only-screen.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sqrt-atan2-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/sqrt-atan2.png +0 -0
- package/docs/recipes/kickassembler/screenshots/stable-raster-irq-control.png +0 -0
- package/docs/recipes/kickassembler/screenshots/stable-raster-irq.png +0 -0
- package/docs/recipes/kickassembler/screenshots/tech-tech-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/tech-tech.png +0 -0
- package/docs/recipes/kickassembler/screenshots/tod-alarm-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/tod-alarm.png +0 -0
- package/docs/recipes/kickassembler/screenshots/topbottom-border-open.png +0 -0
- package/docs/recipes/kickassembler/screenshots/twister-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/twister.png +0 -0
- package/docs/recipes/kickassembler/screenshots/unit-test-driver-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/unit-test-driver.png +0 -0
- package/docs/recipes/kickassembler/screenshots/vector-balls-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/vector-balls.png +0 -0
- package/docs/recipes/kickassembler/screenshots/wireframe-ships-ntsc.png +0 -0
- package/docs/recipes/kickassembler/screenshots/wireframe-ships.png +0 -0
- package/docs/recipes/kickassembler/scroll-panel-split.md +381 -0
- package/docs/recipes/kickassembler/sfx-in-player.md +907 -0
- package/docs/recipes/kickassembler/shadebobs.md +522 -0
- package/docs/recipes/kickassembler/sideborder-open.md +338 -0
- package/docs/recipes/kickassembler/sine-scroller.md +321 -0
- package/docs/recipes/kickassembler/sine-table-runtime.md +815 -0
- package/docs/recipes/kickassembler/software-sprite-preshifted.md +725 -0
- package/docs/recipes/kickassembler/speedcode-generator.md +606 -0
- package/docs/recipes/kickassembler/sprite-border-scroller.md +696 -0
- package/docs/recipes/kickassembler/sprite-cache-flip.md +1071 -0
- package/docs/recipes/kickassembler/sprite-multiplex-24.md +326 -0
- package/docs/recipes/kickassembler/sprite-multiplex-game.md +986 -0
- package/docs/recipes/kickassembler/sprite-priority-classes.md +463 -0
- package/docs/recipes/kickassembler/sprite-sine-chain.md +338 -0
- package/docs/recipes/kickassembler/sprite-stretcher.md +501 -0
- package/docs/recipes/kickassembler/sprites-only-screen.md +550 -0
- package/docs/recipes/kickassembler/sqrt-atan2.md +1014 -0
- package/docs/recipes/kickassembler/stable-raster-irq.md +360 -0
- package/docs/recipes/kickassembler/tape-turbo-loader.md +875 -0
- package/docs/recipes/kickassembler/tech-tech.md +679 -0
- package/docs/recipes/kickassembler/tod-alarm.md +754 -0
- package/docs/recipes/kickassembler/topbottom-border-open.md +407 -0
- package/docs/recipes/kickassembler/twister.md +518 -0
- package/docs/recipes/kickassembler/unit-test-driver.md +314 -0
- package/docs/recipes/kickassembler/vector-balls.md +545 -0
- package/docs/recipes/kickassembler/wireframe-ships.md +2324 -0
- package/docs/recipes/oscar64/actor-activation-window.md +807 -0
- package/docs/recipes/oscar64/adventure-engine.md +1094 -0
- package/docs/recipes/oscar64/attract-replay.md +465 -0
- package/docs/recipes/oscar64/beat-em-up-lanes.md +563 -0
- package/docs/recipes/oscar64/bfs-distance-map.md +723 -0
- package/docs/recipes/oscar64/bitmap-koala-viewer.md +355 -0
- package/docs/recipes/oscar64/car-contact.md +950 -0
- package/docs/recipes/oscar64/cave-scan.md +609 -0
- package/docs/recipes/oscar64/char-bullets.md +477 -0
- package/docs/recipes/oscar64/charset-animation.md +383 -0
- package/docs/recipes/oscar64/charset-parallax.md +401 -0
- package/docs/recipes/oscar64/destructible-terrain.md +1099 -0
- package/docs/recipes/oscar64/difficulty-tables.md +654 -0
- package/docs/recipes/oscar64/dig-and-guards.md +975 -0
- package/docs/recipes/oscar64/directory-reader.md +535 -0
- package/docs/recipes/oscar64/divide-check.md +581 -0
- package/docs/recipes/oscar64/double-buffer-nomirror.md +205 -0
- package/docs/recipes/oscar64/double-buffer.md +255 -0
- package/docs/recipes/oscar64/falling-blocks.md +874 -0
- package/docs/recipes/oscar64/fixed-point-jump-velocity.md +289 -0
- package/docs/recipes/oscar64/fixed-point-jump.md +210 -0
- package/docs/recipes/oscar64/flip-screen-rooms.md +793 -0
- package/docs/recipes/oscar64/frame-sync-loop-overrun.md +214 -0
- package/docs/recipes/oscar64/frame-sync-loop.md +254 -0
- package/docs/recipes/oscar64/game-tree-search.md +743 -0
- package/docs/recipes/oscar64/ghost-targeting.md +867 -0
- package/docs/recipes/oscar64/headless-verify.md +345 -0
- package/docs/recipes/oscar64/hello-world.md +83 -0
- package/docs/recipes/oscar64/high-score-persist.md +772 -0
- package/docs/recipes/oscar64/iffl-kernal-skip.md +421 -0
- package/docs/recipes/oscar64/joystick-input.md +358 -0
- package/docs/recipes/oscar64/lane-pursuit.md +1028 -0
- package/docs/recipes/oscar64/level-rle-decoder.md +620 -0
- package/docs/recipes/oscar64/lfsr-random-seed2.md +338 -0
- package/docs/recipes/oscar64/lfsr-random.md +403 -0
- package/docs/recipes/oscar64/load-asset-runtime.md +492 -0
- package/docs/recipes/oscar64/memory-layout.md +232 -0
- package/docs/recipes/oscar64/mixed-fighters.md +781 -0
- package/docs/recipes/oscar64/multi-sprite-object.md +533 -0
- package/docs/recipes/oscar64/nav-area-pathfinding.md +881 -0
- package/docs/recipes/oscar64/object-pool.md +572 -0
- package/docs/recipes/oscar64/pal-ntsc-detect.md +257 -0
- package/docs/recipes/oscar64/password-state.md +532 -0
- package/docs/recipes/oscar64/per-frame-hitbox.md +651 -0
- package/docs/recipes/oscar64/petscii-screen-codes.md +428 -0
- package/docs/recipes/oscar64/platformer-scaffold.md +1467 -0
- package/docs/recipes/oscar64/print-number.md +367 -0
- package/docs/recipes/oscar64/raster-bars.md +239 -0
- package/docs/recipes/oscar64/raster-profile-bars.md +404 -0
- package/docs/recipes/oscar64/rel-side-sectors.md +391 -0
- package/docs/recipes/oscar64/relative-file-records.md +378 -0
- package/docs/recipes/oscar64/save-load-seq-file.md +365 -0
- package/docs/recipes/oscar64/screenshots/README.md +99 -0
- package/docs/recipes/oscar64/screenshots/actor-activation-window-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/actor-activation-window.png +0 -0
- package/docs/recipes/oscar64/screenshots/adventure-engine-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/adventure-engine.png +0 -0
- package/docs/recipes/oscar64/screenshots/attract-replay-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/attract-replay.png +0 -0
- package/docs/recipes/oscar64/screenshots/beat-em-up-lanes-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/beat-em-up-lanes.png +0 -0
- package/docs/recipes/oscar64/screenshots/bfs-distance-map-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/bfs-distance-map.png +0 -0
- package/docs/recipes/oscar64/screenshots/bitmap-koala-viewer.png +0 -0
- package/docs/recipes/oscar64/screenshots/car-contact-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/car-contact.png +0 -0
- package/docs/recipes/oscar64/screenshots/cave-scan-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/cave-scan.png +0 -0
- package/docs/recipes/oscar64/screenshots/char-bullets-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/char-bullets.png +0 -0
- package/docs/recipes/oscar64/screenshots/charset-animation-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/charset-animation.png +0 -0
- package/docs/recipes/oscar64/screenshots/charset-parallax-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/charset-parallax.png +0 -0
- package/docs/recipes/oscar64/screenshots/destructible-terrain-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/destructible-terrain.png +0 -0
- package/docs/recipes/oscar64/screenshots/difficulty-tables-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/difficulty-tables.png +0 -0
- package/docs/recipes/oscar64/screenshots/dig-and-guards-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/dig-and-guards.png +0 -0
- package/docs/recipes/oscar64/screenshots/directory-reader-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/directory-reader.png +0 -0
- package/docs/recipes/oscar64/screenshots/divide-check-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/divide-check.png +0 -0
- package/docs/recipes/oscar64/screenshots/double-buffer-nomirror.png +0 -0
- package/docs/recipes/oscar64/screenshots/double-buffer-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/double-buffer.png +0 -0
- package/docs/recipes/oscar64/screenshots/falling-blocks-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/falling-blocks.png +0 -0
- package/docs/recipes/oscar64/screenshots/fixed-point-jump-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/fixed-point-jump-velocity-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/fixed-point-jump-velocity.png +0 -0
- package/docs/recipes/oscar64/screenshots/fixed-point-jump.png +0 -0
- package/docs/recipes/oscar64/screenshots/flip-screen-rooms-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/flip-screen-rooms.png +0 -0
- package/docs/recipes/oscar64/screenshots/frame-sync-loop-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/frame-sync-loop-overrun.png +0 -0
- package/docs/recipes/oscar64/screenshots/frame-sync-loop.png +0 -0
- package/docs/recipes/oscar64/screenshots/game-tree-search-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/game-tree-search.png +0 -0
- package/docs/recipes/oscar64/screenshots/ghost-targeting-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/ghost-targeting.png +0 -0
- package/docs/recipes/oscar64/screenshots/headless-verify-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/headless-verify.png +0 -0
- package/docs/recipes/oscar64/screenshots/hello-world.png +0 -0
- package/docs/recipes/oscar64/screenshots/high-score-persist-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/high-score-persist.png +0 -0
- package/docs/recipes/oscar64/screenshots/iffl-kernal-skip-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/iffl-kernal-skip.png +0 -0
- package/docs/recipes/oscar64/screenshots/joystick-input-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/joystick-input.png +0 -0
- package/docs/recipes/oscar64/screenshots/lane-pursuit-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/lane-pursuit.png +0 -0
- package/docs/recipes/oscar64/screenshots/level-rle-decoder-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/level-rle-decoder.png +0 -0
- package/docs/recipes/oscar64/screenshots/lfsr-random-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/lfsr-random-seed2.png +0 -0
- package/docs/recipes/oscar64/screenshots/lfsr-random.png +0 -0
- package/docs/recipes/oscar64/screenshots/load-asset-runtime-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/load-asset-runtime.png +0 -0
- package/docs/recipes/oscar64/screenshots/memory-layout-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/memory-layout.png +0 -0
- package/docs/recipes/oscar64/screenshots/mixed-fighters-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/mixed-fighters.png +0 -0
- package/docs/recipes/oscar64/screenshots/multi-sprite-object-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/multi-sprite-object.png +0 -0
- package/docs/recipes/oscar64/screenshots/nav-area-pathfinding-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/nav-area-pathfinding.png +0 -0
- package/docs/recipes/oscar64/screenshots/object-pool-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/object-pool.png +0 -0
- package/docs/recipes/oscar64/screenshots/pal-ntsc-detect-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/pal-ntsc-detect-oldntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/pal-ntsc-detect-pal.png +0 -0
- package/docs/recipes/oscar64/screenshots/password-state-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/password-state.png +0 -0
- package/docs/recipes/oscar64/screenshots/per-frame-hitbox-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/per-frame-hitbox.png +0 -0
- package/docs/recipes/oscar64/screenshots/petscii-screen-codes-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/petscii-screen-codes.png +0 -0
- package/docs/recipes/oscar64/screenshots/platformer-scaffold-d64-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/platformer-scaffold-d64.png +0 -0
- package/docs/recipes/oscar64/screenshots/platformer-scaffold-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/platformer-scaffold.png +0 -0
- package/docs/recipes/oscar64/screenshots/print-number-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/print-number.png +0 -0
- package/docs/recipes/oscar64/screenshots/raster-bars.png +0 -0
- package/docs/recipes/oscar64/screenshots/raster-profile-bars-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/raster-profile-bars.png +0 -0
- package/docs/recipes/oscar64/screenshots/rel-side-sectors-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/rel-side-sectors.png +0 -0
- package/docs/recipes/oscar64/screenshots/relative-file-records-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/relative-file-records.png +0 -0
- package/docs/recipes/oscar64/screenshots/save-load-seq-file-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/save-load-seq-file.png +0 -0
- package/docs/recipes/oscar64/screenshots/seeded-level-fill-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/seeded-level-fill.png +0 -0
- package/docs/recipes/oscar64/screenshots/sfx-engine-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/sfx-engine.png +0 -0
- package/docs/recipes/oscar64/screenshots/sid-music-player.png +0 -0
- package/docs/recipes/oscar64/screenshots/simple-shmup.png +0 -0
- package/docs/recipes/oscar64/screenshots/slope-collision-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/slope-collision.png +0 -0
- package/docs/recipes/oscar64/screenshots/soft-scroll-h.png +0 -0
- package/docs/recipes/oscar64/screenshots/sprite-animation-table-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/sprite-animation-table.png +0 -0
- package/docs/recipes/oscar64/screenshots/sprite-multiplex-8.png +0 -0
- package/docs/recipes/oscar64/screenshots/stable-raster-irq.png +0 -0
- package/docs/recipes/oscar64/screenshots/text-input-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/text-input.png +0 -0
- package/docs/recipes/oscar64/screenshots/text-overlay-playfield-gameover.png +0 -0
- package/docs/recipes/oscar64/screenshots/text-overlay-playfield-notrail-control.png +0 -0
- package/docs/recipes/oscar64/screenshots/text-overlay-playfield-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/text-overlay-playfield-ungated-control.png +0 -0
- package/docs/recipes/oscar64/screenshots/text-overlay-playfield.png +0 -0
- package/docs/recipes/oscar64/screenshots/text-window-menu-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/text-window-menu.png +0 -0
- package/docs/recipes/oscar64/screenshots/tile-grid-collision-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/tile-grid-collision.png +0 -0
- package/docs/recipes/oscar64/screenshots/tile-map-render-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/tile-map-render.png +0 -0
- package/docs/recipes/oscar64/screenshots/two-player-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/two-player.png +0 -0
- package/docs/recipes/oscar64/screenshots/two-word-parser-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/two-word-parser.png +0 -0
- package/docs/recipes/oscar64/screenshots/vehicle-control-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/vehicle-control.png +0 -0
- package/docs/recipes/oscar64/screenshots/wave-director-ntsc.png +0 -0
- package/docs/recipes/oscar64/screenshots/wave-director.png +0 -0
- package/docs/recipes/oscar64/seeded-level-fill.md +364 -0
- package/docs/recipes/oscar64/sfx-engine.md +514 -0
- package/docs/recipes/oscar64/sid-music-player.md +441 -0
- package/docs/recipes/oscar64/simple-shmup.md +1000 -0
- package/docs/recipes/oscar64/slope-collision.md +610 -0
- package/docs/recipes/oscar64/soft-scroll-h.md +261 -0
- package/docs/recipes/oscar64/sprite-animation-table.md +671 -0
- package/docs/recipes/oscar64/sprite-multiplex-8.md +314 -0
- package/docs/recipes/oscar64/stable-raster-irq.md +236 -0
- package/docs/recipes/oscar64/text-input.md +274 -0
- package/docs/recipes/oscar64/text-overlay-playfield.md +617 -0
- package/docs/recipes/oscar64/text-window-menu.md +551 -0
- package/docs/recipes/oscar64/tile-grid-collision.md +563 -0
- package/docs/recipes/oscar64/tile-map-render.md +395 -0
- package/docs/recipes/oscar64/two-player.md +528 -0
- package/docs/recipes/oscar64/two-word-parser.md +510 -0
- package/docs/recipes/oscar64/vehicle-control.md +771 -0
- package/docs/recipes/oscar64/wave-director.md +787 -0
- package/docs/recipes/runs.json +1606 -0
- package/docs/runtime/sim6502-reference.md +466 -0
- package/docs/runtime/vice-mcp-reference.md +351 -0
- package/docs/runtime/vice-reference.md +1393 -0
- package/docs/techniques/bitmap-modes.md +685 -0
- package/docs/techniques/cpu-cycle-tricks.md +1570 -0
- package/docs/techniques/effects-vector-3d.md +1431 -0
- package/docs/techniques/file-io.md +1427 -0
- package/docs/techniques/input.md +1072 -0
- package/docs/techniques/loaders-packers.md +768 -0
- package/docs/techniques/logic.md +3091 -0
- package/docs/techniques/maths.md +1524 -0
- package/docs/techniques/memory-banking.md +1686 -0
- package/docs/techniques/music-sid.md +1144 -0
- package/docs/techniques/raster.md +1543 -0
- package/docs/techniques/scroll.md +1292 -0
- package/docs/techniques/sprite.md +2566 -0
- package/docs/techniques/text-mode-render.md +717 -0
- package/docs/techniques/text.md +1095 -0
- package/docs/techniques/transitions.md +405 -0
- package/docs/toolchains/cartconv-reference.md +262 -0
- package/docs/toolchains/cc1541-reference.md +316 -0
- package/docs/toolchains/cc65-reference.md +667 -0
- package/docs/toolchains/error-sources/cc65/README.md +1 -0
- package/docs/toolchains/error-sources/cc65/missing-setcpu.s +4 -0
- package/docs/toolchains/error-sources/cc65/range-error.s +4 -0
- package/docs/toolchains/error-sources/cc65/segment-overflow.c +6 -0
- package/docs/toolchains/error-sources/cc65/unresolved-external.c +6 -0
- package/docs/toolchains/error-sources/kickassembler/README.md +1 -0
- package/docs/toolchains/error-sources/kickassembler/branch-out-of-range.asm +4 -0
- package/docs/toolchains/error-sources/kickassembler/failed-assert.asm +3 -0
- package/docs/toolchains/error-sources/kickassembler/flat-lib.asm +4 -0
- package/docs/toolchains/error-sources/kickassembler/import-source-no-namespace.asm +5 -0
- package/docs/toolchains/error-sources/kickassembler/macro-without-parentheses.asm +6 -0
- package/docs/toolchains/error-sources/kickassembler/memory-block-overlap.asm +4 -0
- package/docs/toolchains/error-sources/kickassembler/symbol-clash-lib.asm +5 -0
- package/docs/toolchains/error-sources/kickassembler/symbol-clash-main.asm +6 -0
- package/docs/toolchains/error-sources/kickassembler/unknown-symbol.asm +4 -0
- package/docs/toolchains/error-sources/oscar64/README.md +1 -0
- package/docs/toolchains/error-sources/oscar64/asm-addressing-mode.c +7 -0
- package/docs/toolchains/error-sources/oscar64/region-overflow.c +6 -0
- package/docs/toolchains/error-sources/oscar64/undefined-extern-var.c +5 -0
- package/docs/toolchains/error-sources/oscar64/undefined-extern.c +6 -0
- package/docs/toolchains/error-sources/oscar64/unknown-vic-field.c +6 -0
- package/docs/toolchains/error-sources/oscar64/void-main.c +3 -0
- package/docs/toolchains/kickassembler-reference.md +1020 -0
- package/docs/toolchains/memory-layout-planning.md +362 -0
- package/docs/toolchains/oscar64-headers-reference.md +583 -0
- package/docs/toolchains/oscar64-reference.md +892 -0
- package/docs/toolchains/petcat-reference.md +290 -0
- package/docs/toolchains/png2prg.md +188 -0
- package/docs/toolchains/release-disk.md +314 -0
- package/docs/toolchains/sidreloc.md +236 -0
- package/docs/toolchains/spindle-reference.md +487 -0
- package/docs/toolchains/tape-mastering.md +592 -0
- package/docs/toolchains/unit-testing-6502.md +281 -0
- package/docs/workflow/agent-harness.md +478 -0
- package/package.json +105 -0
- package/templates/_harness/CLAUDE.md.template +91 -0
- package/templates/_harness/PLAN.md.template +72 -0
- package/templates/_harness/check.py +442 -0
- package/templates/_harness/claude/settings.json +17 -0
- package/templates/_harness/gen-asm-header.py +96 -0
- package/templates/_harness/harness.mk +273 -0
- package/templates/_harness/hooks/plan-gate.py +185 -0
- package/templates/_harness/meter/frame_meter.asm +311 -0
- package/templates/_harness/meter/frame_meter.c +155 -0
- package/templates/_harness/meter/frame_meter.h +80 -0
- package/templates/_harness/zp-used.py +104 -0
- package/templates/action-puzzle/.claude/settings.json +17 -0
- package/templates/action-puzzle/.mcp.json +10 -0
- package/templates/action-puzzle/CLAUDE.md +120 -0
- package/templates/action-puzzle/Makefile +125 -0
- package/templates/action-puzzle/PLAN.md +388 -0
- package/templates/action-puzzle/README.md +240 -0
- package/templates/action-puzzle/expect.json +25 -0
- package/templates/action-puzzle/src/cave.c +280 -0
- package/templates/action-puzzle/src/cave.h +80 -0
- package/templates/action-puzzle/src/gen_autopilot.h +20 -0
- package/templates/action-puzzle/src/gen_caves.h +59 -0
- package/templates/action-puzzle/src/gen_notes.h +16 -0
- package/templates/action-puzzle/src/hiscore.c +152 -0
- package/templates/action-puzzle/src/hiscore.h +41 -0
- package/templates/action-puzzle/src/level.c +64 -0
- package/templates/action-puzzle/src/level.h +24 -0
- package/templates/action-puzzle/src/main.c +599 -0
- package/templates/action-puzzle/src/render.c +239 -0
- package/templates/action-puzzle/src/render.h +31 -0
- package/templates/action-puzzle/src/sound.c +158 -0
- package/templates/action-puzzle/src/sound.h +22 -0
- package/templates/action-puzzle/tools/disk_check.py +77 -0
- package/templates/action-puzzle/tools/drive.py +106 -0
- package/templates/action-puzzle/tools/gen.py +454 -0
- package/templates/action-puzzle/tools/model_check.py +49 -0
- package/templates/action-puzzle/tools/verdict_codes.py +24 -0
- package/templates/adventure/.claude/settings.json +17 -0
- package/templates/adventure/.mcp.json +10 -0
- package/templates/adventure/CLAUDE.md +118 -0
- package/templates/adventure/Makefile +113 -0
- package/templates/adventure/PLAN.md +396 -0
- package/templates/adventure/README.md +277 -0
- package/templates/adventure/expect.json +25 -0
- package/templates/adventure/src/engine.c +543 -0
- package/templates/adventure/src/engine.h +48 -0
- package/templates/adventure/src/gen_script.h +141 -0
- package/templates/adventure/src/gen_world.h +511 -0
- package/templates/adventure/src/main.c +439 -0
- package/templates/adventure/src/picture.c +83 -0
- package/templates/adventure/src/picture.h +16 -0
- package/templates/adventure/src/save.c +159 -0
- package/templates/adventure/src/save.h +25 -0
- package/templates/adventure/src/sound.c +71 -0
- package/templates/adventure/src/sound.h +16 -0
- package/templates/adventure/src/text.c +349 -0
- package/templates/adventure/src/text.h +40 -0
- package/templates/adventure/tools/disk_check.py +128 -0
- package/templates/adventure/tools/drive.py +119 -0
- package/templates/adventure/tools/gen.py +1033 -0
- package/templates/adventure/tools/world.py +356 -0
- package/templates/beat-em-up/.claude/settings.json +17 -0
- package/templates/beat-em-up/.mcp.json +10 -0
- package/templates/beat-em-up/CLAUDE.md +136 -0
- package/templates/beat-em-up/Makefile +96 -0
- package/templates/beat-em-up/PLAN.md +502 -0
- package/templates/beat-em-up/README.md +315 -0
- package/templates/beat-em-up/expect-gameover.json +8 -0
- package/templates/beat-em-up/expect.json +29 -0
- package/templates/beat-em-up/src/anim.c +70 -0
- package/templates/beat-em-up/src/art.c +344 -0
- package/templates/beat-em-up/src/autopilot.h +154 -0
- package/templates/beat-em-up/src/brute.c +273 -0
- package/templates/beat-em-up/src/enemy.c +281 -0
- package/templates/beat-em-up/src/engine.asm +512 -0
- package/templates/beat-em-up/src/fighter.c +384 -0
- package/templates/beat-em-up/src/game.h +269 -0
- package/templates/beat-em-up/src/hud.c +135 -0
- package/templates/beat-em-up/src/main.c +341 -0
- package/templates/beat-em-up/src/sound.c +44 -0
- package/templates/beat-em-up/src/street.c +93 -0
- package/templates/beat-em-up/src/verdict.h +523 -0
- package/templates/beat-em-up/src/view.c +362 -0
- package/templates/beat-em-up/tools/drive.py +105 -0
- package/templates/beat-em-up/tools/flickercheck.py +273 -0
- package/templates/demo/.claude/settings.json +17 -0
- package/templates/demo/.mcp.json +10 -0
- package/templates/demo/CLAUDE.md +129 -0
- package/templates/demo/Makefile +66 -0
- package/templates/demo/PLAN.md +401 -0
- package/templates/demo/README.md +322 -0
- package/templates/demo/expect.json +945 -0
- package/templates/demo/src/config.asm +80 -0
- package/templates/demo/src/framework.asm +390 -0
- package/templates/demo/src/main.asm +139 -0
- package/templates/demo/src/music.asm +197 -0
- package/templates/demo/src/part_main.asm +279 -0
- package/templates/demo/src/part_title.asm +79 -0
- package/templates/demo/src/parts.asm +43 -0
- package/templates/demo/src/tables.asm +73 -0
- package/templates/demo/src/verdict.asm +179 -0
- package/templates/demo/tools/audio.py +80 -0
- package/templates/demo/tools/gen_expect.py +167 -0
- package/templates/demo/tools/probe.py +95 -0
- package/templates/hello/.claude/settings.json +17 -0
- package/templates/hello/.mcp.json +10 -0
- package/templates/hello/CLAUDE.md +100 -0
- package/templates/hello/Makefile +38 -0
- package/templates/hello/PLAN.md +200 -0
- package/templates/hello/expect.json +20 -0
- package/templates/hello/src/main.c +161 -0
- package/templates/hello/src/sprite.asm +27 -0
- package/templates/hello-kick/.claude/settings.json +17 -0
- package/templates/hello-kick/.mcp.json +10 -0
- package/templates/hello-kick/CLAUDE.md +100 -0
- package/templates/hello-kick/Makefile +17 -0
- package/templates/hello-kick/PLAN.md +199 -0
- package/templates/hello-kick/expect.json +20 -0
- package/templates/hello-kick/src/main.asm +233 -0
- package/templates/platformer/.claude/settings.json +17 -0
- package/templates/platformer/.mcp.json +10 -0
- package/templates/platformer/CLAUDE.md +125 -0
- package/templates/platformer/Makefile +86 -0
- package/templates/platformer/PLAN.md +360 -0
- package/templates/platformer/README.md +277 -0
- package/templates/platformer/expect.json +32 -0
- package/templates/platformer/src/actors.c +289 -0
- package/templates/platformer/src/anim.c +51 -0
- package/templates/platformer/src/art.c +212 -0
- package/templates/platformer/src/autopilot.h +30 -0
- package/templates/platformer/src/engine.asm +253 -0
- package/templates/platformer/src/game.h +248 -0
- package/templates/platformer/src/hud.c +90 -0
- package/templates/platformer/src/level.c +184 -0
- package/templates/platformer/src/main.c +306 -0
- package/templates/platformer/src/player.c +268 -0
- package/templates/platformer/src/sound.c +42 -0
- package/templates/platformer/src/verdict.h +206 -0
- package/templates/platformer/src/view.c +331 -0
- package/templates/platformer/stage-expect.json +6 -0
- package/templates/platformer/tools/drive.py +112 -0
- package/templates/platformer/tools/tearcheck.py +229 -0
- package/templates/shmup-vertical/.claude/settings.json +17 -0
- package/templates/shmup-vertical/.mcp.json +10 -0
- package/templates/shmup-vertical/CLAUDE.md +137 -0
- package/templates/shmup-vertical/Makefile +131 -0
- package/templates/shmup-vertical/PLAN.md +569 -0
- package/templates/shmup-vertical/README.md +189 -0
- package/templates/shmup-vertical/expect.json +38 -0
- package/templates/shmup-vertical/src/bullets.c +158 -0
- package/templates/shmup-vertical/src/bullets.h +35 -0
- package/templates/shmup-vertical/src/display.c +239 -0
- package/templates/shmup-vertical/src/display.h +40 -0
- package/templates/shmup-vertical/src/game.h +85 -0
- package/templates/shmup-vertical/src/glyph.asm +358 -0
- package/templates/shmup-vertical/src/hiscore.c +141 -0
- package/templates/shmup-vertical/src/hiscore.h +16 -0
- package/templates/shmup-vertical/src/hit.asm +180 -0
- package/templates/shmup-vertical/src/hitbox.c +59 -0
- package/templates/shmup-vertical/src/hitbox.h +25 -0
- package/templates/shmup-vertical/src/kernel.asm +330 -0
- package/templates/shmup-vertical/src/level.c +174 -0
- package/templates/shmup-vertical/src/level.h +27 -0
- package/templates/shmup-vertical/src/main.c +824 -0
- package/templates/shmup-vertical/src/mux.asm +321 -0
- package/templates/shmup-vertical/src/sound.asm +255 -0
- package/templates/shmup-vertical/src/step.asm +169 -0
- package/templates/shmup-vertical/src/waves.c +195 -0
- package/templates/shmup-vertical/src/waves.h +33 -0
- package/templates/shmup-vertical/stage-expect.json +6 -0
- package/templates/shmup-vertical/tools/drive.py +152 -0
- package/templates/shmup-vertical/tools/joytest.py +159 -0
- package/templates/shmup-vertical/tools/meter.py +15 -0
- package/templates/shmup-vertical/tools/phases.py +73 -0
|
@@ -0,0 +1,3091 @@
|
|
|
1
|
+
---
|
|
2
|
+
category: logic
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<!-- doc-type: technique-reference -->
|
|
6
|
+
|
|
7
|
+
# Logic Techniques
|
|
8
|
+
|
|
9
|
+
Game rules that run on the CPU against data the renderer already holds:
|
|
10
|
+
collision against a tile map, and the state that follows from it. Nothing
|
|
11
|
+
here touches a VIC-II collision register; the hardware ones are
|
|
12
|
+
`sprite_collision_detect` in `sprite.md`. Every number below that came
|
|
13
|
+
from an instrument says so; the rest is marked as arithmetic or as not
|
|
14
|
+
measured here.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## tile_grid_collision — Tile-grid collision against a decoded map
|
|
19
|
+
|
|
20
|
+
**Complexity:** medium
|
|
21
|
+
**Region:** both
|
|
22
|
+
**Requires:** tile_map_render
|
|
23
|
+
**Cost:** cycles_per_frame=2345
|
|
24
|
+
**Cost basis:** measured-vice
|
|
25
|
+
**Cost measured on:** oscar64-tile-grid-collision (worst frame, one actor, in the vertical blank)
|
|
26
|
+
|
|
27
|
+
### Why
|
|
28
|
+
|
|
29
|
+
A platformer's floors, walls, ceilings and ladders are characters on the
|
|
30
|
+
text screen, and the player is a sprite. The VIC-II can report that a
|
|
31
|
+
sprite overlaps foreground pixels (`$D01F`), but not which cell, from
|
|
32
|
+
which side, or whether the cell is a floor to stand on or a ladder to
|
|
33
|
+
climb. The game has to answer that itself, from the map it decoded at
|
|
34
|
+
level start, and it has to answer it every frame for every moving actor
|
|
35
|
+
within the frame budget.
|
|
36
|
+
|
|
37
|
+
### How
|
|
38
|
+
|
|
39
|
+
The map is one byte per tile in RAM, indexed `map[row * MAP_W + col]`:
|
|
40
|
+
the array `tile_map_render` fills at level start (its step 2), or the
|
|
41
|
+
same array decoded from any other source. The test reads that array,
|
|
42
|
+
never screen RAM, so the map can be wider than the screen and the HUD
|
|
43
|
+
can overwrite cells without becoming walls.
|
|
44
|
+
|
|
45
|
+
1. **Body, not sprite.** Choose the box inside the 24 x 21 sprite that
|
|
46
|
+
is the actor: the recipe uses columns 8 to 15 and rows 0 to 20. Every
|
|
47
|
+
test is on points of that box.
|
|
48
|
+
2. **Pixel to tile.** A body point at screen pixel `(x, y)` is in tile
|
|
49
|
+
column `(x - 24) >> 3` and screen row `(y - 50) >> 3`, because sprite
|
|
50
|
+
coordinate 24, 50 is the top-left visible text pixel (settled in
|
|
51
|
+
`hardware/vic-ii-reference.md`; the recipe keeps world coordinates
|
|
52
|
+
with the 24 and 50 already removed). Subtract the map's first screen
|
|
53
|
+
row to index the array. Outside the map, answer empty above and solid
|
|
54
|
+
to the sides and below, so nothing can leave.
|
|
55
|
+
3. **Probe spacing.** Test points along an edge no further apart than a
|
|
56
|
+
tile. A 21-pixel edge crosses up to four tile rows, so the side edge
|
|
57
|
+
is probed at 0, 8, 16 and 20 pixels from the head; two corners alone
|
|
58
|
+
miss a single tile between them. The 8-pixel-wide body's top and
|
|
59
|
+
bottom edges need only their two corners.
|
|
60
|
+
4. **Horizontal first.** Move one step sideways, probe the leading edge
|
|
61
|
+
at the new x against the current y. A solid probe cancels the move
|
|
62
|
+
and sets the wall flag.
|
|
63
|
+
5. **Ladder.** A ladder is a tile class. The actor is on a ladder when
|
|
64
|
+
the tile at the centre column of its feet, or one pixel below them,
|
|
65
|
+
is one. While on a ladder and pressing up or down, move one pixel,
|
|
66
|
+
hold the vertical velocity at zero and skip gravity. The climb up
|
|
67
|
+
ends when the feet leave the last ladder tile, which puts them one
|
|
68
|
+
pixel above it; the top rung counts as standable from above (a
|
|
69
|
+
ladder tile with no ladder over it), so the actor can stand on it,
|
|
70
|
+
walk off it and climb down into it.
|
|
71
|
+
6. **Ground test.** Otherwise ask whether either foot corner has a
|
|
72
|
+
standable tile one pixel below it. Yes: velocity zero, a jump may
|
|
73
|
+
start. No: this is a pit or a platform end, and gravity applies.
|
|
74
|
+
7. **Vertical move and snap.** Add the velocity, capped below one tile
|
|
75
|
+
a frame so the feet cannot cross a tile without ending inside it.
|
|
76
|
+
Falling: if either foot corner is now in a standable tile, put the
|
|
77
|
+
feet on the pixel row above its top, `((y + h) & ~7) - h - 1` for a
|
|
78
|
+
body `h + 1` tall, and clear the velocity. Rising: if either head
|
|
79
|
+
corner is in a solid tile, put the head on the row below it,
|
|
80
|
+
`(y | 7) + 1`, and clear the velocity.
|
|
81
|
+
8. **Events last.** A landing is the ground test turning true, not the
|
|
82
|
+
snap: a fall can end exactly on the boundary, feet on the last pixel
|
|
83
|
+
above the tile, and the snap never runs.
|
|
84
|
+
|
|
85
|
+
### Why it works
|
|
86
|
+
|
|
87
|
+
The screen's character grid is the collision grid: a tile is 8 x 8
|
|
88
|
+
pixels because a character cell is, so the pixel-to-tile conversion is a
|
|
89
|
+
shift and the whole test is a handful of array reads and compares per
|
|
90
|
+
probe. Snapping to a tile edge is a mask because the edges are at
|
|
91
|
+
multiples of 8. Holding the fall under one tile a frame is what makes a
|
|
92
|
+
single test at the new position sufficient; a faster fall would need the
|
|
93
|
+
rows between the old and new feet tested as well. The ladder rule works
|
|
94
|
+
because the tile class carries the permission: no code decides where
|
|
95
|
+
ladders are, the map does.
|
|
96
|
+
|
|
97
|
+
### Variations
|
|
98
|
+
|
|
99
|
+
- **Pixel versus corner tests.** The corner (and edge-probe) form above
|
|
100
|
+
reads the tile class only. A pixel form goes on to the character's
|
|
101
|
+
bitmap row for a probe inside a tile that is not fully solid (a ramp,
|
|
102
|
+
a half-height step). It costs a character-ROM or charset read per
|
|
103
|
+
probe and a mask; the tile form is the one to start with.
|
|
104
|
+
- **16-pixel tiles.** A 2 x 2 metatile map (`tile_map_render`) can be
|
|
105
|
+
tested at metatile granularity, `>> 4`, with the mask `& ~15`, when
|
|
106
|
+
every metatile is uniformly solid or empty; a metatile with a solid
|
|
107
|
+
top half and an empty bottom half has to be tested at character
|
|
108
|
+
granularity through its metatile table.
|
|
109
|
+
- **One-way platforms.** A tile class that is standable only when the
|
|
110
|
+
feet were above it last frame: solid in the falling foot test, ignored
|
|
111
|
+
in the rising head test and in the side probes. The ladder top in the
|
|
112
|
+
recipe is a one-way platform in this sense. Not built in the recipe.
|
|
113
|
+
- **Scroll offset.** For a scrolling map, add the scroll position in
|
|
114
|
+
pixels to the sprite's world position before the shift; the map array
|
|
115
|
+
is indexed in level columns, not screen columns.
|
|
116
|
+
|
|
117
|
+
### Cycle budget
|
|
118
|
+
|
|
119
|
+
Measured in VICE x64sc 3.10 with a CIA1 timer B harness around the
|
|
120
|
+
recipe's `player_update`, interrupts masked, Oscar64 `-O2`, one actor
|
|
121
|
+
(rung 1): worst frame 2,345 cycles, best frame 604 (standing still, no
|
|
122
|
+
move). The figures are identical on PAL and NTSC because the update runs
|
|
123
|
+
inside the vertical blank after `vic_waitFrame()` and no badline falls in
|
|
124
|
+
it; an earlier build whose timed region included the asserts ran into
|
|
125
|
+
the NTSC badlines and read 3,863 there. The worst frame has four side
|
|
126
|
+
probes, two foot probes, the ladder tests and a snap, each probe a
|
|
127
|
+
bounds check, a 16-bit index and a load. At 2,345 cycles one actor is
|
|
128
|
+
about 12% of a PAL frame; eight actors of this shape do not fit
|
|
129
|
+
alongside a scroller, which is where the `self_modifying_code` and
|
|
130
|
+
`zero_page_burst` entries in the platformer fingerprints come in. The
|
|
131
|
+
asserts and the HUD are outside the timed region and are not part of
|
|
132
|
+
the figure.
|
|
133
|
+
|
|
134
|
+
### Recipes
|
|
135
|
+
|
|
136
|
+
- `recipes/oscar64/tile-grid-collision.md`
|
|
137
|
+
- `recipes/oscar64/platformer-scaffold.md` — the corner probes, landing snap and head bump inside a whole single-file platformer, with ladders; the page to copy when starting a game
|
|
138
|
+
|
|
139
|
+
## flip_screen_rooms — A world of room records, redrawn whole at every edge
|
|
140
|
+
|
|
141
|
+
**Complexity:** medium
|
|
142
|
+
**Region:** both
|
|
143
|
+
**Uses registers:** D011, D015, DC04, DC05, DC06, DC07, DC0E, DC0F
|
|
144
|
+
**Requires:** tile_map_render, tile_grid_collision, object_pool
|
|
145
|
+
|
|
146
|
+
### Why
|
|
147
|
+
|
|
148
|
+
A flip-screen world costs no scroll code: no soft-scroll register, no
|
|
149
|
+
buffer rotation, no column decode at the seam, and no wide map in RAM.
|
|
150
|
+
Each room is one hand-made screen, so the designer places every wall and
|
|
151
|
+
object by eye, and the player reads the whole room at once. The whole
|
|
152
|
+
world is a table of records that fits in memory beside the code, which
|
|
153
|
+
is the single-load model of the period: one tape or disk load, then the
|
|
154
|
+
game and every room it has. The price is the transition, a full redraw of
|
|
155
|
+
the screen from the next room's record, which is longer than one frame
|
|
156
|
+
and has to be hidden.
|
|
157
|
+
|
|
158
|
+
### How
|
|
159
|
+
|
|
160
|
+
1. **The record.** A header, then the tile stream, then the objects. The
|
|
161
|
+
header holds four exits in a fixed order (up, down, left, right), each
|
|
162
|
+
a target room and an entry cell, with a sentinel for no exit, plus
|
|
163
|
+
whatever the room needs to draw itself (a wall colour, a character set
|
|
164
|
+
number, a tune). The tile stream is the run-length format of
|
|
165
|
+
`tile_map_render`, one stream per row. The decoder returns the byte
|
|
166
|
+
after the stream, so the object list follows it with no offset field.
|
|
167
|
+
2. **The edge.** Every frame, the move is computed first and tested
|
|
168
|
+
against the room's last cell. A move that would leave the room looks
|
|
169
|
+
up the exit for that direction: none means the edge is a wall; a
|
|
170
|
+
target means a transition. The test is on the attempted move, not on
|
|
171
|
+
the cell the player stands in.
|
|
172
|
+
3. **The entry cell.** The record names the cell the player appears in,
|
|
173
|
+
which is the gap on the far side of the next room. Because the edge
|
|
174
|
+
fires only on a move that pushes out of the room, standing on the
|
|
175
|
+
entry cell does not fire it, and the way back is to push at the edge
|
|
176
|
+
again. A rule keyed on position would bounce the player between the
|
|
177
|
+
two rooms for ever.
|
|
178
|
+
4. **The redraw.** Hide the sprites (`$D015` = 0), clear DEN in `$D011`,
|
|
179
|
+
decode the next room's stream straight to the tile map, the screen and
|
|
180
|
+
colour RAM in one pass, load its objects into the pool, place the
|
|
181
|
+
player on the entry cell, set DEN. Set it as soon as the redraw
|
|
182
|
+
returns and before any other work, such as a HUD update: the blank
|
|
183
|
+
lasts from DEN clear to DEN set, not for the redraw alone. The recipe
|
|
184
|
+
does this at raster 251, so the first writes fall in the lower border
|
|
185
|
+
and the next line `$30` sample finds DEN clear.
|
|
186
|
+
5. **Objects.** Clear the pool, then load the room's list into it,
|
|
187
|
+
skipping any object the world's taken-bits say is gone. Write the
|
|
188
|
+
bit when the player takes an object, so a second visit does not put
|
|
189
|
+
it back.
|
|
190
|
+
|
|
191
|
+
### Why it works
|
|
192
|
+
|
|
193
|
+
The frame that a redraw spans is the only hazard. DEN is sampled once
|
|
194
|
+
per frame on line `$30`; a frame that samples it clear shows the border
|
|
195
|
+
colour on every line and makes no badlines. A redraw that starts in the
|
|
196
|
+
lower border with DEN clear therefore writes into a screen nobody is
|
|
197
|
+
reading: the writes before the next `$30` sample land in the border, and
|
|
198
|
+
the frames after it are blank until DEN is set again. No frame is torn,
|
|
199
|
+
because no visible line is drawn while screen RAM is half-written. The
|
|
200
|
+
cost is a short blank, two frames on PAL and three on NTSC for the
|
|
201
|
+
recipe's redraw (arithmetic from the measured cycles, on the interval
|
|
202
|
+
from DEN clear to DEN set). A game that would rather not
|
|
203
|
+
blank can accept the torn frames instead, or draw the new room behind a
|
|
204
|
+
`screen_wipe` from `transitions.md`, which turns the redraw's frames into
|
|
205
|
+
the effect.
|
|
206
|
+
|
|
207
|
+
The edge rule on the attempted move is what makes the entry cell safe.
|
|
208
|
+
The player is placed on a cell, not past it, and the collision code sees
|
|
209
|
+
the room's own tiles there, so a wall under the entry cell is the
|
|
210
|
+
designer's error and not the engine's.
|
|
211
|
+
|
|
212
|
+
### Cycle budget
|
|
213
|
+
|
|
214
|
+
Measured in VICE x64sc 3.10 (`recipes/oscar64/flip-screen-rooms.md`,
|
|
215
|
+
CIA1 timers A and B cascaded, interrupts masked, display blanked so no
|
|
216
|
+
badline stalls are counted): the redraw of a 40 x 22 room, decode and
|
|
217
|
+
draw in one pass with an object load, costs 40,204 to 42,141 cycles over
|
|
218
|
+
the seven transitions of the recipe's walk, for rooms of 149 to 188
|
|
219
|
+
stream bytes. That is 46 to 48 cycles per cell, 2.1 PAL frames or 2.5
|
|
220
|
+
NTSC frames, and about six times the 6,700-cycle race-free window
|
|
221
|
+
`full_field_redraw_exceeds_vblank` gives, which is why the screen is
|
|
222
|
+
blanked. The decoder is 224 bytes of Oscar64 `-O2` code from the `.map`
|
|
223
|
+
file. The first version decoded into the map and then copied 880 cells in
|
|
224
|
+
a second loop with a 16-bit index, and cost 99,262 cycles, five frames; a
|
|
225
|
+
per-row pointer with a `char` index halved it. `level-rle-decoder.md`
|
|
226
|
+
measured the decode alone at 19,158 cycles by hand against 33,562 in C
|
|
227
|
+
for a room of 213 bytes, so a hand-written one-pass decoder should reach
|
|
228
|
+
about half the figure here (not measured here). The per-frame work
|
|
229
|
+
outside a transition, the edge test, two corner probes and the object
|
|
230
|
+
check, was not timed separately.
|
|
231
|
+
|
|
232
|
+
### Variations
|
|
233
|
+
|
|
234
|
+
- **Doors instead of edges.** A door tile carries the same three bytes
|
|
235
|
+
as an exit, target room and entry cell, and fires when the player's
|
|
236
|
+
cell is the door's cell. The rooms no longer have to share an edge, so
|
|
237
|
+
the world can be a graph rather than a grid.
|
|
238
|
+
- **Bigger worlds.** A room of 22 rows costs about 200 bytes packed at
|
|
239
|
+
the recipe's ratios; a world of a hundred rooms is 20 KB and still a
|
|
240
|
+
single load. Past that, keep the records on disk and load the four
|
|
241
|
+
neighbours of the current room while the player is in it.
|
|
242
|
+
- **Rooms with actors.** Load the room's actor list into the pool the
|
|
243
|
+
same way as the objects and let `actor_activation_window` decide which
|
|
244
|
+
of them run; a flip-screen room is the window, and the room change is
|
|
245
|
+
the moment every slot is freed and refilled.
|
|
246
|
+
- **A wipe over the redraw.** Run a `screen_wipe` to black before the
|
|
247
|
+
redraw and back after it, or a `colour_fade`; the redraw then sits
|
|
248
|
+
inside frames the player expects to be dark.
|
|
249
|
+
- **The one-frame form.** Rooms of fewer, larger metatiles, or a room
|
|
250
|
+
drawn from a second screen buffer that was decoded while the player
|
|
251
|
+
was still in the last room, bring the visible change inside one
|
|
252
|
+
vertical blank, which is the short form `game-structure.md` describes
|
|
253
|
+
(not measured here).
|
|
254
|
+
|
|
255
|
+
### Recipes
|
|
256
|
+
|
|
257
|
+
- `recipes/oscar64/flip-screen-rooms.md` — five rooms in a 2 x 2 block
|
|
258
|
+
with a dead end, an autopilot walk through all of them and back, the
|
|
259
|
+
redraw timed and the blank policy stated
|
|
260
|
+
|
|
261
|
+
## object_pool — Fixed-slot object pool for enemies, bullets and effects
|
|
262
|
+
|
|
263
|
+
**Complexity:** low
|
|
264
|
+
**Cost:** cycles_per_frame=380
|
|
265
|
+
**Cost basis:** measured-vice
|
|
266
|
+
**Cost measured on:** oscar64-object-pool (eight live slots, screen blanked)
|
|
267
|
+
|
|
268
|
+
**Why.** A game spawns and kills enemies, bullets and explosions all the
|
|
269
|
+
time, and it has no heap worth the name: eight sprites, a few hundred
|
|
270
|
+
bytes of zero page, and a frame budget that must not vary with how many
|
|
271
|
+
things are alive. A fixed pool of slots, allocated by finding a free one
|
|
272
|
+
and freed by marking it, keeps the cost bounded and the sprite mapping
|
|
273
|
+
static.
|
|
274
|
+
|
|
275
|
+
**How.** Hold the objects as parallel arrays, one per field (state, x,
|
|
276
|
+
y, type, timer), indexed by slot. State 0 means free. To spawn, scan for
|
|
277
|
+
the first zero and fill the fields; if none is free, refuse. To kill,
|
|
278
|
+
write 0. Tick every slot once a frame: move it, test it against the
|
|
279
|
+
screen edges, count its timer down, and free it when it leaves or
|
|
280
|
+
expires. A wave table drives the spawns: each entry says when, how many
|
|
281
|
+
and where. Bind slot n to hardware sprite n so the pool's index is the
|
|
282
|
+
sprite number and no remapping is needed.
|
|
283
|
+
|
|
284
|
+
**Why it works.** Parallel arrays let the 6502 address each field with
|
|
285
|
+
one indexed instruction, `LDA state,X`, where an array of structs would
|
|
286
|
+
need a multiply or a pointer walk. A scan over eight slots is a short
|
|
287
|
+
fixed loop, and the worst case, a full pool, is a known number rather
|
|
288
|
+
than a heap search.
|
|
289
|
+
|
|
290
|
+
**Variations.** A free list (a stack of free slot numbers) makes
|
|
291
|
+
allocation constant time at the price of a push on every free. Two pools
|
|
292
|
+
with different sizes, one for enemies and one for shots. A generation
|
|
293
|
+
counter per slot so a stale reference cannot revive a reused slot.
|
|
294
|
+
|
|
295
|
+
**Cycle budget.** Measured on the recipe with CIA1 timer A over one
|
|
296
|
+
hundred calls: the update pass over eight active slots costs 380 cycles,
|
|
297
|
+
over none 106; a scan allocation costs 27 with slot 0 free, 64 with slot
|
|
298
|
+
3 free and 124 when the pool is full and refuses; a free-list pop and
|
|
299
|
+
push together cost 44; a spawn plus despawn 146. The Cost line carries
|
|
300
|
+
the update pass with all eight slots live, which is the per-frame figure.
|
|
301
|
+
|
|
302
|
+
### Recipes
|
|
303
|
+
|
|
304
|
+
- `recipes/oscar64/object-pool.md` — eight slots, scan allocator, wave table, scripted spawns and despawns checked against a Python checksum, with the cycle harness on screen
|
|
305
|
+
- `recipes/oscar64/platformer-scaffold.md` — six enemy slots in parallel arrays fed by a wave table and an LFSR, despawn off screen, inside a whole single-file platformer
|
|
306
|
+
|
|
307
|
+
## actor_activation_window — Level-placed actors that wake near the view and return to the level table
|
|
308
|
+
|
|
309
|
+
**Complexity:** medium
|
|
310
|
+
**Requires:** object_pool
|
|
311
|
+
**Cost:** cycles_per_frame=2809
|
|
312
|
+
**Cost basis:** measured-vice
|
|
313
|
+
**Cost measured on:** oscar64-actor-activation-window (worst tick, screen blanked)
|
|
314
|
+
|
|
315
|
+
**Why.** In a scrolling platformer or adventure the level designer puts
|
|
316
|
+
each enemy, item and door at a place in the level. The level is far
|
|
317
|
+
wider than the screen and holds more actors than the game can run at
|
|
318
|
+
once. An `object_pool` alone despawns an actor when it leaves the screen
|
|
319
|
+
and forgets it, so a killed guard comes back and a wounded one heals.
|
|
320
|
+
Spawning from a timer cannot put an enemy where the map says. The actors
|
|
321
|
+
have to live in the level data while they are far away, and only the
|
|
322
|
+
ones near the view may take a slot.
|
|
323
|
+
|
|
324
|
+
**How.**
|
|
325
|
+
|
|
326
|
+
1. **The level actor table.** One entry per placed actor, as parallel
|
|
327
|
+
arrays: position in blocks (a column, or a map block of several
|
|
328
|
+
columns), row, type, and the state that must survive: hit points and
|
|
329
|
+
a flag byte. The flag byte holds facing, a *live* bit (the actor is in
|
|
330
|
+
a slot now) and a *dead* bit.
|
|
331
|
+
2. **The activation window.** The view plus a margin on each side, in
|
|
332
|
+
the same block units as the table. Each tick, work out its bounds once
|
|
333
|
+
from the camera and clamp them to the level.
|
|
334
|
+
3. **Scan a slice, not the table.** Each tick examine K entries from
|
|
335
|
+
where the last tick stopped, wrapping at the end. An entry that is
|
|
336
|
+
neither live nor dead and lies inside the window takes a free slot
|
|
337
|
+
from the pool: copy its state in, set its live bit. If no slot is
|
|
338
|
+
free, leave it; the next lap tries again.
|
|
339
|
+
4. **Drop what left.** Each tick test every live slot against a drop
|
|
340
|
+
window a little wider than the activation window. An actor outside it
|
|
341
|
+
goes back: write its block position, hit points and facing into its
|
|
342
|
+
entry, clear the live bit, free the slot. The extra width stops an
|
|
343
|
+
actor on the edge being dropped and woken on alternate ticks.
|
|
344
|
+
5. **Kill by flag.** A killed actor sets its entry's dead bit and frees
|
|
345
|
+
its slot without writing a position. The scan skips dead entries for
|
|
346
|
+
the rest of the level.
|
|
347
|
+
6. **Draw relative to the camera.** A live actor's sprite X is its world
|
|
348
|
+
x minus the camera x plus 24. That runs past 255 in the right part of
|
|
349
|
+
the screen, so the ninth bit goes in `$D010`.
|
|
350
|
+
|
|
351
|
+
**Why the margin must exceed the scroll step.** With N entries and K
|
|
352
|
+
examined per tick, an entry is looked at every N/K ticks. In that time a
|
|
353
|
+
camera moving S blocks a tick moves S × N/K blocks. An entry just outside
|
|
354
|
+
the window when it is examined can be that much closer at its next look.
|
|
355
|
+
So a margin of S × ⌈N/K⌉ blocks is enough to wake every actor before
|
|
356
|
+
it is in view, for any layout (arithmetic). It is not the least. When
|
|
357
|
+
the scan runs before the draw in the same tick, the entry is examined
|
|
358
|
+
again on the tick it would first be in view, and S × (⌈N/K⌉ − 1) blocks
|
|
359
|
+
is enough. Add one block when the camera's step is not a whole number
|
|
360
|
+
of blocks. In the recipe, N = 32, K = 8 and S = 2 columns (16 pixels) a
|
|
361
|
+
tick: 8 columns is safe and 6 is enough. A Python model of the scan over
|
|
362
|
+
adversarial layouts gives a worst of 15 pop-ins at margin 5 and 0 at
|
|
363
|
+
margin 6 (rung 3, a model; not run in VICE). Measured in the recipe:
|
|
364
|
+
0 pop-ins with a margin of 8, 37 with a margin of 1. A smaller margin
|
|
365
|
+
shows as an enemy that appears already on screen. A larger one costs
|
|
366
|
+
slots: the pool must hold every actor in the widest window, so the
|
|
367
|
+
densest stretch of the level sets the pool size.
|
|
368
|
+
|
|
369
|
+
**Why it works.** The table is the only record of an actor while it is
|
|
370
|
+
far away; a slot is a working copy. Because the copy always goes back
|
|
371
|
+
before the slot is freed, the table is correct whenever the actor is not
|
|
372
|
+
live. The live bit stops the scan waking an actor twice. The dead bit
|
|
373
|
+
costs no memory beyond the flag byte and keeps the table's indices
|
|
374
|
+
stable, so a save or a level-state record can refer to an entry by
|
|
375
|
+
number. Position goes back in blocks, so sub-block position is lost:
|
|
376
|
+
the recipe's actor woke at `column × 8` pixels, up to 7 pixels from where
|
|
377
|
+
it was left.
|
|
378
|
+
|
|
379
|
+
**Variations.**
|
|
380
|
+
|
|
381
|
+
- **Sorted by x with a cursor.** Keep the table sorted by block x and
|
|
382
|
+
move two cursors with the window's edges. Each tick then looks only at
|
|
383
|
+
entries crossing an edge, and the latency argument above goes away. An
|
|
384
|
+
actor that moves and is written back at a new x can break the order;
|
|
385
|
+
either re-sort it into place on write-back or keep its placed x as the
|
|
386
|
+
sort key (not measured here).
|
|
387
|
+
- **Several levels or zones in one table.** c64gameframework keeps 96
|
|
388
|
+
entries, each with an origin byte (level data, global or temporary,
|
|
389
|
+
plus a level number) and a zone byte. It wakes only entries of the
|
|
390
|
+
current level and zone, and scans 24 a frame
|
|
391
|
+
(`actor.s`, `defines.s`; read, not run). The Cadaver scroll article
|
|
392
|
+
describes 16-bit positions whose high byte is the map block, so the
|
|
393
|
+
window test compares high bytes only, and about 16 entries checked per
|
|
394
|
+
frame (not measured here).
|
|
395
|
+
- **Bounds patched into the code.** The same source writes the window
|
|
396
|
+
bounds into the immediate operands of the scan's `CMP` instructions once
|
|
397
|
+
a frame, so each compare is two cycles.
|
|
398
|
+
|
|
399
|
+
**Cycle budget.** Measured in VICE x64sc 3.10 with CIA1 timer A over 20
|
|
400
|
+
calls, on the Oscar64 -O2 recipe, identical on PAL and NTSC: a slice of 8
|
|
401
|
+
entries, none waking, 420 cycles; all 32 entries in one loop 1,153; the
|
|
402
|
+
drop pass over 8 live actors 529; window set-up, slice and drop pass
|
|
403
|
+
together 1,076, an average over the 20 timed calls with nothing waking
|
|
404
|
+
or dropping; one wake plus one drop 264. An ordinary scrolling tick
|
|
405
|
+
that wakes one actor and drops one costs about 1,076 + 264 = 1,340
|
|
406
|
+
(arithmetic). The worst tick wakes all K = 8 entries of its slice, then
|
|
407
|
+
runs the drop pass over 8 live: 2,809 cycles, measured, and that is the
|
|
408
|
+
Cost line. Wakes plus drops in one tick cannot exceed the 8 slots, and a
|
|
409
|
+
tick that drops 8 and wakes none measured 1,850, so waking is the heavy
|
|
410
|
+
case. The recipe's first tick wakes 6. Hand-written assembly
|
|
411
|
+
would cost less; the C figures are an upper reference, not a target.
|
|
412
|
+
|
|
413
|
+
### Recipes
|
|
414
|
+
|
|
415
|
+
- `recipes/oscar64/actor-activation-window.md` — 32 placed actors in a 256-column level, 8 slots, a camera out at 16 pixels a tick and back at 8, two kills and a hit that survive, a pop-in check with margins 8 and 1, a Python-checked table, scan costs on screen, PAL and NTSC
|
|
416
|
+
|
|
417
|
+
### Sources
|
|
418
|
+
|
|
419
|
+
- https://cadaver.github.io/rants/scroll.html (Cadaver: 16-bit positions
|
|
420
|
+
with the map block in the high byte; about 16 level entries checked per
|
|
421
|
+
frame; not measured here).
|
|
422
|
+
- https://github.com/cadaver/c64gameframework (MIT): `actor.s`
|
|
423
|
+
(`AddActors`, `LVLACTSEARCH` = 24, window bounds patched into `CMP`
|
|
424
|
+
operands), `defines.s` (`MAX_LVLACT` = 96). Read for facts; no code is
|
|
425
|
+
taken from it.
|
|
426
|
+
|
|
427
|
+
## logic_rate_decoupling — Game logic at half the display rate, with interpolation and frame skip
|
|
428
|
+
|
|
429
|
+
**Complexity:** medium
|
|
430
|
+
**Region:** both
|
|
431
|
+
**Uses registers:** D000, D011, D012, D019, D01A
|
|
432
|
+
**Requires:** frame_sync_loop
|
|
433
|
+
**Cost:** cycles_per_frame=384, irq_slots=1
|
|
434
|
+
**Cost basis:** measured-vice
|
|
435
|
+
**Cost measured on:** kickassembler-logic-rate-decoupling (midpoint frame, eight sprites, in the vertical blank from line 251)
|
|
436
|
+
**Claims:** vic_raster_irq (owns)
|
|
437
|
+
**Claims basis:** derived-listing
|
|
438
|
+
|
|
439
|
+
### Why
|
|
440
|
+
|
|
441
|
+
`frame_sync_loop` runs one logic step per frame and, when the work
|
|
442
|
+
overruns, can only count the frames it missed; the game slows down with the
|
|
443
|
+
display. Once a game has many actors, a scroller and a multiplexer, the
|
|
444
|
+
logic stops fitting in a frame. Shipped games solved it by running the
|
|
445
|
+
logic every second frame (25 Hz on PAL) and drawing every frame (50 Hz),
|
|
446
|
+
with sprite positions interpolated on the frames between logic steps. Cadaver
|
|
447
|
+
describes this for Metal Warrior 4, and his c64gameframework README states
|
|
448
|
+
"50Hz screen update, with actor update each second frame and interpolation
|
|
449
|
+
of sprite movement" (sources below; not measured here). The display stays
|
|
450
|
+
smooth, the logic gets two frames of time, and the game keeps its speed
|
|
451
|
+
when a step overruns.
|
|
452
|
+
|
|
453
|
+
### How
|
|
454
|
+
|
|
455
|
+
1. **Two rates, two owners.** A raster interrupt below the display counts
|
|
456
|
+
display frames and, every second frame, adds one to an `owed` counter.
|
|
457
|
+
The main loop runs the logic: it waits until `owed` is not zero, then
|
|
458
|
+
runs ticks, taking one off `owed` per tick with a single `DEC`. The
|
|
459
|
+
interrupt owns the frame count, the logic owns the tick count.
|
|
460
|
+
2. **Frame skip with the speed counter.** When a tick ends and `owed` is
|
|
461
|
+
still not zero, run the next tick at once and do not publish in
|
|
462
|
+
between. Game time then equals half the display frames however long a
|
|
463
|
+
tick took; the display drops the positions it had no time to show.
|
|
464
|
+
Cadaver's form of the rule: if the last render was N frames ago, run
|
|
465
|
+
the logic N times before rendering again.
|
|
466
|
+
3. **Interpolate positions, never state.** The logic keeps every game
|
|
467
|
+
variable at its own rate. Before moving an actor it copies the position
|
|
468
|
+
to a previous-position field. The display needs only the two positions
|
|
469
|
+
per actor. On the first display frame after a tick it shows the
|
|
470
|
+
midpoint; on the next it shows the new position. c64gameframework does
|
|
471
|
+
the equivalent: it draws the actors, runs the update, then adds half of
|
|
472
|
+
each actor's movement to the drawn sprites for the in-between frame.
|
|
473
|
+
4. **Double-buffered sprite table.** The logic writes the previous and new
|
|
474
|
+
positions into the half of a two-half table the display is not reading,
|
|
475
|
+
then sets a `ready` index and, last, a `fresh` byte. The display update
|
|
476
|
+
takes the fresh half at its next frame. The logic does not write again
|
|
477
|
+
until `fresh` is clear, so the display never draws a half-written table.
|
|
478
|
+
One byte hands the table over, so no `SEI` is needed.
|
|
479
|
+
5. **Re-entrant update interrupt.** The interrupt acknowledges `$D019`,
|
|
480
|
+
does the timekeeping, increments a `busy` byte, and only at 1 runs
|
|
481
|
+
`CLI` and the display update; at 2 it leaves at once. The raster-exact
|
|
482
|
+
interrupts underneath (splits, the multiplexer, music) can then fire
|
|
483
|
+
during a long update, and a second entry cannot start a second update on
|
|
484
|
+
the same data. The timekeeping runs before the guard, so a frame the
|
|
485
|
+
update missed still counts and still owes its tick. Cadaver's article shows
|
|
486
|
+
the same shape: an `INC` of a counter, a compare with 2 and a skip,
|
|
487
|
+
and colour-RAM writes kept after the bottom interrupt.
|
|
488
|
+
|
|
489
|
+
### Why it works
|
|
490
|
+
|
|
491
|
+
The 6510 finishes an instruction before it takes an interrupt, so a `DEC`
|
|
492
|
+
or `INC` on one byte cannot be split between the loop and the handler, and
|
|
493
|
+
a one-byte flag is read and written whole. That is enough to keep the two
|
|
494
|
+
clocks apart without masking interrupts. The midpoint of two unsigned
|
|
495
|
+
bytes is exact with `CLC`, `ADC`, `ROR`, because the carry holds bit 8 of
|
|
496
|
+
the sum. Writing the sprite registers from an interrupt below the lowest
|
|
497
|
+
sprite means the beam has already drawn every sprite of the frame, so no
|
|
498
|
+
sprite is drawn with half its new position. The registers stay on the
|
|
499
|
+
KERNAL's stack frame (`$FF48` pushes A, X, Y; `$EA81` pulls them), so a
|
|
500
|
+
nested entry corrupts nothing.
|
|
501
|
+
|
|
502
|
+
### Variations
|
|
503
|
+
|
|
504
|
+
- **Logic every fourth frame.** More time per step, and larger jumps to
|
|
505
|
+
interpolate. Cadaver says four-frame interpolation "feels indeed quite
|
|
506
|
+
lagged" (not measured here).
|
|
507
|
+
- **Scroll in the update.** In a scroller the fine scroll still moves every
|
|
508
|
+
display frame, and the interpolation adds the scroll offset to each
|
|
509
|
+
sprite. c64gameframework adds both in the same pass.
|
|
510
|
+
- **Interpolation off per actor.** An actor that teleports or spawns must
|
|
511
|
+
not be drawn half-way from its old slot. c64gameframework marks this with
|
|
512
|
+
a flag in the previous position.
|
|
513
|
+
|
|
514
|
+
### Cost and when not to use it
|
|
515
|
+
|
|
516
|
+
Measured in VICE x64sc 3.10 with CIA1 timer B around the display update of
|
|
517
|
+
`recipes/kickassembler/logic-rate-decoupling.md` (rung 1): 384 cycles on a
|
|
518
|
+
midpoint frame for eight sprites, the table pick and the interpolation
|
|
519
|
+
loop, identical on PAL and NTSC. The instruction table gives the same
|
|
520
|
+
count: 49 cycles for the call and the pick, 42 per sprite. The IRQ's
|
|
521
|
+
timekeeping and guard add about 67 cycles more by the instruction table
|
|
522
|
+
(80 while the event checks run; rung 3, not timed), and the KERNAL IRQ
|
|
523
|
+
entry and exit about 58 more, so the IRQ side is near 510 cycles a frame. Memory is
|
|
524
|
+
a table of two halves, each holding a from and a to byte per sprite per
|
|
525
|
+
axis, and a previous position per actor.
|
|
526
|
+
|
|
527
|
+
Do not use it when:
|
|
528
|
+
|
|
529
|
+
- **The game is a fast action game that runs its logic at 50 Hz already.**
|
|
530
|
+
Half-rate logic halves the rate at which collisions and input are
|
|
531
|
+
sampled. A bullet moving 8 pixels a frame moves 16 per tick, and a
|
|
532
|
+
collision test that ran every frame must now test the whole path.
|
|
533
|
+
- **Input latency matters.** Measured in the recipe: an input raised on
|
|
534
|
+
the frame that owes a tick showed 1 frame later; one raised on the other
|
|
535
|
+
frame showed 2 frames later (PAL). By arithmetic, an input that arrives
|
|
536
|
+
just after a tick has sampled it waits for the next tick, two frames on,
|
|
537
|
+
and is drawn one frame after that: 3 frames, against 2 for one logic step
|
|
538
|
+
per frame. The first frame shows only half the move.
|
|
539
|
+
- **It fits.** If the logic fits in a frame, `frame_sync_loop` is simpler
|
|
540
|
+
and has less latency.
|
|
541
|
+
|
|
542
|
+
On NTSC the display is 60 Hz and the logic runs at 30 Hz, so game speed is
|
|
543
|
+
20% higher unless the tick advances less (`pal_ntsc_tempo_mismatch`).
|
|
544
|
+
|
|
545
|
+
### Recipes
|
|
546
|
+
|
|
547
|
+
- `recipes/kickassembler/logic-rate-decoupling.md` — eight sprites, logic every second frame with an overrunning tick every eighth, midpoint interpolation, a double-buffered table, a re-entrant update with one forced guard trip, latency and step records, and a PASS/FAIL check that ticks equal half the frames, PAL and NTSC
|
|
548
|
+
|
|
549
|
+
### Sources
|
|
550
|
+
|
|
551
|
+
- https://cadaver.github.io/rants/interp.html (Cadaver, interpolation,
|
|
552
|
+
frame skip and the re-entrant update; also codebase64 `base:rant9`).
|
|
553
|
+
- https://github.com/cadaver/c64gameframework (MIT): README, `raster.s`
|
|
554
|
+
(the update interrupt), `actor.s` `InterpolateActors`. Read for facts;
|
|
555
|
+
no code is taken from it.
|
|
556
|
+
|
|
557
|
+
## wave_director — Attack waves triggered by scroll position, with path bytecode per enemy
|
|
558
|
+
|
|
559
|
+
**Complexity:** medium
|
|
560
|
+
**Requires:** object_pool
|
|
561
|
+
**Cost:** cycles_per_frame=3188, cycles_per_frame_typical=1170
|
|
562
|
+
**Cost basis:** measured-vice
|
|
563
|
+
**Cost measured on:** oscar64-wave-director (worst frame, screen blanked)
|
|
564
|
+
**Cost includes:** object_pool
|
|
565
|
+
|
|
566
|
+
**Why.** A scrolling shooter is built from attack waves: a group of enemies
|
|
567
|
+
that enters at a set place in the level, flies a set path and leaves. The
|
|
568
|
+
`object_pool` recipe spawns from a table keyed by frame and moves every
|
|
569
|
+
object in a straight line. That is not enough for a shooter. The level
|
|
570
|
+
designer places each wave against the scenery, so the trigger must follow
|
|
571
|
+
the scroll. Enemies need curved entry and exit paths, one wave needs
|
|
572
|
+
several enemies spaced in time, and the game needs to know when a wave was
|
|
573
|
+
destroyed whole.
|
|
574
|
+
|
|
575
|
+
**How.**
|
|
576
|
+
|
|
577
|
+
1. **The wave list.** One record per wave, sorted by trigger position:
|
|
578
|
+
the scroll position (16 bits for a long level), the enemy type, the
|
|
579
|
+
count, the spacing in frames between spawns, the path id and the
|
|
580
|
+
formation (a start x and an x step per enemy, or a table of offsets).
|
|
581
|
+
2. **The cursor.** Each frame, after the scroll moves, compare the scroll
|
|
582
|
+
position with the record under the cursor. While it is due (`>=`,
|
|
583
|
+
never `==`), start it and move the cursor on. Sorting makes the idle
|
|
584
|
+
case one compare.
|
|
585
|
+
3. **The spawner.** A started wave hands its count and spacing to a
|
|
586
|
+
spawner, which takes one slot from the pool every `spacing` frames
|
|
587
|
+
until the count is spawned. A game that lets waves overlap in time
|
|
588
|
+
keeps two or three spawners.
|
|
589
|
+
4. **Paths as bytecode.** A path is a byte string shared by every enemy
|
|
590
|
+
on it. MOVE n, dx, dy moves n frames at that velocity. Control opcodes
|
|
591
|
+
take no frame: FIRE, LOOP count and target, END. Each enemy holds a
|
|
592
|
+
path id, a program counter, the steps left, its velocity and a loop
|
|
593
|
+
counter. Each frame the interpreter fetches while the steps left are 0,
|
|
594
|
+
then adds the velocity. A plainer form is a delta table of (dx, dy)
|
|
595
|
+
pairs, one per frame, which costs more bytes and no fetch logic.
|
|
596
|
+
5. **Exit.** An enemy leaves when its path reaches END or its position
|
|
597
|
+
crosses a screen edge. Either frees its slot.
|
|
598
|
+
6. **Wave accounting.** Count each wave's live and killed enemies. When
|
|
599
|
+
the last one goes and the spawner has finished that wave, it is over:
|
|
600
|
+
all killed earns the wave bonus, otherwise it escaped.
|
|
601
|
+
7. **Difficulty by loop.** When the list runs out and the level ends,
|
|
602
|
+
reset the scroll and the cursor and count a loop. Scale by the loop
|
|
603
|
+
counter: shorter spacing, faster velocities, more shots.
|
|
604
|
+
|
|
605
|
+
**Why keyed to scroll position.** The scroll can stop for a boss, slow on
|
|
606
|
+
a climb, or run faster later. A frame-keyed wave then meets different
|
|
607
|
+
scenery each time. A position-keyed wave waits: in the recipe the scroll
|
|
608
|
+
holds for 50 frames, and the three waves after the hold start 50 frames
|
|
609
|
+
late and each at its exact position (measured in VICE). The `>=` compare
|
|
610
|
+
with a loop means a scroll of several positions a frame starts every
|
|
611
|
+
wave it passes, in order. Corescape keeps one wave id per level block
|
|
612
|
+
of four tile rows and starts it when the scroll brings a new block in
|
|
613
|
+
(`display.cpp`, `enemies.cpp`; read, not run).
|
|
614
|
+
|
|
615
|
+
**Why it works.** Parallel arrays and one shared interpreter keep the
|
|
616
|
+
per-enemy state to a few bytes and the per-enemy cost to a few adds on
|
|
617
|
+
most frames; the fetch runs only when a segment ends. Paths as data let a
|
|
618
|
+
designer change a wave without touching code. With the position held in
|
|
619
|
+
bytes, one compare per axis against 250 catches every edge, because a
|
|
620
|
+
byte that goes below 0 wraps to 255. The cost of that choice is a
|
|
621
|
+
playfield of sprite X 0 to 249 with `$D010` held at 0
|
|
622
|
+
(`sprite_x_high_bit_wrong_register`); a full-width game carries a ninth x
|
|
623
|
+
bit per enemy and writes `$D010` from it.
|
|
624
|
+
|
|
625
|
+
**Variations.**
|
|
626
|
+
|
|
627
|
+
- **Compile-time path tables.** Corescape builds entry and exit curves
|
|
628
|
+
as tables at compile time with Oscar64 `#for`, for example a 32-entry
|
|
629
|
+
parabola, (31 - i)² / 10 (`enemies.cpp`; read, not run). A table
|
|
630
|
+
costs a byte or two per frame of path against 3 bytes per MOVE segment.
|
|
631
|
+
- **Behaviour switched by a hit.** Iridis Alpha's per-level record is 40
|
|
632
|
+
bytes, with movement pattern and rates, a pull toward the player, a
|
|
633
|
+
spawn rate, and pointers to the wave data to switch to when an enemy is
|
|
634
|
+
first hit (mwenge disassembly; from the research notes, not read here).
|
|
635
|
+
- **Fixed-velocity waves.** Death Weapon's wave record is 6 bytes: X
|
|
636
|
+
speed, Y speed, colour, first and last animation frame and a quota
|
|
637
|
+
(C64CD source; from the research notes, not read here). The record has
|
|
638
|
+
no path field.
|
|
639
|
+
- **More enemies than sprites.** Put the enemies through a multiplexer
|
|
640
|
+
(`sprite_multiplex_24`); the director does not change.
|
|
641
|
+
|
|
642
|
+
**Cycle budget.** Measured in VICE x64sc 3.10 with CIA1 timer A on the
|
|
643
|
+
Oscar64 -O2 recipe, identical on PAL and NTSC: one interpreter step with
|
|
644
|
+
steps left, 80 cycles; a step that fetches a MOVE, 158; FIRE, LOOP and a
|
|
645
|
+
MOVE fetch in one step, 289; the director with no wave due, 45. A frame
|
|
646
|
+
with eight live enemies mid-MOVE, director and spawner idle, costs 1,170,
|
|
647
|
+
about 146 per enemy with the edge and hit tests included. The worst frame
|
|
648
|
+
measured costs 3,188: seven enemies on the FIRE, LOOP and MOVE step, a
|
|
649
|
+
wave triggering, and its first enemy spawned into the last free slot and
|
|
650
|
+
fetching its first MOVE. The Cost line carries that figure, and 1,170 as
|
|
651
|
+
the typical frame. It covers the director, the spawner, the spawn into
|
|
652
|
+
`object_pool`'s slots and the enemy updates, so the Cost includes line
|
|
653
|
+
names `object_pool` and a plan that lists both counts the pool once. It
|
|
654
|
+
leaves out the scroll advance, the sprite writes, and `gone()` with its end-of-wave accounting,
|
|
655
|
+
which runs only when an enemy leaves. Code layout moves these figures by
|
|
656
|
+
a few cycles. Hand-written assembly would cost less; the C figures are an upper
|
|
657
|
+
reference.
|
|
658
|
+
|
|
659
|
+
On NTSC the frame rate is 60 Hz, so a scroll that moves one position a
|
|
660
|
+
frame, and every wave with it, runs about 19% faster than on PAL unless
|
|
661
|
+
the step is scaled (arithmetic from 59.826 / 50.125 Hz).
|
|
662
|
+
|
|
663
|
+
### Recipes
|
|
664
|
+
|
|
665
|
+
- `recipes/oscar64/wave-director.md` — a scroll counter on autopilot with a 50-frame hold, six waves on three paths (dive, weave, swoop) as MOVE/FIRE/LOOP/END bytecode, eight sprite slots, an autopilot gun column and a wave-cleared bonus, a second loop at half spacing, triggers checked against the table and a Python model, interpreter costs on screen, PAL and NTSC
|
|
666
|
+
|
|
667
|
+
### Sources
|
|
668
|
+
|
|
669
|
+
- https://github.com/drmortalwombat/corescape (GPL-3.0): `display.cpp`
|
|
670
|
+
(wave check on scroll), `enemies.cpp` (`wave_start`, `wave_loop`,
|
|
671
|
+
path tables). Read for facts; no code is taken from it.
|
|
672
|
+
- https://github.com/mwenge/iridisalpha `src/level_data/level_data2.asm`
|
|
673
|
+
(disassembly of a commercial game; facts only, not read here).
|
|
674
|
+
- https://github.com/C64CD/Death-Weapon-C64 `includes/levels.asm` (not
|
|
675
|
+
read here).
|
|
676
|
+
|
|
677
|
+
## slope_collision — Slopes, ground snap and drop-through platforms from a per-cell attribute byte
|
|
678
|
+
|
|
679
|
+
**Complexity:** medium
|
|
680
|
+
**Region:** both
|
|
681
|
+
**Requires:** tile_grid_collision
|
|
682
|
+
**Cost:** cycles_per_frame=455
|
|
683
|
+
**Cost basis:** measured-vice
|
|
684
|
+
**Cost measured on:** oscar64-slope-collision (worst frame, one actor, in the vertical blank)
|
|
685
|
+
|
|
686
|
+
### Why
|
|
687
|
+
|
|
688
|
+
`tile_grid_collision` reads one class per cell and snaps to cell edges,
|
|
689
|
+
so its ground is flat and a hill is a staircase the actor has to jump. A
|
|
690
|
+
platformer with slopes needs the ground row inside a cell to change with
|
|
691
|
+
the x pixel, and it needs the actor to follow that row down a hill
|
|
692
|
+
instead of stepping off it into a fall. The answer is one attribute byte
|
|
693
|
+
per map cell and a height table per slope type; Cadaver's
|
|
694
|
+
c64gameframework `physics.s` is a public example (source below).
|
|
695
|
+
|
|
696
|
+
### How
|
|
697
|
+
|
|
698
|
+
1. **Attribute byte.** One byte per map cell, kept in its own array
|
|
699
|
+
beside the map. The recipe's layout: bit 0 ground (the cell has a
|
|
700
|
+
surface), bit 1 wall (stops a sideways move), bit 2 drop-through
|
|
701
|
+
(one-way), bits 4 to 6 slope type. A ladder is one more bit, handled
|
|
702
|
+
as in `tile_grid_collision`. c64gameframework uses ground $01, wall
|
|
703
|
+
$02, climb $04, drop $08, a slope-end bit $10 and the slope type in
|
|
704
|
+
bits 5 to 7.
|
|
705
|
+
2. **Height table.** Eight types of eight entries, one per pixel column
|
|
706
|
+
of an 8-pixel cell: the row, 0 to 7, where the ground starts. Flat is
|
|
707
|
+
all 0. 45 degrees rising to the right is 7 down to 0; falling, 0 up
|
|
708
|
+
to 7. A 1-in-2 slope (26.6 degrees) takes two cells per 8 pixels of
|
|
709
|
+
height: the lower cell 7, 7, 6, 6, 5, 5, 4, 4 and the upper cell 3,
|
|
710
|
+
3, 2, 2, 1, 1, 0, 0, so the ground continues across the boundary in
|
|
711
|
+
1-pixel steps. With
|
|
712
|
+
the type in bits 4 to 6, `((a & $70) >> 1) | (x & 7)` indexes the
|
|
713
|
+
64-byte table with no multiply. Store it, or generate it at start
|
|
714
|
+
from the gradient; draw the slope glyphs from it either way, a pixel
|
|
715
|
+
set where its row is at or below the column's entry, so the picture
|
|
716
|
+
cannot disagree with the collision. The cells under a hill are flat
|
|
717
|
+
ground; only surface cells carry a slope type.
|
|
718
|
+
3. **One foot point.** Test the ground under the middle column of the
|
|
719
|
+
body, not its corners. On a slope the corners are at different
|
|
720
|
+
heights; the middle is where the actor stands. Keep the foot row as
|
|
721
|
+
the ground row under the feet, one below the sprite's last row. Foot
|
|
722
|
+
rows run to 199 on a 25-row map, past 127, so compare them unsigned
|
|
723
|
+
(`CMP` then `BCC`/`BCS`); a `BMI` after `SBC` gives the wrong order
|
|
724
|
+
when the difference leaves -128 to 127.
|
|
725
|
+
4. **Walking step, ground snap.** Move x, then read the cell that holds
|
|
726
|
+
the foot row. If it is ground and the cell above is ground too, the
|
|
727
|
+
feet have walked into the fill under the next slope cell: the
|
|
728
|
+
surface is in the cell above. If it is air, the feet have walked off
|
|
729
|
+
the end of a slope cell: the surface, if any, is in the cell below.
|
|
730
|
+
Put the feet on the surface found. Doing this every step is the
|
|
731
|
+
ground snap: walking down a hill the feet follow it pixel by pixel
|
|
732
|
+
instead of dropping in steps under gravity.
|
|
733
|
+
5. **Limits per step.** A rise larger than a set limit is a wall: undo
|
|
734
|
+
the x move. A drop larger than a set limit is a ledge: keep the x
|
|
735
|
+
move and start a fall. The rise limit must be at least the walking
|
|
736
|
+
speed times the steepest gradient (1 pixel at 1 pixel a frame on 45
|
|
737
|
+
degrees); the recipe uses 2 for both limits.
|
|
738
|
+
6. **Transitions.** Slope to flat and flat to slope need no code. A
|
|
739
|
+
45-degree cell ends at row 0 or row 7, and the next cell along, at the
|
|
740
|
+
same height or one cell up or down, starts one pixel away, which step
|
|
741
|
+
4 finds.
|
|
742
|
+
7. **Falling and landing.** Add gravity with the fall capped below a cell
|
|
743
|
+
a frame. Test the cell the feet left and the cell they reached; land
|
|
744
|
+
on a surface that lies between the old and new foot rows.
|
|
745
|
+
8. **Drop-through.** A drop-through cell is ground from above only.
|
|
746
|
+
Down + fire on one moves the feet one row below its surface and starts
|
|
747
|
+
a fall, so step 7 never catches it again. The actor walking under it
|
|
748
|
+
never tests it, because the foot cell is on the ground below, and the
|
|
749
|
+
side probe tests the wall bit only. That needs the platform at least
|
|
750
|
+
two cells above the ground: step 4 reads the cell above the foot
|
|
751
|
+
cell, so a one-way cell there is taken as a surface 8 pixels up and
|
|
752
|
+
refused as a wall (rung 3, read from the recipe's code, not run).
|
|
753
|
+
|
|
754
|
+
### Why it works
|
|
755
|
+
|
|
756
|
+
The height table turns a cell's shape into data: the walker needs no
|
|
757
|
+
per-slope code, only a table read, and a new slope shape is eight more
|
|
758
|
+
bytes and a type number. Looking at one neighbour cell, above or below,
|
|
759
|
+
is enough because the foot row moves at most one pixel per step on a
|
|
760
|
+
45-degree slope at 1 pixel a frame, so the surface can never be more
|
|
761
|
+
than one cell away. Snapping every step keeps the feet exactly on the
|
|
762
|
+
surface, and a rise limit turns any step steeper than a slope into a
|
|
763
|
+
wall without a separate test.
|
|
764
|
+
|
|
765
|
+
### Variations
|
|
766
|
+
|
|
767
|
+
- **Larger blocks.** c64gameframework keeps 16 entries per type,
|
|
768
|
+
indexed by the actor's x within the block shifted right three times,
|
|
769
|
+
and eight types: flat, 45 degrees each way, the
|
|
770
|
+
two halves of a 1-in-2 slope each way, and a flat block at half height
|
|
771
|
+
(`slopeTbl` in `aligneddata.s`). Its grounded step checks the block
|
|
772
|
+
above or below only when the current block has no ground, choosing by
|
|
773
|
+
the half of the block the feet are in.
|
|
774
|
+
- **Diagonal landing.** When the actor falls with an x speed onto a
|
|
775
|
+
slope, c64gameframework adds the absolute x speed to the y speed in
|
|
776
|
+
the landing test so a diagonal crossing of the surface is not missed.
|
|
777
|
+
The recipe freezes x while airborne and does not need it.
|
|
778
|
+
- **Faster walking.** At 2 pixels a frame on 45 degrees the foot row
|
|
779
|
+
changes by 2 per step, the recipe's limit; any faster walk needs larger
|
|
780
|
+
limits, or the actor stalls on the slope (rung 3, not run).
|
|
781
|
+
|
|
782
|
+
### Cycle budget
|
|
783
|
+
|
|
784
|
+
Measured in VICE x64sc 3.10 with a CIA1 timer B harness around the
|
|
785
|
+
recipe's `actor_update`, interrupts masked, Oscar64 `-O2`, one actor
|
|
786
|
+
(rung 1): worst frame 455 cycles, best 87 (standing still), identical on
|
|
787
|
+
PAL and NTSC. A build that timed each state separately put the worst
|
|
788
|
+
frames at 455 walking, 370 landing, 329 falling and 250 against a wall.
|
|
789
|
+
One foot point and one neighbour cell are why this is far under the
|
|
790
|
+
2,345 of `tile_grid_collision`, whose worst step probes four points of a
|
|
791
|
+
side edge, two feet, the ladder and a snap; a game that needs both runs the box probes for walls and heads and this
|
|
792
|
+
for the ground.
|
|
793
|
+
|
|
794
|
+
### Recipes
|
|
795
|
+
|
|
796
|
+
- `recipes/oscar64/slope-collision.md` — flat ground, a 45-degree hill, a 1-in-2 hill and a drop-through platform, drawn with glyphs generated from the height table; a scripted walk right and back, the foot row logged per x and compared with a column-scan profile (error 0), PASS/FAIL and cycles per step, PAL and NTSC
|
|
797
|
+
|
|
798
|
+
### Sources
|
|
799
|
+
|
|
800
|
+
- https://github.com/cadaver/c64gameframework (MIT): `physics.s` (the
|
|
801
|
+
block-info bits and `MoveWithGravity`), `aligneddata.s` (`slopeTbl`).
|
|
802
|
+
Read for facts; no code is taken from it.
|
|
803
|
+
|
|
804
|
+
---
|
|
805
|
+
|
|
806
|
+
## world_state_bits — One bit per persistent object, packed per level and written back on level change
|
|
807
|
+
|
|
808
|
+
**Complexity:** low
|
|
809
|
+
|
|
810
|
+
**Why.** An adventure or a game with levels you can walk back into has
|
|
811
|
+
to remember what the player changed: the key taken, the door opened, the
|
|
812
|
+
boss killed. `actor_activation_window` keeps that state in the level
|
|
813
|
+
actor table, but only while the level is loaded. When the next level
|
|
814
|
+
loads over it, the table is gone, and without a record the key is back
|
|
815
|
+
on its ledge. The full table is too big to keep for every level. One bit
|
|
816
|
+
per object is enough.
|
|
817
|
+
|
|
818
|
+
**How.**
|
|
819
|
+
|
|
820
|
+
1. **Number the persistent objects per level.** Each level lists its
|
|
821
|
+
objects in a fixed order; an object's index is its bit number. The
|
|
822
|
+
order must not change between builds that share save files or
|
|
823
|
+
passwords.
|
|
824
|
+
2. **One packed area for the whole game.** A table of byte offsets, one
|
|
825
|
+
per level, worked out from the object counts:
|
|
826
|
+
`start[l + 1] = start[l] + (count[l] + 7) / 8`. Levels of different
|
|
827
|
+
sizes share one array with no padding beyond the last byte of each.
|
|
828
|
+
3. **Leaving a level.** Clear the level's bytes, then set bit i for each
|
|
829
|
+
object i that is done (collected, opened, killed). Bit i is byte
|
|
830
|
+
`i >> 3`, mask `1 << (i & 7)`, from an 8-byte mask table.
|
|
831
|
+
4. **Entering a level.** Rebuild the working state from the bits before
|
|
832
|
+
the actors are placed. With `actor_activation_window`, a set bit is
|
|
833
|
+
the entry's dead bit: the scan never wakes that actor.
|
|
834
|
+
5. **Global flags.** Story events that belong to no level (the generator
|
|
835
|
+
switched on, a character met) go in a separate small bit area,
|
|
836
|
+
addressed the same way by flag number.
|
|
837
|
+
6. **Saving.** A save file, a checkpoint or a password takes the packed
|
|
838
|
+
areas and the global flags as they are. Run step 3 for the current
|
|
839
|
+
level first, or the save misses what happened since it was entered.
|
|
840
|
+
|
|
841
|
+
**What not to persist.** Anything the game rebuilds on entry: effects,
|
|
842
|
+
bullets, respawning enemies, timers. Position and hit points of a live
|
|
843
|
+
actor: on return the actor starts at its placed position with full
|
|
844
|
+
health, and only its dead bit survives. A design that needs more (an
|
|
845
|
+
actor that follows the player between levels) keeps a few full records
|
|
846
|
+
aside. Hessian keeps 24 such records in a checkpoint (`MAX_SAVEACT`,
|
|
847
|
+
`memory.s`; read, not run).
|
|
848
|
+
|
|
849
|
+
**Size.** One bit per object (arithmetic): 40 objects is 5 bytes, a game
|
|
850
|
+
of 20 levels with 60 objects each is 160 bytes (8 a level; arithmetic). Hessian's limits are 80
|
|
851
|
+
level actors and 96 level objects a level, at most 22 bytes a level, and
|
|
852
|
+
16 global flags in 2 bytes (`memory.s`; arithmetic from its constants).
|
|
853
|
+
A password holds far less: 6 letters carry 20 bits once a 10-bit
|
|
854
|
+
checksum is paid (`password_encoding` below), so a password game keeps a
|
|
855
|
+
few global flags, not per-level bits.
|
|
856
|
+
|
|
857
|
+
**Why it works.** The bits are a projection of the level table onto the
|
|
858
|
+
one fact that must outlive it. Clearing the level's bytes before setting
|
|
859
|
+
bits makes the save idempotent, so leaving a level twice writes the same
|
|
860
|
+
bytes. The polarity is a choice: the recipe sets a bit for done and
|
|
861
|
+
starts a new game from all zeros. Hessian sets a bit for an actor that
|
|
862
|
+
still exists, and a new game fills the actor bits with `$FF` and the
|
|
863
|
+
object bits with `$00` (`level.s` `SaveLevelState`, `script00.s`; read,
|
|
864
|
+
not run).
|
|
865
|
+
|
|
866
|
+
**Variations.**
|
|
867
|
+
|
|
868
|
+
- **Only some objects persist.** Hessian gives a bit only to objects
|
|
869
|
+
that are switches or animate, and none to objects that deactivate by
|
|
870
|
+
themselves (`IsLevelObjectPersistent` in `level.s`; read, not run).
|
|
871
|
+
That saves bits and keeps a door that closes on a timer from being
|
|
872
|
+
saved open.
|
|
873
|
+
- **Bits in place of the working copy.** A game with few objects can test
|
|
874
|
+
and set the bits directly while playing and skip steps 3 and 4. A
|
|
875
|
+
bit test costs a shift, a table read and an AND each time, so this
|
|
876
|
+
suits objects touched rarely.
|
|
877
|
+
|
|
878
|
+
**Cycle budget.** Runs on a level change, outside the frame loop.
|
|
879
|
+
Measured in VICE x64sc 3.10 with CIA1 timer A on the Oscar64 -O2 recipe,
|
|
880
|
+
a 40-object level with every object done, identical on PAL and NTSC:
|
|
881
|
+
leaving 2,224 cycles, entering 2,102, about 55 an object. That is a C
|
|
882
|
+
upper reference; a shift loop in assembly costs less (not measured here).
|
|
883
|
+
|
|
884
|
+
### Recipes
|
|
885
|
+
|
|
886
|
+
- `recipes/oscar64/password-state.md` — three levels of 20, 13 and 40 objects in one 10-byte area, done objects restored after two level changes, a global flag, the area checked against a Python value; also the password encoder below; PAL and NTSC
|
|
887
|
+
|
|
888
|
+
### Sources
|
|
889
|
+
|
|
890
|
+
- https://github.com/cadaver/hessian (MIT): `level.s` (`ChangeLevel`
|
|
891
|
+
calls `SaveLevelState`; `SaveLevelState` runs on level change and on
|
|
892
|
+
save; `IsLevelObjectPersistent`), `memory.s` (`MAX_LVLDATAACT` = 80,
|
|
893
|
+
`MAX_LVLOBJ` = 96, `MAX_PLOTBITS` = 16, `MAX_SAVEACT` = 24),
|
|
894
|
+
`script.s` (`DecodeBit`, `SetPlotBit`), `script00.s` (new-game fill).
|
|
895
|
+
Read for facts; no code is taken from it.
|
|
896
|
+
|
|
897
|
+
---
|
|
898
|
+
|
|
899
|
+
## password_encoding — A game state as a short password: bit packing, checksum, scramble and a safe alphabet
|
|
900
|
+
|
|
901
|
+
**Complexity:** low
|
|
902
|
+
|
|
903
|
+
**Why.** A tape or cartridge game with no disk has nowhere to save.
|
|
904
|
+
A password shown at the end of a level, and typed back later, carries the
|
|
905
|
+
state instead. Players copy it by hand, misread it and mistype it, so it
|
|
906
|
+
has to be short, use letters that cannot be confused, and reject a wrong
|
|
907
|
+
entry instead of loading a broken state. It should also not be editable:
|
|
908
|
+
if level 3 and level 4 give passwords one letter apart, players find the
|
|
909
|
+
pattern.
|
|
910
|
+
|
|
911
|
+
**How.**
|
|
912
|
+
|
|
913
|
+
1. **Choose the state and its bits.** Only what the game cannot rebuild:
|
|
914
|
+
level 0 to 31 is 5 bits, lives 0 to 7 is 3, eight items 8, four story
|
|
915
|
+
flags 4. The recipe's total is 20 bits. Pack them into bytes with
|
|
916
|
+
shifts and masks.
|
|
917
|
+
2. **Checksum.** Append C check bits computed from the state. The recipe
|
|
918
|
+
uses a 10-bit CRC (polynomial `$233`) over the 20 bits. A random
|
|
919
|
+
string of valid letters then passes with odds of 1 in 2^C, 1 in 1,024
|
|
920
|
+
here (arithmetic).
|
|
921
|
+
3. **Length.** With a 32-letter alphabet each letter carries 5 bits, so
|
|
922
|
+
the password is ⌈(state bits + C) / 5⌉ letters: 30 bits make 6
|
|
923
|
+
(arithmetic). Each extra 5 bits of state costs one letter.
|
|
924
|
+
4. **Scramble.** Permute the bits, so no field sits in one letter, then
|
|
925
|
+
XOR each 5-bit group with a fixed key, so the empty state does not
|
|
926
|
+
print as `000000`. The recipe sends bit i to bit (7 × i) mod 30.
|
|
927
|
+
5. **Alphabet.** 32 letters, with one of each look-alike pair left out:
|
|
928
|
+
the recipe uses `012345679ABCDEFGHJKMNPQRSTUVWXYZ`, no O, I, L or 8.
|
|
929
|
+
On decode, O reads as 0, I and L as 1, 8 as B, so a misread copy
|
|
930
|
+
still works. Keep the password as PETSCII: digits are `$30` to `$39`
|
|
931
|
+
and letters `$41` to `$5A`, the bytes GETIN delivers. On screen the
|
|
932
|
+
letters are screen codes `$01` to `$1A`; convert when drawing.
|
|
933
|
+
6. **Decode.** Map each letter to its value, refusing any byte outside
|
|
934
|
+
the alphabet. Undo the key and the permutation, recompute the
|
|
935
|
+
checksum from the state bits and compare. On a mismatch load
|
|
936
|
+
nothing: tell the player, and leave the typed text in the field to
|
|
937
|
+
correct. Check field ranges too when a field does not use all its
|
|
938
|
+
values (22 levels in a 5-bit field).
|
|
939
|
+
|
|
940
|
+
**Why a single mistype is always caught.** The CRC and the permutation
|
|
941
|
+
are linear over bits and the key is a constant XOR. So a wrong letter
|
|
942
|
+
changes the decoded 30 bits by a pattern that depends only on the
|
|
943
|
+
position and the wrong value, never on the state. The recipe's Python
|
|
944
|
+
model tries all 6 × 31 = 186 such patterns and none passes the CRC, which
|
|
945
|
+
covers every state (rung 1 for the model, rung 3 for the argument). The
|
|
946
|
+
C64 program repeats it on 16 passwords, 2,976 strings, all rejected,
|
|
947
|
+
measured in VICE. The model also caught all 5,079,040 swaps of two unequal neighbouring
|
|
948
|
+
letters over all 2^20 states (run here).
|
|
949
|
+
|
|
950
|
+
**How unrelated neighbouring states look.** A linear scheme cannot
|
|
951
|
+
change every letter. In the model, states one bit apart give passwords
|
|
952
|
+
that differ in 2 to 5 of the 6 letters, and level n and n + 1 at 3 lives
|
|
953
|
+
differ in 3 to 6 (every state from 0 by a stride of 997, and all 31
|
|
954
|
+
level pairs). The CRC does the spreading: one state bit moves one
|
|
955
|
+
password bit, but it changes several CRC bits. A nonlinear mix, such as
|
|
956
|
+
a few Feistel rounds with a table lookup, spreads further, and loses the
|
|
957
|
+
state-independent proof above.
|
|
958
|
+
|
|
959
|
+
**Variations.**
|
|
960
|
+
|
|
961
|
+
- **Compress before packing.** Ouroboros, a C64 game, stores 50
|
|
962
|
+
level-complete flags. It run-length codes them with a prefix-free
|
|
963
|
+
code, permutes the bits, and prints usually 5 letters from a 32-letter
|
|
964
|
+
alphabet with no 0, O, 1 or I, with a checksum (devlog; not measured
|
|
965
|
+
here).
|
|
966
|
+
- **Larger alphabets and longer passwords.** On the NES, Metroid's
|
|
967
|
+
password is 24 characters from a 64-letter alphabet, 144 bits with an
|
|
968
|
+
8-bit checksum and a roll byte; Simon's Quest uses 16 characters from
|
|
969
|
+
32 (forum report, not measured here). A 64-letter alphabet needs lower
|
|
970
|
+
case or punctuation, which a C64 player types with SHIFT.
|
|
971
|
+
|
|
972
|
+
**Cycle budget.** Runs once per level end or prompt, outside the frame
|
|
973
|
+
loop. Measured in VICE x64sc 3.10 with CIA1 timer A on the Oscar64 -O2
|
|
974
|
+
recipe, all 20 state bits set, identical on PAL and NTSC: encode 3,639
|
|
975
|
+
cycles, decode 3,737, a decode that fails the checksum 3,702, one that
|
|
976
|
+
meets a bad letter at the first byte 8. The bit loops dominate; a
|
|
977
|
+
hand-written loop that shifts the word through the carry costs less
|
|
978
|
+
(not measured here).
|
|
979
|
+
|
|
980
|
+
### Recipes
|
|
981
|
+
|
|
982
|
+
- `recipes/oscar64/password-state.md` — 20 state bits plus a 10-bit CRC in 6 letters; 4,096 states round trip, every single-letter mistype of 16 passwords rejected, look-alike letters folded, a Python model of the encoder, cycles on screen; also per-level world bits; PAL and NTSC
|
|
983
|
+
|
|
984
|
+
### Sources
|
|
985
|
+
|
|
986
|
+
- https://aardvark-soup.itch.io/ouroboros64/devlog/1635214/designing-a-retro-password-save-system-thats-not-a-pain-to-use
|
|
987
|
+
(Ouroboros: 50 flags, run-length and prefix-free coding, bit
|
|
988
|
+
permutation, 32-letter alphabet without 0, O, 1, I, usually 5 letters,
|
|
989
|
+
a checksum; devlog, not measured here).
|
|
990
|
+
- https://forums.nesdev.org/viewtopic.php?t=8657 (Metroid and Simon's
|
|
991
|
+
Quest password formats; forum report, not measured here).
|
|
992
|
+
|
|
993
|
+
## nav_area_pathfinding — Platform-graph routes for chasing enemies, with a next-hop table and time-sliced line of sight
|
|
994
|
+
|
|
995
|
+
**Complexity:** medium
|
|
996
|
+
**Region:** both
|
|
997
|
+
**Cost:** cycles_per_frame=369
|
|
998
|
+
**Cost basis:** measured-vice
|
|
999
|
+
**Cost measured on:** oscar64-nav-area-pathfinding (per actor, worst line-of-sight step, in the vertical blank)
|
|
1000
|
+
|
|
1001
|
+
### Why
|
|
1002
|
+
|
|
1003
|
+
A chasing enemy in a side-view game cannot walk straight at the player:
|
|
1004
|
+
the player may be on a platform above, reached by a ladder at the far
|
|
1005
|
+
end, or below a drop the enemy has to walk off. `tile_grid_collision`
|
|
1006
|
+
and `slope_collision` move an actor through the map; they do not say
|
|
1007
|
+
which way to go. A search over map cells answers that, but at a cost per
|
|
1008
|
+
query the CPU cannot pay for several actors every frame. A platform
|
|
1009
|
+
level is mostly runs of floor, so the search can run over those runs
|
|
1010
|
+
instead: a graph of a dozen areas rather than hundreds of cells.
|
|
1011
|
+
|
|
1012
|
+
### How
|
|
1013
|
+
|
|
1014
|
+
1. **Areas.** At level load, scan the map row by row. A cell is
|
|
1015
|
+
standable when it is not solid and the cell below is solid, or is the
|
|
1016
|
+
top rung of a ladder. A maximal run of standable cells on one row is
|
|
1017
|
+
an area: `(row, x_start, x_end)`. Keep an area number per standable
|
|
1018
|
+
cell, or scan the area list, to find an actor's area from its cell.
|
|
1019
|
+
2. **Links.** Join the areas with the moves an actor can make, each with
|
|
1020
|
+
a cost (the recipe counts cells moved):
|
|
1021
|
+
- **walk** off an end cell onto an area one row lower, and back up;
|
|
1022
|
+
- **drop** off an end cell onto the first area below, one way only;
|
|
1023
|
+
- **jump** over a gap of one or two cells to an area on the same row,
|
|
1024
|
+
both ways, if the cells over the gap are clear;
|
|
1025
|
+
- **ladder** from the area over its top rung to the area at its foot,
|
|
1026
|
+
both ways.
|
|
1027
|
+
Each link stores its start cell in the source area and its end cell
|
|
1028
|
+
in the target, so the actor knows where to walk before it takes it.
|
|
1029
|
+
3. **Next-hop table.** For every pair (from, to), store the first link
|
|
1030
|
+
on a cheapest route: N² bytes for N areas. Build it at level load
|
|
1031
|
+
with one search per destination (Dijkstra over the links read
|
|
1032
|
+
backwards, or Floyd-Warshall over the whole graph), then, for each
|
|
1033
|
+
pair, take the lowest-numbered link whose cost plus the rest of the
|
|
1034
|
+
route equals the route cost. A fixed tie rule makes the table
|
|
1035
|
+
repeatable.
|
|
1036
|
+
4. **Per-frame query.** A standing actor looks up its area and the
|
|
1037
|
+
target's, reads the table, and walks one step toward the link's start
|
|
1038
|
+
cell, or starts the link if it is there. In the same area it walks
|
|
1039
|
+
straight at the target. A link in progress is played cell by cell and
|
|
1040
|
+
needs no query.
|
|
1041
|
+
5. **Line of sight.** Step a line from the actor's cell to the target's
|
|
1042
|
+
through the tile map, one cell per call, comparing a doubled signed
|
|
1043
|
+
error term with the deltas, and stop at the first solid
|
|
1044
|
+
cell or at the target. One step per actor per frame, on frames the
|
|
1045
|
+
actor does not query, keeps the cost flat; a ray of k cells answers
|
|
1046
|
+
after k frames.
|
|
1047
|
+
|
|
1048
|
+
### Why it works
|
|
1049
|
+
|
|
1050
|
+
The area count, not the map size, sets the cost. Twelve areas and 43
|
|
1051
|
+
links cover the recipe's 40 x 22 map, so one search visits twelve nodes,
|
|
1052
|
+
and the whole answer set is 144 bytes. After the build, a decision is
|
|
1053
|
+
two lookups and one table read, cheap enough to run for every actor
|
|
1054
|
+
every frame. The links carry the movement rules, so the route chosen
|
|
1055
|
+
over areas is exactly the path the actor plays out on screen.
|
|
1056
|
+
|
|
1057
|
+
### Variations
|
|
1058
|
+
|
|
1059
|
+
- **Search at run time.** Without the table, run one search from the
|
|
1060
|
+
target each time it changes area: about 16,000 cycles for twelve areas
|
|
1061
|
+
in the recipe, most of a PAL frame. It saves the N² bytes and suits a
|
|
1062
|
+
level with many areas and one pursuer.
|
|
1063
|
+
- **Greedy one hop.** Cadaver's c64gameframework (`ai.s`, MIT) stores
|
|
1064
|
+
up to 48 nav areas per zone (`defines.s`) as left, right, top and
|
|
1065
|
+
bottom bounds plus a type (platform, ladder, slope up-right, slope
|
|
1066
|
+
up-left), built by its editor and loaded after the zone map
|
|
1067
|
+
(`level.s`). It does no global search: an actor picks, among areas
|
|
1068
|
+
whose edges touch its own, the one nearest the target. That is cheap
|
|
1069
|
+
and needs no table, but it can walk into a dead end that a real route
|
|
1070
|
+
avoids.
|
|
1071
|
+
- **Walking cost.** The recipe's costs count cells moved during links
|
|
1072
|
+
only, so walking inside an area is free to the search. Splitting
|
|
1073
|
+
areas at link points and adding the walk from entry to exit makes
|
|
1074
|
+
routes exact at the price of more areas.
|
|
1075
|
+
- **Hand-placed junctions.** Paradroid's robots follow patrol routes
|
|
1076
|
+
drawn by hand: Braybrook keyed the junction points and the valid
|
|
1077
|
+
directions from each into the assembler, and robots pause at a
|
|
1078
|
+
junction before moving off (his diary, part 3; no search, not measured
|
|
1079
|
+
here).
|
|
1080
|
+
- **Rounds of line of sight.** c64gameframework runs one actor's line
|
|
1081
|
+
check per frame, round-robin, capped at 19 steps (`MAX_LINEDIST` in
|
|
1082
|
+
`ai.s`), instead of one step per actor per frame.
|
|
1083
|
+
- **Pixel coordinates.** The recipe works in cells, so every coordinate
|
|
1084
|
+
and ray error term fits a signed byte (the listing uses 16-bit `int`
|
|
1085
|
+
anyway). A ray stepped in pixels, or
|
|
1086
|
+
across a map more than 63 cells wide, has deltas or an error term past
|
|
1087
|
+
127, and a `BMI` on its error term gives the wrong answer;
|
|
1088
|
+
compare with the carry or in 16 bits (`signed_compare_bmi_overflow`).
|
|
1089
|
+
- **Diagonal corners.** A ray that moves one cell in x and y in one step
|
|
1090
|
+
passes between two solid cells that touch at a corner. Step x and y
|
|
1091
|
+
separately if walls must not leak.
|
|
1092
|
+
|
|
1093
|
+
### Cycle budget
|
|
1094
|
+
|
|
1095
|
+
Measured in VICE x64sc 3.10 with CIA1 timers, interrupts masked, Oscar64
|
|
1096
|
+
`-O2`, one actor, identical on PAL and NTSC (rung 1): the worst query is
|
|
1097
|
+
154 cycles and the worst line-of-sight step 369, including 5 cycles of
|
|
1098
|
+
timer overhead. They never share a frame, so the technique's worst frame
|
|
1099
|
+
is 369 cycles per actor, about 1.9% of a PAL frame; a frame that moves
|
|
1100
|
+
the actor along a link in progress runs neither. Eight actors on their
|
|
1101
|
+
ray frames at once would be about 2,950 cycles (arithmetic).
|
|
1102
|
+
|
|
1103
|
+
The build runs once, at level load: 703,502 cycles on PAL (709,939 on
|
|
1104
|
+
NTSC, where the screen's badlines take a larger share), about 36 PAL
|
|
1105
|
+
frames. Of that, finding areas and links is 407,948, the twelve searches
|
|
1106
|
+
at most 16,061 each, and the next-hop pass 105,602.
|
|
1107
|
+
|
|
1108
|
+
### Recipes
|
|
1109
|
+
|
|
1110
|
+
- `recipes/oscar64/nav-area-pathfinding.md` — twelve areas joined by walks, drops, jumps and ladders; a chaser follows a player marker over eight waypoints; every hop checked against a Floyd-Warshall table from Python, the trail measured on the screenshot, build, query and ray-step cycles on screen, PAL and NTSC
|
|
1111
|
+
|
|
1112
|
+
### Sources
|
|
1113
|
+
|
|
1114
|
+
- https://github.com/cadaver/c64gameframework (MIT): `ai.s` (nav area
|
|
1115
|
+
types, the nearest-connected-area choice, `DoLineCheck`,
|
|
1116
|
+
`MAX_LINEDIST`), `actor.s` (one line check per frame), `level.s` and
|
|
1117
|
+
`defines.s` (nav areas loaded per zone, `MAX_NAVAREAS`). Read for
|
|
1118
|
+
facts; no code is taken from it.
|
|
1119
|
+
- https://codetapper.com/c64/diary-of-a-game/paradroid/birth-of-a-paradroid-part-3/
|
|
1120
|
+
(Andrew Braybrook's Paradroid diary; patrol junctions; not measured
|
|
1121
|
+
here).
|
|
1122
|
+
|
|
1123
|
+
---
|
|
1124
|
+
|
|
1125
|
+
## two_player_state_swap — Two players from one game loop: a per-player state block swapped on death, or both ports read every frame
|
|
1126
|
+
|
|
1127
|
+
**Complexity:** low
|
|
1128
|
+
**Region:** both
|
|
1129
|
+
**Uses registers:** DC00, DC01
|
|
1130
|
+
**Requires:** joystick_edge_detect, keyboard_matrix_scan
|
|
1131
|
+
**Cost:** cycles_per_frame=65, bytes_data=24
|
|
1132
|
+
**Cost basis:** measured-vice
|
|
1133
|
+
**Cost measured on:** oscar64-two-player (the per-frame port read; the swap runs once per death)
|
|
1134
|
+
|
|
1135
|
+
### Why
|
|
1136
|
+
|
|
1137
|
+
A second player is cheap on the C64 and expensive to add late. The
|
|
1138
|
+
machine has two control ports, and the arcade convention of the time
|
|
1139
|
+
gave a game two shapes: players take turns, each continuing their own
|
|
1140
|
+
game when the other dies, or both play at once on one screen. Both
|
|
1141
|
+
shapes break the same way when they are bolted on: alternating play
|
|
1142
|
+
that keeps one set of score, lives and level variables hands player 2
|
|
1143
|
+
player 1's game; simultaneous play that reads port 1 the way it reads
|
|
1144
|
+
port 2 picks up the keyboard. The pattern that avoids both is one
|
|
1145
|
+
per-player block and one careful port read.
|
|
1146
|
+
|
|
1147
|
+
### How
|
|
1148
|
+
|
|
1149
|
+
**The block.** Put everything that belongs to a player in one struct
|
|
1150
|
+
and hold two of them in an array. The recipe's block is eight bytes:
|
|
1151
|
+
|
|
1152
|
+
```
|
|
1153
|
+
offset size field
|
|
1154
|
+
0 2 score
|
|
1155
|
+
2 1 lives
|
|
1156
|
+
3 1 level
|
|
1157
|
+
4 1 cx cell column
|
|
1158
|
+
5 1 cy cell row
|
|
1159
|
+
6 2 rng 16-bit xorshift seed, stepped only on that player's frames
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
The map file of the recipe's build gives the array a size of `0010`,
|
|
1163
|
+
sixteen bytes, and the copy in play a size of `0008`, eight more, so
|
|
1164
|
+
the data cost of two players over one is sixteen bytes.
|
|
1165
|
+
The seed is in the block on purpose. A game that shares one random
|
|
1166
|
+
generator between alternating players gives player 2 a sequence that
|
|
1167
|
+
depends on how long player 1 survived, and two players who die at the
|
|
1168
|
+
same point in a level see different enemies. With the seed swapped in
|
|
1169
|
+
and out, each player's game is a function of their own input alone.
|
|
1170
|
+
|
|
1171
|
+
**The swap.** The game loop works on a copy in play, `g`. On death:
|
|
1172
|
+
take the life and reset the position in `g`; store `g` into
|
|
1173
|
+
`players[cur]`; flip `cur` if the other player still has lives; load
|
|
1174
|
+
`g` from `players[cur]`. That is two eight-byte copies, a compare and a
|
|
1175
|
+
branch, and the loop writes `g` back to `players[cur]` every frame so
|
|
1176
|
+
the array is always current. Measured with CIA2 timer A in the lower
|
|
1177
|
+
border, KERNAL IRQ held off, net of an empty span: 206 cycles per swap
|
|
1178
|
+
in the Oscar64 build, the same on PAL and NTSC. The entry routine of
|
|
1179
|
+
the next state (`game-design/game-structure.md`, game_state_machine)
|
|
1180
|
+
redraws the HUD from both blocks and marks whose turn it is, so the
|
|
1181
|
+
display is derived from the array and never carries state of its own.
|
|
1182
|
+
|
|
1183
|
+
**Simultaneous play and the port-1 hazard.** With both blocks live,
|
|
1184
|
+
each frame steps `players[0]` from port 2 and `players[1]` from port 1.
|
|
1185
|
+
Port 2 is `$DC00` bits 0 to 4. Port 1 is `$DC01` bits 0 to 4, and
|
|
1186
|
+
`$DC01` is also the keyboard's row input: a key pulls its row low while
|
|
1187
|
+
its column is driven low on `$DC00`. With the KERNAL IRQ live, SCNKEY
|
|
1188
|
+
runs every jiffy and leaves `$DC00` at `$7F`, column 7 selected, so
|
|
1189
|
+
between scans a held 1, left-arrow, CTRL, 2 or SPACE reads on port 1 as
|
|
1190
|
+
up, down, left, right or fire (`pitfalls/input.md`,
|
|
1191
|
+
joystick2_scan_phantom_press, Fix C). The mitigation is ordering under
|
|
1192
|
+
a held-off interrupt: `php`, `sei`, write `$FF` to `$DC00`, read `$DC00`
|
|
1193
|
+
for port 2 and `$DC01` for port 1, `plp`, every frame. No column is
|
|
1194
|
+
selected during the reads. The store alone is not enough: the jiffy IRQ
|
|
1195
|
+
comes from CIA1 timer A every 16,422 cycles on PAL and 17,046 on NTSC
|
|
1196
|
+
(`hardware/cia-reference.md`), the game loop is locked to the raster,
|
|
1197
|
+
so the interrupt drifts through the loop and can be taken between the
|
|
1198
|
+
store and the read, and then the scan puts `$7F` back before the read.
|
|
1199
|
+
The recipe measured it: the bare store-then-read shape, 50,000 times in
|
|
1200
|
+
a tight loop with the IRQ live, found `$7F` on the read after the store
|
|
1201
|
+
9 times; the held-off shape, 0 times. The compiled window was 19
|
|
1202
|
+
cycles, which is about one frame in 860 on PAL, a few times a minute in
|
|
1203
|
+
a real game. Holding the interrupt off for the 22 cycles of the pair
|
|
1204
|
+
delays the scan and loses none of it, and `plp` rather than `cli` keeps
|
|
1205
|
+
the interrupt off for a caller that already had it off. Leave the
|
|
1206
|
+
direction registers as IOINIT set them; clearing `$DC02` to read a
|
|
1207
|
+
joystick kills the keyboard (`pitfalls/input.md`,
|
|
1208
|
+
cia1_ddr_cleared_kills_keyboard). The recipe counts reads taken with a
|
|
1209
|
+
column selected: 0 of 40 with the held-off store, 40 of 40 without, on
|
|
1210
|
+
both models. It cannot count the phantom itself, because a headless
|
|
1211
|
+
VICE run holds no key; that half rests on the wiring in
|
|
1212
|
+
`hardware/cia-reference.md` and was not measured here.
|
|
1213
|
+
|
|
1214
|
+
### Why it works
|
|
1215
|
+
|
|
1216
|
+
The swap is correct because the block is complete: nothing a player
|
|
1217
|
+
would notice is kept outside it, so loading a block restores a game
|
|
1218
|
+
exactly, including the next random number. The port read is correct
|
|
1219
|
+
because it removes both halves of the port-1 hazard: the state the scan
|
|
1220
|
+
leaves, column 7 selected, which the `$FF` store clears, and the one
|
|
1221
|
+
timing case, the scan running between the store and the read, which
|
|
1222
|
+
the held-off interrupt makes impossible. A main-loop read never lands
|
|
1223
|
+
inside the scan (`pitfalls/input.md`, measured there), and with the
|
|
1224
|
+
scan unable to run between the store and the read, a read that first
|
|
1225
|
+
deselects every column sees only the stick.
|
|
1226
|
+
|
|
1227
|
+
### Variations
|
|
1228
|
+
|
|
1229
|
+
- Alternating play with a shared level: keep the level's world state in
|
|
1230
|
+
a third block and reload it from the level data on each turn, or both
|
|
1231
|
+
players play the same half-cleared screen.
|
|
1232
|
+
- A two-player game with the KERNAL IRQ replaced (`keyboard_matrix_scan`)
|
|
1233
|
+
needs no `$FF` store, since nothing else writes `$DC00`; the matrix
|
|
1234
|
+
scan and the port-1 read must then be ordered by the game itself, and
|
|
1235
|
+
a port-1 stick still reads as keys during the scan.
|
|
1236
|
+
- Three or four players through a port adapter are out of scope here;
|
|
1237
|
+
the block array extends, the port reads do not.
|
|
1238
|
+
|
|
1239
|
+
### Recipes
|
|
1240
|
+
|
|
1241
|
+
- `recipes/oscar64/two-player.md` — two sprites, one listing, `MODE`
|
|
1242
|
+
selects alternating or simultaneous from a byte at start; the
|
|
1243
|
+
alternating blocks checked against a host model after two scripted
|
|
1244
|
+
deaths; the simultaneous cells checked, the column-selected count 0
|
|
1245
|
+
with the `$FF` store and 40 without; swap and read cycles on screen;
|
|
1246
|
+
PAL and NTSC
|
|
1247
|
+
|
|
1248
|
+
## difficulty_ramp_tables — Level tables read from data, held at the last row, scaled by region
|
|
1249
|
+
|
|
1250
|
+
**Complexity:** low
|
|
1251
|
+
**Region:** both
|
|
1252
|
+
**Requires:** pal_ntsc_detection, object_pool, lfsr_random
|
|
1253
|
+
**Cost:** cycles_per_frame=415, cycles_per_frame_typical=53
|
|
1254
|
+
**Cost basis:** measured-vice
|
|
1255
|
+
**Cost measured on:** oscar64-difficulty-tables (spawn frame, screen on)
|
|
1256
|
+
|
|
1257
|
+
**Why.** A ramp built from constants cannot be tuned without a rebuild
|
|
1258
|
+
and cannot be tested by anyone but the programmer. It also cannot be
|
|
1259
|
+
scaled: a spawn interval of 60 written as a literal means 1.2 seconds
|
|
1260
|
+
on a PAL machine and 1.0 on an NTSC one, and every level of the game
|
|
1261
|
+
arrives a fifth early on the second. Put the knobs in a table with one
|
|
1262
|
+
row per level and every one of them becomes a byte a designer can
|
|
1263
|
+
change, a row a test can read, and a value a region scaler can pass
|
|
1264
|
+
through once at level start.
|
|
1265
|
+
|
|
1266
|
+
**How.** One row per level, one column per knob: enemy speed in 8.8
|
|
1267
|
+
pixels per frame, spawn interval in frames, most enemies alive at once,
|
|
1268
|
+
the end condition (a coin quota, a kill count, a distance) and a flag
|
|
1269
|
+
byte that switches hazards on. Write every column for one region, PAL,
|
|
1270
|
+
and say so in the comment above the table. At level start, clamp the
|
|
1271
|
+
level index to the last row, copy the row into working variables, and
|
|
1272
|
+
pass the frame and speed columns through a scaler: on NTSC multiply
|
|
1273
|
+
frames by 6/5 and speeds by 5/6, rounding to nearest, so the interval
|
|
1274
|
+
lasts as long and the enemy covers the same distance in the same real
|
|
1275
|
+
time. The spawner and movers read only the working variables; the frame
|
|
1276
|
+
loop never reads the table. Advance the level on
|
|
1277
|
+
the row's end condition, not on a timer, so a slower player gets a
|
|
1278
|
+
longer level and not a harder one. The recipe reaches level 3 at 1128
|
|
1279
|
+
frames on PAL and 1352 on NTSC, 22,497 and 22,577 milliseconds by the
|
|
1280
|
+
CIA, against 18,847 milliseconds when the same table runs unscaled on
|
|
1281
|
+
NTSC.
|
|
1282
|
+
|
|
1283
|
+
**Why it works.** The table is the whole ramp, so a level's difficulty
|
|
1284
|
+
is one row of bytes and the game's curve is the table read down a
|
|
1285
|
+
column. Clamping the index makes every level past the table a copy of
|
|
1286
|
+
the last row rather than a read of whatever follows it in memory, which
|
|
1287
|
+
is the pattern page's first check. The scaler is exact for the
|
|
1288
|
+
interval column when the row's frames divide by five, and within one
|
|
1289
|
+
frame otherwise; for speed the 8.8 fraction keeps the rounding under
|
|
1290
|
+
one part in two hundred. Doing the scaling once per level start keeps
|
|
1291
|
+
the per-frame path to a compare and a countdown.
|
|
1292
|
+
|
|
1293
|
+
**Variations.** A rank counter beside the table, as the pattern page
|
|
1294
|
+
describes from the Gradius account: a few additive counters the player
|
|
1295
|
+
never sees (frames survived, stages cleared, power-ups held), summed,
|
|
1296
|
+
shifted down and capped at a small number, then used as a second index
|
|
1297
|
+
that picks a harder row or adds to a column. It rises with success, and
|
|
1298
|
+
the cap is the whole point. Rubber banding by outcome rather than by
|
|
1299
|
+
time: scale the damage a hit does, or the drop rate of health, by how
|
|
1300
|
+
far the player is ahead of or behind the row's expectation, and never
|
|
1301
|
+
below a floor. Two whole tables, one per region, selected once by
|
|
1302
|
+
`pal_ntsc_detection`, when the rounding of a scaler is not acceptable
|
|
1303
|
+
for some column. The last-row hold can also be a loop: index modulo the
|
|
1304
|
+
row count, with a separate counter that adds to speed on each pass.
|
|
1305
|
+
|
|
1306
|
+
**Cycle budget.** Measured on the recipe with CIA1 timer A over one
|
|
1307
|
+
hundred calls, screen on, so badline stalls are inside the figures and
|
|
1308
|
+
they wander by a cycle or two between runs. The Cost line carries the
|
|
1309
|
+
spawner's worst frame, the one on which the interval has run out and
|
|
1310
|
+
it counts the eight-slot pool, rolls the LFSR once or twice, allocates
|
|
1311
|
+
a slot and fills it: 409 cycles on PAL and 412 on NTSC in the
|
|
1312
|
+
compensated build, 410 and 415 with the scaler compiled out, and the
|
|
1313
|
+
line states the largest of the four. The spawn itself is `object_pool`
|
|
1314
|
+
work, but the technique's spawner is what runs it, so that is the frame
|
|
1315
|
+
a plan has to fit. The ordinary frame, when the interval is counting
|
|
1316
|
+
down and nothing spawns, costs 51 cycles on PAL and 53 on NTSC. A
|
|
1317
|
+
level start, the clamp, the row copy and both scalers, costs 181
|
|
1318
|
+
cycles on PAL and 267 on NTSC in the compensated build, where the NTSC
|
|
1319
|
+
path runs the multiply and divide, and 113 or 118 with the scaler
|
|
1320
|
+
compiled out. A level start happens a handful of times in a game and
|
|
1321
|
+
is not a per-frame figure.
|
|
1322
|
+
|
|
1323
|
+
### Recipes
|
|
1324
|
+
|
|
1325
|
+
- `recipes/oscar64/difficulty-tables.md` — six-row table, eight-slot pool, coin quota per level, spikes from level 3, one define for the region scaler, frame and CIA time at level 3 printed on both models
|
|
1326
|
+
|
|
1327
|
+
---
|
|
1328
|
+
|
|
1329
|
+
## ghost_target_tile_ai — Maze-chase ghosts that steer by target tiles: look-ahead, no reversing, per-ghost targets and a scatter/chase timer
|
|
1330
|
+
|
|
1331
|
+
**Complexity:** medium
|
|
1332
|
+
**Region:** both
|
|
1333
|
+
**Cost:** cycles_per_frame=7227, cycles_per_frame_typical=4171
|
|
1334
|
+
**Cost basis:** measured-vice
|
|
1335
|
+
**Cost measured on:** oscar64-ghost-targeting (built worst frame, screen blanked)
|
|
1336
|
+
|
|
1337
|
+
### Why
|
|
1338
|
+
|
|
1339
|
+
A maze-chase game needs four or so pursuers that feel different, never
|
|
1340
|
+
dither, and fit in a frame together. A path search from each ghost to
|
|
1341
|
+
the player every time it reaches a junction does not fit: even the
|
|
1342
|
+
twelve-node run-time search of `nav_area_pathfinding` costs about 16,000
|
|
1343
|
+
cycles, and a tile maze has hundreds of tiles and dozens of junctions (the recipe's: 245 and 34). Pac-Man (Namco, 1980)
|
|
1344
|
+
does no search at all. Each ghost has a target tile and, at each
|
|
1345
|
+
junction, takes the exit whose next tile is nearest that target. The
|
|
1346
|
+
target rule gives each ghost its character. This page describes that
|
|
1347
|
+
rule as Jamey Pittman's Pac-Man Dossier documents it, adapted to a C64
|
|
1348
|
+
character maze.
|
|
1349
|
+
|
|
1350
|
+
### How
|
|
1351
|
+
|
|
1352
|
+
1. **Maze.** One byte per tile: bit d set when the neighbour in
|
|
1353
|
+
direction d is open. Directions are numbered in the tie order: up 0,
|
|
1354
|
+
left 1, down 2, right 3, so the reverse of d is d XOR 2. Build the
|
|
1355
|
+
table at level start from the map. The maze must have no dead ends,
|
|
1356
|
+
because a ghost may not turn back. The arcade screen is 28 x 36 tiles
|
|
1357
|
+
of 8 x 8 pixels (Dossier); a C64 text screen has 8 x 8 character
|
|
1358
|
+
cells, so one character is one tile.
|
|
1359
|
+
2. **Look-ahead.** When a ghost enters a tile, it looks at the next tile
|
|
1360
|
+
along its heading and decides now which way it will leave that tile.
|
|
1361
|
+
On arrival it turns to the stored direction and looks ahead again
|
|
1362
|
+
(Dossier: "whenever a ghost enters a new tile, it looks ahead to the
|
|
1363
|
+
next tile").
|
|
1364
|
+
3. **Junction test.** Take the look-ahead tile's exit mask and clear
|
|
1365
|
+
the reverse of the heading. One bit left is a corridor or a corner:
|
|
1366
|
+
take it, with no arithmetic. Two or three bits is a decision.
|
|
1367
|
+
4. **Choice.** For each remaining exit, the test tile is one step
|
|
1368
|
+
beyond the junction in that direction. Take the exit whose test tile
|
|
1369
|
+
is nearest the target; on a tie take the lowest direction number,
|
|
1370
|
+
that is up, then left, then down, then right. The Dossier says the
|
|
1371
|
+
ghost "triangulates" the distance, a straight-line measure; compare
|
|
1372
|
+
squared distances, dx² + dy², which order the candidates the same
|
|
1373
|
+
way and need only a table of squares (arithmetic: squaring is
|
|
1374
|
+
monotonic for non-negative numbers).
|
|
1375
|
+
5. **Targets.** Chase mode, per ghost (Dossier):
|
|
1376
|
+
- Blinky: Pac-Man's tile.
|
|
1377
|
+
- Pinky: four tiles ahead of Pac-Man in his direction of travel.
|
|
1378
|
+
- Inky: take the tile two ahead of Pac-Man, draw the vector from
|
|
1379
|
+
Blinky's tile to it, and double it. Inky needs Blinky's tile, so
|
|
1380
|
+
update Blinky first.
|
|
1381
|
+
- Clyde: Pac-Man's tile while Clyde is more than eight tiles from
|
|
1382
|
+
him, otherwise Clyde's scatter tile. The recipe tests squared
|
|
1383
|
+
distance > 64.
|
|
1384
|
+
Scatter mode: a fixed tile per ghost, outside the maze near its
|
|
1385
|
+
home corner, which it can never reach, so it circles the nearest
|
|
1386
|
+
block of walls.
|
|
1387
|
+
6. **The up bug.** In the arcade, when Pac-Man faces up, Pinky's offset
|
|
1388
|
+
is four up and four left, and Inky's intermediate tile two up and two
|
|
1389
|
+
left, from an overflow in the offset code (Dossier). A port chooses
|
|
1390
|
+
whether to reproduce it; the recipe does, behind `UP_BUG`.
|
|
1391
|
+
7. **Frightened mode.** A frightened ghost ignores its target. At a
|
|
1392
|
+
decision it takes a pseudo-random direction if that exit is open,
|
|
1393
|
+
otherwise the first open exit in the order up, left, down, right.
|
|
1394
|
+
The arcade reseeds its PRNG to the same value every level and every
|
|
1395
|
+
life (Dossier), so frightened paths repeat. A shift-register PRNG
|
|
1396
|
+
seeded with zero stays at zero, and every frightened ghost then
|
|
1397
|
+
tries up first (`lfsr_zero_state_lockup`); the recipe's xorshift
|
|
1398
|
+
seed is a fixed non-zero constant.
|
|
1399
|
+
8. **Reversals.** A ghost never reverses on its own. The game forces
|
|
1400
|
+
every ghost to reverse when the mode changes from chase to scatter,
|
|
1401
|
+
scatter to chase, or either into frightened; not when frightened
|
|
1402
|
+
ends (Dossier). A forced reversal points the ghost back at the tile
|
|
1403
|
+
it came from and makes a fresh look-ahead from there.
|
|
1404
|
+
9. **Mode timer.** Scatter and chase alternate on a timer that pauses
|
|
1405
|
+
while the ghosts are frightened. In seconds, from the Dossier:
|
|
1406
|
+
|
|
1407
|
+
| Phase | Level 1 | Levels 2-4 | Level 5 on |
|
|
1408
|
+
|---|---|---|---|
|
|
1409
|
+
| Scatter | 7 | 7 | 5 |
|
|
1410
|
+
| Chase | 20 | 20 | 20 |
|
|
1411
|
+
| Scatter | 7 | 7 | 5 |
|
|
1412
|
+
| Chase | 20 | 20 | 20 |
|
|
1413
|
+
| Scatter | 5 | 5 | 5 |
|
|
1414
|
+
| Chase | 20 | 1033 | 1037 |
|
|
1415
|
+
| Scatter | 5 | 1/60 | 1/60 |
|
|
1416
|
+
| Chase | for good | for good | for good |
|
|
1417
|
+
|
|
1418
|
+
Keep the table in seconds and convert with the region's frame rate
|
|
1419
|
+
(50 PAL, 60 NTSC); frame counts written for PAL run about 20% fast on
|
|
1420
|
+
NTSC (`pal_ntsc_tempo_mismatch`).
|
|
1421
|
+
|
|
1422
|
+
### Why it works
|
|
1423
|
+
|
|
1424
|
+
A decision looks at no more than three tiles and needs no memory
|
|
1425
|
+
between frames beyond each ghost's tile, heading and planned turn. The
|
|
1426
|
+
no-reverse rule keeps a ghost from oscillating between two tiles when
|
|
1427
|
+
its target moves, and it commits the ghost to a corridor once chosen.
|
|
1428
|
+
The four target rules are the whole of the ghosts' personalities: the
|
|
1429
|
+
same choice code serves all four modes, and only the target tile
|
|
1430
|
+
differs. Pinky aims ahead and so tends to arrive from the front; Inky's
|
|
1431
|
+
target depends on Blinky and swings widely; Clyde's switch at eight
|
|
1432
|
+
tiles makes him approach and retreat.
|
|
1433
|
+
|
|
1434
|
+
What it gets wrong: it is greedy by one tile. A ghost can take an exit
|
|
1435
|
+
that leads away from its target round a long wall, and it can circle
|
|
1436
|
+
for ever. The Dossier shows Clyde circling one block indefinitely,
|
|
1437
|
+
because his target flips between Pac-Man and his corner as he crosses
|
|
1438
|
+
the eight-tile ring. Scatter targets exist only to make each ghost
|
|
1439
|
+
circle its corner block. A game that wants a pursuer that always finds
|
|
1440
|
+
the player needs a path search or a distance map, at the cost above.
|
|
1441
|
+
|
|
1442
|
+
### Variations
|
|
1443
|
+
|
|
1444
|
+
- **Choose on arrival.** Deciding at the tile the ghost has just
|
|
1445
|
+
entered, not one ahead, drops the planned-turn byte. Targets are then
|
|
1446
|
+
one tile fresher, and the paths differ from the arcade's.
|
|
1447
|
+
- **No-up tiles.** The arcade forbids upward turns in two zones of its
|
|
1448
|
+
maze in scatter and chase, not in frightened (Dossier). A
|
|
1449
|
+
per-tile flag that clears the up bit from the mask does it. The
|
|
1450
|
+
recipe's maze has none.
|
|
1451
|
+
- **Manhattan distance.** |dx| + |dy| needs no squares, but ties become
|
|
1452
|
+
far more common and the tie order then decides most turns. The paths
|
|
1453
|
+
differ from the arcade's.
|
|
1454
|
+
- **Speeds.** The arcade slows frightened ghosts, nearly halves a
|
|
1455
|
+
ghost's speed in the side tunnels, and speeds Blinky up as the dots
|
|
1456
|
+
run out (Dossier).
|
|
1457
|
+
The recipe moves every actor one tile every four frames.
|
|
1458
|
+
|
|
1459
|
+
### Cycle budget
|
|
1460
|
+
|
|
1461
|
+
Measured in VICE x64sc 3.10 with CIA1 timer B, the display and sprites
|
|
1462
|
+
off, Oscar64 `-O2`, identical on PAL and NTSC (rung 1). The single
|
|
1463
|
+
figures include one 5-cycle timer pair, the frame figures two.
|
|
1464
|
+
|
|
1465
|
+
| Work | Cycles |
|
|
1466
|
+
|---|---|
|
|
1467
|
+
| One 3-way decision (`choose` only; the target is set before timing) | 578 |
|
|
1468
|
+
| One 1-exit tile (a corridor or a corner) | 89 |
|
|
1469
|
+
| Built worst frame: scatter ends, all four ghosts reverse onto a 4-way junction, then all four step onto another 3-way decision in chase | 6,943 |
|
|
1470
|
+
| The same frame with frightened mode starting too: a second reversal, step decisions from the PRNG | 7,227 |
|
|
1471
|
+
| Worst frame of the recipe's 1,600-frame run | 4,171 |
|
|
1472
|
+
|
|
1473
|
+
The Cost line states the 7,227-cycle frame, 37% of a PAL frame (19,656
|
|
1474
|
+
cycles; arithmetic), and the run's worst frame, 4,171, as its typical
|
|
1475
|
+
figure: play reaches it, while the built frame needs every ghost at a
|
|
1476
|
+
junction on a mode switch. A mode switch reverses all four ghosts at once, so
|
|
1477
|
+
the worst frame is a switch frame that is also a step frame. On the
|
|
1478
|
+
three frames in four when no actor steps and no mode changes, the work
|
|
1479
|
+
is the mode-timer update only (not timed separately). An assembler version
|
|
1480
|
+
would be cheaper; not measured here. The recipe's technique code is
|
|
1481
|
+
757 bytes and its tables 882 bytes, of which 704 are the exit masks at
|
|
1482
|
+
a 32-byte row stride (Oscar64 map).
|
|
1483
|
+
|
|
1484
|
+
### Recipes
|
|
1485
|
+
|
|
1486
|
+
- `recipes/oscar64/ghost-targeting.md` — four ghosts as sprites and a Pac-Man stand-in on a scripted loop in an original 27 x 22 character maze; the level-1 mode table at 16 frames per second with one frightened spell; every frame's ghost tiles checksummed against a Python model; decision and worst-frame cycles on screen, PAL and NTSC
|
|
1487
|
+
|
|
1488
|
+
### Sources
|
|
1489
|
+
|
|
1490
|
+
- https://www.gamedeveloper.com/design/the-pac-man-dossier (Jamey
|
|
1491
|
+
Pittman, The Pac-Man Dossier): tile grid, look-ahead, test tiles, tie
|
|
1492
|
+
order, no-reverse rule and forced reversals, the four chase targets
|
|
1493
|
+
and the up-direction overflow, scatter targets in dead space,
|
|
1494
|
+
frightened PRNG, the mode table, the no-up zones and Clyde's endless
|
|
1495
|
+
loop. Rules only; no arcade code was read.
|
|
1496
|
+
|
|
1497
|
+
## falling_block_rules — Falling-block rules: collision, rotation, gravity table, DAS, lock, line clear, scoring and a 7-bag
|
|
1498
|
+
|
|
1499
|
+
**Complexity:** medium
|
|
1500
|
+
**Region:** both
|
|
1501
|
+
**Requires:** joystick_autorepeat, lfsr_random
|
|
1502
|
+
**Cost:** cycles_per_frame=5888
|
|
1503
|
+
**Cost basis:** measured-vice
|
|
1504
|
+
**Cost measured on:** oscar64-falling-blocks (constructed upper bound, screen on)
|
|
1505
|
+
|
|
1506
|
+
**Why.** A falling-block game is small, but its rules decide whether it
|
|
1507
|
+
feels right. The renderer in `text_mode_overlay_render` draws a board and
|
|
1508
|
+
a piece; it does not say when the piece falls, how a held direction
|
|
1509
|
+
repeats, what rotating against a wall does, or how cleared rows collapse.
|
|
1510
|
+
Each of those is a table or a counter, and each has a reference value in
|
|
1511
|
+
NES Tetris that players know. This technique is that rules layer, with
|
|
1512
|
+
the rules the recipe implements named and their sources cited.
|
|
1513
|
+
|
|
1514
|
+
**How.** The rules the recipe implements:
|
|
1515
|
+
|
|
1516
|
+
| Rule | This page | Reference |
|
|
1517
|
+
|---|---|---|
|
|
1518
|
+
| Board | 20 rows of 10 bytes, 0 empty, 1-8 a colour; a fill count per row | own |
|
|
1519
|
+
| Pieces | seven, each four cells in a 4x4 box, two tables (x and y) per piece and rotation | own tables; I, S, Z have two states, O one |
|
|
1520
|
+
| Rotation | clockwise on the fire press; try the new state in place, then one right, then one left | NES has no kick (tetris.wiki) |
|
|
1521
|
+
| Gravity | frames per row by level, 48 at level 0 down to 1 at level 29 on NTSC; 36 down to 1 at level 19 on PAL | NES NTSC and PAL tables (tetris.wiki) |
|
|
1522
|
+
| DAS | first step on the press, second 16 frames later, then every 6 (NTSC); 12 and 4 on PAL; the counter clears on release | NES values (tetris.wiki); NES clears the counter on a new press, not on release, and a blocked tap charges it fully; the recipe's DAS is the simple form |
|
|
1523
|
+
| Soft drop | down held: one row every 2 frames, or gravity if faster | NES 1/2 G (tetris.wiki) |
|
|
1524
|
+
| Lock | at once, on the frame a step down fails | NES has no lock delay (tetris.wiki) |
|
|
1525
|
+
| Entry delay | none: the next piece spawns in the lock frame | NES 10-18 frames ARE by lock height, plus a line-clear delay of 17-20 frames (tetris.wiki) |
|
|
1526
|
+
| Score | 40, 100, 300, 1200 for 1-4 lines at once, times level + 1, at the level after the clear's level-up | NES the same (tetris.wiki Scoring) |
|
|
1527
|
+
| Level | +1 every 10 lines from level 0 | NES from level 0; a higher start level waits min(10s + 10, max(100, 10s - 50)) lines first (tetris.wiki) |
|
|
1528
|
+
| Randomiser | 7-bag: a shuffled set of all seven, dealt out, then reshuffled | NES instead rolls 0-7 and rerolls once, 0-6, on a repeat or 7 (tetris.wiki) |
|
|
1529
|
+
| Game over | the new piece does not fit where it spawns | NES the same |
|
|
1530
|
+
|
|
1531
|
+
The collision test is the whole engine. `fits(piece, rot, x, y)` adds
|
|
1532
|
+
each of the four cell offsets to (x, y), refuses a cell outside the well
|
|
1533
|
+
and refuses a cell whose board byte is non-zero. Everything else calls
|
|
1534
|
+
it: a move tries x ± 1, a gravity step tries y + 1, a rotation tries the
|
|
1535
|
+
next state at x, x + 1 and x - 1, and a spawn tries the spawn position.
|
|
1536
|
+
Do the range test on the sum as an unsigned byte and one compare catches
|
|
1537
|
+
both a negative column and one past 9. With the recipe's tables, an
|
|
1538
|
+
upright I at the left wall cannot rotate (every try overlaps the wall)
|
|
1539
|
+
and one at the right wall kicks one left (run on the Python model on the
|
|
1540
|
+
recipe page; the J kick at the right wall is the one the C program
|
|
1541
|
+
checks).
|
|
1542
|
+
|
|
1543
|
+
DAS is `joystick_autorepeat` with its delay and rate set per region: one
|
|
1544
|
+
age counter per direction, cleared on release, and a step on age 1, on
|
|
1545
|
+
age 1 + DELAY, and every RATE frames after that. Gravity is a counter
|
|
1546
|
+
compared with the table entry for the level, clamped at level 29; soft
|
|
1547
|
+
drop lowers the limit to 2. When the step down fails the piece is written
|
|
1548
|
+
into the board and each touched row's fill count goes up by one.
|
|
1549
|
+
|
|
1550
|
+
Line clear reads only the fill counts of the rows the piece touched, at
|
|
1551
|
+
most four. If none reached 10 the frame is done. Otherwise collapse in
|
|
1552
|
+
one pass from the lowest full row upward: skip a full row, copy any other
|
|
1553
|
+
row down to the next free destination, and zero the rows left at the
|
|
1554
|
+
top. Keep the highest occupied row in a byte: the pass stops there, the
|
|
1555
|
+
rows above it are already empty, and only the rows from the old top to
|
|
1556
|
+
the lowest cleared row need redrawing.
|
|
1557
|
+
|
|
1558
|
+
**Why it works.** The fill count turns the full-row test into one
|
|
1559
|
+
compare per touched row instead of ten reads. A single pass from the
|
|
1560
|
+
bottom up is enough because a destination is always at or below its
|
|
1561
|
+
source, so no row is overwritten before it is copied. Clearing only
|
|
1562
|
+
touched rows is complete because a row that was not full before the lock
|
|
1563
|
+
and was not touched cannot be full after it. The bag bounds the drought
|
|
1564
|
+
of any piece: the longest run between two of the same piece is 12 others,
|
|
1565
|
+
when it is first in one bag and last in the next (arithmetic). A plain
|
|
1566
|
+
LFSR modulo 7 has no bound. The modulo in the shuffle has a bias of at
|
|
1567
|
+
most 7 in 65,535 on a 16-bit state (arithmetic), too small to matter.
|
|
1568
|
+
That figure treats each draw as independent, and they are not: a Galois
|
|
1569
|
+
step shifts the state one bit right and flips at most the four tap bits
|
|
1570
|
+
of `$B400`, so consecutive states share most of their 15 shifted bits
|
|
1571
|
+
and the draws within one shuffle are correlated (arithmetic from the
|
|
1572
|
+
recipe's `rnd`, not measured as a distribution here).
|
|
1573
|
+
|
|
1574
|
+
**Region.** The NES tables are frame counts. The recipe finds the
|
|
1575
|
+
region by looking for raster line 280, which only PAL has, and selects
|
|
1576
|
+
the NES PAL gravity and DAS tables on PAL. NES PAL runs at 50.007 Hz and
|
|
1577
|
+
NTSC at 60.099 Hz (tetris.wiki); the C64 at 50.125 Hz and 59.826 Hz
|
|
1578
|
+
(985,248 / 19,656 and 1,022,727 / 17,095, from
|
|
1579
|
+
`hardware/pal-ntsc-reference.md`), so the NES tables carry over within
|
|
1580
|
+
0.5 per cent (0.24 on PAL, 0.45 on NTSC, arithmetic). One table on both regions makes every
|
|
1581
|
+
level a fifth faster on NTSC (`pal_ntsc_tempo_mismatch`).
|
|
1582
|
+
|
|
1583
|
+
**Variations.** A lock delay: count frames on the ground and lock at a
|
|
1584
|
+
limit, reset by a move or a rotation, with a cap on resets. A wider kick
|
|
1585
|
+
table (try ±2 for the I) or none at all, as on the NES. A two-row
|
|
1586
|
+
representation where each row is a 16-bit mask with wall bits set: the
|
|
1587
|
+
collision test becomes four ANDs of a shifted piece row, at the cost of
|
|
1588
|
+
separate colour storage. Hard drop on up: step down until `fits` fails,
|
|
1589
|
+
then lock in the same frame. A next-piece preview is the bag's next
|
|
1590
|
+
entry.
|
|
1591
|
+
|
|
1592
|
+
**Cycle budget.** Measured in VICE x64sc with CIA1 timer A, KERNAL IRQ
|
|
1593
|
+
off, screen on, starting at raster line 250, so badline stalls are in
|
|
1594
|
+
the figures. The Cost line is the rules part of the recipe's worst-frame
|
|
1595
|
+
subject: a full-height stack with column 9 open in every row, an upright
|
|
1596
|
+
I locking into the bottom four with fire, left and right held, so three
|
|
1597
|
+
rotation tries and both moves are tested and refused before the lock,
|
|
1598
|
+
the four-line clear, sixteen rows collapsed and a spawn. It read 5,717
|
|
1599
|
+
cycles on PAL and 5,888 on NTSC, 29 and 34 per cent of the 19,656 and
|
|
1600
|
+
17,095 cycle frames. That stack cannot occur in play, because the spawn
|
|
1601
|
+
would fail first, and a stick cannot hold left and right at once, so it
|
|
1602
|
+
is an upper bound. The render of the same frame, timed apart, is
|
|
1603
|
+
`text_mode_overlay_render`'s work: erasing and redrawing the locked
|
|
1604
|
+
piece, redrawing all twenty rows and drawing the new piece took 9,311
|
|
1605
|
+
cycles on PAL and 9,394 on NTSC. The four-line clear in the scripted
|
|
1606
|
+
game, rules and render together with the stack top at row 14, took 6,276
|
|
1607
|
+
on PAL and 6,491 on NTSC and was the dearest frame of the run; frames
|
|
1608
|
+
with no lock peaked at 2,113 on both.
|
|
1609
|
+
|
|
1610
|
+
### Recipes
|
|
1611
|
+
|
|
1612
|
+
- `recipes/oscar64/falling-blocks.md` — the game in character mode, joystick port 2; the default build plays five scripted pieces through the input path (single, double, triple, four-line clear, level-up, DAS moves, a kick at the wall) and checks board, score, bag and spawn against a Python model and the screen against the board, including a piece moved on its lock frame, with the worst-frame subject's rules and render timed apart on PAL and NTSC
|
|
1613
|
+
|
|
1614
|
+
### Sources
|
|
1615
|
+
|
|
1616
|
+
- https://tetris.wiki/Tetris_(NES) (NES Tetris): gravity tables (NTSC and PAL), DAS 16/6 and 12/4 and when its counter resets, soft drop 1/2 G, no lock delay, no wall kick, ARE and line-clear delay, level rule, randomiser, frame rates.
|
|
1617
|
+
- https://tetris.wiki/Scoring (scoring): 40, 100, 300, 1200 times level + 1, at the level after the clear.
|
|
1618
|
+
|
|
1619
|
+
---
|
|
1620
|
+
|
|
1621
|
+
## dig_and_refill — Dig-and-refill bricks, trapped guards and a greedy ladder chase (Lode Runner rules)
|
|
1622
|
+
|
|
1623
|
+
**Complexity:** medium
|
|
1624
|
+
**Region:** both
|
|
1625
|
+
**Cost:** cycles_per_frame=19759, cycles_per_frame_typical=4118
|
|
1626
|
+
**Cost basis:** measured-vice
|
|
1627
|
+
**Cost measured on:** oscar64-dig-and-guards (constructed stress tick, screen on)
|
|
1628
|
+
|
|
1629
|
+
### Why
|
|
1630
|
+
|
|
1631
|
+
Lode Runner's rules are exact and testable. The player digs a brick,
|
|
1632
|
+
the hole traps a guard, the guard climbs out after a while, and a hole
|
|
1633
|
+
that refills on a guard kills it. Guards chase over ladders and fall
|
|
1634
|
+
into holes. None of that is collision or path search. It is timers on
|
|
1635
|
+
map cells and a per-guard state, and it needs a chase rule cheap enough
|
|
1636
|
+
for several guards a frame. No C64 primary source was found: what
|
|
1637
|
+
follows about the original comes from the Apple II literate disassembly
|
|
1638
|
+
(Sources below), read and not run here. The C64 port was not examined.
|
|
1639
|
+
|
|
1640
|
+
### How
|
|
1641
|
+
|
|
1642
|
+
1. **Tile states.** Map cells are one byte: empty, brick (diggable),
|
|
1643
|
+
solid (not diggable), ladder, hole, refilling. Only brick and solid
|
|
1644
|
+
stop a move. A hole and a refilling cell are passable; an actor in
|
|
1645
|
+
one is below the floor.
|
|
1646
|
+
2. **Dig.** The player digs diagonally, below-left or below-right. The
|
|
1647
|
+
dig succeeds only if that cell is brick and the cell above it, beside
|
|
1648
|
+
the player, is empty with no guard in it. The Apple II original also
|
|
1649
|
+
refuses a dig on the bottom row or at the edge column (read from its
|
|
1650
|
+
`TRY_DIGGING_LEFT`).
|
|
1651
|
+
3. **Hole list.** A dig writes the hole into the map and puts the cell
|
|
1652
|
+
and a timer into a free slot of a small list. One loop per tick
|
|
1653
|
+
counts each live slot down. At a set count the cell turns to the
|
|
1654
|
+
refilling state, so the player can see it closing. At 0 the cell is
|
|
1655
|
+
brick again and the loop checks the cell: a guard in it dies and
|
|
1656
|
+
respawns on the top row, the player in it dies. The original's dig
|
|
1657
|
+
routine searches 30 slots (`BRICK_FILL_TIMERS`) and starts a timer at
|
|
1658
|
+
180; the timer loop draws two refill frames at 20 and 10, and at
|
|
1659
|
+
expiry bricks over gold lying in the cell and takes it off the gold
|
|
1660
|
+
count (read from `DROP_PLAYER_IN_HOLE` and `HANDLE_TIMERS`, not run
|
|
1661
|
+
here). The recipe keeps 8 slots, 80 ticks, refill glyph from 16.
|
|
1662
|
+
4. **Standing.** An actor is held up when its own cell is a ladder, or
|
|
1663
|
+
the cell below is brick, solid or ladder, or the cell below is a hole
|
|
1664
|
+
holding a trapped guard. The last clause is the walk-over rule: the
|
|
1665
|
+
player crosses a hole on the trapped guard's head. It also keeps a
|
|
1666
|
+
second guard out of an occupied hole. Otherwise the actor falls one
|
|
1667
|
+
cell a step.
|
|
1668
|
+
5. **Guard states.** Free, trapped, escaping. A free guard that falls
|
|
1669
|
+
into a hole cell becomes trapped with a counter (30 guard ticks in
|
|
1670
|
+
the recipe). At 0 it climbs one cell, into the floor row, and is
|
|
1671
|
+
escaping: on its next tick it steps sideways toward the player
|
|
1672
|
+
without the fall test, so it does not drop straight back into the
|
|
1673
|
+
hole. A hole timer that runs out first kills it (step 3). In the
|
|
1674
|
+
original, a guard that lands in a dug hole has its per-guard timer
|
|
1675
|
+
reset to a value chosen at level start from a 13-entry table (38 to
|
|
1676
|
+
80), and the player scores 75; the move routine reads that timer to
|
|
1677
|
+
send a guard in a hole upward (read, not traced further).
|
|
1678
|
+
6. **Greedy chase.** Once per guard move, first match wins:
|
|
1679
|
+
- on the player's row, if every cell up to the player can be stood
|
|
1680
|
+
on, step toward the player;
|
|
1681
|
+
- player above and the guard on a ladder with room above: climb;
|
|
1682
|
+
player below and a ladder or empty cell below: descend;
|
|
1683
|
+
- otherwise scan the guard's row outward, left before right at each
|
|
1684
|
+
distance, for the nearest column offering that vertical move, and
|
|
1685
|
+
step toward it; a side stops at a wall, the map edge or a cell the
|
|
1686
|
+
guard would fall from;
|
|
1687
|
+
- otherwise step toward the player's column.
|
|
1688
|
+
|
|
1689
|
+
The original is also greedy but scores candidates. On the player's
|
|
1690
|
+
row it walks straight at the player when the cells between are
|
|
1691
|
+
ladder, rope or floored. Otherwise it finds how far the guard can go
|
|
1692
|
+
left and right, and scores the up, down, left and right candidates
|
|
1693
|
+
by a pseudo-distance: the column distance if the candidate row is the
|
|
1694
|
+
player's, 100 plus the row distance if it is above the player, 200
|
|
1695
|
+
plus the row distance if below (row 0 is the top). The smallest
|
|
1696
|
+
score wins, so a guard prefers to end above the player, from where
|
|
1697
|
+
it can drop to him (`DETERMINE_GUARD_MOVE`; `PSEUDO_DISTANCE` at
|
|
1698
|
+
`$72D4`). The disassembly's prose describes the two cases the other
|
|
1699
|
+
way round; this follows its code. The recipe's nearest-column rule
|
|
1700
|
+
is its own simplification: it has no above-or-below preference.
|
|
1701
|
+
7. **Guards and gold.** In the original each guard has a gold timer.
|
|
1702
|
+
A guard on a cell with gold picks it up, and drops it later on a
|
|
1703
|
+
cell boundary with nothing there, when the timer allows
|
|
1704
|
+
(`CHECK_FOR_GOLD_PICKED_UP_BY_GUARD`, `GUARD_DROP_GOLD`; the timer's
|
|
1705
|
+
full cycle was not traced here). Not built in the recipe.
|
|
1706
|
+
|
|
1707
|
+
### Why it works
|
|
1708
|
+
|
|
1709
|
+
Every rule reads the map and one small list, so the state is the map,
|
|
1710
|
+
the hole slots and a few bytes per guard. The hole slot and the map
|
|
1711
|
+
cell change together, so they cannot disagree, and the kill test runs
|
|
1712
|
+
only when a slot expires. Putting the trapped guard into the standing
|
|
1713
|
+
test gives walk-over for free, with no special case in the player code.
|
|
1714
|
+
The chase rule is greedy: it never plans past the next vertical move,
|
|
1715
|
+
which is why guards walk into holes the player digs in their way. That
|
|
1716
|
+
is the game, not a defect. A route search (`nav_area_pathfinding`)
|
|
1717
|
+
would walk round the hole.
|
|
1718
|
+
|
|
1719
|
+
### Variations
|
|
1720
|
+
|
|
1721
|
+
- **Pseudo-distance scoring.** Score each candidate move as the original
|
|
1722
|
+
does (step 6) instead of taking the nearest column. It prefers a move
|
|
1723
|
+
that ends above the player to one that ends below.
|
|
1724
|
+
- **Staggered guards.** Move one or two guards per frame instead of all
|
|
1725
|
+
on the same tick; four full-row scans at once overrun a frame in
|
|
1726
|
+
the recipe (Cycle budget). The original picks a guard move pattern at level start from
|
|
1727
|
+
the guard count (`GUARD_PATTERNS_LIST`) and keeps a phase
|
|
1728
|
+
(`GUARD_PHASE`); how that spreads the moves was not traced here.
|
|
1729
|
+
- **Pixel movement.** The original moves actors in sub-cell steps
|
|
1730
|
+
(per-guard `GUARD_X_ADJS`, `GUARD_Y_ADJS`). Draw actors as sprites
|
|
1731
|
+
with the same cell rules;
|
|
1732
|
+
`tile_grid_collision` has the pixel-to-cell conversion.
|
|
1733
|
+
- **Bars (ropes).** A cell class the actor hangs from: held up in it,
|
|
1734
|
+
moves sideways along it, drops on down. The original's same-row check
|
|
1735
|
+
treats rope like ladder. Not built in the recipe.
|
|
1736
|
+
|
|
1737
|
+
### Cycle budget
|
|
1738
|
+
|
|
1739
|
+
Measured in VICE x64sc 3.10 with a CIA1 timer B harness, interrupts
|
|
1740
|
+
masked, Oscar64 `-O2`, screen on (rung 1). The Cost line is the recipe's
|
|
1741
|
+
stress tick on PAL: 19,759 cycles, 19,716 on NTSC. That is 101% of a PAL
|
|
1742
|
+
frame (19,656 cycles) and 115% of an NTSC one (17,095), so it is over a
|
|
1743
|
+
frame on both; a budget that sums per-frame costs cannot fit it. The
|
|
1744
|
+
tick is built as the worst frame: four free guards each scan their whole
|
|
1745
|
+
row, because the player is above and no column offers a climb; eight
|
|
1746
|
+
live holes sit off that row, one expiring and one turning to the refill
|
|
1747
|
+
glyph (659 cycles for the list); the player tries a dig with the list
|
|
1748
|
+
full. More cannot expire at once: one dig a tick and a fixed hole life
|
|
1749
|
+
put at most one expiry in any tick. The slowest guard took 4,780
|
|
1750
|
+
cycles, about 165 a probed cell by arithmetic over 29 probes.
|
|
1751
|
+
|
|
1752
|
+
Four guards scanning full rows overrun a frame in this C, so four or
|
|
1753
|
+
more need staggering. With the fourth guard switched off the same tick
|
|
1754
|
+
took 15,085 cycles on PAL (77%) and 15,299 on NTSC (89%): three fit.
|
|
1755
|
+
|
|
1756
|
+
The row scan is the whole cost, and about 165 cycles a cell is Oscar64
|
|
1757
|
+
call overhead per probe, not the rule. Ways to bound it, none measured
|
|
1758
|
+
here: stagger guard decisions over frames (Variations); keep a per-row
|
|
1759
|
+
table of ladder columns, since ladders do not move, and look up the
|
|
1760
|
+
nearest one instead of probing each cell (holes still cut a side short,
|
|
1761
|
+
so check the cells between); write the probe loop in assembly.
|
|
1762
|
+
|
|
1763
|
+
The typical frame is far smaller. Over the recipe's 168-tick scenario,
|
|
1764
|
+
one guard and at most two holes, the worst whole tick is 4,075 cycles on
|
|
1765
|
+
PAL and 4,118 on NTSC, one guard's update at most 3,516 and 3,559, and
|
|
1766
|
+
the hole list at most 366. The worst player update, a dig, is 505. PAL
|
|
1767
|
+
and NTSC differ only by badline cycles, where the work runs past the
|
|
1768
|
+
vertical blank.
|
|
1769
|
+
|
|
1770
|
+
### Recipes
|
|
1771
|
+
|
|
1772
|
+
- `recipes/oscar64/dig-and-guards.md` — 28 x 16 level with bricks and ladders; an autopilot digs two holes, traps a guard, walks over it and lets the second hole refill on it; the guard respawns and chases down a ladder; map and guard states checked every 16 ticks against a Python model (checksum), a built worst-frame stress tick, PASS/FAIL and cycles, PAL and NTSC
|
|
1773
|
+
|
|
1774
|
+
### Sources
|
|
1775
|
+
|
|
1776
|
+
- https://github.com/XekriRedmane/lode_runner_reveng (Apple II literate
|
|
1777
|
+
disassembly, CC BY-SA 4.0): `main.nw`, sections "Digging" and the
|
|
1778
|
+
guard routines: `BRICK_FILL_TIMERS`, `HANDLE_TIMERS`,
|
|
1779
|
+
`DROP_PLAYER_IN_HOLE`, `DETERMINE_GUARD_MOVE`, `PSEUDO_DISTANCE`,
|
|
1780
|
+
`GUARD_GOLD_TIMER_START_VALUES`, `GUARD_RESURRECTIONS`. Read for
|
|
1781
|
+
facts; no code or prose is taken from it.
|
|
1782
|
+
- https://github.com/fschuhi/a2-lode-runner (research built on the
|
|
1783
|
+
above; its platform-neutral game spec is not written yet). Read for
|
|
1784
|
+
context only.
|
|
1785
|
+
|
|
1786
|
+
---
|
|
1787
|
+
|
|
1788
|
+
## cave_scan_engine — One-pass cave scan with a scanned bit: falling and rolling objects, digging and a wall-following enemy (Boulder Dash rules)
|
|
1789
|
+
|
|
1790
|
+
**Complexity:** medium
|
|
1791
|
+
**Region:** both
|
|
1792
|
+
**Cost:** cycles_per_frame=18559
|
|
1793
|
+
**Cost basis:** measured-vice
|
|
1794
|
+
**Cost measured on:** oscar64-cave-scan (one scan, run every fourth frame; slowest game-cave scan, NTSC, screen on)
|
|
1795
|
+
|
|
1796
|
+
**Why.** A Boulder Dash style game is a grid of one-byte cells in which
|
|
1797
|
+
every boulder, diamond, enemy and the player act once per game tick.
|
|
1798
|
+
The cheap way to update it is one scan over the grid per tick, in place,
|
|
1799
|
+
with no second buffer. Scanned in place, an object that moves into a
|
|
1800
|
+
cell the scan has not reached yet gets processed again when the scan
|
|
1801
|
+
arrives there. A boulder falls the whole height of a shaft in one tick;
|
|
1802
|
+
the player tunnels across a row on one step. Games of this kind
|
|
1803
|
+
(sand, water, push chains) all meet the same bug.
|
|
1804
|
+
|
|
1805
|
+
**How.** Keep the cave as a byte per cell: 40x22 with a steel border and
|
|
1806
|
+
a 38x20 play area is Boulder Dash I's size (elmerproductions, below).
|
|
1807
|
+
Use the low bits for the element and bit 7 as the scanned bit. Give the
|
|
1808
|
+
falling state its own code, one above the resting code, because the
|
|
1809
|
+
rules differ: only a falling object kills the player. Once per cave
|
|
1810
|
+
frame, walk the interior row by row, top to bottom, each row left to
|
|
1811
|
+
right, the order the Boulder Dash forum thread describes (below). For
|
|
1812
|
+
each cell:
|
|
1813
|
+
|
|
1814
|
+
- Space, dirt, brick, steel: nothing. Make this path as short as the
|
|
1815
|
+
compiler allows; it is most of the cave.
|
|
1816
|
+
- Bit 7 set: clear it and do nothing else. The object moved here during
|
|
1817
|
+
this scan and has had its turn.
|
|
1818
|
+
- Resting boulder or diamond: space below, start falling and move down.
|
|
1819
|
+
A round object below (boulder, diamond or brick in the recipe): roll,
|
|
1820
|
+
left if the left cell and the cell below it are empty, else right
|
|
1821
|
+
under the same test. The recipe's round set and left-first order are
|
|
1822
|
+
its own choice; Boulder Dash's exact rules are not established here. A tick counted in display frames runs 20 % fast
|
|
1823
|
+
on NTSC (`pal_ntsc_tempo_mismatch`).
|
|
1824
|
+
- Falling boulder or diamond: space below, move down. The player below:
|
|
1825
|
+
kill him. A falling object below: wait. A round object below: try to
|
|
1826
|
+
roll, else land (the resting code). Anything else: land.
|
|
1827
|
+
- The player: take this tick's move; enter space or dirt (dig), or a
|
|
1828
|
+
diamond (collect); otherwise stay.
|
|
1829
|
+
- An enemy: if the player is next to it, kill him; else turn left if
|
|
1830
|
+
that cell is empty, go on if not, and turn right on the spot if both
|
|
1831
|
+
are blocked. That keeps it running round the wall on its left.
|
|
1832
|
+
|
|
1833
|
+
Every move goes through one routine that writes the destination and
|
|
1834
|
+
clears the source. When the destination is later in scan order, to the
|
|
1835
|
+
right or below, it also sets bit 7. A move left or up lands on a cell
|
|
1836
|
+
already passed and needs no mark.
|
|
1837
|
+
|
|
1838
|
+
**Why it works.** The scan reaches a cell to the right or below after
|
|
1839
|
+
the object that moved there, so the mark always meets the scan before
|
|
1840
|
+
the next cave frame, and clearing it on contact leaves the cave with no
|
|
1841
|
+
marks at the end of every scan. There is no second pass to clear flags
|
|
1842
|
+
(the recipe's model asserts that no cell keeps bit 7). Boulder Dash I
|
|
1843
|
+
keeps separate "scanned this frame" codes for its moving elements for
|
|
1844
|
+
the same reason (elmerproductions); the forum thread describes the
|
|
1845
|
+
states being reset at the end of the frame. Under the recipe's rules,
|
|
1846
|
+
top-to-bottom order also spreads a column of falling boulders: the upper
|
|
1847
|
+
one sees the lower one still in place, waits a tick, and a gap opens.
|
|
1848
|
+
|
|
1849
|
+
A worked example of the bug, from the recipe's Python model with the
|
|
1850
|
+
scanned bit left out, whose final cave the unflagged C build matched.
|
|
1851
|
+
In cave frame 1 Rockford at column 2 is told to step right once. He moves
|
|
1852
|
+
to column 3; the scan reaches column 3, finds him again and moves him
|
|
1853
|
+
to 4, and so on: he ends the scan at column 38, having dug the whole
|
|
1854
|
+
row. A boulder that rolled right off a brick fell two more cells in the
|
|
1855
|
+
same scan. The run's cave differs from the model with the bit in 47 of
|
|
1856
|
+
880 cells after 40 cave frames and matches a model without the bit in
|
|
1857
|
+
all 880.
|
|
1858
|
+
|
|
1859
|
+
**Variations.** Boulder Dash's form: a separate element code for each
|
|
1860
|
+
scanned state instead of a shared bit (elmerproductions lists one per
|
|
1861
|
+
moving element). A second buffer: read the old grid, write a new one,
|
|
1862
|
+
swap. That needs no marks but doubles the RAM and costs a copy or a
|
|
1863
|
+
pointer swap per tick. Scan bottom to top for gravity only: falling
|
|
1864
|
+
objects then move into cells already passed and need no mark, but
|
|
1865
|
+
anything that moves up or sideways needs one again. Scrolling: Boulder
|
|
1866
|
+
Dash drew each object as 2x2 characters and scrolled a window of about
|
|
1867
|
+
19.5 by 11.5 objects over the cave; with one character per cell, as in
|
|
1868
|
+
the recipe, the 40x22 cave fits a 40x25 screen with three rows left for
|
|
1869
|
+
the status line.
|
|
1870
|
+
|
|
1871
|
+
**Cave frame rate.** The scan runs on a tick slower than the display.
|
|
1872
|
+
The recipe runs one cave frame per four display frames, counted by a
|
|
1873
|
+
raster IRQ (`frame_sync_loop`), and redraws only the cells the scan
|
|
1874
|
+
changed. The logic-at-a-lower-rate pattern is `logic_rate_decoupling`
|
|
1875
|
+
above. Boulder Dash's own tick, and how its cave-delay byte maps to it,
|
|
1876
|
+
are not established here.
|
|
1877
|
+
|
|
1878
|
+
**Cycle budget.** Measured in the recipe with CIA1 timer A cascaded into
|
|
1879
|
+
timer B, display on, VICE x64sc 3.10, Oscar64 -O2. The game cave's
|
|
1880
|
+
scans took 16,609 to 18,175 cycles on PAL and 17,115 to 18,559 on NTSC:
|
|
1881
|
+
close to a whole PAL frame of 19,656 cycles, which is why the recipe
|
|
1882
|
+
scans once per four frames. On NTSC every game scan exceeds the 17,095-cycle
|
|
1883
|
+
frame (263 x 65, arithmetic), so a scan never fits in one NTSC frame, and
|
|
1884
|
+
the NTSC figures include one raster-IRQ service that fires inside the
|
|
1885
|
+
timed scan. A 38x20 interior of dirt costs 14,206 (PAL)
|
|
1886
|
+
and 14,421 (NTSC), about 19 cycles a cell: the floor for any cave. The
|
|
1887
|
+
Cost line carries the game cave's slowest scan (18,559, NTSC), which runs
|
|
1888
|
+
in one burst in the frame it starts in. Plan a cave from the measured
|
|
1889
|
+
parts: the dirt floor plus about 325 cycles per falling object plus about
|
|
1890
|
+
610 per moving enemy (arithmetic). A cave where everything moves is a
|
|
1891
|
+
bound spanning several frames, not a per-frame cost: 380 falling
|
|
1892
|
+
boulders on odd rows over empty rows took 137,818 cycles on PAL and 138,892 on NTSC, about
|
|
1893
|
+
seven PAL frames, about 325 cycles per falling boulder over the dirt
|
|
1894
|
+
floor (that scan's 64-entry dirty list fills after 32 moves and records
|
|
1895
|
+
no more, so a renderer that records every change costs slightly more).
|
|
1896
|
+
190 fireflies moving at once cost 130,451 and 131,531, about 610 each.
|
|
1897
|
+
A cave where everything moves cannot be scanned in one frame in this
|
|
1898
|
+
C; a scan in assembly would be cheaper, not measured here. The per-cell
|
|
1899
|
+
floor is the figure to plan around. In Oscar64 the object rules must
|
|
1900
|
+
stay out of the scan loop: with them inline, the dirt scan took about
|
|
1901
|
+
48,000 cycles (47,731 when rebuilt with `cell()` inlined).
|
|
1902
|
+
|
|
1903
|
+
**Left out of the recipe.** Amoeba, magic wall, explosions, butterflies,
|
|
1904
|
+
pushing boulders, the exit and the cave timer. A kill removes Rockford
|
|
1905
|
+
from the cave; nothing explodes.
|
|
1906
|
+
|
|
1907
|
+
### Recipes
|
|
1908
|
+
|
|
1909
|
+
- `recipes/oscar64/cave-scan.md` — a 40x22 original cave; boulders and diamonds fall and roll, Rockford digs, collects one diamond and is killed by a boulder on a scripted path, one firefly; the cave after 40 cave frames checked against a Python model, scan cycles printed on both models; `-dSCAN_FLAG=0` shows the double move
|
|
1910
|
+
|
|
1911
|
+
### Sources
|
|
1912
|
+
|
|
1913
|
+
- https://www.elmerproductions.com/sp/peterb/rawCaveData.html (Peter
|
|
1914
|
+
Broadribb, Boulder Dash I raw cave data): the 40x22 cave with a steel
|
|
1915
|
+
border, the 38x20 play area, the visible area of 19.5 by 11.5
|
|
1916
|
+
objects, and the element codes with their "scanned this frame"
|
|
1917
|
+
variants, whose purpose it states as stopping an object being scanned
|
|
1918
|
+
more than once per frame. Falling boulders kill Rockford; stationary
|
|
1919
|
+
ones do not.
|
|
1920
|
+
- https://www.boulder-dash.nl/forum/viewtopic.php?t=652 ("Cave Scanning
|
|
1921
|
+
Order" thread): scan order row by row, top to bottom, each row left to
|
|
1922
|
+
right; moved or grown elements take a delay state for the rest of the
|
|
1923
|
+
frame; the states reset at the end of the frame. Forum report, not
|
|
1924
|
+
measured here.
|
|
1925
|
+
|
|
1926
|
+
## game_tree_search — Board-game AI: move generation, evaluation and alpha-beta search within a frame budget
|
|
1927
|
+
|
|
1928
|
+
**Complexity:** high
|
|
1929
|
+
**Region:** both
|
|
1930
|
+
**Cost:** cycles_per_frame=9316, cycles_per_frame_typical=5325, bytes_code=704, bytes_data=199
|
|
1931
|
+
**Cost basis:** derived-listing
|
|
1932
|
+
**Cost measured on:** oscar64-game-tree-search (sliced, four nodes a frame, a bound from four worst calls, NTSC, display on)
|
|
1933
|
+
|
|
1934
|
+
**Why.** A board game needs an opponent that looks ahead. The standard
|
|
1935
|
+
method is to generate every legal move, play each on a copy of the
|
|
1936
|
+
position, look at the replies to some depth, and score the positions at
|
|
1937
|
+
the bottom with a static evaluation. On a 1 MHz 6502 the question is how
|
|
1938
|
+
many positions a second it can visit, and how to spend a fixed number of
|
|
1939
|
+
frames on one move without freezing the game.
|
|
1940
|
+
|
|
1941
|
+
**How.** Five parts.
|
|
1942
|
+
|
|
1943
|
+
- **Board.** Use a byte per square with a border of sentinel bytes, so a
|
|
1944
|
+
step off the board reads a value that matches no piece and needs no
|
|
1945
|
+
bounds test. The recipe's Connect Four board is 8x8 bytes for a 7x6
|
|
1946
|
+
game: a sentinel row above and below and a sentinel column, and the
|
|
1947
|
+
four line directions are +1, +8, +9 and +7. Chess programs use the
|
|
1948
|
+
0x88 layout: the square is a byte with the rank in the high nibble and
|
|
1949
|
+
the file in the low one, so `AND #$88` is non-zero exactly when a step
|
|
1950
|
+
has left the board. Microchess (below) tests its moves that way.
|
|
1951
|
+
Bitboards, one bit per square, suit 64-bit CPUs; on the 6502 every
|
|
1952
|
+
shift or mask of a 42- or 64-bit board is six or eight byte operations
|
|
1953
|
+
(arithmetic), so a byte array is the usual choice. The bitboard cost
|
|
1954
|
+
was not measured here.
|
|
1955
|
+
- **Move generation.** List the legal moves in the order they will be
|
|
1956
|
+
tried. For Connect Four a move is a column and legality is one compare
|
|
1957
|
+
with the column's height. Chess needs a table of step offsets per
|
|
1958
|
+
piece and a loop along each direction.
|
|
1959
|
+
- **Make and unmake.** Change the board in place and undo it on the way
|
|
1960
|
+
back, rather than copying it per ply. Keep anything the evaluation
|
|
1961
|
+
needs up to date in the same two routines: the recipe keeps a sum of
|
|
1962
|
+
per-square weights, so a leaf costs a load and a negate.
|
|
1963
|
+
- **Evaluation.** A static score from the view of one side: material,
|
|
1964
|
+
mobility, square weights. The recipe's weight is the number of
|
|
1965
|
+
four-in-a-row windows through a cell, 3 in a corner to 13 in the
|
|
1966
|
+
middle. A win scores a constant less the ply, so a quicker win scores
|
|
1967
|
+
higher and a slower loss scores higher than a quick one.
|
|
1968
|
+
- **Search.** Negamax with alpha-beta: each ply returns the best value
|
|
1969
|
+
for the side to move, a child's value is negated on the way up, and a
|
|
1970
|
+
window (alpha, beta) is passed down negated and swapped. When a move
|
|
1971
|
+
scores at least beta, the ply stops: the opponent above already has a
|
|
1972
|
+
better line and will not allow this one.
|
|
1973
|
+
|
|
1974
|
+
**Move ordering.** Alpha-beta cuts most when the best move is tried
|
|
1975
|
+
first. Counted with the recipe's Python model from the empty board: with
|
|
1976
|
+
columns tried centre first, a depth-5 search visits 755 positions and a
|
|
1977
|
+
depth-7 search 6,062; tried left to right, 4,072 and 56,996. Without
|
|
1978
|
+
pruning a depth-5 search visits 7 + 7^2 + ... + 7^5 = 19,607 (arithmetic;
|
|
1979
|
+
no game ends that early). A good static order (centre first, captures
|
|
1980
|
+
first in chess) is worth more than a faster evaluation.
|
|
1981
|
+
|
|
1982
|
+
**Iterative deepening under a frame budget.** Search to depth 1, then 2,
|
|
1983
|
+
then 3, keeping the best move of the last depth that finished. Before
|
|
1984
|
+
each node, compare a frame counter with the budget; when it runs out,
|
|
1985
|
+
abandon the unfinished depth and play the kept move. The early depths
|
|
1986
|
+
are cheap: from the empty board, depths 1 to 5 together visit 7 + 23 +
|
|
1987
|
+
75 + 172 + 755 = 1,032 positions against 755 for depth 5 alone, 37 %
|
|
1988
|
+
more (model counts, arithmetic). Trying the previous depth's best move
|
|
1989
|
+
first at the root cuts that overhead; not measured here. Budget in
|
|
1990
|
+
frames on the model the game runs on: an NTSC frame is 17,095 cycles and
|
|
1991
|
+
a PAL frame 19,656 (arithmetic). At the recipe's blocking rates, 901
|
|
1992
|
+
cycles a node on PAL and 912 on NTSC, that is 21.8 and 18.7 nodes a
|
|
1993
|
+
frame: the same frame budget buys 14 % fewer nodes on NTSC (arithmetic).
|
|
1994
|
+
|
|
1995
|
+
**Transposition table (optional).** Different move orders reach the same
|
|
1996
|
+
position. A hash of the position (Zobrist: an XOR of one random word per
|
|
1997
|
+
piece and square, updated in make and unmake) indexes a table of a few
|
|
1998
|
+
KB holding each stored position's depth, value and best move; a hit
|
|
1999
|
+
saves the subtree, and the stored best move is a good first move to try.
|
|
2000
|
+
It pays most at depth; the recipe does not use one, and its gain on a
|
|
2001
|
+
C64 was not measured here.
|
|
2002
|
+
|
|
2003
|
+
**Recursion and the stack.** Written as a recursive function, the search
|
|
2004
|
+
needs a frame per ply. The 6502's hardware stack is 256 bytes, so an
|
|
2005
|
+
assembly search keeps its per-ply state in tables indexed by ply and uses
|
|
2006
|
+
the stack only for return addresses. Oscar64 allocates locals statically
|
|
2007
|
+
from its call graph and cannot do that for a function that calls itself
|
|
2008
|
+
(`toolchains/oscar64-reference.md`, "Avoid recursion and function
|
|
2009
|
+
pointers"); it gives such a function a frame on its software stack.
|
|
2010
|
+
Measured in the recipe, the recursive build took an 18-byte frame per ply
|
|
2011
|
+
and ran about 9 % slower (981 cycles a node against 901 on PAL). The
|
|
2012
|
+
iterative form has a second use: its state is all in the per-ply tables,
|
|
2013
|
+
so it can stop after any node and go on in the next frame.
|
|
2014
|
+
|
|
2015
|
+
**Time slicing.** Give the search a node budget per frame and keep the
|
|
2016
|
+
game running around it. The recipe's `search_step(n)` makes at most `n`
|
|
2017
|
+
nodes; when the budget runs out it stores the ply and the side to move
|
|
2018
|
+
and returns, and the per-ply records (window, best value, next move to
|
|
2019
|
+
try, move made) hold the rest. The root's state is in the same static
|
|
2020
|
+
records, so nothing lives on a stack between frames. The budget is
|
|
2021
|
+
tested before each node, so the returns up the plies after a slice's
|
|
2022
|
+
last node run in the next slice. Measured in the recipe on PAL and NTSC:
|
|
2023
|
+
at four nodes a frame the sliced game made the same 42 moves with the
|
|
2024
|
+
same node counts as the blocking search and the Python model, and the
|
|
2025
|
+
main loop ran in every one of its 4,880 frames (no frame missed) while
|
|
2026
|
+
it advanced a spinner and a frame counter. Size the budget from the
|
|
2027
|
+
worst single node, not the average: the worst `search_step(1)` call was
|
|
2028
|
+
2,301 cycles on PAL and 2,329 on NTSC, about two and a half times the
|
|
2029
|
+
average node. The call includes the returns up the plies before its
|
|
2030
|
+
node and the call itself, and can include badlines and the frame
|
|
2031
|
+
interrupt; the parts were not separated here. The cost of slicing is time: the recipe's slowest move takes
|
|
2032
|
+
591 frames, about 11.8 s on PAL, against 103 frames blocking.
|
|
2033
|
+
|
|
2034
|
+
**Scores are signed and span the whole range.** Alpha, beta and the
|
|
2035
|
+
values run from -INF to +INF. A 16-bit compare of `v > best` done with a
|
|
2036
|
+
subtract and `BMI` gives the wrong order when the difference overflows
|
|
2037
|
+
(30,000 - (-30,000) does); use the overflow-corrected compare in
|
|
2038
|
+
`compare_16bit_and_signed`. Choose INF at most 32,767 so that -INF can
|
|
2039
|
+
be negated.
|
|
2040
|
+
|
|
2041
|
+
**Microchess.** Peter Jennings's Microchess (1976, KIM-1) fitted the
|
|
2042
|
+
program and its data in 924 bytes of the KIM-1's 1,024 bytes of RAM, by
|
|
2043
|
+
his account (benlo.com). From the source on 6502.org: the position is a
|
|
2044
|
+
32-byte list of piece squares, one byte per piece, not a board of
|
|
2045
|
+
squares; a square byte holds rank and file in nibbles and `AND #$88`
|
|
2046
|
+
rejects steps off the board; each move's from-square, piece, captured
|
|
2047
|
+
piece and move index are pushed on a second stack, exchanged with the
|
|
2048
|
+
hardware stack by `TSX`/`TXS`, so the move can be unmade; and a
|
|
2049
|
+
`REVERSE` routine flips the position so one generator serves both sides.
|
|
2050
|
+
Jennings describes the search as a state machine that allowed recursion
|
|
2051
|
+
and a move stack that retraced moves to the starting position. A full
|
|
2052
|
+
chess engine is larger than this technique's recipe; the parts above
|
|
2053
|
+
(0x88, piece list, make and unmake from a stack) are the chess-specific
|
|
2054
|
+
pieces.
|
|
2055
|
+
|
|
2056
|
+
**Cycle budget.** Measured in the recipe (Oscar64 1.32.271 -O2, VICE
|
|
2057
|
+
x64sc 3.10, display on, a one-IRQ-a-frame counter running). Blocking:
|
|
2058
|
+
901 cycles per node on PAL and 912 on NTSC, averaged over 19,292 nodes,
|
|
2059
|
+
about 1,090 and 1,120 nodes a second. A node there is make, a
|
|
2060
|
+
four-direction win test, the leaf or descent step and unmake, for a
|
|
2061
|
+
seven-column game. The slowest of the 42 depth-5 searches took 2,019,814
|
|
2062
|
+
cycles on PAL (103 frames) and 2,043,608 on NTSC (120 frames), about two
|
|
2063
|
+
seconds. The blocking search is multi-frame: its cost must not be summed
|
|
2064
|
+
into a frame budget. The Cost line's `cycles_per_frame` is the sliced
|
|
2065
|
+
form at four nodes a frame: four times the worst single call, 4 x 2,329
|
|
2066
|
+
= 9,316 cycles (the NTSC figure, the larger), half a PAL frame. It is a
|
|
2067
|
+
bound; the worst four-node slice measured was 5,024 cycles on PAL and
|
|
2068
|
+
5,325 on NTSC, which the Cost line carries as its typical figure, and a
|
|
2069
|
+
typical slice is about 4 x 900 (arithmetic from the average). A chess node, with a
|
|
2070
|
+
longer move generator and evaluation, costs more; not measured here. The
|
|
2071
|
+
bytes are the search, make, unmake, win test and leaf routines (704) and
|
|
2072
|
+
the board, weights, move order, per-ply tables at depth 5 and the saved
|
|
2073
|
+
ply and side (199), from the Oscar64 map.
|
|
2074
|
+
|
|
2075
|
+
### Recipes
|
|
2076
|
+
|
|
2077
|
+
- `recipes/oscar64/game-tree-search.md` — Connect Four played by the machine against itself at depth 5, 42 moves to a drawn full board, three times: blocking, one node a call and sliced at four nodes a frame with a per-frame counter running and no frame missed; every column and node count of each game checked against a Python model of the same search; nodes per second, cycles per node, frames per move, the worst single call and the worst slice printed on PAL and NTSC; `-dRECURSIVE=1` builds the recursive form
|
|
2078
|
+
|
|
2079
|
+
### Sources
|
|
2080
|
+
|
|
2081
|
+
- https://benlo.com/microchess/ (Peter Jennings's Microchess history):
|
|
2082
|
+
shipped for the KIM-1 in 1976; the program and data in 924 bytes, the
|
|
2083
|
+
KIM-1 with 1,024 bytes of RAM; a state machine design allowing
|
|
2084
|
+
recursion; a move stack to retrace moves; an evaluation after each
|
|
2085
|
+
generated move.
|
|
2086
|
+
- https://6502.org/source/games/uchess/uchess.htm (Microchess source,
|
|
2087
|
+
posted with Jennings's permission; Daryl Rictor's 2002 serial-port
|
|
2088
|
+
adaptation): the 32-byte piece list `BOARD` at $50, the `AND #$88`
|
|
2089
|
+
off-board test in `CMOVE`, the second stack `SP2` swapped with the
|
|
2090
|
+
hardware stack in `MOVE` and `UMOVE`, and the `REVERSE` routine. Read
|
|
2091
|
+
for facts only; no code is reproduced here.
|
|
2092
|
+
|
|
2093
|
+
---
|
|
2094
|
+
|
|
2095
|
+
## creature_state_machine — Many small creatures, each a state machine on pixel probes: walk, climb, turn, fall, dig, build, block
|
|
2096
|
+
|
|
2097
|
+
**Complexity:** medium
|
|
2098
|
+
**Region:** both
|
|
2099
|
+
**Requires:** destructible_char_terrain, char_bullets
|
|
2100
|
+
**Cost:** cycles_per_frame=14465
|
|
2101
|
+
**Cost basis:** measured-vice
|
|
2102
|
+
**Cost measured on:** oscar64-destructible-terrain (worst frame, 24 creatures, no terrain edits or draw, NTSC)
|
|
2103
|
+
|
|
2104
|
+
### Why
|
|
2105
|
+
|
|
2106
|
+
A Lemmings-style game has dozens of identical creatures that walk on
|
|
2107
|
+
their own and change the landscape when told to. Each one is a few
|
|
2108
|
+
bytes of state and a handful of rules, but the rules must agree with
|
|
2109
|
+
the terrain to the pixel: a creature that sinks one pixel into a slope
|
|
2110
|
+
or walks through a thin wall looks broken. The form here runs one small
|
|
2111
|
+
state machine per creature against pixel probes of the character
|
|
2112
|
+
terrain in `destructible_char_terrain`, and draws the creatures into
|
|
2113
|
+
the characters as well.
|
|
2114
|
+
|
|
2115
|
+
### How
|
|
2116
|
+
|
|
2117
|
+
Keep per creature: x and y of its feet in pixels, a direction of +1 or
|
|
2118
|
+
-1, a state, a fall counter, a step timer and a brick count. Once per
|
|
2119
|
+
tick, update every creature in slot order. The recipe's rules, with
|
|
2120
|
+
`solid(x, y)` the terrain probe (outside the level counts as solid):
|
|
2121
|
+
|
|
2122
|
+
| State | Rule each tick |
|
|
2123
|
+
|---|---|
|
|
2124
|
+
| walk | `nx = x + dir`. Off the level, or onto a blocker's column within 3 pixels of its height: turn. If `solid(nx, y)`: climb 1 if `(nx, y-1)` is clear, else 2 if `(nx, y-2)` is clear, else turn. Move. If `(x, y+1)` is clear, start falling with the counter at 0 |
|
|
2125
|
+
| fall | If `(x, y+1)` is solid, land: dead if the counter is over 32, else walk. At the level's bottom, dead. Otherwise move down 1 and count |
|
|
2126
|
+
| dig | Every 2nd tick. If none of `(x-1..x+1, y+1)` is solid, fall. Else clear those 3 pixels and move down 1 |
|
|
2127
|
+
| build | Every 4th tick. After 12 bricks, or if `(x+dir, y-1)` is solid, walk. Else set `(x+dir, y)` and `(x+2·dir, y)` and move to `(x+dir, y-1)` |
|
|
2128
|
+
| block | Nothing. Walkers turn at its column |
|
|
2129
|
+
| dead | Nothing; not drawn |
|
|
2130
|
+
|
|
2131
|
+
Roles (dig, build, block) are given only to a walker; an order to a
|
|
2132
|
+
creature in any other state is ignored and counted. Blockers go in a
|
|
2133
|
+
short list when they are given the role, so a walker checks a few
|
|
2134
|
+
entries, not every creature.
|
|
2135
|
+
|
|
2136
|
+
**Drawing.** Creatures are drawn after all updates and removed before
|
|
2137
|
+
the next tick's updates, so the probes never see them. The recipe draws
|
|
2138
|
+
each one into the characters the `char_bullets` way: save the code of
|
|
2139
|
+
each cell the 4-pixel body covers (one or two), copy that glyph into a
|
|
2140
|
+
code the creature owns, OR the body in, write the creature's code, and
|
|
2141
|
+
restore in reverse order. In multicolour, OR-ing pair 11 gives the
|
|
2142
|
+
body the colour-RAM colour over any terrain. Hardware sprites do not
|
|
2143
|
+
suit this: creatures bunch on the same raster lines (sixteen stand on
|
|
2144
|
+
one ledge at the end of the recipe), and eight sprites a line is the
|
|
2145
|
+
limit even with a multiplexer. The cost is two codes per creature from
|
|
2146
|
+
the charset's 256.
|
|
2147
|
+
|
|
2148
|
+
### Why it works
|
|
2149
|
+
|
|
2150
|
+
Every rule reads the terrain through the same probe the renderer's
|
|
2151
|
+
glyphs feed, so a creature stands exactly on the pixels the player
|
|
2152
|
+
sees. Updating in slot order with the terrain edited in place makes the
|
|
2153
|
+
result depend only on that order, so a model in another language can
|
|
2154
|
+
reproduce it: the recipe's Python model and the C agree on every
|
|
2155
|
+
terrain pixel, position and state after 360 ticks, and on the terrain
|
|
2156
|
+
after 24 dig steps with the pool cut to 32 codes, where 23 edits are
|
|
2157
|
+
refused.
|
|
2158
|
+
|
|
2159
|
+
The fall counter makes fatal height a rule of the creature, not of the
|
|
2160
|
+
level. The climb test looks at one or two pixels above the obstacle,
|
|
2161
|
+
which is what lets a creature walk up a builder's staircase (one pixel
|
|
2162
|
+
a step) and turn at an 8-pixel wall.
|
|
2163
|
+
|
|
2164
|
+
### Variations
|
|
2165
|
+
|
|
2166
|
+
- **Blockers in the terrain.** Write an invisible solid mark into a
|
|
2167
|
+
collision copy instead of keeping a list; walkers then need no
|
|
2168
|
+
blocker check. That needs a second map, since the glyphs are the
|
|
2169
|
+
map here. Not built here.
|
|
2170
|
+
- **Fewer updates per frame.** Stagger the step timers, update in
|
|
2171
|
+
groups, or run the logic at a lower rate than the display
|
|
2172
|
+
(`logic_rate_decoupling`), when the count does not fit one frame.
|
|
2173
|
+
The scheme is under "Many creatures" below.
|
|
2174
|
+
- **More states.** Climbing walls, floating down and bashing sideways
|
|
2175
|
+
are more rows in the same table, each with its own probes.
|
|
2176
|
+
|
|
2177
|
+
### Cycle budget
|
|
2178
|
+
|
|
2179
|
+
Measured in VICE x64sc 3.10 with the recipe's CIA timers (Oscar64
|
|
2180
|
+
`-O2`). Worst single creature, over the 360-tick scenario and the
|
|
2181
|
+
worst-frame subjects:
|
|
2182
|
+
|
|
2183
|
+
| Work per creature | PAL | NTSC |
|
|
2184
|
+
|---|---|---|
|
|
2185
|
+
| fall | 241 | 241 |
|
|
2186
|
+
| walk | 562 | 648 |
|
|
2187
|
+
| dig step (3 terrain edits included) | 1,403 | 1,532 |
|
|
2188
|
+
| brick (2 terrain edits included) | 1,577 | 1,706 |
|
|
2189
|
+
| draw, two cells (a 24-creature draw / 24) | about 796 | about 805 |
|
|
2190
|
+
| restore (1,633 / 24) | about 68 | about 68 |
|
|
2191
|
+
|
|
2192
|
+
A probe is 52 cycles. Of a dig step, about 1,356 on NTSC is terrain
|
|
2193
|
+
work (`destructible_char_terrain`, 32,555 for 72 edits / 24); the
|
|
2194
|
+
walk, fall and brick figures include the timer reads around each
|
|
2195
|
+
update.
|
|
2196
|
+
|
|
2197
|
+
The Cost line is the creatures' own logic in its worst frame, without
|
|
2198
|
+
terrain edits and without the draw: all 24 walking into a 2-pixel wall
|
|
2199
|
+
and climbing it (four probes and a blocker check each), 14,465 cycles
|
|
2200
|
+
on NTSC and 14,209 on PAL. Terrain edits belong to
|
|
2201
|
+
`destructible_char_terrain`'s Cost line; a plan adds them per edit.
|
|
2202
|
+
|
|
2203
|
+
The draw and restore are the `char_bullets`-style render the recipe
|
|
2204
|
+
uses, not this technique: 19,319 and 1,633 cycles for 24 creatures on
|
|
2205
|
+
NTSC (19,104 and 1,633 on PAL). A whole tick with all 24 on a dig
|
|
2206
|
+
step, restore, updates and draw, took 57,606 on NTSC and 57,299 on
|
|
2207
|
+
PAL; all 24 laying a brick, 56,701 and 56,357. The scenario's slowest
|
|
2208
|
+
tick was 27,802 and 27,280.
|
|
2209
|
+
|
|
2210
|
+
**How many fit a frame** (arithmetic from these figures, rung 3). The
|
|
2211
|
+
CIA figures already include the badlines, so the budget is the whole
|
|
2212
|
+
frame: 19,656 cycles on PAL, 17,095 on NTSC. A walker costs update,
|
|
2213
|
+
draw and restore: 1,426 on PAL and 1,521 on NTSC, so about 13 and 11
|
|
2214
|
+
fit. A digger costs a twenty-fourth of the dig tick, about 2,390 and
|
|
2215
|
+
2,400, so about 8 and 7. With 24 creatures this C cannot run a whole
|
|
2216
|
+
tick in one frame on either model.
|
|
2217
|
+
|
|
2218
|
+
**Many creatures** (Lemmings scale). The recipe's pattern, restore all,
|
|
2219
|
+
update all, draw all, does not scale. Redrawing all 24 costs 20,952
|
|
2220
|
+
cycles on NTSC (19,319 + 1,633), more than a frame on its own. And
|
|
2221
|
+
between the restore and the draw every creature is off the screen for
|
|
2222
|
+
the whole update, about 36,500 cycles in the dig tick, so they flicker.
|
|
2223
|
+
This is `full_field_redraw_exceeds_vblank` in another form. What to do
|
|
2224
|
+
instead, none of it measured here:
|
|
2225
|
+
|
|
2226
|
+
- Offset each creature's step timer by its slot, so only a fraction
|
|
2227
|
+
act on a given tick: diggers start with `ct = slot & 1`, builders
|
|
2228
|
+
with `ct = slot & 3`. Then half the diggers and a quarter of the
|
|
2229
|
+
builders edit terrain on any one tick.
|
|
2230
|
+
- Update in groups across frames. For 24 diggers that is at least 4
|
|
2231
|
+
groups on NTSC (57,606 / 4 = 14,402) and 3 on PAL (57,299 / 3 =
|
|
2232
|
+
19,100, which leaves little).
|
|
2233
|
+
- Make the probe look through creature glyphs: a screen code from
|
|
2234
|
+
`CR_BASE` up to the pool is a creature's, so read `under[]` for that
|
|
2235
|
+
cell instead, again if that is another creature's code. Then no
|
|
2236
|
+
restore-all is needed before the updates, and only a creature that
|
|
2237
|
+
moved is restored and drawn again.
|
|
2238
|
+
- Or draw into a second screen and flip `$D018` when it is complete
|
|
2239
|
+
(`screen_double_buffer_d018`, the pitfall's listed mitigation). The
|
|
2240
|
+
creatures never leave the visible screen. It costs 1 KB, a copy or
|
|
2241
|
+
redraw of the changed cells, and a second set of 48 creature codes,
|
|
2242
|
+
since the draw rewrites the glyphs of the codes the shown screen
|
|
2243
|
+
uses; with the recipe's budget those come out of the pool.
|
|
2244
|
+
|
|
2245
|
+
Hand-written assembly would also be cheaper; not measured here.
|
|
2246
|
+
|
|
2247
|
+
### Recipes
|
|
2248
|
+
|
|
2249
|
+
- `recipes/oscar64/destructible-terrain.md` — 24 creatures released from a hatch, a blocker, two diggers and a builder given their roles at fixed ticks, one fatal-fall rule; terrain and creature states checked against a Python model after 360 ticks; worst frames timed on both models
|
|
2250
|
+
## seeded_level_fill — A whole level from a seed, three thresholds and a short object list
|
|
2251
|
+
|
|
2252
|
+
**Complexity:** low
|
|
2253
|
+
**Region:** both
|
|
2254
|
+
**Requires:** lfsr_random
|
|
2255
|
+
**Cost:** bytes_data=880
|
|
2256
|
+
**Cost basis:** arithmetic
|
|
2257
|
+
|
|
2258
|
+
**Why.** A hand-drawn 40 by 22 tile field is 880 bytes, or a few hundred
|
|
2259
|
+
after a run-length pass; sixteen of them are a large share of the
|
|
2260
|
+
memory a game has left. A level made from a seed is two bytes plus a
|
|
2261
|
+
row of thresholds and a handful of placed objects, and a game can have
|
|
2262
|
+
as many levels as it has table rows. Determinism is the design value
|
|
2263
|
+
that makes this safe: the same seed gives the same field on every
|
|
2264
|
+
machine and every run, so the level a designer tuned is the level the
|
|
2265
|
+
player gets, and a recorded input script replays true. The game-design
|
|
2266
|
+
page `../game-design/game-structure.md` gives that property as the one
|
|
2267
|
+
Liepa wanted for Boulder Dash and the one attract-mode replay depends on.
|
|
2268
|
+
|
|
2269
|
+
**How.** One table row per level: a 16-bit seed, thresholds for each
|
|
2270
|
+
tile class, and an object list. Generation sets the LFSR state from the
|
|
2271
|
+
seed, then visits every cell in one fixed order, steps the generator
|
|
2272
|
+
once per cell and reads the low byte of the state. The thresholds are
|
|
2273
|
+
cumulative: below the first the cell is wall, below the second dirt,
|
|
2274
|
+
below the third a gem, else empty; the frame round the field is forced
|
|
2275
|
+
to wall. After the fill, the object list runs and writes over whatever
|
|
2276
|
+
the fill left in those cells, so the player start, the exit and any
|
|
2277
|
+
guaranteed items are exact. The difficulty table is the threshold
|
|
2278
|
+
columns: raising the wall and dirt thresholds and lowering the gem span
|
|
2279
|
+
makes a denser, poorer field without touching the generator. Clamp the
|
|
2280
|
+
level index to the last row. Keep a checksum routine over the field as
|
|
2281
|
+
a test aid: one 16-bit number on screen, or in a byte a harness reads,
|
|
2282
|
+
says whether a build still generates the level it did last week. The
|
|
2283
|
+
field is a tile map; a text screen draws it one character per cell as
|
|
2284
|
+
the recipe does, and a metatile game hands it to its renderer.
|
|
2285
|
+
|
|
2286
|
+
**Why it works.** The LFSR is a permutation of its non-zero states, so
|
|
2287
|
+
from a given seed the sequence of low bytes is fixed and every cell's
|
|
2288
|
+
class is a pure function of the seed and its position in the visiting
|
|
2289
|
+
order. The recipe generates level 1 twice into two buffers and finds no
|
|
2290
|
+
differing byte in 880, with checksum `$1731` both times; level 2's seed
|
|
2291
|
+
and thresholds give `$D622` (measured in VICE x64sc 3.10, both models).
|
|
2292
|
+
The low byte over one period takes every value 256 times and zero 255
|
|
2293
|
+
(`lfsr_random`), so a threshold of `n` selects close to `n` in 256
|
|
2294
|
+
cells: level 2's thresholds of 64, 192 and 204 gave 290 wall cells of
|
|
2295
|
+
which 120 are the frame, 400 dirt and 190 empty or sparse-glyph cells in
|
|
2296
|
+
the picture. That is a proportion, not a guarantee: successive low bytes
|
|
2297
|
+
of a right-shifting register are correlated, so the field has streaks, and
|
|
2298
|
+
a design that needs a clean distribution mixes the state, or steps the
|
|
2299
|
+
register more than once per cell, or takes a byte from a separate
|
|
2300
|
+
8-bit register.
|
|
2301
|
+
|
|
2302
|
+
**Variations.** A solvability check: flood-fill from the player start
|
|
2303
|
+
through the non-wall cells and require the exit and every guaranteed
|
|
2304
|
+
gem to be reached; when one is not, step to the next seed and fill
|
|
2305
|
+
again, and store the seed that passed in the table so the check runs
|
|
2306
|
+
at design time, not on the player's machine. The design layer's
|
|
2307
|
+
difficulty pattern asks that a level be finishable with the starting
|
|
2308
|
+
kit; this is that check for a generated field. Described here, not
|
|
2309
|
+
built. A zero-seed guard: a table entry of `$0000` never leaves state
|
|
2310
|
+
zero (`lfsr_zero_state_lockup`, `pitfalls/cpu.md`), so replace it with a
|
|
2311
|
+
constant before the first step, as the recipe does. A per-level
|
|
2312
|
+
generator mode byte can select a second tile set or a second threshold
|
|
2313
|
+
table for the same seed. A larger field than the screen fills a scroll
|
|
2314
|
+
map in the same pass.
|
|
2315
|
+
|
|
2316
|
+
**Cycle budget.** Generation is a level-start cost, not a per-frame
|
|
2317
|
+
one, so the Cost line above carries only the field buffer (40 by 22)
|
|
2318
|
+
and no `cycles_per_frame`. The recipe times one call of its generator
|
|
2319
|
+
with CIA1 timers A and B chained, interrupts off, screen on so badline
|
|
2320
|
+
stalls are inside the figure: 97,643 cycles on PAL and 98,417 on NTSC,
|
|
2321
|
+
about five PAL frames for 880 cells, or about 111 cycles per cell with
|
|
2322
|
+
the loop, the compare chain and the frame test (measured in VICE x64sc
|
|
2323
|
+
3.10). The LFSR step is a small part of that (`lfsr_random` gives its
|
|
2324
|
+
per-call figure); the rest is C loop and classification, and an
|
|
2325
|
+
assembler inner loop would cut it. Spread the fill over several frames
|
|
2326
|
+
behind a level-start screen if five frames of black matter.
|
|
2327
|
+
|
|
2328
|
+
### Recipes
|
|
2329
|
+
|
|
2330
|
+
- `recipes/oscar64/seeded-level-fill.md` — three-row table (seed, three thresholds, five objects), 40 by 22 field generated twice and compared, checksums of two seeds, CIA-timed generation, verdict byte and border, levels drawn in turn on both models
|
|
2331
|
+
|
|
2332
|
+
---
|
|
2333
|
+
|
|
2334
|
+
## bfs_distance_map — One breadth-first flood from the player, sliced across frames, and every chaser steps downhill
|
|
2335
|
+
|
|
2336
|
+
**Complexity:** medium
|
|
2337
|
+
**Region:** both
|
|
2338
|
+
**Requires:** tile_grid_collision, object_pool
|
|
2339
|
+
**Cost:** cycles_per_frame=8710
|
|
2340
|
+
**Cost basis:** measured-vice
|
|
2341
|
+
**Cost measured on:** oscar64-bfs-distance-map (one 32-cell flood slice, SHOW_DIST 0 build; 10,655 with the digit display the pinned picture shows)
|
|
2342
|
+
|
|
2343
|
+
### Why
|
|
2344
|
+
|
|
2345
|
+
A maze game with several chasers cannot afford a search per chaser.
|
|
2346
|
+
`nav_area_pathfinding` cuts a platform level to a dozen areas and
|
|
2347
|
+
answers from a table, but a tile maze has hundreds of open cells and no
|
|
2348
|
+
useful areas. `ghost_target_tile_ai` avoids search altogether by steering
|
|
2349
|
+
toward a target tile, which is right for a maze with no dead ends and
|
|
2350
|
+
ghosts that may not reverse; in a maze with dead ends it walks into
|
|
2351
|
+
them. A distance map is the third choice: one flood from the player
|
|
2352
|
+
gives every cell its distance, and a chaser anywhere finds its next step
|
|
2353
|
+
by reading four neighbours. The flood's cost is paid once per player
|
|
2354
|
+
move, not once per chaser, and it is easy to slice across frames.
|
|
2355
|
+
|
|
2356
|
+
### How
|
|
2357
|
+
|
|
2358
|
+
1. **The map.** One byte per cell, 255 for a wall or a cell not yet
|
|
2359
|
+
reached. Keep two: the live map the chasers read and the work map
|
|
2360
|
+
the flood writes. Keep a separate wall byte per cell (or the tile
|
|
2361
|
+
map itself) so the flood can test a wall without reading the map it
|
|
2362
|
+
is filling.
|
|
2363
|
+
2. **The queue.** A ring of 256 x bytes and 256 y bytes with byte head
|
|
2364
|
+
and tail indices; they wrap by themselves. The frontier of a
|
|
2365
|
+
breadth-first flood on a screen-sized grid is one or two rings of
|
|
2366
|
+
cells, far below 256, so no overflow test is needed. Seed it with
|
|
2367
|
+
the player's cell at distance 0.
|
|
2368
|
+
3. **The slice.** Each frame, pop up to N cells. For each, look at its
|
|
2369
|
+
four neighbours: if the neighbour is not a wall and reads 255, write
|
|
2370
|
+
the cell's distance plus one and push it. Breadth-first order means
|
|
2371
|
+
the first write to a cell is its final distance. When the queue is
|
|
2372
|
+
empty, swap the two map pointers. The recipe clears the work map as
|
|
2373
|
+
the first two frames of each flood, 440 bytes each, so the clear
|
|
2374
|
+
never shares a frame with a full slice.
|
|
2375
|
+
4. **The trigger.** Remember the cell the current flood started from.
|
|
2376
|
+
When no flood is running and the player is on a different cell,
|
|
2377
|
+
start one. A flood in progress runs to the end; the player's newer
|
|
2378
|
+
position is picked up by the next one. The live map is at most one
|
|
2379
|
+
flood old.
|
|
2380
|
+
5. **The step.** A chaser reads its own cell and its four neighbours
|
|
2381
|
+
from the live map and moves to the lowest value that is strictly
|
|
2382
|
+
lower than its own; if none is, it stays. Test the neighbours in a
|
|
2383
|
+
fixed order and take the first minimum, so a tie is settled the same
|
|
2384
|
+
way on every run. Walls are 255 and can never be lower, so no wall
|
|
2385
|
+
test is needed for the step.
|
|
2386
|
+
|
|
2387
|
+
### Why it works
|
|
2388
|
+
|
|
2389
|
+
Breadth-first order visits cells by non-decreasing distance, so each
|
|
2390
|
+
open cell is written once and popped once: a maze of 404 open cells is
|
|
2391
|
+
404 pops, whatever the number of chasers. Stepping to a strictly lower
|
|
2392
|
+
neighbour follows a shortest path, because every cell at distance d has
|
|
2393
|
+
a neighbour at d minus 1 by construction, and it cannot loop because
|
|
2394
|
+
the value falls at every step. Two maps make the slicing safe: the
|
|
2395
|
+
chasers never see a half-flooded map, and the swap is one pointer
|
|
2396
|
+
exchange. A chaser on a slightly stale map moves toward where the
|
|
2397
|
+
player was a few cells ago and corrects when the next map lands.
|
|
2398
|
+
|
|
2399
|
+
### Variations
|
|
2400
|
+
|
|
2401
|
+
- **Flee by stepping uphill.** The same map, read the other way: a
|
|
2402
|
+
frightened enemy moves to the highest neighbour below 255. Seeding
|
|
2403
|
+
the queue with several cells at distance 0 (the player and its
|
|
2404
|
+
bullets, or every chaser to make a map the player's helper avoids)
|
|
2405
|
+
gives a distance to the nearest of them in one flood.
|
|
2406
|
+
- **A cost map.** Give each tile a step cost (mud two, floor one) and
|
|
2407
|
+
the plain queue no longer gives shortest routes; a small bucket
|
|
2408
|
+
queue, one list per distance value, keeps the pops in order at the
|
|
2409
|
+
price of memory. For costs of one and two, two queues suffice.
|
|
2410
|
+
- **A window round the player.** Flood only a region of r cells round
|
|
2411
|
+
the player and stop when the queue empties or the ring is full;
|
|
2412
|
+
chasers outside it fall back to walking toward the player's
|
|
2413
|
+
coordinates (`ghost_target_tile_ai`'s rule) until they enter the
|
|
2414
|
+
window. Cuts the flood to about (2r)² cells.
|
|
2415
|
+
- **Larger slice, fewer frames.** The recipe's 32 cells per frame is
|
|
2416
|
+
about 44% of a PAL frame in Oscar64 and takes fifteen frames per
|
|
2417
|
+
flood; an assembler inner loop or a smaller slice moves that trade
|
|
2418
|
+
either way. A chaser that moves one cell every four frames does not
|
|
2419
|
+
see a map that is fifteen frames old.
|
|
2420
|
+
- **Chasers as the flood source.** Seeding from the chasers and having
|
|
2421
|
+
the player's marker read the map gives the player a "danger" value
|
|
2422
|
+
per cell for an escort or an autopilot.
|
|
2423
|
+
|
|
2424
|
+
### Cycle budget
|
|
2425
|
+
|
|
2426
|
+
Measured in VICE x64sc 3.10 with CIA1 timers, interrupts masked, Oscar64
|
|
2427
|
+
`-O2` (rung 1). One slice of 32 cells, expanding four neighbours each
|
|
2428
|
+
and no drawing, is 8,710 cycles on PAL and 9,010 on NTSC, about 272
|
|
2429
|
+
cycles per cell in C; with each cell's digit and colour drawn as it is
|
|
2430
|
+
popped, 10,655 and 10,915. The NTSC figures are higher because the
|
|
2431
|
+
slice runs on past the vertical blank into the badlines, and the CIA
|
|
2432
|
+
counts the stolen cycles (arithmetic, rung 3). A full flood of 404
|
|
2433
|
+
cells is 115,912 cycles on PAL as the sum of its slices, over fifteen
|
|
2434
|
+
frames: two clearing the 880-byte work map at 440 bytes each, thirteen
|
|
2435
|
+
expanding. Four chasers stepping once is 1,220 cycles for all four
|
|
2436
|
+
including the timer, so the per-chaser cost is about 300 cycles every
|
|
2437
|
+
step and nothing between steps. The recipe's whole iteration, actors
|
|
2438
|
+
lifted and redrawn, one slice, the player and four chasers, peaks at
|
|
2439
|
+
11,246 cycles on PAL without the digits and 13,196 with them; when the
|
|
2440
|
+
880-byte clear shared a frame with a slice it peaked at 19,226, over an
|
|
2441
|
+
NTSC frame, which is why the clear is sliced too.
|
|
2442
|
+
|
|
2443
|
+
### Recipes
|
|
2444
|
+
|
|
2445
|
+
- `recipes/oscar64/bfs-distance-map.md` — 40 by 22 maze, scripted player route, ring-queue flood at 32 cells per frame into a second map, four chasers stepping downhill every fourth frame, distance digits in colour bands on demand, map compared byte for byte with a Python flood and its checksum, arrival bound and wall check as the verdict, slice and flood cycles on screen, PAL and NTSC
|
|
2446
|
+
|
|
2447
|
+
## lane_depth_engine — Beat-em-up depth: plane Y as depth, a persistent Y-sort that sets sprite priority and hit order, and hits gated by a Y window and an active-frame table
|
|
2448
|
+
|
|
2449
|
+
**Complexity:** medium
|
|
2450
|
+
**Region:** both
|
|
2451
|
+
**Uses registers:** D000, D001, D010, D027
|
|
2452
|
+
**Requires:** object_pool
|
|
2453
|
+
**Cost:** cycles_per_frame=1413
|
|
2454
|
+
**Cost basis:** measured-vice
|
|
2455
|
+
**Cost measured on:** oscar64-beat-em-up-lanes (four actors: sort, priority draw and hit test in one step, screen on)
|
|
2456
|
+
|
|
2457
|
+
### Why
|
|
2458
|
+
|
|
2459
|
+
A beat-em-up puts its actors on a ground plane seen from the side and
|
|
2460
|
+
a little above. Walking down the screen brings an actor nearer the
|
|
2461
|
+
viewer, so its Y is both a screen position and a depth. Two things
|
|
2462
|
+
follow that a flat game never meets. A nearer actor must be drawn over
|
|
2463
|
+
a farther one, whatever order the actors were spawned in, or a fighter
|
|
2464
|
+
behind another walks through him. And a punch that looks right on
|
|
2465
|
+
screen must not connect with an actor who is standing a lane away: the
|
|
2466
|
+
two sprites overlap in X and nearly in Y, and only the plane Y says
|
|
2467
|
+
they are not on the same ground. `per_frame_hitbox` answers which pair
|
|
2468
|
+
touched; it does not answer whether the pair shares a lane, and a box
|
|
2469
|
+
test alone lands hits across lanes all game long.
|
|
2470
|
+
|
|
2471
|
+
### How
|
|
2472
|
+
|
|
2473
|
+
1. **The plane.** Each actor keeps a plane Y, the line its feet stand
|
|
2474
|
+
on, in the actor arrays (`object_pool`). The sprite is placed at
|
|
2475
|
+
plane Y less its height. The plane is a band of tile rows; lanes are
|
|
2476
|
+
bands of the plane, drawn in different colours so a player can read
|
|
2477
|
+
depth, and lane = (plane Y - plane top) / lane height is a HUD and
|
|
2478
|
+
AI value, not what the hit test uses.
|
|
2479
|
+
2. **The sort.** An index array holds the actors far to near and is
|
|
2480
|
+
never reset. Each frame an insertion sort repairs it: an actor that
|
|
2481
|
+
has not passed a neighbour costs one compare, one that has passed k
|
|
2482
|
+
neighbours is shifted k places. This is the persistent sort
|
|
2483
|
+
`sprite_multiplex_game` uses; here it runs over a handful of actors.
|
|
2484
|
+
3. **The priority assignment.** Walk the sorted list from the near
|
|
2485
|
+
end and write actor k into hardware sprite k: position, the `$D010`
|
|
2486
|
+
bit, the pointer and the colour all move with the actor. The VIC-II
|
|
2487
|
+
draws a lower-numbered sprite over a higher one, so the nearest
|
|
2488
|
+
actor is sprite 0 and overlaps every other. A Y-sorted multiplexer
|
|
2489
|
+
does the opposite: `sprite_multiplex_game` assigns slot = sorted
|
|
2490
|
+
index mod 8 in ascending Y, so of two overlapping actors the farther
|
|
2491
|
+
(upper) one gets the lower sprite number and is drawn in front
|
|
2492
|
+
(measured in VICE x64sc 3.10 with that recipe's listing, both orders
|
|
2493
|
+
of two pinned actors; the #39 beat-em-up review). An earlier version
|
|
2494
|
+
of this step said the depth order and the raster order agree because
|
|
2495
|
+
both are Y; they are opposite. The `beat-em-up` starter keeps its
|
|
2496
|
+
fighters in a band of their own, written near to far.
|
|
2497
|
+
4. **The hit window.** For each attacker on an active frame, in sorted
|
|
2498
|
+
order, test each other actor: the absolute difference of the two
|
|
2499
|
+
plane Ys must be within a small window (six lines in the recipe),
|
|
2500
|
+
then the target's X offset, with its sign chosen by the attacker's
|
|
2501
|
+
facing, must be inside the reach. The Y compare goes first because
|
|
2502
|
+
it fails for most pairs and costs one byte compare.
|
|
2503
|
+
5. **The active-frame table.** An attack is a short animation; a table
|
|
2504
|
+
indexed by its frame says which image to show and a parallel table
|
|
2505
|
+
says whether that frame can land. The frame gate is then an indexed
|
|
2506
|
+
load, and a flag per attacker stops one attack scoring twice across
|
|
2507
|
+
its active frames.
|
|
2508
|
+
|
|
2509
|
+
### Why it works
|
|
2510
|
+
|
|
2511
|
+
Between sprites the VIC-II has one priority, the sprite number, and it
|
|
2512
|
+
cannot be changed per pixel or per line except by which actor is in
|
|
2513
|
+
which sprite. Reassigning sprites from a sorted list turns that fixed
|
|
2514
|
+
rule into a depth order at the cost of one table walk, and because the
|
|
2515
|
+
sort is persistent and actors move a line or two a frame, the walk is
|
|
2516
|
+
almost always the best case. The hit window uses the same plane Y the
|
|
2517
|
+
sort used, so what the picture shows in front is also what the rules
|
|
2518
|
+
treat as near; a game that sorts on one value and tests hits on another
|
|
2519
|
+
has fights that look wrong at the edges. Keying the active frames to
|
|
2520
|
+
the animation frame ties the moment a punch can land to the frames on
|
|
2521
|
+
which the punch image is on screen, so the player sees the hit when it
|
|
2522
|
+
happens.
|
|
2523
|
+
|
|
2524
|
+
### Variations
|
|
2525
|
+
|
|
2526
|
+
- **Shadow sprites.** A flat shadow sprite at the actor's plane Y under
|
|
2527
|
+
each fighter makes depth readable when an actor jumps; the shadow
|
|
2528
|
+
stays on the ground and the body leaves it.
|
|
2529
|
+
- **A jump.** Keep the plane Y as the ground value and add a height;
|
|
2530
|
+
the sprite is drawn at plane Y less height, the sort and the hit
|
|
2531
|
+
window still use the plane Y, and a stored ground Y means landing
|
|
2532
|
+
restores the lane without a search.
|
|
2533
|
+
- **More actors.** Past eight sprites the sort is still this one, but
|
|
2534
|
+
`sprite_multiplex_game`'s slot order draws the farther actor in
|
|
2535
|
+
front (step 3), so depth priority needs the slots inside each zone
|
|
2536
|
+
assigned near to far, which the recipe does not do (not built here).
|
|
2537
|
+
The hit order still follows the sorted list. An earlier version said
|
|
2538
|
+
the multiplexer's slot order gives the priority directly.
|
|
2539
|
+
- **Boxes per frame.** Replace the fixed reach with `per_frame_hitbox`
|
|
2540
|
+
boxes emitted at draw time, keeping the plane Y window as the first
|
|
2541
|
+
gate before the box compare.
|
|
2542
|
+
|
|
2543
|
+
### Cycle budget
|
|
2544
|
+
|
|
2545
|
+
Measured in VICE x64sc 3.10 with CIA1 timer B, interrupts masked,
|
|
2546
|
+
Oscar64 `-O2`, on the recipe (rung 1). The insertion sort over four
|
|
2547
|
+
actors costs 262 cycles from a reversed order and 157 already sorted,
|
|
2548
|
+
screen blanked; over the run with the screen on it peaks at 269 on PAL
|
|
2549
|
+
and 279 on NTSC, badline stalls landing inside it, and bottoms at 174.
|
|
2550
|
+
The whole engine step, sort, priority draw of four sprites and the hit
|
|
2551
|
+
test, peaks at 1,413 cycles with the screen on and an attack active,
|
|
2552
|
+
and is 784 on a frame with no active attack, the sort and the draw
|
|
2553
|
+
with no pair tests. An earlier version of this section gave that floor
|
|
2554
|
+
as 84; the recipe's HUD had cut the figure to two digits. The Cost line
|
|
2555
|
+
carries the peak. A game with more actors pays the pair loop
|
|
2556
|
+
per attacker on active frames only; the sort grows by one compare per
|
|
2557
|
+
actor on a quiet frame.
|
|
2558
|
+
|
|
2559
|
+
### Recipes
|
|
2560
|
+
|
|
2561
|
+
- `recipes/oscar64/beat-em-up-lanes.md` — four-lane tile plane, a player and three enemies on scripts, persistent insertion sort into sprite 0 to 3 by depth, six-line hit window and reach by facing, a six-frame attack with two active frames from a table, hit log re-checked against the window, counts and sort order as the verdict, sort and engine cycles on screen, PAL and NTSC
|
|
2562
|
+
|
|
2563
|
+
## vehicle_control — Top-down driving on a vertical scroll: throttle is the scroll speed, steering with momentum and grip, surface read under the wheels
|
|
2564
|
+
|
|
2565
|
+
**Complexity:** medium
|
|
2566
|
+
**Region:** both
|
|
2567
|
+
**Uses registers:** (none)
|
|
2568
|
+
**Requires:** fixed_point_8_8, tile_grid_collision, soft_scroll_v
|
|
2569
|
+
**Cost:** cycles_per_frame=655, cycles_per_frame_typical=557
|
|
2570
|
+
**Cost basis:** measured-vice
|
|
2571
|
+
**Cost measured on:** oscar64-vehicle-control (worst frame is a crash under braking while sliding left on the verge over its limit, one car, in the vertical blank)
|
|
2572
|
+
**Claims:** none
|
|
2573
|
+
**Claims basis:** derived-listing
|
|
2574
|
+
|
|
2575
|
+
### Why
|
|
2576
|
+
|
|
2577
|
+
In a top-down road game the car stays in one band of the screen and the
|
|
2578
|
+
road moves under it. The player's throttle is therefore the scroll
|
|
2579
|
+
speed, and it has to go from standing still to several pixels a frame
|
|
2580
|
+
without jerks. Steering has to feel like a car, not a cursor: the car
|
|
2581
|
+
keeps sliding after the stick is centred, less on grass, much more on
|
|
2582
|
+
water. And the ground under the wheels decides what happens next: road,
|
|
2583
|
+
a verge that slows the car, water for a boat, or rock that ends the run.
|
|
2584
|
+
None of this touches the VIC-II; it is rules over numbers, and the
|
|
2585
|
+
picture follows from them.
|
|
2586
|
+
|
|
2587
|
+
### How
|
|
2588
|
+
|
|
2589
|
+
**State**, all 8.8 fixed point (`fixed_point_8_8`): the forward speed in
|
|
2590
|
+
pixels a frame (unsigned), the lateral velocity (signed), the car's x
|
|
2591
|
+
(unsigned), and the distance travelled as 16.8, a whole-pixel word and a
|
|
2592
|
+
fraction byte. The car's screen y is a constant. Two bytes more: the
|
|
2593
|
+
surface class found last frame and a crash timer.
|
|
2594
|
+
|
|
2595
|
+
**One frame, in this order:**
|
|
2596
|
+
|
|
2597
|
+
1. **Input.** Read the stick once.
|
|
2598
|
+
2. **Speed.** Up adds the acceleration while the speed is below the
|
|
2599
|
+
limit of last frame's surface, down subtracts the brake, no input
|
|
2600
|
+
subtracts a small coast drag; none go below zero. Then, above the
|
|
2601
|
+
limit, the speed falls by a fixed drag a frame, not at once. Up adds
|
|
2602
|
+
nothing over the limit, so driving onto grass at full speed with the
|
|
2603
|
+
throttle held slows the car from 4 to 1.5 pixels a frame over 40
|
|
2604
|
+
frames. If up still added its step, the net fall would be the drag
|
|
2605
|
+
minus the acceleration, and a car at full throttle would barely slow.
|
|
2606
|
+
3. **Scroll accumulator.** Add the speed to the distance. The carry out
|
|
2607
|
+
of the fraction byte moves into the pixel word, so 2.81 pixels a frame
|
|
2608
|
+
advances 2 or 3 pixels and never drifts. The picture is a function of
|
|
2609
|
+
the distance alone: YSCROLL is `distance & 7` and the top map row on
|
|
2610
|
+
screen is `BASE - (distance >> 3)`. A change in `distance >> 3` is a
|
|
2611
|
+
row crossing, and the scroll technique redraws or shifts the matrix.
|
|
2612
|
+
4. **Lateral.** Left or right adds or subtracts the surface's steering
|
|
2613
|
+
step to the lateral velocity, only above a minimum speed so a car
|
|
2614
|
+
standing still cannot slide sideways. With the stick centred, the
|
|
2615
|
+
surface's grip moves the velocity towards zero by a fixed step a
|
|
2616
|
+
frame. Clamp to the maximum lateral speed, then add it to x.
|
|
2617
|
+
5. **Surface probe.** At the new x and the new distance, read the tile
|
|
2618
|
+
map (`tile_grid_collision`) at four wheel points. Screen pixel row
|
|
2619
|
+
`yy` below line 48 shows map pixel row `BASE * 8 + yy - distance`, so
|
|
2620
|
+
each wheel is a row lookup and a column shift. The worst class of the
|
|
2621
|
+
four wins.
|
|
2622
|
+
6. **Response.** Rock is a crash. Anything else becomes the surface for
|
|
2623
|
+
next frame's limit, steering and grip.
|
|
2624
|
+
|
|
2625
|
+
The probe comes after the scroll so it reads the rows the next picture
|
|
2626
|
+
shows. The speed limit uses last frame's surface because the probe needs
|
|
2627
|
+
the new position; the lag is one frame.
|
|
2628
|
+
|
|
2629
|
+
**Surface table** in the recipe. Tile code bits 7 and 6 are the class;
|
|
2630
|
+
speeds in pixels a frame.
|
|
2631
|
+
|
|
2632
|
+
| Class | Code bits 7-6 | Speed limit | Steering step | Grip step | Response |
|
|
2633
|
+
|---|---|---|---|---|---|
|
|
2634
|
+
| road | 00 | 4.00 (`$0400`) | 0.08 (`$0014`) | 0.06 (`$0010`) | none |
|
|
2635
|
+
| verge | 01 | 1.50 (`$0180`) | 0.08 | 0.06 | above the limit, speed falls 0.06 (`$0010`) a frame, plus the brake if down is held |
|
|
2636
|
+
| water | 10 | 3.00 (`$0300`) | 0.03 (`$0008`) | 0.01 (`$0003`) | boat handling: the drift outlasts the input |
|
|
2637
|
+
| rock | 11 | none | none | none | crash |
|
|
2638
|
+
|
|
2639
|
+
Lateral speed is capped at 1.50 (`$0180`), acceleration is 0.05
|
|
2640
|
+
(`$000C`) a frame and the minimum speed for steering 0.25 (`$0040`).
|
|
2641
|
+
|
|
2642
|
+
**Crash rule.** Any wheel on rock: speed and lateral velocity to zero,
|
|
2643
|
+
a 50-frame timer starts, and while it runs input is ignored and the
|
|
2644
|
+
distance does not change, so the road stands still. When it ends the car
|
|
2645
|
+
is put on the road's centre column of the map row under its front
|
|
2646
|
+
wheels, from a per-row centre table built with the map, and the surface
|
|
2647
|
+
is set to road.
|
|
2648
|
+
|
|
2649
|
+
**Several pixels a frame.** The fine scroll moves by the whole speed
|
|
2650
|
+
each frame, and a row crossing comes every 8 pixels. Up to 8 pixels a
|
|
2651
|
+
frame there is at most one crossing a frame; up to 4, at most one every
|
|
2652
|
+
two frames. The recipe caps the road at 4 so it can spread the redraw:
|
|
2653
|
+
it draws the hidden screen matrix for the next top row, half a matrix a
|
|
2654
|
+
frame, and flips `$D018` on the frame the top row equals it
|
|
2655
|
+
(`screen_double_buffer_d018`). A crossing that arrives before the hidden
|
|
2656
|
+
matrix is ready is counted and would show a wrong row; a game that can
|
|
2657
|
+
outrun its redraw has to hold YSCROLL at its last value until the flip,
|
|
2658
|
+
which stalls the picture for a frame and not the logic. Above 8 pixels a
|
|
2659
|
+
frame two rows cross in one frame. An in-place shift (`char_scroll_buffer_v`)
|
|
2660
|
+
would move the rows twice, at least 26,500 cycles by `scroll_panel_split`'s
|
|
2661
|
+
13,262-cycle carry of twenty rows (arithmetic; a full 25-row matrix
|
|
2662
|
+
costs more), more than a frame. A redraw from the
|
|
2663
|
+
map costs the same however many rows moved, because it copies the whole
|
|
2664
|
+
matrix at the new origin. `eight_way_scroll_double_buffer` avoids
|
|
2665
|
+
tearing the same way, drawing the spare matrix for the origin the next
|
|
2666
|
+
crossing will produce and flipping only on an exact match; its colour
|
|
2667
|
+
RAM, which cannot be double buffered, is spread over four fields, so it
|
|
2668
|
+
needs five fields between crossings. A vertical scroll that moves colour
|
|
2669
|
+
RAM that way is limited to 8 pixels in 5 frames, 1.6 pixels a frame
|
|
2670
|
+
(arithmetic). The recipe takes colour from the tile code instead: in
|
|
2671
|
+
extended colour mode (`ecm_mode`) bits 7 and 6 pick the background
|
|
2672
|
+
register, so the surface class and its colour are the same two bits and
|
|
2673
|
+
colour RAM never changes.
|
|
2674
|
+
|
|
2675
|
+
### Why it works
|
|
2676
|
+
|
|
2677
|
+
Everything the player sees is derived from four numbers, and each is
|
|
2678
|
+
advanced by one 8.8 add a frame. The scroll position cannot disagree
|
|
2679
|
+
with the probe because both come from the same distance. Momentum is the
|
|
2680
|
+
lateral velocity carried from frame to frame; grip is only the size of
|
|
2681
|
+
the step that brings it back to zero, so one table row per surface
|
|
2682
|
+
changes the handling without new code. Forward speed of at most 4 and
|
|
2683
|
+
lateral of at most 1.5 pixels a frame are both under a tile, so the
|
|
2684
|
+
front wheels meet every map row the car drives over and no wheel steps
|
|
2685
|
+
over a tile sideways. Across the body the wheel columns are 13 pixels
|
|
2686
|
+
apart, so a single rock tile can pass between them, as it could between
|
|
2687
|
+
a real car's wheels; probe the centre column too if that must crash.
|
|
2688
|
+
|
|
2689
|
+
### Variations
|
|
2690
|
+
|
|
2691
|
+
- **Steering in proportion to speed.** Scale the steering step by the
|
|
2692
|
+
forward speed with `table_multiply_8x8` instead of the minimum-speed
|
|
2693
|
+
threshold. Not built in the recipe.
|
|
2694
|
+
- **Other vehicles.** An enemy car keeps its own distance; its screen y
|
|
2695
|
+
is the player's distance minus its own, plus the player's band, so it
|
|
2696
|
+
scrolls with the road for free. Slots from `object_pool`.
|
|
2697
|
+
- **More than 4 pixels a frame.** Redraw the whole matrix in one frame.
|
|
2698
|
+
The recipe's C copy loop measured 20,330 cycles for it on PAL and
|
|
2699
|
+
20,414 on NTSC, more than a frame on either; a fully unrolled `LDA abs` / `STA abs` copy is 8 cycles a
|
|
2700
|
+
byte, 8,000 for the matrix (arithmetic, `char_scroll_buffer_v`).
|
|
2701
|
+
- **NTSC tempo.** Speed is per frame, so the road passes 20% faster on
|
|
2702
|
+
a 60 Hz machine for the same numbers (`pal_ntsc_tempo_mismatch`).
|
|
2703
|
+
If the game must feel the same, detect the model at start with
|
|
2704
|
+
`pal_ntsc_detection` and scale on NTSC: speed limits by 5/6; the
|
|
2705
|
+
steps added to a speed every frame (acceleration, brake, coast,
|
|
2706
|
+
over-limit drag, steering, grip) by (5/6)² = 25/36, because they are
|
|
2707
|
+
per frame squared; frame counts such as the 50-frame crash timer by
|
|
2708
|
+
6/5 (arithmetic). `difficulty_ramp_tables` scales frames and speeds
|
|
2709
|
+
the same way.
|
|
2710
|
+
|
|
2711
|
+
### Cycle budget
|
|
2712
|
+
|
|
2713
|
+
Measured in VICE x64sc 3.10 with CIA1 timer B around the recipe's
|
|
2714
|
+
`car_update`, interrupts masked, Oscar64 `-O2`, over the 508-frame
|
|
2715
|
+
script (rung 1): worst frame 655 cycles, at frame 247. The script builds
|
|
2716
|
+
that frame on purpose, because it is the longest path through the
|
|
2717
|
+
update: the brake, the verge over-limit drag, the grip step on a slide
|
|
2718
|
+
to the left (the last branch the lateral update tests), the four probes
|
|
2719
|
+
and the crash response. The mean over the 458 frames the car was driving is
|
|
2720
|
+
557. A crashed frame is a timer decrement. The figures are the same on
|
|
2721
|
+
PAL and NTSC because the update runs from line 256, before the first
|
|
2722
|
+
badline, on both. A build with its code laid out differently can move
|
|
2723
|
+
them by a cycle (656 and 558 in a variant of the recipe). 655 cycles is
|
|
2724
|
+
3.3% of a PAL frame.
|
|
2725
|
+
|
|
2726
|
+
The row crossing is not in these figures; it belongs to the scroll. In
|
|
2727
|
+
the recipe the half-matrix redraw measured up to 10,110 cycles on PAL
|
|
2728
|
+
and 10,368 on NTSC with the display on, and the worst frame's whole
|
|
2729
|
+
work, update plus redraw plus the recipe's logging, 11,393 and 11,640.
|
|
2730
|
+
The in-place figure for comparison is `scroll_panel_split`'s carry
|
|
2731
|
+
frame, twenty rows moved, 13,262 cycles on PAL and 13,519 on NTSC.
|
|
2732
|
+
|
|
2733
|
+
### Recipes
|
|
2734
|
+
|
|
2735
|
+
- `recipes/oscar64/vehicle-control.md` — a road that scrolls up to 4 pixels a frame over 220 map rows with verges, a narrowing and a water stretch; the car on a scripted input (accelerate, verge at full throttle, a brake into rock while sliding, restart, coast, boat); a per-frame fold of x, speed, distance and surface against a Python model, PASS/FAIL, worst and typical update cycles, PAL and NTSC
|
|
2736
|
+
|
|
2737
|
+
## car_contact_response — Car contact: push apart on the shallow axis, trade velocity by mass, crash off the road
|
|
2738
|
+
|
|
2739
|
+
**Complexity:** medium
|
|
2740
|
+
**Region:** both
|
|
2741
|
+
**Uses registers:** (none)
|
|
2742
|
+
**Requires:** fixed_point_8_8, tile_grid_collision
|
|
2743
|
+
**Cost:** cycles_per_frame=17549, cycles_per_frame_typical=2081
|
|
2744
|
+
**Cost basis:** measured-vice
|
|
2745
|
+
**Cost measured on:** oscar64-car-contact (worst frame: a built frame, not a bound, eight cars packed so all 28 pairs are in contact and 7 get an impulse, with the tile probe, screen blanked; typical: the mean over one 256-frame pass of the six-car game, screen on, PAL)
|
|
2746
|
+
**Claims:** none
|
|
2747
|
+
**Claims basis:** derived-listing
|
|
2748
|
+
|
|
2749
|
+
### Why
|
|
2750
|
+
|
|
2751
|
+
`per_frame_hitbox` says which pair touched. A driving game also has to
|
|
2752
|
+
say what the touch does, and players read the answer at once: a car
|
|
2753
|
+
nudged sideways must slide, a truck must not, a car shunted from behind
|
|
2754
|
+
must pick up speed, and a car pushed off the tarmac must crash. Leave the
|
|
2755
|
+
overlap alone and cars drive through each other. Kick velocities every
|
|
2756
|
+
frame the boxes overlap and a pair that stays in contact for a few frames
|
|
2757
|
+
takes a few kicks and flies apart.
|
|
2758
|
+
|
|
2759
|
+
### How
|
|
2760
|
+
|
|
2761
|
+
Each car keeps an 8.8 centre and an 8.8 velocity on each axis
|
|
2762
|
+
(`fixed_point_8_8`), a half width and a half height, a mass class (light
|
|
2763
|
+
or heavy), a cooling counter and a crash flag, in parallel arrays. The
|
|
2764
|
+
frame is: drive and move every car, run the pair loop, then probe the
|
|
2765
|
+
tiles.
|
|
2766
|
+
|
|
2767
|
+
1. **The pair loop.** For each pair `i < j` where neither has crashed,
|
|
2768
|
+
take the Y distance from the high bytes and compare it with the sum of
|
|
2769
|
+
the half heights. On a road the cars are spread along it, so most
|
|
2770
|
+
pairs end at this one compare. Then the same for X.
|
|
2771
|
+
2. **The shallow axis.** For a pair that overlaps, the overlap on each
|
|
2772
|
+
axis is the half-size sum less the distance. The smaller one is the
|
|
2773
|
+
axis of contact: side by side, it is X; nose to tail, Y. Resolve on
|
|
2774
|
+
that axis only.
|
|
2775
|
+
3. **Separate by mass.** Split that overlap between the two cars, each
|
|
2776
|
+
moved away from the other: half each for equal masses; for a truck
|
|
2777
|
+
and a car, the truck takes `pen >> 2` and the car the rest. That share
|
|
2778
|
+
is a chosen value that keeps a truck nearly still, not the mass ratio
|
|
2779
|
+
(1/8). Do this on every frame of contact, so cars never pass through
|
|
2780
|
+
each other.
|
|
2781
|
+
4. **The impulse.** Let `rv` be `v[i] - v[j]` on the contact axis. The
|
|
2782
|
+
pair is closing when `rv` has the sign of the side `j` is on. If it is
|
|
2783
|
+
closing and not both cars are cooling, `j` gains `g` and `i` loses
|
|
2784
|
+
`2 * rv - g`, where `g` is `rv` for equal masses, `rv >> 2` when `j`
|
|
2785
|
+
is the heavy one and `2 * rv - (rv >> 2)` when `i` is. This is the
|
|
2786
|
+
elastic result for masses 1 and 7; no multiply is needed. Equal cars
|
|
2787
|
+
swap their velocities on that axis, so a side-swipe hands the sideways
|
|
2788
|
+
speed to the other car and a bump from behind hands over the forward
|
|
2789
|
+
speed. A light car bounces off a truck and the truck takes a quarter of
|
|
2790
|
+
the closing speed.
|
|
2791
|
+
5. **Cooling.** After an impulse, set both cars' counters (eight frames
|
|
2792
|
+
in the recipe). While both cars in a pair are cooling, step 3 still
|
|
2793
|
+
runs but step 4 does not. A car pinned against a truck by its own
|
|
2794
|
+
steering then stays against it instead of kicking it every frame, and
|
|
2795
|
+
a third car that is not cooling can still hit either of them. Because
|
|
2796
|
+
the counter is per car, two cars that have each just hit something
|
|
2797
|
+
else get only the push on their first touch with each other.
|
|
2798
|
+
6. **Crash probe.** After the pair loop, look up the tile class under
|
|
2799
|
+
each car's centre in the level map (`tile_grid_collision`). Grass,
|
|
2800
|
+
water or an edge tile sets the crash flag and zeroes the velocity. The
|
|
2801
|
+
probe comes after the pair loop because a push can put a car off the
|
|
2802
|
+
road. A crashed car leaves the pair loop.
|
|
2803
|
+
|
|
2804
|
+
### Why it works
|
|
2805
|
+
|
|
2806
|
+
The axis of least overlap is the direction of the shortest move that
|
|
2807
|
+
separates the boxes. While no car moves more than a few pixels a frame,
|
|
2808
|
+
the overlap on the axis they met along is still the smaller one, so the
|
|
2809
|
+
push goes back the way they came. The elastic formula for two masses is
|
|
2810
|
+
`v_j' = v_j + 2m_i / (m_i + m_j) * rv` and
|
|
2811
|
+
`v_i' = v_i - 2m_j / (m_i + m_j) * rv`; choosing
|
|
2812
|
+
masses whose sums are powers of two (1 + 1 = 2, 1 + 7 = 8) turns both
|
|
2813
|
+
fractions into shifts, and momentum is kept to the rounding of one shift
|
|
2814
|
+
(arithmetic, rung 3).
|
|
2815
|
+
The closing test stops a pair that is already separating from being
|
|
2816
|
+
kicked back together. The cooling rule is per car, not per pair, so it
|
|
2817
|
+
costs one byte per car, not one per pair. Because every step is integer
|
|
2818
|
+
arithmetic in a fixed order, a model in another language reproduces the
|
|
2819
|
+
run bit for bit, which is how the recipe is checked.
|
|
2820
|
+
|
|
2821
|
+
### Variations
|
|
2822
|
+
|
|
2823
|
+
- **Restitution.** The rule above is fully elastic. Halving both
|
|
2824
|
+
changes, `g >> 1` for `j` and `(2 * rv - g) >> 1` for `i`, gives the
|
|
2825
|
+
fully inelastic result: the pair ends at one common speed on that axis.
|
|
2826
|
+
Not built in the recipe.
|
|
2827
|
+
- **Other mass ratios.** Masses 1 and 3, or 1 and 15, also sum to a
|
|
2828
|
+
power of two and change only the shift count. A third class must make
|
|
2829
|
+
a power-of-two sum with each of the others, which 1, 3 and 15 do not
|
|
2830
|
+
(3 + 15 = 18); past two classes, a small table of the two fractions
|
|
2831
|
+
per class pair, rounded to shifts and adds, is the general form.
|
|
2832
|
+
- **Spin-out.** A side hit above a speed threshold sets a timer that
|
|
2833
|
+
locks steering, instead of or before a crash.
|
|
2834
|
+
- **Scrolling road.** The pair test takes the high byte of an unsigned
|
|
2835
|
+
8.8 centre, so the recipe's world is 256 pixels on each axis. A road
|
|
2836
|
+
longer than that needs either a 16-bit pixel Y (the pair test then
|
|
2837
|
+
compares 16-bit distances), or cars kept in screen coordinates with the
|
|
2838
|
+
scroll subtracted each frame. Either way the probe reads the map row
|
|
2839
|
+
under the car, not the screen row, and must clamp that row: the
|
|
2840
|
+
recipe's probe does no bounds check.
|
|
2841
|
+
|
|
2842
|
+
### Cycle budget
|
|
2843
|
+
|
|
2844
|
+
Measured in VICE x64sc 3.10 with CIA1 timer B, interrupts masked, Oscar64
|
|
2845
|
+
`-O2`, on the recipe (rung 1). One pair costs 87 cycles when the Y test
|
|
2846
|
+
ends it and 147 when the X test does. Two light cars in contact cost 634
|
|
2847
|
+
with an impulse and 505 while both are cooling. A truck and a light car
|
|
2848
|
+
cost 701 with an impulse when the truck is `i` (`g = 2 * rv - (rv >> 2)`),
|
|
2849
|
+
666 when it is `j` (`g = rv >> 2`), and 513 while both are cooling. These
|
|
2850
|
+
are single calls through a wrapper, screen blanked, less an empty call.
|
|
2851
|
+
|
|
2852
|
+
The Cost line's worst frame is a built frame: eight cars packed so that
|
|
2853
|
+
all 28 pairs are still in contact when their turn comes and 7 get an
|
|
2854
|
+
impulse, 17,549 cycles with the probe. Those counts are the most one pass
|
|
2855
|
+
allows: 28 pairs, and 7 impulses, because each impulse needs a car that
|
|
2856
|
+
is not yet cooling and the first uses two. The frame is not a bound,
|
|
2857
|
+
because its contacts mix light and truck pairs. The bound by arithmetic
|
|
2858
|
+
(rung 3), with every impulse at 701 and every other contact at 513, is
|
|
2859
|
+
17,549 + 7 x 67 + 21 x 8 = 18,186 cycles, assuming no contact in the
|
|
2860
|
+
pile costs less than the light-car figures; not measured. Eight cars in a
|
|
2861
|
+
2 x 4 grid, neighbours overlapping by 2 pixels, cost 10,949 (12 contacts,
|
|
2862
|
+
7 impulses).
|
|
2863
|
+
|
|
2864
|
+
The typical figure is the mean of the timed step over the 256 frames of
|
|
2865
|
+
one pass of the recipe's six-car game, screen on: 2,081 on PAL (2,079 for
|
|
2866
|
+
the next pass) and 2,152 on NTSC. The worst game frame over two passes is
|
|
2867
|
+
3,695 on PAL and 3,910 on NTSC, and the best 828, after the crashes have
|
|
2868
|
+
taken cars out of the pair loop. At 634 cycles for a contact, a C pair
|
|
2869
|
+
loop is affordable for a handful of cars that rarely touch; hand-written
|
|
2870
|
+
assembly would be cheaper, not measured here.
|
|
2871
|
+
|
|
2872
|
+
In assembly the two signed steps need care. The closing test is the sign
|
|
2873
|
+
of `rv`. The recipe's 8.8 velocities stay within ±1,536 (6 pixels a
|
|
2874
|
+
frame, the model's largest over the pass), so the 16-bit difference
|
|
2875
|
+
cannot overflow and its high byte has the right sign. It can overflow
|
|
2876
|
+
when velocities are kept in signed bytes, where `rv` passes 127, or when
|
|
2877
|
+
16-bit velocities can exceed ±16,383: then `bmi` after the subtract gives
|
|
2878
|
+
the wrong order (`signed_compare_bmi_overflow`). And `rv >> 2` must be an
|
|
2879
|
+
arithmetic shift: `lda hi / cmp #$80 / ror / sta hi / ror lo`, twice,
|
|
2880
|
+
where `cmp #$80` copies the sign into the carry (`asr1` in
|
|
2881
|
+
`sine_table_generation`). `lsr` would turn a negative velocity positive.
|
|
2882
|
+
The recipe's pass includes a negative `rv` of -379, whose floor shift is
|
|
2883
|
+
-95; a build using a truncating divide (-94) fails the checksum.
|
|
2884
|
+
|
|
2885
|
+
### Recipes
|
|
2886
|
+
|
|
2887
|
+
- `recipes/oscar64/car-contact.md` — a road with grass and water edges, a player and five enemy cars of two masses on scripts: a side-swipe into the water, a bump from behind, a truck that moves 5 pixels, two contacts in one frame that put two cars on the grass, a nine-frame shove that exercises the cooling rule and the truck's share of the push; a per-frame checksum of positions, velocities and crash flags against a Python model, PASS/FAIL, cycles per pair (light and truck) and per frame, PAL and NTSC
|
|
2888
|
+
|
|
2889
|
+
### Sources
|
|
2890
|
+
|
|
2891
|
+
- Elastic collision in one dimension, the two-mass velocity formulas:
|
|
2892
|
+
https://en.wikipedia.org/wiki/Elastic_collision
|
|
2893
|
+
|
|
2894
|
+
## lane_pursuit_ai — Road pursuit cars: approach, pull alongside, ram with a lead, back off, leave when hit, with swept look-ahead probes to stay on the road
|
|
2895
|
+
|
|
2896
|
+
**Complexity:** medium
|
|
2897
|
+
**Region:** both
|
|
2898
|
+
**Requires:** fixed_point_8_8, tile_grid_collision, object_pool
|
|
2899
|
+
**Cost:** cycles_per_frame=14204, cycles_per_frame_typical=9871
|
|
2900
|
+
**Cost basis:** measured-vice
|
|
2901
|
+
**Cost measured on:** oscar64-lane-pursuit (worst frame: the spawn table's two same-frame spawns, then four cars on the ram path, one in contact, each with an eight-probe swept span and both room scans, screen blanked; typical: worst frame of the 780-step run, screen blanked)
|
|
2902
|
+
**Claims:** none
|
|
2903
|
+
**Claims basis:** derived-listing
|
|
2904
|
+
|
|
2905
|
+
### Why
|
|
2906
|
+
|
|
2907
|
+
The enemy cars in a road shooter are the main threat, and they have to
|
|
2908
|
+
drive like drivers: catch up, sit beside the player, swerve into it,
|
|
2909
|
+
drop back, and never drive through the scenery. Path bytecode
|
|
2910
|
+
(`wave_director`) cannot do it, because the player moves and the road
|
|
2911
|
+
changes under both cars. A path search (`nav_area_pathfinding`) is more
|
|
2912
|
+
than a road needs: the road is one wide corridor, and the question is
|
|
2913
|
+
only where across it to be. The answer is a small state machine per car
|
|
2914
|
+
that picks a target point relative to the player, two capped
|
|
2915
|
+
proportional controllers that move the car toward it, and a probe of the
|
|
2916
|
+
map ahead that overrides the target when the road runs out.
|
|
2917
|
+
|
|
2918
|
+
### How
|
|
2919
|
+
|
|
2920
|
+
1. **The car record.** Parallel arrays per slot (`object_pool`): state,
|
|
2921
|
+
timer, side, x in 8.8, lateral speed in 8.8, world y, and forward
|
|
2922
|
+
speed relative to the player. World y spans more than 256 pixels, so
|
|
2923
|
+
the recipe keeps y and the forward speed in 1/16 pixel in 16 bits.
|
|
2924
|
+
A spawner (`wave_director`, or a table) fills a slot with state
|
|
2925
|
+
APPROACH, an x, and an offset behind or ahead of the player.
|
|
2926
|
+
2. **The states.** Each state names a lateral target and a forward
|
|
2927
|
+
target; the recipe's values:
|
|
2928
|
+
|
|
2929
|
+
| State | Lateral target | Forward target | Next |
|
|
2930
|
+
|---|---|---|---|
|
|
2931
|
+
| APPROACH | player x ± 24 px, on its side | level with the player | within 8 px of level and 6 px of the slot: ALONG |
|
|
2932
|
+
| ALONG | player x ± 24 px | level | after 40 frames: RAM |
|
|
2933
|
+
| RAM | player x + 8 × player's lateral speed | level | bodies within 14 px across and 16 px along: contact, BACK; else after 24 frames: BACK |
|
|
2934
|
+
| BACK | player x ± 40 px | 40 px behind | after 50 frames: APPROACH |
|
|
2935
|
+
| LEAVE | its own x | 200 px behind, higher speed cap | 150 px behind: slot freed |
|
|
2936
|
+
|
|
2937
|
+
A hit sends a car in any state to LEAVE; that is the "give up".
|
|
2938
|
+
3. **One car per side.** A car in APPROACH claims the side of the player
|
|
2939
|
+
it is on, or the other side if that is taken. With both taken it
|
|
2940
|
+
queues 40 px behind at its own x. The claim is released on BACK and
|
|
2941
|
+
LEAVE, so rams come one per side at a time.
|
|
2942
|
+
4. **Steering rule.** Lateral error e = target x - x in pixels. The wanted
|
|
2943
|
+
lateral speed is e × 16 in 8.8 (1/16 pixel a frame per pixel of
|
|
2944
|
+
error), clamped to the state's speed cap: 1 px a frame, 3 in RAM, 2
|
|
2945
|
+
while avoiding. The lateral speed then moves toward the wanted speed
|
|
2946
|
+
by at most the state's acceleration cap: 1/8 px a frame per frame,
|
|
2947
|
+
3/8 in RAM, 1/2 while avoiding. Add the speed to x. The forward
|
|
2948
|
+
controller has the same shape on the offset from the player: wanted
|
|
2949
|
+
relative speed (target offset - offset) / 8, capped at 1.5 px a
|
|
2950
|
+
frame (3 in LEAVE), changing by at most 1/8 px a frame per frame. The
|
|
2951
|
+
speed is relative, so the pursuers keep pace when the player speeds
|
|
2952
|
+
up (the recipe's player holds one speed). Divide a signed error by
|
|
2953
|
+
shifting its magnitude and restoring the sign. That rounds toward
|
|
2954
|
+
zero, the same way left and right, and does not depend on how a
|
|
2955
|
+
language shifts a negative number, so a model can match the code
|
|
2956
|
+
exactly. A 6502 has no arithmetic shift; `CMP #$80 : ROR` floors
|
|
2957
|
+
instead, which leaves -1 to -7 at -1, a one-unit pull to the left.
|
|
2958
|
+
5. **The lead.** RAM aims at the player's x plus eight frames of its
|
|
2959
|
+
lateral speed: where the player will be rather than where it is. In
|
|
2960
|
+
the recipe's model it did not raise the hit rate: 3 contacts from 7
|
|
2961
|
+
rams with the 8-frame lead, 4 from 7 with none, 4 from 6 with 4. The
|
|
2962
|
+
autopilot player's lateral speed is at most 2 px a frame against the
|
|
2963
|
+
ram's 3. A lead matters
|
|
2964
|
+
when the target moves sideways about as fast as the rammer.
|
|
2965
|
+
6. **Spacing.** Test the other pursuers: the first one within 20 px
|
|
2966
|
+
across and 32 px along moves this car's lateral target 20 px away
|
|
2967
|
+
from it and, if it is ahead, the forward target to 32 px behind it.
|
|
2968
|
+
7. **Look-ahead probe.** Probe the map row 32 px past the car's nose
|
|
2969
|
+
across the span from its x now to the x its lateral speed gives it 16
|
|
2970
|
+
frames on, every 8 pixels and at the far edge, so no tile is skipped
|
|
2971
|
+
(`tile_grid_collision`, step 3). If any probe is not road, count the
|
|
2972
|
+
road tiles in the 8 tiles either side of the span's centre on that
|
|
2973
|
+
row, set the lateral target 32 px toward the side with more road, and
|
|
2974
|
+
use the avoiding caps. At a narrowing that is the open side; at a fork
|
|
2975
|
+
the wider channel, which need not be the player's (see below).
|
|
2976
|
+
8. **Side probe.** Otherwise probe one point 12 px beyond the body on the
|
|
2977
|
+
side the car is steering toward, on its rear row and 8 px past its
|
|
2978
|
+
nose. If it is not road, hold the lateral target at the car's own x
|
|
2979
|
+
and brake with the avoiding acceleration cap. The player's slot can
|
|
2980
|
+
lie on the verge; the car then waits beside the edge instead of
|
|
2981
|
+
weaving.
|
|
2982
|
+
|
|
2983
|
+
### Why it works
|
|
2984
|
+
|
|
2985
|
+
The speed cap limits how fast a car crosses the road. The acceleration
|
|
2986
|
+
cap gives it weight: a ram builds over several frames, so the player
|
|
2987
|
+
sees it coming and can dodge. The same weight is why the probe must look
|
|
2988
|
+
ahead. A car cannot stop sideways at once. The side probe sits 12 px
|
|
2989
|
+
beyond the body, so a car moving at most 3 px a frame first sees an
|
|
2990
|
+
edge 9 to 12 px away. The hold then brakes at the avoiding cap of 1/2 px
|
|
2991
|
+
a frame per frame; the code changes the speed before it moves, so the
|
|
2992
|
+
car stops within 7.5 px (arithmetic). At the ram cap of 3/8 it would
|
|
2993
|
+
need 10.5 px and could clip the verge by 1.5 px. The look-ahead must
|
|
2994
|
+
cover the frames a turn takes. An early draft of the
|
|
2995
|
+
recipe's model probed 24 px ahead at the car's current x, with no side
|
|
2996
|
+
probe and no spacing, and counted 226 car-frames with a car on the verge
|
|
2997
|
+
or the water in 800 frames: ramming cars slid onto the island. The
|
|
2998
|
+
final rules, with the swept span, 32 px from the nose and the side
|
|
2999
|
+
probe, count 0 in 780.
|
|
3000
|
+
|
|
3001
|
+
At a fork the rule is greedy by one row, and it separates pursuers from
|
|
3002
|
+
the player. In the recipe's model, on car-frames with the player and a
|
|
3003
|
+
pursuer both beside the island, the pursuer is across the water in 375
|
|
3004
|
+
of 452. The cause is the lateral cap against the short look. Cars on the
|
|
3005
|
+
player's right when it dives into the left channel are 75 px or more
|
|
3006
|
+
from it. At the ALONG cap of 1 px a frame that is 75 frames, 150 px of
|
|
3007
|
+
road at 2 px a frame (arithmetic), while the probe sees 32 px past the
|
|
3008
|
+
nose.
|
|
3009
|
+
|
|
3010
|
+
Fixes tried in the model, none kept:
|
|
3011
|
+
|
|
3012
|
+
| Rule | Across the water | Off-road car-frames |
|
|
3013
|
+
|---|---|---|
|
|
3014
|
+
| Room only (the recipe) | 375 of 452 | 0 |
|
|
3015
|
+
| Prefer the player's side if the car can clear into it within 24 px | 375 of 452 | 0 |
|
|
3016
|
+
| A fork marker per map row, read 96 px past the nose, names the left channel; avoiding caps while it is seen | 20 of 441 | 28 |
|
|
3017
|
+
| The same, read 160 px past the nose | 203 of 437 | 1 |
|
|
3018
|
+
| The marker's channel is the player's when read, 160 px, ALONG caps | 346 of 394 | 0 |
|
|
3019
|
+
|
|
3020
|
+
A marker that names the channel does bring the cars across, but these
|
|
3021
|
+
simple forms fight the edge probes and put cars on the water. A game
|
|
3022
|
+
that needs pursuers through a split wants a planned lane change: a
|
|
3023
|
+
per-row channel table read well ahead, a target moved into the chosen
|
|
3024
|
+
channel, and the edge probes told which channel is meant. Until the
|
|
3025
|
+
channels join, a pursuer across a split is out of play.
|
|
3026
|
+
|
|
3027
|
+
### Variations
|
|
3028
|
+
|
|
3029
|
+
- **Edge table.** Store each map row's left edge, right edge and island
|
|
3030
|
+
span, and compare the span against them. That replaces the 16-tile
|
|
3031
|
+
room scan, most of the probe cost below, with a few compares (not
|
|
3032
|
+
measured here).
|
|
3033
|
+
- **Personalities.** Per-type rows of speed caps, acceleration caps,
|
|
3034
|
+
timers and slot offsets: a car that only rams from behind, a car that
|
|
3035
|
+
holds ALONG and fires. A heavy truck might take a RAM cap of 2 px and
|
|
3036
|
+
1/4 acceleration, a wider body (CW 16) with the probes widened to
|
|
3037
|
+
match, and a longer ALONG (not built here).
|
|
3038
|
+
- **Contact.** The recipe counts a contact and backs off. A game calls
|
|
3039
|
+
`car_contact_response` on contact, and moves a car to LEAVE once that
|
|
3040
|
+
pushes it off the road; the player car itself is `vehicle_control`.
|
|
3041
|
+
- **Traffic.** Slower civilian cars that pursuers must not hit go in
|
|
3042
|
+
the spacing scan with the pursuers. The spacing box then steers a
|
|
3043
|
+
pursuer round them (not built here).
|
|
3044
|
+
|
|
3045
|
+
### Cycle budget
|
|
3046
|
+
|
|
3047
|
+
Measured in VICE x64sc 3.10 with CIA1 timer B, the display and sprites
|
|
3048
|
+
off, Oscar64 `-O2`, identical on PAL and NTSC (rung 1). Badlines and
|
|
3049
|
+
sprite DMA lengthen every figure in a game. A "frame" here is one logic
|
|
3050
|
+
step. The single-car figures include one 5-cycle timer pair. The run's
|
|
3051
|
+
worst frame includes five (spawn and four cars, 25 cycles); the built
|
|
3052
|
+
frame includes two.
|
|
3053
|
+
|
|
3054
|
+
| Work | Cycles |
|
|
3055
|
+
|---|---|
|
|
3056
|
+
| One car update, mean over the 780-step run | 1,981 |
|
|
3057
|
+
| One car update, worst of the run | 3,162 |
|
|
3058
|
+
| One car on the built worst path: contact on the ram step, full spacing scan, eight-probe span blocked at its last probe, both room scans | 3,361 |
|
|
3059
|
+
| The edge probes alone: eight probes on a clear row and both room scans | 1,649 |
|
|
3060
|
+
| Worst frame of the run: the spawn and four car updates | 9,871 |
|
|
3061
|
+
| Built worst frame: the table's two same-frame spawns, then four cars on the ram path (car 0 in contact, three whose ram timer runs out) | 14,204 |
|
|
3062
|
+
|
|
3063
|
+
The Cost line carries the built frame, 72% of a PAL frame (19,656
|
|
3064
|
+
cycles) and 83% of an NTSC one (17,095; arithmetic), and the run's worst
|
|
3065
|
+
frame as the typical figure. Four cars at the mean cost about 7,924
|
|
3066
|
+
(arithmetic). These are C figures and an upper reference; hand-written
|
|
3067
|
+
assembly would cost less (not measured here).
|
|
3068
|
+
|
|
3069
|
+
The recipe's own screen loop does not fit a frame. With the display on,
|
|
3070
|
+
its pass 1 runs one step every second video frame: 39,370 cycles a step on
|
|
3071
|
+
PAL and 34,130 on NTSC, 2.00 frames each (measured). The step, a
|
|
3072
|
+
21-row redraw and the sprite placement overrun one frame.
|
|
3073
|
+
|
|
3074
|
+
A game that also scrolls, multiplexes and plays music cannot pay 14,000
|
|
3075
|
+
cycles for its pursuers. Two cuts, neither measured here. Run the probes,
|
|
3076
|
+
about half the built car's cost, for two cars per frame in turn: each car
|
|
3077
|
+
then sees the road every second frame, and its look-ahead shrinks by one
|
|
3078
|
+
frame of travel: 2 px forward plus up to 1.5 px of relative speed, 2 to
|
|
3079
|
+
3.5 px (arithmetic). Or run the whole AI at 25 Hz under `logic_rate_decoupling`. Timers and speeds are
|
|
3080
|
+
per frame, so on NTSC every pursuer runs about 19% faster unless they
|
|
3081
|
+
are scaled (arithmetic from 59.826 / 50.125 Hz).
|
|
3082
|
+
|
|
3083
|
+
### Recipes
|
|
3084
|
+
|
|
3085
|
+
- `recipes/oscar64/lane-pursuit.md` — an original road that narrows and splits round a water island, a weaving autopilot player, four pursuit cars from a spawn table that approach, pull alongside, ram with a lead and back off, one that steers off a narrowing edge and one shot that leaves; every frame's car states and x checksummed against a Python model; per-car and worst-frame cycles on screen, PAL and NTSC
|
|
3086
|
+
|
|
3087
|
+
### Sources
|
|
3088
|
+
|
|
3089
|
+
- Original rules and code, written for this page; no game's code was
|
|
3090
|
+
read. The state names follow the brief in issue #38
|
|
3091
|
+
(https://github.com/bdgscotland/c64-kb/issues/38).
|