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.
Files changed (970) hide show
  1. package/CHANGELOG.md +2082 -0
  2. package/LICENSE +29 -0
  3. package/README.md +559 -0
  4. package/VERSION +25 -0
  5. package/dist/cli/setup.js +66 -0
  6. package/dist/cli/setup.js.map +1 -0
  7. package/dist/cli-memorize-audit.js +97 -0
  8. package/dist/cli-memorize-audit.js.map +1 -0
  9. package/dist/cli.js +335 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.js +83 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/context.js +53 -0
  14. package/dist/context.js.map +1 -0
  15. package/dist/domain/budget-notes.js +104 -0
  16. package/dist/domain/budget-notes.js.map +1 -0
  17. package/dist/domain/budget.js +417 -0
  18. package/dist/domain/budget.js.map +1 -0
  19. package/dist/domain/game-design.js +77 -0
  20. package/dist/domain/game-design.js.map +1 -0
  21. package/dist/domain/timing.js +39 -0
  22. package/dist/domain/timing.js.map +1 -0
  23. package/dist/graph/apply.js +132 -0
  24. package/dist/graph/apply.js.map +1 -0
  25. package/dist/graph/claims.js +231 -0
  26. package/dist/graph/claims.js.map +1 -0
  27. package/dist/graph/extract/archetype.js +118 -0
  28. package/dist/graph/extract/archetype.js.map +1 -0
  29. package/dist/graph/extract/common.js +69 -0
  30. package/dist/graph/extract/common.js.map +1 -0
  31. package/dist/graph/extract/failure.js +37 -0
  32. package/dist/graph/extract/failure.js.map +1 -0
  33. package/dist/graph/extract/game-design.js +189 -0
  34. package/dist/graph/extract/game-design.js.map +1 -0
  35. package/dist/graph/extract/hardware.js +124 -0
  36. package/dist/graph/extract/hardware.js.map +1 -0
  37. package/dist/graph/extract/pitfall.js +62 -0
  38. package/dist/graph/extract/pitfall.js.map +1 -0
  39. package/dist/graph/extract/raster-band.js +56 -0
  40. package/dist/graph/extract/raster-band.js.map +1 -0
  41. package/dist/graph/extract/recipe.js +71 -0
  42. package/dist/graph/extract/recipe.js.map +1 -0
  43. package/dist/graph/extract/technique-entities.js +180 -0
  44. package/dist/graph/extract/technique-entities.js.map +1 -0
  45. package/dist/graph/extract/technique.js +179 -0
  46. package/dist/graph/extract/technique.js.map +1 -0
  47. package/dist/graph/extract/toolchain.js +67 -0
  48. package/dist/graph/extract/toolchain.js.map +1 -0
  49. package/dist/graph/extract/types.js +3 -0
  50. package/dist/graph/extract/types.js.map +1 -0
  51. package/dist/graph/extract/vocabulary.js +65 -0
  52. package/dist/graph/extract/vocabulary.js.map +1 -0
  53. package/dist/graph/extract.js +43 -0
  54. package/dist/graph/extract.js.map +1 -0
  55. package/dist/graph/kernal-clobbers.js +64 -0
  56. package/dist/graph/kernal-clobbers.js.map +1 -0
  57. package/dist/graph/machine-variants.js +208 -0
  58. package/dist/graph/machine-variants.js.map +1 -0
  59. package/dist/ingest/files.js +99 -0
  60. package/dist/ingest/files.js.map +1 -0
  61. package/dist/ingest/passes.js +104 -0
  62. package/dist/ingest/passes.js.map +1 -0
  63. package/dist/ingest/points.js +50 -0
  64. package/dist/ingest/points.js.map +1 -0
  65. package/dist/ingest/report.js +140 -0
  66. package/dist/ingest/report.js.map +1 -0
  67. package/dist/ingest/tally.js +81 -0
  68. package/dist/ingest/tally.js.map +1 -0
  69. package/dist/ingest/verified-on.js +43 -0
  70. package/dist/ingest/verified-on.js.map +1 -0
  71. package/dist/ingest.js +135 -0
  72. package/dist/ingest.js.map +1 -0
  73. package/dist/schemas/cost-basis.js +5 -0
  74. package/dist/schemas/cost-basis.js.map +1 -0
  75. package/dist/schemas/machine-variant.js +25 -0
  76. package/dist/schemas/machine-variant.js.map +1 -0
  77. package/dist/schemas/plan-budget.js +121 -0
  78. package/dist/schemas/plan-budget.js.map +1 -0
  79. package/dist/schemas/tool-outputs.js +526 -0
  80. package/dist/schemas/tool-outputs.js.map +1 -0
  81. package/dist/server/define-tool.js +52 -0
  82. package/dist/server/define-tool.js.map +1 -0
  83. package/dist/server/defined-only.js +9 -0
  84. package/dist/server/defined-only.js.map +1 -0
  85. package/dist/server/tools-briefings.js +70 -0
  86. package/dist/server/tools-briefings.js.map +1 -0
  87. package/dist/server/tools-lookup.js +180 -0
  88. package/dist/server/tools-lookup.js.map +1 -0
  89. package/dist/server/tools-maintenance.js +116 -0
  90. package/dist/server/tools-maintenance.js.map +1 -0
  91. package/dist/server/tools-pitfalls.js +89 -0
  92. package/dist/server/tools-pitfalls.js.map +1 -0
  93. package/dist/server/tools-recipes.js +296 -0
  94. package/dist/server/tools-recipes.js.map +1 -0
  95. package/dist/server/tools-runtime.js +48 -0
  96. package/dist/server/tools-runtime.js.map +1 -0
  97. package/dist/server/tools.js +43 -0
  98. package/dist/server/tools.js.map +1 -0
  99. package/dist/server.js +95 -0
  100. package/dist/server.js.map +1 -0
  101. package/dist/services/analytics/schema.js +78 -0
  102. package/dist/services/analytics/schema.js.map +1 -0
  103. package/dist/services/analytics/statements.js +99 -0
  104. package/dist/services/analytics/statements.js.map +1 -0
  105. package/dist/services/analytics.js +195 -0
  106. package/dist/services/analytics.js.map +1 -0
  107. package/dist/services/bm25.js +158 -0
  108. package/dist/services/bm25.js.map +1 -0
  109. package/dist/services/chunker.js +81 -0
  110. package/dist/services/chunker.js.map +1 -0
  111. package/dist/services/embeddings.js +58 -0
  112. package/dist/services/embeddings.js.map +1 -0
  113. package/dist/services/falkor/base.js +85 -0
  114. package/dist/services/falkor/base.js.map +1 -0
  115. package/dist/services/falkor/links.js +361 -0
  116. package/dist/services/falkor/links.js.map +1 -0
  117. package/dist/services/falkor/nodes.js +183 -0
  118. package/dist/services/falkor/nodes.js.map +1 -0
  119. package/dist/services/falkor/params.js +45 -0
  120. package/dist/services/falkor/params.js.map +1 -0
  121. package/dist/services/falkor/schema.js +77 -0
  122. package/dist/services/falkor/schema.js.map +1 -0
  123. package/dist/services/falkor.js +100 -0
  124. package/dist/services/falkor.js.map +1 -0
  125. package/dist/services/qdrant.js +238 -0
  126. package/dist/services/qdrant.js.map +1 -0
  127. package/dist/services/versions.js +73 -0
  128. package/dist/services/versions.js.map +1 -0
  129. package/dist/services/vice-bin.js +51 -0
  130. package/dist/services/vice-bin.js.map +1 -0
  131. package/dist/tools/briefings/archetype.js +174 -0
  132. package/dist/tools/briefings/archetype.js.map +1 -0
  133. package/dist/tools/briefings/budget.js +177 -0
  134. package/dist/tools/briefings/budget.js.map +1 -0
  135. package/dist/tools/briefings/build-order.js +135 -0
  136. package/dist/tools/briefings/build-order.js.map +1 -0
  137. package/dist/tools/briefings/build.js +185 -0
  138. package/dist/tools/briefings/build.js.map +1 -0
  139. package/dist/tools/briefings/discovery.js +290 -0
  140. package/dist/tools/briefings/discovery.js.map +1 -0
  141. package/dist/tools/briefings/plan-pitfalls.js +69 -0
  142. package/dist/tools/briefings/plan-pitfalls.js.map +1 -0
  143. package/dist/tools/briefings/render.js +151 -0
  144. package/dist/tools/briefings/render.js.map +1 -0
  145. package/dist/tools/briefings/rows.js +41 -0
  146. package/dist/tools/briefings/rows.js.map +1 -0
  147. package/dist/tools/briefings/toolchain.js +63 -0
  148. package/dist/tools/briefings/toolchain.js.map +1 -0
  149. package/dist/tools/briefings/why-proposed.js +69 -0
  150. package/dist/tools/briefings/why-proposed.js.map +1 -0
  151. package/dist/tools/briefings.js +44 -0
  152. package/dist/tools/briefings.js.map +1 -0
  153. package/dist/tools/feedback.js +97 -0
  154. package/dist/tools/feedback.js.map +1 -0
  155. package/dist/tools/hydrate.js +75 -0
  156. package/dist/tools/hydrate.js.map +1 -0
  157. package/dist/tools/intelligence.js +67 -0
  158. package/dist/tools/intelligence.js.map +1 -0
  159. package/dist/tools/lint/asm-decimal.js +86 -0
  160. package/dist/tools/lint/asm-decimal.js.map +1 -0
  161. package/dist/tools/lint/asm-rules.js +221 -0
  162. package/dist/tools/lint/asm-rules.js.map +1 -0
  163. package/dist/tools/lint/asm-shared.js +3 -0
  164. package/dist/tools/lint/asm-shared.js.map +1 -0
  165. package/dist/tools/lint/c-rules.js +215 -0
  166. package/dist/tools/lint/c-rules.js.map +1 -0
  167. package/dist/tools/lint/text.js +122 -0
  168. package/dist/tools/lint/text.js.map +1 -0
  169. package/dist/tools/lint/types.js +40 -0
  170. package/dist/tools/lint/types.js.map +1 -0
  171. package/dist/tools/lint.js +76 -0
  172. package/dist/tools/lint.js.map +1 -0
  173. package/dist/tools/memorization-mcp.js +111 -0
  174. package/dist/tools/memorization-mcp.js.map +1 -0
  175. package/dist/tools/pitfalls/bm25.js +53 -0
  176. package/dist/tools/pitfalls/bm25.js.map +1 -0
  177. package/dist/tools/pitfalls/format.js +58 -0
  178. package/dist/tools/pitfalls/format.js.map +1 -0
  179. package/dist/tools/pitfalls/graph.js +124 -0
  180. package/dist/tools/pitfalls/graph.js.map +1 -0
  181. package/dist/tools/pitfalls.js +134 -0
  182. package/dist/tools/pitfalls.js.map +1 -0
  183. package/dist/tools/prompts.js +81 -0
  184. package/dist/tools/prompts.js.map +1 -0
  185. package/dist/tools/query/compatibility/closure.js +62 -0
  186. package/dist/tools/query/compatibility/closure.js.map +1 -0
  187. package/dist/tools/query/compatibility/facts.js +28 -0
  188. package/dist/tools/query/compatibility/facts.js.map +1 -0
  189. package/dist/tools/query/compatibility/fetch.js +164 -0
  190. package/dist/tools/query/compatibility/fetch.js.map +1 -0
  191. package/dist/tools/query/compatibility/hard-rules.js +159 -0
  192. package/dist/tools/query/compatibility/hard-rules.js.map +1 -0
  193. package/dist/tools/query/compatibility/index.js +27 -0
  194. package/dist/tools/query/compatibility/index.js.map +1 -0
  195. package/dist/tools/query/compatibility/kernal-zp-rule.js +90 -0
  196. package/dist/tools/query/compatibility/kernal-zp-rule.js.map +1 -0
  197. package/dist/tools/query/compatibility/render.js +106 -0
  198. package/dist/tools/query/compatibility/render.js.map +1 -0
  199. package/dist/tools/query/compatibility/rules.js +337 -0
  200. package/dist/tools/query/compatibility/rules.js.map +1 -0
  201. package/dist/tools/query/compatibility/unit-rules.js +253 -0
  202. package/dist/tools/query/compatibility/unit-rules.js.map +1 -0
  203. package/dist/tools/query/game-design.js +83 -0
  204. package/dist/tools/query/game-design.js.map +1 -0
  205. package/dist/tools/query/lookups.js +220 -0
  206. package/dist/tools/query/lookups.js.map +1 -0
  207. package/dist/tools/query/plan-budget.js +266 -0
  208. package/dist/tools/query/plan-budget.js.map +1 -0
  209. package/dist/tools/query/recipes.js +214 -0
  210. package/dist/tools/query/recipes.js.map +1 -0
  211. package/dist/tools/query/retrieval.js +128 -0
  212. package/dist/tools/query/retrieval.js.map +1 -0
  213. package/dist/tools/query/shared.js +121 -0
  214. package/dist/tools/query/shared.js.map +1 -0
  215. package/dist/tools/query/techniques.js +331 -0
  216. package/dist/tools/query/techniques.js.map +1 -0
  217. package/dist/tools/query/timing.js +106 -0
  218. package/dist/tools/query/timing.js.map +1 -0
  219. package/dist/tools/query/types.js +11 -0
  220. package/dist/tools/query/types.js.map +1 -0
  221. package/dist/tools/query.js +21 -0
  222. package/dist/tools/query.js.map +1 -0
  223. package/dist/tools/resources.js +186 -0
  224. package/dist/tools/resources.js.map +1 -0
  225. package/dist/tools/run-game.js +249 -0
  226. package/dist/tools/run-game.js.map +1 -0
  227. package/dist/tools/selfimprovement.js +367 -0
  228. package/dist/tools/selfimprovement.js.map +1 -0
  229. package/docker-compose.yml +35 -0
  230. package/docs/ARCHITECTURE.md +164 -0
  231. package/docs/CONVENTIONS-archetypes.md +158 -0
  232. package/docs/CONVENTIONS-failures.md +46 -0
  233. package/docs/CONVENTIONS-game-designs.md +75 -0
  234. package/docs/CONVENTIONS-hardware-reference.md +184 -0
  235. package/docs/CONVENTIONS-pitfalls.md +91 -0
  236. package/docs/CONVENTIONS-recipes.md +106 -0
  237. package/docs/CONVENTIONS-techniques.md +309 -0
  238. package/docs/CONVENTIONS-toolchain-reference.md +89 -0
  239. package/docs/ONTOLOGY.md +655 -0
  240. package/docs/art/art-production-reference.md +157 -0
  241. package/docs/art/asset-pipelines.md +586 -0
  242. package/docs/c64-failure-patterns.md +693 -0
  243. package/docs/demo-design/demo-composition.md +349 -0
  244. package/docs/demo-design/demo-design-philosophy.md +320 -0
  245. package/docs/demo-design/intro-cracktro-patterns.md +435 -0
  246. package/docs/figures/ctm-spd-embed-probe.png +0 -0
  247. package/docs/figures/dot-flag-mid-5000000.png +0 -0
  248. package/docs/figures/dot-flag-no-erase-12000000.png +0 -0
  249. package/docs/figures/edges.json +1 -0
  250. package/docs/figures/fig6-architecture.png +0 -0
  251. package/docs/figures/fig7-ontology.png +0 -0
  252. package/docs/figures/screen-dissolve-mid-frame25.png +0 -0
  253. package/docs/figures/screen-dissolve-wipe-frame25.png +0 -0
  254. package/docs/figures/spindle-two-parts-part1.png +0 -0
  255. package/docs/figures/spindle-two-parts-part2.png +0 -0
  256. package/docs/figures/starters/action-puzzle.png +0 -0
  257. package/docs/figures/starters/adventure.png +0 -0
  258. package/docs/figures/starters/beat-em-up.png +0 -0
  259. package/docs/figures/starters/demo.png +0 -0
  260. package/docs/figures/starters/platformer.png +0 -0
  261. package/docs/figures/starters/shmup-vertical.png +0 -0
  262. package/docs/figures/tape-kernal-master-ntsc.png +0 -0
  263. package/docs/figures/tape-kernal-master-pal.png +0 -0
  264. package/docs/figures/tape-turbo-loader-ntsc.png +0 -0
  265. package/docs/figures/tape-turbo-loader-pal.png +0 -0
  266. package/docs/figures/tape-turbo-master-ntsc.png +0 -0
  267. package/docs/figures/tape-turbo-master-pal.png +0 -0
  268. package/docs/figures/tech-tech-pad2-12000000.png +0 -0
  269. package/docs/figures/tech-tech-pad4-12000000.png +0 -0
  270. package/docs/figures/tech-tech-seven-12000000.png +0 -0
  271. package/docs/figures/twister-mid-5000000.png +0 -0
  272. package/docs/figures/twister-straight-12000000.png +0 -0
  273. package/docs/figures/vector-balls-nosort-ntsc.png +0 -0
  274. package/docs/figures/vector-balls-nosort.png +0 -0
  275. package/docs/formats/c64-file-formats.md +1212 -0
  276. package/docs/formats/iec-disk-reference.md +754 -0
  277. package/docs/game-design/c64-game-archetypes.md +273 -0
  278. package/docs/game-design/designs/falling-blocks.md +54 -0
  279. package/docs/game-design/designs/platformer-scaffold.md +48 -0
  280. package/docs/game-design/designs/simple-shmup.md +27 -0
  281. package/docs/game-design/enemy-behaviour-and-difficulty.md +349 -0
  282. package/docs/game-design/game-design-patterns.md +1189 -0
  283. package/docs/game-design/game-structure.md +354 -0
  284. package/docs/game-design/production-planning.md +349 -0
  285. package/docs/game-design/reference-game-sources.md +90 -0
  286. package/docs/hardware/6502-illegal-opcodes.md +1353 -0
  287. package/docs/hardware/6510-cpu-reference.md +2112 -0
  288. package/docs/hardware/c64-memory-map.md +3184 -0
  289. package/docs/hardware/c64-registers-reference.md +996 -0
  290. package/docs/hardware/cia-reference.md +1281 -0
  291. package/docs/hardware/kernal-routines-reference.md +1836 -0
  292. package/docs/hardware/pal-ntsc-reference.md +813 -0
  293. package/docs/hardware/sid-reference.md +1137 -0
  294. package/docs/hardware/vic-ii-reference.md +1714 -0
  295. package/docs/music/music-production-reference.md +403 -0
  296. package/docs/pitfalls/banking.md +1067 -0
  297. package/docs/pitfalls/cia.md +1147 -0
  298. package/docs/pitfalls/cpu.md +874 -0
  299. package/docs/pitfalls/input.md +282 -0
  300. package/docs/pitfalls/kernal-and-io.md +1615 -0
  301. package/docs/pitfalls/loader.md +1191 -0
  302. package/docs/pitfalls/logic.md +94 -0
  303. package/docs/pitfalls/maths.md +187 -0
  304. package/docs/pitfalls/raster-and-badline.md +721 -0
  305. package/docs/pitfalls/region-timing.md +660 -0
  306. package/docs/pitfalls/scroll.md +207 -0
  307. package/docs/pitfalls/sid.md +738 -0
  308. package/docs/pitfalls/sprite.md +979 -0
  309. package/docs/pitfalls/text-mode-render.md +1172 -0
  310. package/docs/recipes/cc65/cartridge-8k.md +398 -0
  311. package/docs/recipes/cc65/hello-world-conio.md +76 -0
  312. package/docs/recipes/cc65/memory-layout.md +312 -0
  313. package/docs/recipes/cc65/screenshots/README.md +14 -0
  314. package/docs/recipes/cc65/screenshots/cartridge-8k-ntsc.png +0 -0
  315. package/docs/recipes/cc65/screenshots/cartridge-8k.png +0 -0
  316. package/docs/recipes/cc65/screenshots/hello-world-conio.png +0 -0
  317. package/docs/recipes/cc65/screenshots/memory-layout-ntsc.png +0 -0
  318. package/docs/recipes/cc65/screenshots/memory-layout.png +0 -0
  319. package/docs/recipes/kickassembler/basic-float-calls.md +578 -0
  320. package/docs/recipes/kickassembler/basic-wedge.md +398 -0
  321. package/docs/recipes/kickassembler/big-font-scroller.md +620 -0
  322. package/docs/recipes/kickassembler/charset-copy-rom-to-ram.md +623 -0
  323. package/docs/recipes/kickassembler/cia-revision-detect.md +366 -0
  324. package/docs/recipes/kickassembler/colour-cycling.md +343 -0
  325. package/docs/recipes/kickassembler/colour-fade.md +303 -0
  326. package/docs/recipes/kickassembler/compare-16bit-signed.md +1007 -0
  327. package/docs/recipes/kickassembler/cracktro-template.md +526 -0
  328. package/docs/recipes/kickassembler/crt-banked.md +276 -0
  329. package/docs/recipes/kickassembler/dos-error-codes.md +466 -0
  330. package/docs/recipes/kickassembler/dot-flag.md +576 -0
  331. package/docs/recipes/kickassembler/drive-job-queue.md +667 -0
  332. package/docs/recipes/kickassembler/drive-via-probe.md +690 -0
  333. package/docs/recipes/kickassembler/dycp-scroller.md +568 -0
  334. package/docs/recipes/kickassembler/dypp-sprite-scroller.md +681 -0
  335. package/docs/recipes/kickassembler/dysp.md +670 -0
  336. package/docs/recipes/kickassembler/easyflash-save.md +800 -0
  337. package/docs/recipes/kickassembler/eight-way-scroll.md +1484 -0
  338. package/docs/recipes/kickassembler/file-io-roundtrip.md +546 -0
  339. package/docs/recipes/kickassembler/fire-effect.md +420 -0
  340. package/docs/recipes/kickassembler/fld.md +414 -0
  341. package/docs/recipes/kickassembler/fli-image.md +336 -0
  342. package/docs/recipes/kickassembler/four-player-read.md +672 -0
  343. package/docs/recipes/kickassembler/headless-verify.md +228 -0
  344. package/docs/recipes/kickassembler/hello-world.md +98 -0
  345. package/docs/recipes/kickassembler/high-score-insert.md +660 -0
  346. package/docs/recipes/kickassembler/hires-plot-line.md +723 -0
  347. package/docs/recipes/kickassembler/irq-chain.md +316 -0
  348. package/docs/recipes/kickassembler/irq-owns-port.md +668 -0
  349. package/docs/recipes/kickassembler/isometric-room.md +1145 -0
  350. package/docs/recipes/kickassembler/light-pen-read.md +926 -0
  351. package/docs/recipes/kickassembler/logic-rate-decoupling.md +670 -0
  352. package/docs/recipes/kickassembler/mci-interlace.md +543 -0
  353. package/docs/recipes/kickassembler/memory-layout.md +277 -0
  354. package/docs/recipes/kickassembler/mouse-1351-read.md +801 -0
  355. package/docs/recipes/kickassembler/nmi-timer-tick.md +562 -0
  356. package/docs/recipes/kickassembler/own-keyscan.md +881 -0
  357. package/docs/recipes/kickassembler/paddle-read.md +751 -0
  358. package/docs/recipes/kickassembler/palette-cells.md +219 -0
  359. package/docs/recipes/kickassembler/pseudo-3d-road.md +930 -0
  360. package/docs/recipes/kickassembler/pwm-digi.md +463 -0
  361. package/docs/recipes/kickassembler/raster-bars.md +264 -0
  362. package/docs/recipes/kickassembler/reu-dma.md +1019 -0
  363. package/docs/recipes/kickassembler/runtime-relocation.md +743 -0
  364. package/docs/recipes/kickassembler/screen-dissolve.md +545 -0
  365. package/docs/recipes/kickassembler/screen-wipe.md +408 -0
  366. package/docs/recipes/kickassembler/screenshots/README.md +72 -0
  367. package/docs/recipes/kickassembler/screenshots/basic-float-calls-ntsc.png +0 -0
  368. package/docs/recipes/kickassembler/screenshots/basic-float-calls.png +0 -0
  369. package/docs/recipes/kickassembler/screenshots/basic-wedge-ntsc.png +0 -0
  370. package/docs/recipes/kickassembler/screenshots/basic-wedge.png +0 -0
  371. package/docs/recipes/kickassembler/screenshots/big-font-scroller-ntsc.png +0 -0
  372. package/docs/recipes/kickassembler/screenshots/big-font-scroller.png +0 -0
  373. package/docs/recipes/kickassembler/screenshots/charset-copy-rom-to-ram-ntsc.png +0 -0
  374. package/docs/recipes/kickassembler/screenshots/charset-copy-rom-to-ram.png +0 -0
  375. package/docs/recipes/kickassembler/screenshots/cia-revision-detect-ntsc.png +0 -0
  376. package/docs/recipes/kickassembler/screenshots/cia-revision-detect.png +0 -0
  377. package/docs/recipes/kickassembler/screenshots/colour-cycling-ntsc.png +0 -0
  378. package/docs/recipes/kickassembler/screenshots/colour-cycling.png +0 -0
  379. package/docs/recipes/kickassembler/screenshots/colour-fade-ntsc.png +0 -0
  380. package/docs/recipes/kickassembler/screenshots/colour-fade.png +0 -0
  381. package/docs/recipes/kickassembler/screenshots/compare-16bit-signed-ntsc.png +0 -0
  382. package/docs/recipes/kickassembler/screenshots/compare-16bit-signed.png +0 -0
  383. package/docs/recipes/kickassembler/screenshots/cracktro-template.png +0 -0
  384. package/docs/recipes/kickassembler/screenshots/crt-banked-ntsc.png +0 -0
  385. package/docs/recipes/kickassembler/screenshots/crt-banked.png +0 -0
  386. package/docs/recipes/kickassembler/screenshots/dos-error-codes-ntsc.png +0 -0
  387. package/docs/recipes/kickassembler/screenshots/dos-error-codes.png +0 -0
  388. package/docs/recipes/kickassembler/screenshots/dot-flag-ntsc.png +0 -0
  389. package/docs/recipes/kickassembler/screenshots/dot-flag.png +0 -0
  390. package/docs/recipes/kickassembler/screenshots/drive-job-queue-ntsc.png +0 -0
  391. package/docs/recipes/kickassembler/screenshots/drive-job-queue.png +0 -0
  392. package/docs/recipes/kickassembler/screenshots/drive-via-probe-ntsc.png +0 -0
  393. package/docs/recipes/kickassembler/screenshots/drive-via-probe.png +0 -0
  394. package/docs/recipes/kickassembler/screenshots/dycp-scroller-ntsc.png +0 -0
  395. package/docs/recipes/kickassembler/screenshots/dycp-scroller.png +0 -0
  396. package/docs/recipes/kickassembler/screenshots/dypp-sprite-scroller-ntsc.png +0 -0
  397. package/docs/recipes/kickassembler/screenshots/dypp-sprite-scroller.png +0 -0
  398. package/docs/recipes/kickassembler/screenshots/dysp-ntsc.png +0 -0
  399. package/docs/recipes/kickassembler/screenshots/dysp.png +0 -0
  400. package/docs/recipes/kickassembler/screenshots/easyflash-save-ntsc-run2.png +0 -0
  401. package/docs/recipes/kickassembler/screenshots/easyflash-save-ntsc.png +0 -0
  402. package/docs/recipes/kickassembler/screenshots/easyflash-save-run2.png +0 -0
  403. package/docs/recipes/kickassembler/screenshots/easyflash-save.png +0 -0
  404. package/docs/recipes/kickassembler/screenshots/eight-way-scroll-ntsc.png +0 -0
  405. package/docs/recipes/kickassembler/screenshots/eight-way-scroll.png +0 -0
  406. package/docs/recipes/kickassembler/screenshots/file-io-roundtrip-ntsc.png +0 -0
  407. package/docs/recipes/kickassembler/screenshots/file-io-roundtrip.png +0 -0
  408. package/docs/recipes/kickassembler/screenshots/fire-effect-ntsc.png +0 -0
  409. package/docs/recipes/kickassembler/screenshots/fire-effect.png +0 -0
  410. package/docs/recipes/kickassembler/screenshots/fld-ntsc.png +0 -0
  411. package/docs/recipes/kickassembler/screenshots/fld.png +0 -0
  412. package/docs/recipes/kickassembler/screenshots/fli-image.png +0 -0
  413. package/docs/recipes/kickassembler/screenshots/four-player-read-ntsc.png +0 -0
  414. package/docs/recipes/kickassembler/screenshots/four-player-read.png +0 -0
  415. package/docs/recipes/kickassembler/screenshots/headless-verify-ntsc.png +0 -0
  416. package/docs/recipes/kickassembler/screenshots/headless-verify.png +0 -0
  417. package/docs/recipes/kickassembler/screenshots/hello-world.png +0 -0
  418. package/docs/recipes/kickassembler/screenshots/high-score-insert-ntsc.png +0 -0
  419. package/docs/recipes/kickassembler/screenshots/high-score-insert.png +0 -0
  420. package/docs/recipes/kickassembler/screenshots/hires-plot-line-ntsc.png +0 -0
  421. package/docs/recipes/kickassembler/screenshots/hires-plot-line.png +0 -0
  422. package/docs/recipes/kickassembler/screenshots/irq-chain-ntsc.png +0 -0
  423. package/docs/recipes/kickassembler/screenshots/irq-chain.png +0 -0
  424. package/docs/recipes/kickassembler/screenshots/irq-owns-port-ntsc.png +0 -0
  425. package/docs/recipes/kickassembler/screenshots/irq-owns-port.png +0 -0
  426. package/docs/recipes/kickassembler/screenshots/isometric-room-ntsc.png +0 -0
  427. package/docs/recipes/kickassembler/screenshots/isometric-room.png +0 -0
  428. package/docs/recipes/kickassembler/screenshots/light-pen-read-ntsc.png +0 -0
  429. package/docs/recipes/kickassembler/screenshots/light-pen-read.png +0 -0
  430. package/docs/recipes/kickassembler/screenshots/logic-rate-decoupling-ntsc.png +0 -0
  431. package/docs/recipes/kickassembler/screenshots/logic-rate-decoupling.png +0 -0
  432. package/docs/recipes/kickassembler/screenshots/mci-interlace-average.png +0 -0
  433. package/docs/recipes/kickassembler/screenshots/mci-interlace-frame-b.png +0 -0
  434. package/docs/recipes/kickassembler/screenshots/mci-interlace-ntsc.png +0 -0
  435. package/docs/recipes/kickassembler/screenshots/mci-interlace.png +0 -0
  436. package/docs/recipes/kickassembler/screenshots/memory-layout-ntsc.png +0 -0
  437. package/docs/recipes/kickassembler/screenshots/memory-layout.png +0 -0
  438. package/docs/recipes/kickassembler/screenshots/mouse-1351-read-ntsc.png +0 -0
  439. package/docs/recipes/kickassembler/screenshots/mouse-1351-read.png +0 -0
  440. package/docs/recipes/kickassembler/screenshots/nmi-timer-tick-ntsc.png +0 -0
  441. package/docs/recipes/kickassembler/screenshots/nmi-timer-tick.png +0 -0
  442. package/docs/recipes/kickassembler/screenshots/own-keyscan-ntsc.png +0 -0
  443. package/docs/recipes/kickassembler/screenshots/own-keyscan.png +0 -0
  444. package/docs/recipes/kickassembler/screenshots/paddle-read-ntsc.png +0 -0
  445. package/docs/recipes/kickassembler/screenshots/paddle-read.png +0 -0
  446. package/docs/recipes/kickassembler/screenshots/palette-cells-ntsc.png +0 -0
  447. package/docs/recipes/kickassembler/screenshots/palette-cells.png +0 -0
  448. package/docs/recipes/kickassembler/screenshots/pseudo-3d-road-ntsc.png +0 -0
  449. package/docs/recipes/kickassembler/screenshots/pseudo-3d-road.png +0 -0
  450. package/docs/recipes/kickassembler/screenshots/pwm-digi-ntsc.png +0 -0
  451. package/docs/recipes/kickassembler/screenshots/pwm-digi.png +0 -0
  452. package/docs/recipes/kickassembler/screenshots/raster-bars.png +0 -0
  453. package/docs/recipes/kickassembler/screenshots/reu-dma-ntsc.png +0 -0
  454. package/docs/recipes/kickassembler/screenshots/reu-dma.png +0 -0
  455. package/docs/recipes/kickassembler/screenshots/runtime-relocation-ntsc.png +0 -0
  456. package/docs/recipes/kickassembler/screenshots/runtime-relocation.png +0 -0
  457. package/docs/recipes/kickassembler/screenshots/screen-dissolve-ntsc.png +0 -0
  458. package/docs/recipes/kickassembler/screenshots/screen-dissolve.png +0 -0
  459. package/docs/recipes/kickassembler/screenshots/screen-wipe-ntsc.png +0 -0
  460. package/docs/recipes/kickassembler/screenshots/screen-wipe.png +0 -0
  461. package/docs/recipes/kickassembler/screenshots/scroll-panel-split-ntsc.png +0 -0
  462. package/docs/recipes/kickassembler/screenshots/scroll-panel-split.png +0 -0
  463. package/docs/recipes/kickassembler/screenshots/sfx-in-player-ntsc.png +0 -0
  464. package/docs/recipes/kickassembler/screenshots/sfx-in-player.png +0 -0
  465. package/docs/recipes/kickassembler/screenshots/shadebobs-ntsc.png +0 -0
  466. package/docs/recipes/kickassembler/screenshots/shadebobs.png +0 -0
  467. package/docs/recipes/kickassembler/screenshots/sideborder-open.png +0 -0
  468. package/docs/recipes/kickassembler/screenshots/sine-scroller.png +0 -0
  469. package/docs/recipes/kickassembler/screenshots/sine-table-runtime-ntsc.png +0 -0
  470. package/docs/recipes/kickassembler/screenshots/sine-table-runtime.png +0 -0
  471. package/docs/recipes/kickassembler/screenshots/software-sprite-preshifted-ntsc.png +0 -0
  472. package/docs/recipes/kickassembler/screenshots/software-sprite-preshifted.png +0 -0
  473. package/docs/recipes/kickassembler/screenshots/speedcode-generator-ntsc.png +0 -0
  474. package/docs/recipes/kickassembler/screenshots/speedcode-generator.png +0 -0
  475. package/docs/recipes/kickassembler/screenshots/sprite-border-scroller-ntsc.png +0 -0
  476. package/docs/recipes/kickassembler/screenshots/sprite-border-scroller.png +0 -0
  477. package/docs/recipes/kickassembler/screenshots/sprite-cache-flip-ntsc.png +0 -0
  478. package/docs/recipes/kickassembler/screenshots/sprite-cache-flip.png +0 -0
  479. package/docs/recipes/kickassembler/screenshots/sprite-multiplex-24.png +0 -0
  480. package/docs/recipes/kickassembler/screenshots/sprite-multiplex-game-ntsc.png +0 -0
  481. package/docs/recipes/kickassembler/screenshots/sprite-multiplex-game.png +0 -0
  482. package/docs/recipes/kickassembler/screenshots/sprite-priority-classes-ntsc.png +0 -0
  483. package/docs/recipes/kickassembler/screenshots/sprite-priority-classes.png +0 -0
  484. package/docs/recipes/kickassembler/screenshots/sprite-sine-chain-ntsc.png +0 -0
  485. package/docs/recipes/kickassembler/screenshots/sprite-sine-chain.png +0 -0
  486. package/docs/recipes/kickassembler/screenshots/sprite-stretcher-ntsc.png +0 -0
  487. package/docs/recipes/kickassembler/screenshots/sprite-stretcher.png +0 -0
  488. package/docs/recipes/kickassembler/screenshots/sprites-only-screen-ntsc.png +0 -0
  489. package/docs/recipes/kickassembler/screenshots/sprites-only-screen.png +0 -0
  490. package/docs/recipes/kickassembler/screenshots/sqrt-atan2-ntsc.png +0 -0
  491. package/docs/recipes/kickassembler/screenshots/sqrt-atan2.png +0 -0
  492. package/docs/recipes/kickassembler/screenshots/stable-raster-irq-control.png +0 -0
  493. package/docs/recipes/kickassembler/screenshots/stable-raster-irq.png +0 -0
  494. package/docs/recipes/kickassembler/screenshots/tech-tech-ntsc.png +0 -0
  495. package/docs/recipes/kickassembler/screenshots/tech-tech.png +0 -0
  496. package/docs/recipes/kickassembler/screenshots/tod-alarm-ntsc.png +0 -0
  497. package/docs/recipes/kickassembler/screenshots/tod-alarm.png +0 -0
  498. package/docs/recipes/kickassembler/screenshots/topbottom-border-open.png +0 -0
  499. package/docs/recipes/kickassembler/screenshots/twister-ntsc.png +0 -0
  500. package/docs/recipes/kickassembler/screenshots/twister.png +0 -0
  501. package/docs/recipes/kickassembler/screenshots/unit-test-driver-ntsc.png +0 -0
  502. package/docs/recipes/kickassembler/screenshots/unit-test-driver.png +0 -0
  503. package/docs/recipes/kickassembler/screenshots/vector-balls-ntsc.png +0 -0
  504. package/docs/recipes/kickassembler/screenshots/vector-balls.png +0 -0
  505. package/docs/recipes/kickassembler/screenshots/wireframe-ships-ntsc.png +0 -0
  506. package/docs/recipes/kickassembler/screenshots/wireframe-ships.png +0 -0
  507. package/docs/recipes/kickassembler/scroll-panel-split.md +381 -0
  508. package/docs/recipes/kickassembler/sfx-in-player.md +907 -0
  509. package/docs/recipes/kickassembler/shadebobs.md +522 -0
  510. package/docs/recipes/kickassembler/sideborder-open.md +338 -0
  511. package/docs/recipes/kickassembler/sine-scroller.md +321 -0
  512. package/docs/recipes/kickassembler/sine-table-runtime.md +815 -0
  513. package/docs/recipes/kickassembler/software-sprite-preshifted.md +725 -0
  514. package/docs/recipes/kickassembler/speedcode-generator.md +606 -0
  515. package/docs/recipes/kickassembler/sprite-border-scroller.md +696 -0
  516. package/docs/recipes/kickassembler/sprite-cache-flip.md +1071 -0
  517. package/docs/recipes/kickassembler/sprite-multiplex-24.md +326 -0
  518. package/docs/recipes/kickassembler/sprite-multiplex-game.md +986 -0
  519. package/docs/recipes/kickassembler/sprite-priority-classes.md +463 -0
  520. package/docs/recipes/kickassembler/sprite-sine-chain.md +338 -0
  521. package/docs/recipes/kickassembler/sprite-stretcher.md +501 -0
  522. package/docs/recipes/kickassembler/sprites-only-screen.md +550 -0
  523. package/docs/recipes/kickassembler/sqrt-atan2.md +1014 -0
  524. package/docs/recipes/kickassembler/stable-raster-irq.md +360 -0
  525. package/docs/recipes/kickassembler/tape-turbo-loader.md +875 -0
  526. package/docs/recipes/kickassembler/tech-tech.md +679 -0
  527. package/docs/recipes/kickassembler/tod-alarm.md +754 -0
  528. package/docs/recipes/kickassembler/topbottom-border-open.md +407 -0
  529. package/docs/recipes/kickassembler/twister.md +518 -0
  530. package/docs/recipes/kickassembler/unit-test-driver.md +314 -0
  531. package/docs/recipes/kickassembler/vector-balls.md +545 -0
  532. package/docs/recipes/kickassembler/wireframe-ships.md +2324 -0
  533. package/docs/recipes/oscar64/actor-activation-window.md +807 -0
  534. package/docs/recipes/oscar64/adventure-engine.md +1094 -0
  535. package/docs/recipes/oscar64/attract-replay.md +465 -0
  536. package/docs/recipes/oscar64/beat-em-up-lanes.md +563 -0
  537. package/docs/recipes/oscar64/bfs-distance-map.md +723 -0
  538. package/docs/recipes/oscar64/bitmap-koala-viewer.md +355 -0
  539. package/docs/recipes/oscar64/car-contact.md +950 -0
  540. package/docs/recipes/oscar64/cave-scan.md +609 -0
  541. package/docs/recipes/oscar64/char-bullets.md +477 -0
  542. package/docs/recipes/oscar64/charset-animation.md +383 -0
  543. package/docs/recipes/oscar64/charset-parallax.md +401 -0
  544. package/docs/recipes/oscar64/destructible-terrain.md +1099 -0
  545. package/docs/recipes/oscar64/difficulty-tables.md +654 -0
  546. package/docs/recipes/oscar64/dig-and-guards.md +975 -0
  547. package/docs/recipes/oscar64/directory-reader.md +535 -0
  548. package/docs/recipes/oscar64/divide-check.md +581 -0
  549. package/docs/recipes/oscar64/double-buffer-nomirror.md +205 -0
  550. package/docs/recipes/oscar64/double-buffer.md +255 -0
  551. package/docs/recipes/oscar64/falling-blocks.md +874 -0
  552. package/docs/recipes/oscar64/fixed-point-jump-velocity.md +289 -0
  553. package/docs/recipes/oscar64/fixed-point-jump.md +210 -0
  554. package/docs/recipes/oscar64/flip-screen-rooms.md +793 -0
  555. package/docs/recipes/oscar64/frame-sync-loop-overrun.md +214 -0
  556. package/docs/recipes/oscar64/frame-sync-loop.md +254 -0
  557. package/docs/recipes/oscar64/game-tree-search.md +743 -0
  558. package/docs/recipes/oscar64/ghost-targeting.md +867 -0
  559. package/docs/recipes/oscar64/headless-verify.md +345 -0
  560. package/docs/recipes/oscar64/hello-world.md +83 -0
  561. package/docs/recipes/oscar64/high-score-persist.md +772 -0
  562. package/docs/recipes/oscar64/iffl-kernal-skip.md +421 -0
  563. package/docs/recipes/oscar64/joystick-input.md +358 -0
  564. package/docs/recipes/oscar64/lane-pursuit.md +1028 -0
  565. package/docs/recipes/oscar64/level-rle-decoder.md +620 -0
  566. package/docs/recipes/oscar64/lfsr-random-seed2.md +338 -0
  567. package/docs/recipes/oscar64/lfsr-random.md +403 -0
  568. package/docs/recipes/oscar64/load-asset-runtime.md +492 -0
  569. package/docs/recipes/oscar64/memory-layout.md +232 -0
  570. package/docs/recipes/oscar64/mixed-fighters.md +781 -0
  571. package/docs/recipes/oscar64/multi-sprite-object.md +533 -0
  572. package/docs/recipes/oscar64/nav-area-pathfinding.md +881 -0
  573. package/docs/recipes/oscar64/object-pool.md +572 -0
  574. package/docs/recipes/oscar64/pal-ntsc-detect.md +257 -0
  575. package/docs/recipes/oscar64/password-state.md +532 -0
  576. package/docs/recipes/oscar64/per-frame-hitbox.md +651 -0
  577. package/docs/recipes/oscar64/petscii-screen-codes.md +428 -0
  578. package/docs/recipes/oscar64/platformer-scaffold.md +1467 -0
  579. package/docs/recipes/oscar64/print-number.md +367 -0
  580. package/docs/recipes/oscar64/raster-bars.md +239 -0
  581. package/docs/recipes/oscar64/raster-profile-bars.md +404 -0
  582. package/docs/recipes/oscar64/rel-side-sectors.md +391 -0
  583. package/docs/recipes/oscar64/relative-file-records.md +378 -0
  584. package/docs/recipes/oscar64/save-load-seq-file.md +365 -0
  585. package/docs/recipes/oscar64/screenshots/README.md +99 -0
  586. package/docs/recipes/oscar64/screenshots/actor-activation-window-ntsc.png +0 -0
  587. package/docs/recipes/oscar64/screenshots/actor-activation-window.png +0 -0
  588. package/docs/recipes/oscar64/screenshots/adventure-engine-ntsc.png +0 -0
  589. package/docs/recipes/oscar64/screenshots/adventure-engine.png +0 -0
  590. package/docs/recipes/oscar64/screenshots/attract-replay-ntsc.png +0 -0
  591. package/docs/recipes/oscar64/screenshots/attract-replay.png +0 -0
  592. package/docs/recipes/oscar64/screenshots/beat-em-up-lanes-ntsc.png +0 -0
  593. package/docs/recipes/oscar64/screenshots/beat-em-up-lanes.png +0 -0
  594. package/docs/recipes/oscar64/screenshots/bfs-distance-map-ntsc.png +0 -0
  595. package/docs/recipes/oscar64/screenshots/bfs-distance-map.png +0 -0
  596. package/docs/recipes/oscar64/screenshots/bitmap-koala-viewer.png +0 -0
  597. package/docs/recipes/oscar64/screenshots/car-contact-ntsc.png +0 -0
  598. package/docs/recipes/oscar64/screenshots/car-contact.png +0 -0
  599. package/docs/recipes/oscar64/screenshots/cave-scan-ntsc.png +0 -0
  600. package/docs/recipes/oscar64/screenshots/cave-scan.png +0 -0
  601. package/docs/recipes/oscar64/screenshots/char-bullets-ntsc.png +0 -0
  602. package/docs/recipes/oscar64/screenshots/char-bullets.png +0 -0
  603. package/docs/recipes/oscar64/screenshots/charset-animation-ntsc.png +0 -0
  604. package/docs/recipes/oscar64/screenshots/charset-animation.png +0 -0
  605. package/docs/recipes/oscar64/screenshots/charset-parallax-ntsc.png +0 -0
  606. package/docs/recipes/oscar64/screenshots/charset-parallax.png +0 -0
  607. package/docs/recipes/oscar64/screenshots/destructible-terrain-ntsc.png +0 -0
  608. package/docs/recipes/oscar64/screenshots/destructible-terrain.png +0 -0
  609. package/docs/recipes/oscar64/screenshots/difficulty-tables-ntsc.png +0 -0
  610. package/docs/recipes/oscar64/screenshots/difficulty-tables.png +0 -0
  611. package/docs/recipes/oscar64/screenshots/dig-and-guards-ntsc.png +0 -0
  612. package/docs/recipes/oscar64/screenshots/dig-and-guards.png +0 -0
  613. package/docs/recipes/oscar64/screenshots/directory-reader-ntsc.png +0 -0
  614. package/docs/recipes/oscar64/screenshots/directory-reader.png +0 -0
  615. package/docs/recipes/oscar64/screenshots/divide-check-ntsc.png +0 -0
  616. package/docs/recipes/oscar64/screenshots/divide-check.png +0 -0
  617. package/docs/recipes/oscar64/screenshots/double-buffer-nomirror.png +0 -0
  618. package/docs/recipes/oscar64/screenshots/double-buffer-ntsc.png +0 -0
  619. package/docs/recipes/oscar64/screenshots/double-buffer.png +0 -0
  620. package/docs/recipes/oscar64/screenshots/falling-blocks-ntsc.png +0 -0
  621. package/docs/recipes/oscar64/screenshots/falling-blocks.png +0 -0
  622. package/docs/recipes/oscar64/screenshots/fixed-point-jump-ntsc.png +0 -0
  623. package/docs/recipes/oscar64/screenshots/fixed-point-jump-velocity-ntsc.png +0 -0
  624. package/docs/recipes/oscar64/screenshots/fixed-point-jump-velocity.png +0 -0
  625. package/docs/recipes/oscar64/screenshots/fixed-point-jump.png +0 -0
  626. package/docs/recipes/oscar64/screenshots/flip-screen-rooms-ntsc.png +0 -0
  627. package/docs/recipes/oscar64/screenshots/flip-screen-rooms.png +0 -0
  628. package/docs/recipes/oscar64/screenshots/frame-sync-loop-ntsc.png +0 -0
  629. package/docs/recipes/oscar64/screenshots/frame-sync-loop-overrun.png +0 -0
  630. package/docs/recipes/oscar64/screenshots/frame-sync-loop.png +0 -0
  631. package/docs/recipes/oscar64/screenshots/game-tree-search-ntsc.png +0 -0
  632. package/docs/recipes/oscar64/screenshots/game-tree-search.png +0 -0
  633. package/docs/recipes/oscar64/screenshots/ghost-targeting-ntsc.png +0 -0
  634. package/docs/recipes/oscar64/screenshots/ghost-targeting.png +0 -0
  635. package/docs/recipes/oscar64/screenshots/headless-verify-ntsc.png +0 -0
  636. package/docs/recipes/oscar64/screenshots/headless-verify.png +0 -0
  637. package/docs/recipes/oscar64/screenshots/hello-world.png +0 -0
  638. package/docs/recipes/oscar64/screenshots/high-score-persist-ntsc.png +0 -0
  639. package/docs/recipes/oscar64/screenshots/high-score-persist.png +0 -0
  640. package/docs/recipes/oscar64/screenshots/iffl-kernal-skip-ntsc.png +0 -0
  641. package/docs/recipes/oscar64/screenshots/iffl-kernal-skip.png +0 -0
  642. package/docs/recipes/oscar64/screenshots/joystick-input-ntsc.png +0 -0
  643. package/docs/recipes/oscar64/screenshots/joystick-input.png +0 -0
  644. package/docs/recipes/oscar64/screenshots/lane-pursuit-ntsc.png +0 -0
  645. package/docs/recipes/oscar64/screenshots/lane-pursuit.png +0 -0
  646. package/docs/recipes/oscar64/screenshots/level-rle-decoder-ntsc.png +0 -0
  647. package/docs/recipes/oscar64/screenshots/level-rle-decoder.png +0 -0
  648. package/docs/recipes/oscar64/screenshots/lfsr-random-ntsc.png +0 -0
  649. package/docs/recipes/oscar64/screenshots/lfsr-random-seed2.png +0 -0
  650. package/docs/recipes/oscar64/screenshots/lfsr-random.png +0 -0
  651. package/docs/recipes/oscar64/screenshots/load-asset-runtime-ntsc.png +0 -0
  652. package/docs/recipes/oscar64/screenshots/load-asset-runtime.png +0 -0
  653. package/docs/recipes/oscar64/screenshots/memory-layout-ntsc.png +0 -0
  654. package/docs/recipes/oscar64/screenshots/memory-layout.png +0 -0
  655. package/docs/recipes/oscar64/screenshots/mixed-fighters-ntsc.png +0 -0
  656. package/docs/recipes/oscar64/screenshots/mixed-fighters.png +0 -0
  657. package/docs/recipes/oscar64/screenshots/multi-sprite-object-ntsc.png +0 -0
  658. package/docs/recipes/oscar64/screenshots/multi-sprite-object.png +0 -0
  659. package/docs/recipes/oscar64/screenshots/nav-area-pathfinding-ntsc.png +0 -0
  660. package/docs/recipes/oscar64/screenshots/nav-area-pathfinding.png +0 -0
  661. package/docs/recipes/oscar64/screenshots/object-pool-ntsc.png +0 -0
  662. package/docs/recipes/oscar64/screenshots/object-pool.png +0 -0
  663. package/docs/recipes/oscar64/screenshots/pal-ntsc-detect-ntsc.png +0 -0
  664. package/docs/recipes/oscar64/screenshots/pal-ntsc-detect-oldntsc.png +0 -0
  665. package/docs/recipes/oscar64/screenshots/pal-ntsc-detect-pal.png +0 -0
  666. package/docs/recipes/oscar64/screenshots/password-state-ntsc.png +0 -0
  667. package/docs/recipes/oscar64/screenshots/password-state.png +0 -0
  668. package/docs/recipes/oscar64/screenshots/per-frame-hitbox-ntsc.png +0 -0
  669. package/docs/recipes/oscar64/screenshots/per-frame-hitbox.png +0 -0
  670. package/docs/recipes/oscar64/screenshots/petscii-screen-codes-ntsc.png +0 -0
  671. package/docs/recipes/oscar64/screenshots/petscii-screen-codes.png +0 -0
  672. package/docs/recipes/oscar64/screenshots/platformer-scaffold-d64-ntsc.png +0 -0
  673. package/docs/recipes/oscar64/screenshots/platformer-scaffold-d64.png +0 -0
  674. package/docs/recipes/oscar64/screenshots/platformer-scaffold-ntsc.png +0 -0
  675. package/docs/recipes/oscar64/screenshots/platformer-scaffold.png +0 -0
  676. package/docs/recipes/oscar64/screenshots/print-number-ntsc.png +0 -0
  677. package/docs/recipes/oscar64/screenshots/print-number.png +0 -0
  678. package/docs/recipes/oscar64/screenshots/raster-bars.png +0 -0
  679. package/docs/recipes/oscar64/screenshots/raster-profile-bars-ntsc.png +0 -0
  680. package/docs/recipes/oscar64/screenshots/raster-profile-bars.png +0 -0
  681. package/docs/recipes/oscar64/screenshots/rel-side-sectors-ntsc.png +0 -0
  682. package/docs/recipes/oscar64/screenshots/rel-side-sectors.png +0 -0
  683. package/docs/recipes/oscar64/screenshots/relative-file-records-ntsc.png +0 -0
  684. package/docs/recipes/oscar64/screenshots/relative-file-records.png +0 -0
  685. package/docs/recipes/oscar64/screenshots/save-load-seq-file-ntsc.png +0 -0
  686. package/docs/recipes/oscar64/screenshots/save-load-seq-file.png +0 -0
  687. package/docs/recipes/oscar64/screenshots/seeded-level-fill-ntsc.png +0 -0
  688. package/docs/recipes/oscar64/screenshots/seeded-level-fill.png +0 -0
  689. package/docs/recipes/oscar64/screenshots/sfx-engine-ntsc.png +0 -0
  690. package/docs/recipes/oscar64/screenshots/sfx-engine.png +0 -0
  691. package/docs/recipes/oscar64/screenshots/sid-music-player.png +0 -0
  692. package/docs/recipes/oscar64/screenshots/simple-shmup.png +0 -0
  693. package/docs/recipes/oscar64/screenshots/slope-collision-ntsc.png +0 -0
  694. package/docs/recipes/oscar64/screenshots/slope-collision.png +0 -0
  695. package/docs/recipes/oscar64/screenshots/soft-scroll-h.png +0 -0
  696. package/docs/recipes/oscar64/screenshots/sprite-animation-table-ntsc.png +0 -0
  697. package/docs/recipes/oscar64/screenshots/sprite-animation-table.png +0 -0
  698. package/docs/recipes/oscar64/screenshots/sprite-multiplex-8.png +0 -0
  699. package/docs/recipes/oscar64/screenshots/stable-raster-irq.png +0 -0
  700. package/docs/recipes/oscar64/screenshots/text-input-ntsc.png +0 -0
  701. package/docs/recipes/oscar64/screenshots/text-input.png +0 -0
  702. package/docs/recipes/oscar64/screenshots/text-overlay-playfield-gameover.png +0 -0
  703. package/docs/recipes/oscar64/screenshots/text-overlay-playfield-notrail-control.png +0 -0
  704. package/docs/recipes/oscar64/screenshots/text-overlay-playfield-ntsc.png +0 -0
  705. package/docs/recipes/oscar64/screenshots/text-overlay-playfield-ungated-control.png +0 -0
  706. package/docs/recipes/oscar64/screenshots/text-overlay-playfield.png +0 -0
  707. package/docs/recipes/oscar64/screenshots/text-window-menu-ntsc.png +0 -0
  708. package/docs/recipes/oscar64/screenshots/text-window-menu.png +0 -0
  709. package/docs/recipes/oscar64/screenshots/tile-grid-collision-ntsc.png +0 -0
  710. package/docs/recipes/oscar64/screenshots/tile-grid-collision.png +0 -0
  711. package/docs/recipes/oscar64/screenshots/tile-map-render-ntsc.png +0 -0
  712. package/docs/recipes/oscar64/screenshots/tile-map-render.png +0 -0
  713. package/docs/recipes/oscar64/screenshots/two-player-ntsc.png +0 -0
  714. package/docs/recipes/oscar64/screenshots/two-player.png +0 -0
  715. package/docs/recipes/oscar64/screenshots/two-word-parser-ntsc.png +0 -0
  716. package/docs/recipes/oscar64/screenshots/two-word-parser.png +0 -0
  717. package/docs/recipes/oscar64/screenshots/vehicle-control-ntsc.png +0 -0
  718. package/docs/recipes/oscar64/screenshots/vehicle-control.png +0 -0
  719. package/docs/recipes/oscar64/screenshots/wave-director-ntsc.png +0 -0
  720. package/docs/recipes/oscar64/screenshots/wave-director.png +0 -0
  721. package/docs/recipes/oscar64/seeded-level-fill.md +364 -0
  722. package/docs/recipes/oscar64/sfx-engine.md +514 -0
  723. package/docs/recipes/oscar64/sid-music-player.md +441 -0
  724. package/docs/recipes/oscar64/simple-shmup.md +1000 -0
  725. package/docs/recipes/oscar64/slope-collision.md +610 -0
  726. package/docs/recipes/oscar64/soft-scroll-h.md +261 -0
  727. package/docs/recipes/oscar64/sprite-animation-table.md +671 -0
  728. package/docs/recipes/oscar64/sprite-multiplex-8.md +314 -0
  729. package/docs/recipes/oscar64/stable-raster-irq.md +236 -0
  730. package/docs/recipes/oscar64/text-input.md +274 -0
  731. package/docs/recipes/oscar64/text-overlay-playfield.md +617 -0
  732. package/docs/recipes/oscar64/text-window-menu.md +551 -0
  733. package/docs/recipes/oscar64/tile-grid-collision.md +563 -0
  734. package/docs/recipes/oscar64/tile-map-render.md +395 -0
  735. package/docs/recipes/oscar64/two-player.md +528 -0
  736. package/docs/recipes/oscar64/two-word-parser.md +510 -0
  737. package/docs/recipes/oscar64/vehicle-control.md +771 -0
  738. package/docs/recipes/oscar64/wave-director.md +787 -0
  739. package/docs/recipes/runs.json +1606 -0
  740. package/docs/runtime/sim6502-reference.md +466 -0
  741. package/docs/runtime/vice-mcp-reference.md +351 -0
  742. package/docs/runtime/vice-reference.md +1393 -0
  743. package/docs/techniques/bitmap-modes.md +685 -0
  744. package/docs/techniques/cpu-cycle-tricks.md +1570 -0
  745. package/docs/techniques/effects-vector-3d.md +1431 -0
  746. package/docs/techniques/file-io.md +1427 -0
  747. package/docs/techniques/input.md +1072 -0
  748. package/docs/techniques/loaders-packers.md +768 -0
  749. package/docs/techniques/logic.md +3091 -0
  750. package/docs/techniques/maths.md +1524 -0
  751. package/docs/techniques/memory-banking.md +1686 -0
  752. package/docs/techniques/music-sid.md +1144 -0
  753. package/docs/techniques/raster.md +1543 -0
  754. package/docs/techniques/scroll.md +1292 -0
  755. package/docs/techniques/sprite.md +2566 -0
  756. package/docs/techniques/text-mode-render.md +717 -0
  757. package/docs/techniques/text.md +1095 -0
  758. package/docs/techniques/transitions.md +405 -0
  759. package/docs/toolchains/cartconv-reference.md +262 -0
  760. package/docs/toolchains/cc1541-reference.md +316 -0
  761. package/docs/toolchains/cc65-reference.md +667 -0
  762. package/docs/toolchains/error-sources/cc65/README.md +1 -0
  763. package/docs/toolchains/error-sources/cc65/missing-setcpu.s +4 -0
  764. package/docs/toolchains/error-sources/cc65/range-error.s +4 -0
  765. package/docs/toolchains/error-sources/cc65/segment-overflow.c +6 -0
  766. package/docs/toolchains/error-sources/cc65/unresolved-external.c +6 -0
  767. package/docs/toolchains/error-sources/kickassembler/README.md +1 -0
  768. package/docs/toolchains/error-sources/kickassembler/branch-out-of-range.asm +4 -0
  769. package/docs/toolchains/error-sources/kickassembler/failed-assert.asm +3 -0
  770. package/docs/toolchains/error-sources/kickassembler/flat-lib.asm +4 -0
  771. package/docs/toolchains/error-sources/kickassembler/import-source-no-namespace.asm +5 -0
  772. package/docs/toolchains/error-sources/kickassembler/macro-without-parentheses.asm +6 -0
  773. package/docs/toolchains/error-sources/kickassembler/memory-block-overlap.asm +4 -0
  774. package/docs/toolchains/error-sources/kickassembler/symbol-clash-lib.asm +5 -0
  775. package/docs/toolchains/error-sources/kickassembler/symbol-clash-main.asm +6 -0
  776. package/docs/toolchains/error-sources/kickassembler/unknown-symbol.asm +4 -0
  777. package/docs/toolchains/error-sources/oscar64/README.md +1 -0
  778. package/docs/toolchains/error-sources/oscar64/asm-addressing-mode.c +7 -0
  779. package/docs/toolchains/error-sources/oscar64/region-overflow.c +6 -0
  780. package/docs/toolchains/error-sources/oscar64/undefined-extern-var.c +5 -0
  781. package/docs/toolchains/error-sources/oscar64/undefined-extern.c +6 -0
  782. package/docs/toolchains/error-sources/oscar64/unknown-vic-field.c +6 -0
  783. package/docs/toolchains/error-sources/oscar64/void-main.c +3 -0
  784. package/docs/toolchains/kickassembler-reference.md +1020 -0
  785. package/docs/toolchains/memory-layout-planning.md +362 -0
  786. package/docs/toolchains/oscar64-headers-reference.md +583 -0
  787. package/docs/toolchains/oscar64-reference.md +892 -0
  788. package/docs/toolchains/petcat-reference.md +290 -0
  789. package/docs/toolchains/png2prg.md +188 -0
  790. package/docs/toolchains/release-disk.md +314 -0
  791. package/docs/toolchains/sidreloc.md +236 -0
  792. package/docs/toolchains/spindle-reference.md +487 -0
  793. package/docs/toolchains/tape-mastering.md +592 -0
  794. package/docs/toolchains/unit-testing-6502.md +281 -0
  795. package/docs/workflow/agent-harness.md +478 -0
  796. package/package.json +105 -0
  797. package/templates/_harness/CLAUDE.md.template +91 -0
  798. package/templates/_harness/PLAN.md.template +72 -0
  799. package/templates/_harness/check.py +442 -0
  800. package/templates/_harness/claude/settings.json +17 -0
  801. package/templates/_harness/gen-asm-header.py +96 -0
  802. package/templates/_harness/harness.mk +273 -0
  803. package/templates/_harness/hooks/plan-gate.py +185 -0
  804. package/templates/_harness/meter/frame_meter.asm +311 -0
  805. package/templates/_harness/meter/frame_meter.c +155 -0
  806. package/templates/_harness/meter/frame_meter.h +80 -0
  807. package/templates/_harness/zp-used.py +104 -0
  808. package/templates/action-puzzle/.claude/settings.json +17 -0
  809. package/templates/action-puzzle/.mcp.json +10 -0
  810. package/templates/action-puzzle/CLAUDE.md +120 -0
  811. package/templates/action-puzzle/Makefile +125 -0
  812. package/templates/action-puzzle/PLAN.md +388 -0
  813. package/templates/action-puzzle/README.md +240 -0
  814. package/templates/action-puzzle/expect.json +25 -0
  815. package/templates/action-puzzle/src/cave.c +280 -0
  816. package/templates/action-puzzle/src/cave.h +80 -0
  817. package/templates/action-puzzle/src/gen_autopilot.h +20 -0
  818. package/templates/action-puzzle/src/gen_caves.h +59 -0
  819. package/templates/action-puzzle/src/gen_notes.h +16 -0
  820. package/templates/action-puzzle/src/hiscore.c +152 -0
  821. package/templates/action-puzzle/src/hiscore.h +41 -0
  822. package/templates/action-puzzle/src/level.c +64 -0
  823. package/templates/action-puzzle/src/level.h +24 -0
  824. package/templates/action-puzzle/src/main.c +599 -0
  825. package/templates/action-puzzle/src/render.c +239 -0
  826. package/templates/action-puzzle/src/render.h +31 -0
  827. package/templates/action-puzzle/src/sound.c +158 -0
  828. package/templates/action-puzzle/src/sound.h +22 -0
  829. package/templates/action-puzzle/tools/disk_check.py +77 -0
  830. package/templates/action-puzzle/tools/drive.py +106 -0
  831. package/templates/action-puzzle/tools/gen.py +454 -0
  832. package/templates/action-puzzle/tools/model_check.py +49 -0
  833. package/templates/action-puzzle/tools/verdict_codes.py +24 -0
  834. package/templates/adventure/.claude/settings.json +17 -0
  835. package/templates/adventure/.mcp.json +10 -0
  836. package/templates/adventure/CLAUDE.md +118 -0
  837. package/templates/adventure/Makefile +113 -0
  838. package/templates/adventure/PLAN.md +396 -0
  839. package/templates/adventure/README.md +277 -0
  840. package/templates/adventure/expect.json +25 -0
  841. package/templates/adventure/src/engine.c +543 -0
  842. package/templates/adventure/src/engine.h +48 -0
  843. package/templates/adventure/src/gen_script.h +141 -0
  844. package/templates/adventure/src/gen_world.h +511 -0
  845. package/templates/adventure/src/main.c +439 -0
  846. package/templates/adventure/src/picture.c +83 -0
  847. package/templates/adventure/src/picture.h +16 -0
  848. package/templates/adventure/src/save.c +159 -0
  849. package/templates/adventure/src/save.h +25 -0
  850. package/templates/adventure/src/sound.c +71 -0
  851. package/templates/adventure/src/sound.h +16 -0
  852. package/templates/adventure/src/text.c +349 -0
  853. package/templates/adventure/src/text.h +40 -0
  854. package/templates/adventure/tools/disk_check.py +128 -0
  855. package/templates/adventure/tools/drive.py +119 -0
  856. package/templates/adventure/tools/gen.py +1033 -0
  857. package/templates/adventure/tools/world.py +356 -0
  858. package/templates/beat-em-up/.claude/settings.json +17 -0
  859. package/templates/beat-em-up/.mcp.json +10 -0
  860. package/templates/beat-em-up/CLAUDE.md +136 -0
  861. package/templates/beat-em-up/Makefile +96 -0
  862. package/templates/beat-em-up/PLAN.md +502 -0
  863. package/templates/beat-em-up/README.md +315 -0
  864. package/templates/beat-em-up/expect-gameover.json +8 -0
  865. package/templates/beat-em-up/expect.json +29 -0
  866. package/templates/beat-em-up/src/anim.c +70 -0
  867. package/templates/beat-em-up/src/art.c +344 -0
  868. package/templates/beat-em-up/src/autopilot.h +154 -0
  869. package/templates/beat-em-up/src/brute.c +273 -0
  870. package/templates/beat-em-up/src/enemy.c +281 -0
  871. package/templates/beat-em-up/src/engine.asm +512 -0
  872. package/templates/beat-em-up/src/fighter.c +384 -0
  873. package/templates/beat-em-up/src/game.h +269 -0
  874. package/templates/beat-em-up/src/hud.c +135 -0
  875. package/templates/beat-em-up/src/main.c +341 -0
  876. package/templates/beat-em-up/src/sound.c +44 -0
  877. package/templates/beat-em-up/src/street.c +93 -0
  878. package/templates/beat-em-up/src/verdict.h +523 -0
  879. package/templates/beat-em-up/src/view.c +362 -0
  880. package/templates/beat-em-up/tools/drive.py +105 -0
  881. package/templates/beat-em-up/tools/flickercheck.py +273 -0
  882. package/templates/demo/.claude/settings.json +17 -0
  883. package/templates/demo/.mcp.json +10 -0
  884. package/templates/demo/CLAUDE.md +129 -0
  885. package/templates/demo/Makefile +66 -0
  886. package/templates/demo/PLAN.md +401 -0
  887. package/templates/demo/README.md +322 -0
  888. package/templates/demo/expect.json +945 -0
  889. package/templates/demo/src/config.asm +80 -0
  890. package/templates/demo/src/framework.asm +390 -0
  891. package/templates/demo/src/main.asm +139 -0
  892. package/templates/demo/src/music.asm +197 -0
  893. package/templates/demo/src/part_main.asm +279 -0
  894. package/templates/demo/src/part_title.asm +79 -0
  895. package/templates/demo/src/parts.asm +43 -0
  896. package/templates/demo/src/tables.asm +73 -0
  897. package/templates/demo/src/verdict.asm +179 -0
  898. package/templates/demo/tools/audio.py +80 -0
  899. package/templates/demo/tools/gen_expect.py +167 -0
  900. package/templates/demo/tools/probe.py +95 -0
  901. package/templates/hello/.claude/settings.json +17 -0
  902. package/templates/hello/.mcp.json +10 -0
  903. package/templates/hello/CLAUDE.md +100 -0
  904. package/templates/hello/Makefile +38 -0
  905. package/templates/hello/PLAN.md +200 -0
  906. package/templates/hello/expect.json +20 -0
  907. package/templates/hello/src/main.c +161 -0
  908. package/templates/hello/src/sprite.asm +27 -0
  909. package/templates/hello-kick/.claude/settings.json +17 -0
  910. package/templates/hello-kick/.mcp.json +10 -0
  911. package/templates/hello-kick/CLAUDE.md +100 -0
  912. package/templates/hello-kick/Makefile +17 -0
  913. package/templates/hello-kick/PLAN.md +199 -0
  914. package/templates/hello-kick/expect.json +20 -0
  915. package/templates/hello-kick/src/main.asm +233 -0
  916. package/templates/platformer/.claude/settings.json +17 -0
  917. package/templates/platformer/.mcp.json +10 -0
  918. package/templates/platformer/CLAUDE.md +125 -0
  919. package/templates/platformer/Makefile +86 -0
  920. package/templates/platformer/PLAN.md +360 -0
  921. package/templates/platformer/README.md +277 -0
  922. package/templates/platformer/expect.json +32 -0
  923. package/templates/platformer/src/actors.c +289 -0
  924. package/templates/platformer/src/anim.c +51 -0
  925. package/templates/platformer/src/art.c +212 -0
  926. package/templates/platformer/src/autopilot.h +30 -0
  927. package/templates/platformer/src/engine.asm +253 -0
  928. package/templates/platformer/src/game.h +248 -0
  929. package/templates/platformer/src/hud.c +90 -0
  930. package/templates/platformer/src/level.c +184 -0
  931. package/templates/platformer/src/main.c +306 -0
  932. package/templates/platformer/src/player.c +268 -0
  933. package/templates/platformer/src/sound.c +42 -0
  934. package/templates/platformer/src/verdict.h +206 -0
  935. package/templates/platformer/src/view.c +331 -0
  936. package/templates/platformer/stage-expect.json +6 -0
  937. package/templates/platformer/tools/drive.py +112 -0
  938. package/templates/platformer/tools/tearcheck.py +229 -0
  939. package/templates/shmup-vertical/.claude/settings.json +17 -0
  940. package/templates/shmup-vertical/.mcp.json +10 -0
  941. package/templates/shmup-vertical/CLAUDE.md +137 -0
  942. package/templates/shmup-vertical/Makefile +131 -0
  943. package/templates/shmup-vertical/PLAN.md +569 -0
  944. package/templates/shmup-vertical/README.md +189 -0
  945. package/templates/shmup-vertical/expect.json +38 -0
  946. package/templates/shmup-vertical/src/bullets.c +158 -0
  947. package/templates/shmup-vertical/src/bullets.h +35 -0
  948. package/templates/shmup-vertical/src/display.c +239 -0
  949. package/templates/shmup-vertical/src/display.h +40 -0
  950. package/templates/shmup-vertical/src/game.h +85 -0
  951. package/templates/shmup-vertical/src/glyph.asm +358 -0
  952. package/templates/shmup-vertical/src/hiscore.c +141 -0
  953. package/templates/shmup-vertical/src/hiscore.h +16 -0
  954. package/templates/shmup-vertical/src/hit.asm +180 -0
  955. package/templates/shmup-vertical/src/hitbox.c +59 -0
  956. package/templates/shmup-vertical/src/hitbox.h +25 -0
  957. package/templates/shmup-vertical/src/kernel.asm +330 -0
  958. package/templates/shmup-vertical/src/level.c +174 -0
  959. package/templates/shmup-vertical/src/level.h +27 -0
  960. package/templates/shmup-vertical/src/main.c +824 -0
  961. package/templates/shmup-vertical/src/mux.asm +321 -0
  962. package/templates/shmup-vertical/src/sound.asm +255 -0
  963. package/templates/shmup-vertical/src/step.asm +169 -0
  964. package/templates/shmup-vertical/src/waves.c +195 -0
  965. package/templates/shmup-vertical/src/waves.h +33 -0
  966. package/templates/shmup-vertical/stage-expect.json +6 -0
  967. package/templates/shmup-vertical/tools/drive.py +152 -0
  968. package/templates/shmup-vertical/tools/joytest.py +159 -0
  969. package/templates/shmup-vertical/tools/meter.py +15 -0
  970. 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).