web-a2e 1.0.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 (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,232 @@
1
+ /*
2
+ * test_gcr_encoding.cpp - Unit tests for GCR encoding routines
3
+ */
4
+
5
+ #define CATCH_CONFIG_MAIN
6
+ #include "catch.hpp"
7
+
8
+ #include "gcr_encoding.hpp"
9
+
10
+ #include <set>
11
+ #include <cstring>
12
+
13
+ using namespace a2e;
14
+
15
+ // ============================================================================
16
+ // encode4and4 tests
17
+ // ============================================================================
18
+
19
+ TEST_CASE("encode4and4 splits byte into odd and even bit pairs", "[gcr][4and4]") {
20
+ SECTION("zero encodes to (0xAA, 0xAA)") {
21
+ auto [odd, even] = GCR::encode4and4(0x00);
22
+ CHECK(odd == 0xAA);
23
+ CHECK(even == 0xAA);
24
+ }
25
+
26
+ SECTION("0xFF encodes correctly") {
27
+ auto [odd, even] = GCR::encode4and4(0xFF);
28
+ CHECK(odd == 0xFF);
29
+ CHECK(even == 0xFF);
30
+ }
31
+
32
+ SECTION("both bytes always have high bit set") {
33
+ for (int v = 0; v < 256; v++) {
34
+ auto [odd, even] = GCR::encode4and4(static_cast<uint8_t>(v));
35
+ REQUIRE((odd & 0x80) != 0);
36
+ REQUIRE((even & 0x80) != 0);
37
+ }
38
+ }
39
+
40
+ SECTION("decoding recovers original value") {
41
+ // 4-and-4 decoding: AND the two bytes and strip extra bits
42
+ for (int v = 0; v < 256; v++) {
43
+ auto [odd, even] = GCR::encode4and4(static_cast<uint8_t>(v));
44
+ // Odd byte has bits 7,5,3,1 set; even byte has bits 6,4,2,0.
45
+ // To decode: shift odd left 1 then AND, combined with even.
46
+ uint8_t decoded = ((odd << 1) | 0x01) & even;
47
+ CHECK(decoded == static_cast<uint8_t>(v));
48
+ }
49
+ }
50
+
51
+ SECTION("specific known value 0xFE") {
52
+ auto [odd, even] = GCR::encode4and4(0xFE);
53
+ // 0xFE = 1111 1110
54
+ // odd bits (7,5,3,1) = 1,1,1,1 -> positions 6,4,2,0 = 0x55 | 0xAA = 0xFF
55
+ // even bits (6,4,2,0) = 1,1,1,0 -> positions 6,4,2,0 = 0x54 | 0xAA = 0xFE
56
+ CHECK(odd == 0xFF);
57
+ CHECK(even == 0xFE);
58
+ }
59
+ }
60
+
61
+ // ============================================================================
62
+ // encode6and2 tests
63
+ // ============================================================================
64
+
65
+ TEST_CASE("encode6and2 produces 343 nibbles from 256-byte sector", "[gcr][6and2]") {
66
+ uint8_t sector[256];
67
+ memset(sector, 0, sizeof(sector));
68
+
69
+ auto nibbles = GCR::encode6and2(sector);
70
+ REQUIRE(nibbles.size() == 343);
71
+ }
72
+
73
+ TEST_CASE("encode6and2 all nibbles have bit 7 set", "[gcr][6and2]") {
74
+ // Fill sector with sequential pattern
75
+ uint8_t sector[256];
76
+ for (int i = 0; i < 256; i++) {
77
+ sector[i] = static_cast<uint8_t>(i);
78
+ }
79
+
80
+ auto nibbles = GCR::encode6and2(sector);
81
+ REQUIRE(nibbles.size() == 343);
82
+
83
+ for (size_t i = 0; i < nibbles.size(); i++) {
84
+ INFO("Nibble at position " << i << " is 0x" << std::hex << (int)nibbles[i]);
85
+ REQUIRE((nibbles[i] & 0x80) != 0);
86
+ }
87
+ }
88
+
89
+ TEST_CASE("encode6and2 produces valid encoded nibbles from lookup table", "[gcr][6and2]") {
90
+ // All output nibbles should be values found in the ENCODE_6_AND_2 table
91
+ uint8_t sector[256];
92
+ for (int i = 0; i < 256; i++) {
93
+ sector[i] = static_cast<uint8_t>(i);
94
+ }
95
+
96
+ auto nibbles = GCR::encode6and2(sector);
97
+
98
+ std::set<uint8_t> validNibbles(GCR::ENCODE_6_AND_2.begin(), GCR::ENCODE_6_AND_2.end());
99
+
100
+ for (size_t i = 0; i < nibbles.size(); i++) {
101
+ INFO("Nibble at position " << i << " is 0x" << std::hex << (int)nibbles[i]);
102
+ REQUIRE(validNibbles.count(nibbles[i]) == 1);
103
+ }
104
+ }
105
+
106
+ TEST_CASE("encode6and2 different input produces different output", "[gcr][6and2]") {
107
+ uint8_t sector_a[256], sector_b[256];
108
+ memset(sector_a, 0x00, sizeof(sector_a));
109
+ memset(sector_b, 0xFF, sizeof(sector_b));
110
+
111
+ auto nibbles_a = GCR::encode6and2(sector_a);
112
+ auto nibbles_b = GCR::encode6and2(sector_b);
113
+
114
+ REQUIRE(nibbles_a.size() == 343);
115
+ REQUIRE(nibbles_b.size() == 343);
116
+ REQUIRE(nibbles_a != nibbles_b);
117
+ }
118
+
119
+ // ============================================================================
120
+ // buildSector tests
121
+ // ============================================================================
122
+
123
+ TEST_CASE("buildSector contains address prologue D5 AA 96", "[gcr][buildSector]") {
124
+ uint8_t sector[256];
125
+ memset(sector, 0, sizeof(sector));
126
+
127
+ auto stream = GCR::buildSector(254, 0, 0, sector);
128
+
129
+ bool found = false;
130
+ for (size_t i = 0; i + 2 < stream.size(); i++) {
131
+ if (stream[i] == 0xD5 && stream[i + 1] == 0xAA && stream[i + 2] == 0x96) {
132
+ found = true;
133
+ break;
134
+ }
135
+ }
136
+ REQUIRE(found);
137
+ }
138
+
139
+ TEST_CASE("buildSector contains data prologue D5 AA AD", "[gcr][buildSector]") {
140
+ uint8_t sector[256];
141
+ memset(sector, 0, sizeof(sector));
142
+
143
+ auto stream = GCR::buildSector(254, 0, 0, sector);
144
+
145
+ bool found = false;
146
+ for (size_t i = 0; i + 2 < stream.size(); i++) {
147
+ if (stream[i] == 0xD5 && stream[i + 1] == 0xAA && stream[i + 2] == 0xAD) {
148
+ found = true;
149
+ break;
150
+ }
151
+ }
152
+ REQUIRE(found);
153
+ }
154
+
155
+ TEST_CASE("buildSector address prologue appears before data prologue", "[gcr][buildSector]") {
156
+ uint8_t sector[256];
157
+ memset(sector, 0, sizeof(sector));
158
+
159
+ auto stream = GCR::buildSector(254, 10, 5, sector);
160
+
161
+ size_t addrPos = 0, dataPos = 0;
162
+ bool foundAddr = false, foundData = false;
163
+
164
+ for (size_t i = 0; i + 2 < stream.size(); i++) {
165
+ if (!foundAddr && stream[i] == 0xD5 && stream[i + 1] == 0xAA && stream[i + 2] == 0x96) {
166
+ addrPos = i;
167
+ foundAddr = true;
168
+ }
169
+ if (!foundData && stream[i] == 0xD5 && stream[i + 1] == 0xAA && stream[i + 2] == 0xAD) {
170
+ dataPos = i;
171
+ foundData = true;
172
+ }
173
+ }
174
+
175
+ REQUIRE(foundAddr);
176
+ REQUIRE(foundData);
177
+ REQUIRE(addrPos < dataPos);
178
+ }
179
+
180
+ TEST_CASE("buildSector begins with sync bytes", "[gcr][buildSector]") {
181
+ uint8_t sector[256];
182
+ memset(sector, 0, sizeof(sector));
183
+
184
+ auto stream = GCR::buildSector(254, 0, 0, sector);
185
+ REQUIRE(stream.size() > 3);
186
+
187
+ // First bytes should be sync bytes (0xFF)
188
+ CHECK(stream[0] == GCR::SYNC_BYTE);
189
+ CHECK(stream[1] == GCR::SYNC_BYTE);
190
+ }
191
+
192
+ // ============================================================================
193
+ // ENCODE_6_AND_2 table tests
194
+ // ============================================================================
195
+
196
+ TEST_CASE("ENCODE_6_AND_2 table has exactly 64 entries", "[gcr][table]") {
197
+ REQUIRE(GCR::ENCODE_6_AND_2.size() == 64);
198
+ }
199
+
200
+ TEST_CASE("ENCODE_6_AND_2 all entries have bit 7 set", "[gcr][table]") {
201
+ for (size_t i = 0; i < GCR::ENCODE_6_AND_2.size(); i++) {
202
+ INFO("Entry " << i << " = 0x" << std::hex << (int)GCR::ENCODE_6_AND_2[i]);
203
+ REQUIRE((GCR::ENCODE_6_AND_2[i] & 0x80) != 0);
204
+ }
205
+ }
206
+
207
+ TEST_CASE("ENCODE_6_AND_2 all entries are unique", "[gcr][table]") {
208
+ std::set<uint8_t> seen;
209
+ for (size_t i = 0; i < GCR::ENCODE_6_AND_2.size(); i++) {
210
+ INFO("Duplicate at index " << i << " value 0x" << std::hex << (int)GCR::ENCODE_6_AND_2[i]);
211
+ REQUIRE(seen.insert(GCR::ENCODE_6_AND_2[i]).second);
212
+ }
213
+ REQUIRE(seen.size() == 64);
214
+ }
215
+
216
+ // ============================================================================
217
+ // Sync byte constant
218
+ // ============================================================================
219
+
220
+ TEST_CASE("SYNC_BYTE is 0xFF", "[gcr][constants]") {
221
+ REQUIRE(GCR::SYNC_BYTE == 0xFF);
222
+ }
223
+
224
+ TEST_CASE("Address and data prologue constants are correct", "[gcr][constants]") {
225
+ CHECK(GCR::ADDR_PROLOGUE[0] == 0xD5);
226
+ CHECK(GCR::ADDR_PROLOGUE[1] == 0xAA);
227
+ CHECK(GCR::ADDR_PROLOGUE[2] == 0x96);
228
+
229
+ CHECK(GCR::DATA_PROLOGUE[0] == 0xD5);
230
+ CHECK(GCR::DATA_PROLOGUE[1] == 0xAA);
231
+ CHECK(GCR::DATA_PROLOGUE[2] == 0xAD);
232
+ }
@@ -0,0 +1,262 @@
1
+ /*
2
+ * test_keyboard.cpp - Unit tests for Apple IIe keyboard input handling
3
+ */
4
+
5
+ #define CATCH_CONFIG_MAIN
6
+ #include "catch.hpp"
7
+
8
+ #include "keyboard.hpp"
9
+
10
+ using namespace a2e;
11
+
12
+ // ============================================================================
13
+ // Basic key translation (handleKeyDown)
14
+ // ============================================================================
15
+
16
+ TEST_CASE("handleKeyDown returns translated Apple II keycode", "[keyboard][basic]") {
17
+ Keyboard kb;
18
+ // 'A' key (browser keycode 65) with no modifiers
19
+ // translateKeycode maps 65 -> 0x61 ('a' lowercase)
20
+ int result = kb.handleKeyDown(65, false, false, false, false, false);
21
+ // Without caps lock or shift, should return lowercase 'a' = 0x61
22
+ CHECK(result == 0x61);
23
+ }
24
+
25
+ TEST_CASE("Letter keys A-Z map to lowercase ASCII without modifiers", "[keyboard][letters]") {
26
+ Keyboard kb;
27
+ // Browser keycodes 65-90 for A-Z
28
+ for (int browserKey = 65; browserKey <= 90; browserKey++) {
29
+ int result = kb.handleKeyDown(browserKey, false, false, false, false, false);
30
+ int expected = browserKey + 32; // lowercase ASCII
31
+ INFO("Browser keycode " << browserKey << " expected 0x" << std::hex << expected);
32
+ CHECK(result == expected);
33
+ }
34
+ }
35
+
36
+ // ============================================================================
37
+ // Shift modifier
38
+ // ============================================================================
39
+
40
+ TEST_CASE("Shift+letter gives uppercase ASCII", "[keyboard][shift]") {
41
+ Keyboard kb;
42
+ // Browser keycode 65 ('A') with shift
43
+ int result = kb.handleKeyDown(65, true, false, false, false, false);
44
+ CHECK(result == 0x41); // 'A' uppercase
45
+ }
46
+
47
+ TEST_CASE("Shift modifier uppercase for all letters", "[keyboard][shift]") {
48
+ Keyboard kb;
49
+ for (int browserKey = 65; browserKey <= 90; browserKey++) {
50
+ int result = kb.handleKeyDown(browserKey, true, false, false, false, false);
51
+ int expected = browserKey; // uppercase ASCII = same as browser keycode
52
+ INFO("Browser keycode " << browserKey);
53
+ CHECK(result == expected);
54
+ }
55
+ }
56
+
57
+ TEST_CASE("Caps lock gives uppercase without shift", "[keyboard][capslock]") {
58
+ Keyboard kb;
59
+ int result = kb.handleKeyDown(65, false, false, false, false, true);
60
+ CHECK(result == 0x41); // 'A' uppercase
61
+ }
62
+
63
+ // ============================================================================
64
+ // Control modifier
65
+ // ============================================================================
66
+
67
+ TEST_CASE("Ctrl+A gives control character 0x01", "[keyboard][ctrl]") {
68
+ Keyboard kb;
69
+ // Browser keycode 65 ('A'), ctrl pressed
70
+ int result = kb.handleKeyDown(65, false, true, false, false, false);
71
+ CHECK(result == 0x01);
72
+ }
73
+
74
+ TEST_CASE("Ctrl+letters produce control characters 0x01-0x1A", "[keyboard][ctrl]") {
75
+ Keyboard kb;
76
+ for (int browserKey = 65; browserKey <= 90; browserKey++) {
77
+ int result = kb.handleKeyDown(browserKey, false, true, false, false, false);
78
+ int expected = browserKey - 64; // Ctrl+A=1, Ctrl+B=2, ...
79
+ INFO("Ctrl+" << (char)browserKey << " expected 0x" << std::hex << expected);
80
+ CHECK(result == expected);
81
+ }
82
+ }
83
+
84
+ // ============================================================================
85
+ // Open Apple / Closed Apple (button state)
86
+ // ============================================================================
87
+
88
+ TEST_CASE("Alt key sets Open Apple pressed state", "[keyboard][apple]") {
89
+ Keyboard kb;
90
+ CHECK(kb.isOpenApplePressed() == false);
91
+
92
+ // Alt key down (browser keycode 18)
93
+ kb.handleKeyDown(18, false, false, true, false, false);
94
+ CHECK(kb.isOpenApplePressed() == true);
95
+ }
96
+
97
+ TEST_CASE("Meta key sets Closed Apple pressed state", "[keyboard][apple]") {
98
+ Keyboard kb;
99
+ CHECK(kb.isClosedApplePressed() == false);
100
+
101
+ // Left Meta key down (browser keycode 91)
102
+ kb.handleKeyDown(91, false, false, false, true, false);
103
+ CHECK(kb.isClosedApplePressed() == true);
104
+ }
105
+
106
+ TEST_CASE("Right Meta key also sets Closed Apple", "[keyboard][apple]") {
107
+ Keyboard kb;
108
+ // Right Meta key down (browser keycode 93)
109
+ kb.handleKeyDown(93, false, false, false, true, false);
110
+ CHECK(kb.isClosedApplePressed() == true);
111
+ }
112
+
113
+ // ============================================================================
114
+ // handleKeyUp clears button state
115
+ // ============================================================================
116
+
117
+ TEST_CASE("handleKeyUp clears Open Apple", "[keyboard][keyup]") {
118
+ Keyboard kb;
119
+
120
+ kb.handleKeyDown(18, false, false, true, false, false);
121
+ CHECK(kb.isOpenApplePressed() == true);
122
+
123
+ kb.handleKeyUp(18, false, false, true, false);
124
+ CHECK(kb.isOpenApplePressed() == false);
125
+ }
126
+
127
+ TEST_CASE("handleKeyUp clears Closed Apple", "[keyboard][keyup]") {
128
+ Keyboard kb;
129
+
130
+ kb.handleKeyDown(91, false, false, false, true, false);
131
+ CHECK(kb.isClosedApplePressed() == true);
132
+
133
+ kb.handleKeyUp(91, false, false, false, true);
134
+ CHECK(kb.isClosedApplePressed() == false);
135
+ }
136
+
137
+ // ============================================================================
138
+ // reset
139
+ // ============================================================================
140
+
141
+ TEST_CASE("reset clears modifier states", "[keyboard][reset]") {
142
+ Keyboard kb;
143
+
144
+ // Set both apple buttons
145
+ kb.handleKeyDown(18, false, false, true, false, false);
146
+ kb.handleKeyDown(91, false, false, false, true, false);
147
+ CHECK(kb.isOpenApplePressed() == true);
148
+ CHECK(kb.isClosedApplePressed() == true);
149
+
150
+ kb.reset();
151
+
152
+ CHECK(kb.isOpenApplePressed() == false);
153
+ CHECK(kb.isClosedApplePressed() == false);
154
+ }
155
+
156
+ // ============================================================================
157
+ // Special keys
158
+ // ============================================================================
159
+
160
+ TEST_CASE("Enter key maps to CR (0x0D)", "[keyboard][special]") {
161
+ Keyboard kb;
162
+ int result = kb.handleKeyDown(13, false, false, false, false, false);
163
+ CHECK(result == 0x0D);
164
+ }
165
+
166
+ TEST_CASE("Escape key maps to 0x1B", "[keyboard][special]") {
167
+ Keyboard kb;
168
+ int result = kb.handleKeyDown(27, false, false, false, false, false);
169
+ CHECK(result == 0x1B);
170
+ }
171
+
172
+ TEST_CASE("Space key maps to 0x20", "[keyboard][special]") {
173
+ Keyboard kb;
174
+ int result = kb.handleKeyDown(32, false, false, false, false, false);
175
+ CHECK(result == 0x20);
176
+ }
177
+
178
+ TEST_CASE("Left arrow maps to 0x08", "[keyboard][special]") {
179
+ Keyboard kb;
180
+ int result = kb.handleKeyDown(37, false, false, false, false, false);
181
+ CHECK(result == 0x08);
182
+ }
183
+
184
+ TEST_CASE("Right arrow maps to 0x15", "[keyboard][special]") {
185
+ Keyboard kb;
186
+ int result = kb.handleKeyDown(39, false, false, false, false, false);
187
+ CHECK(result == 0x15);
188
+ }
189
+
190
+ TEST_CASE("Up arrow maps to 0x0B", "[keyboard][special]") {
191
+ Keyboard kb;
192
+ int result = kb.handleKeyDown(38, false, false, false, false, false);
193
+ CHECK(result == 0x0B);
194
+ }
195
+
196
+ TEST_CASE("Down arrow maps to 0x0A", "[keyboard][special]") {
197
+ Keyboard kb;
198
+ int result = kb.handleKeyDown(40, false, false, false, false, false);
199
+ CHECK(result == 0x0A);
200
+ }
201
+
202
+ // ============================================================================
203
+ // Unmapped keys return -1
204
+ // ============================================================================
205
+
206
+ TEST_CASE("Unmapped key returns -1", "[keyboard][unmapped]") {
207
+ Keyboard kb;
208
+ // F-keys and other non-mapped keys
209
+ int result = kb.handleKeyDown(112, false, false, false, false, false); // F1
210
+ CHECK(result == -1);
211
+ }
212
+
213
+ TEST_CASE("Pure modifier keys return -1", "[keyboard][unmapped]") {
214
+ Keyboard kb;
215
+ // Shift key (16) returns -1
216
+ CHECK(kb.handleKeyDown(16, true, false, false, false, false) == -1);
217
+ // Ctrl key (17) returns -1
218
+ CHECK(kb.handleKeyDown(17, false, true, false, false, false) == -1);
219
+ // Alt key (18) returns -1 (but sets Open Apple state)
220
+ CHECK(kb.handleKeyDown(18, false, false, true, false, false) == -1);
221
+ }
222
+
223
+ // ============================================================================
224
+ // charToAppleKey
225
+ // ============================================================================
226
+
227
+ TEST_CASE("charToAppleKey converts printable ASCII", "[keyboard][charToAppleKey]") {
228
+ CHECK(charToAppleKey(0x20) == 0x20); // space
229
+ CHECK(charToAppleKey(0x41) == 0x41); // 'A'
230
+ CHECK(charToAppleKey(0x61) == 0x61); // 'a'
231
+ CHECK(charToAppleKey(0x7E) == 0x7E); // '~'
232
+ CHECK(charToAppleKey(0x30) == 0x30); // '0'
233
+ }
234
+
235
+ TEST_CASE("charToAppleKey converts newline to CR", "[keyboard][charToAppleKey]") {
236
+ CHECK(charToAppleKey(0x0A) == 0x0D); // LF -> CR
237
+ CHECK(charToAppleKey(0x0D) == 0x0D); // CR -> CR
238
+ }
239
+
240
+ TEST_CASE("charToAppleKey converts tab", "[keyboard][charToAppleKey]") {
241
+ CHECK(charToAppleKey(0x09) == 0x09);
242
+ }
243
+
244
+ TEST_CASE("charToAppleKey returns -1 for unmappable characters", "[keyboard][charToAppleKey]") {
245
+ CHECK(charToAppleKey(0x00) == -1);
246
+ CHECK(charToAppleKey(0x01) == -1);
247
+ CHECK(charToAppleKey(0x7F) == -1);
248
+ CHECK(charToAppleKey(0x100) == -1); // Beyond ASCII
249
+ }
250
+
251
+ // ============================================================================
252
+ // Shift+number row
253
+ // ============================================================================
254
+
255
+ TEST_CASE("Shift+number produces correct symbols", "[keyboard][shift_symbols]") {
256
+ Keyboard kb;
257
+ CHECK(kb.handleKeyDown(49, true, false, false, false, false) == 0x21); // 1 -> !
258
+ CHECK(kb.handleKeyDown(50, true, false, false, false, false) == 0x40); // 2 -> @
259
+ CHECK(kb.handleKeyDown(51, true, false, false, false, false) == 0x23); // 3 -> #
260
+ CHECK(kb.handleKeyDown(48, true, false, false, false, false) == 0x29); // 0 -> )
261
+ CHECK(kb.handleKeyDown(57, true, false, false, false, false) == 0x28); // 9 -> (
262
+ }