@bongos/core 1.17.1 → 1.17.3

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 (233) hide show
  1. package/.bongos-core.json +521 -281
  2. package/.claude/skills/blocker-review/SKILL.md +1 -1
  3. package/.claude/skills/builder-claim/SKILL.md +2 -0
  4. package/.claude/skills/builder-exit/SKILL.md +2 -2
  5. package/.claude/skills/builder-redteam/SKILL.md +3 -3
  6. package/.claude/skills/builder-setup/SKILL.md +2 -2
  7. package/.claude/skills/builder-start/SKILL.md +1 -1
  8. package/.claude/skills/dev/SKILL.md +1 -1
  9. package/.claude/skills/idea-triage/SKILL.md +2 -2
  10. package/.claude/skills/ideate/SKILL.md +1 -1
  11. package/.claude/skills/merge-mode/SKILL.md +1 -1
  12. package/.claude/skills/new-project/SKILL.md +3 -0
  13. package/.claude/skills/paint/SKILL.md +1 -1
  14. package/GOVERNANCE.md +1 -1
  15. package/README.md +1 -1
  16. package/clients/bongos-client/README.md +1 -1
  17. package/clients/bongos-client/bongos-client.global.js +2 -0
  18. package/clients/bongos-client/index.cjs +2 -0
  19. package/clients/bongos-client/index.d.ts +4 -2
  20. package/clients/bongos-client/index.mjs +2 -0
  21. package/docs/adr/0001-stack-choice-single-vps.md +1 -1
  22. package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
  23. package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
  24. package/docs/adr/0020-security-model-public-stub.md +1 -1
  25. package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
  26. package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
  27. package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
  28. package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
  29. package/docs/adr/0030-builders-hall-live-updates.md +1 -1
  30. package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
  31. package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
  32. package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
  33. package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
  34. package/docs/adr/0044-per-box-live-game-preview.md +1 -1
  35. package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
  36. package/docs/adr/0057-container-cost-ledger.md +3 -3
  37. package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
  38. package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
  39. package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
  40. package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
  41. package/docs/adr/0096-require-reward-before-workable.md +9 -9
  42. package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
  43. package/docs/adr/0100-two-repo-instance-model.md +1 -1
  44. package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
  45. package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
  46. package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
  47. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
  48. package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
  49. package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
  50. package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
  51. package/docs/adr/README.md +9 -6
  52. package/docs/api/openapi.json +65 -26
  53. package/docs/api-reference.md +3 -2
  54. package/docs/architecture.md +2 -2
  55. package/docs/branding-contract.md +5 -4
  56. package/docs/canonical-permissions.md +1 -1
  57. package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
  58. package/docs/file-map.md +18 -17
  59. package/docs/handoff-template.md +2 -2
  60. package/docs/modules-contract.md +6 -1
  61. package/docs/onboarding/diagrams/README.md +7 -7
  62. package/docs/onboarding/drachmae-and-karma.md +18 -18
  63. package/docs/onboarding/primer.md +9 -9
  64. package/docs/recipes/audit-replay.md +2 -2
  65. package/docs/recipes/builder-box-lifecycle.md +2 -2
  66. package/docs/recipes/claude-design-loop.md +1 -1
  67. package/docs/recipes/ops-gotchas.md +1 -1
  68. package/docs/recipes/private-npm-distribution.md +17 -0
  69. package/docs/recipes/staging-environment.md +1 -1
  70. package/docs/recipes/upgrading-the-core.md +55 -0
  71. package/docs/research/cursor-competitive-analysis.md +1 -1
  72. package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
  73. package/modules/discord/module.json +2 -1
  74. package/modules/economy/achievements.js +9 -1
  75. package/modules/economy/credits.js +10 -1
  76. package/modules/hall-ui/public/builders.js +137 -620
  77. package/modules/hall-ui/public/goal-inbox.js +232 -0
  78. package/modules/hall-ui/public/goals-page.js +4 -1
  79. package/modules/hall-ui/public/goals.html +8 -0
  80. package/modules/hall-ui/public/hall-kit.js +474 -0
  81. package/modules/hall-ui/public/hall-widgets.js +14 -5
  82. package/modules/hall-ui/public/index.html +3 -0
  83. package/modules/hall-ui/public/instances-panel.js +24 -3
  84. package/modules/hall-ui/public/new-project-wizard.js +7 -5
  85. package/modules/hall-ui/public/style.css +168 -0
  86. package/modules/hall-ui/public/work.css +194 -0
  87. package/modules/hall-ui/public/work.html +46 -374
  88. package/modules/hall-ui/public/work.js +137 -102
  89. package/modules/lifecycle/criterion-suggest.js +80 -0
  90. package/modules/lifecycle/db.js +42 -5
  91. package/modules/lifecycle/routes/goals.js +26 -0
  92. package/modules/lifecycle/routes/tasks.js +21 -0
  93. package/modules/provisioning/CLAUDE.md +15 -0
  94. package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
  95. package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
  96. package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
  97. package/modules/provisioning/module.json +17 -0
  98. package/modules/provisioning/onboard-plan.js +37 -0
  99. package/modules/provisioning/provisioning.js +539 -0
  100. package/modules/provisioning/rate-limit.js +35 -0
  101. package/modules/provisioning/routes/provisioning.js +607 -0
  102. package/modules/provisioning/tests/provisioning.mjs +232 -0
  103. package/modules/provisioning/ui/.gitkeep +0 -0
  104. package/modules/security/secret-scrub.js +10 -0
  105. package/package-lock.json +2 -2
  106. package/package.json +7 -2
  107. package/scripts/discord/README.md +1 -1
  108. package/scripts/gds/claim.js +98 -48
  109. package/scripts/gds/claude-materialize.js +30 -9
  110. package/scripts/gds/cutover-codemod.js +241 -0
  111. package/scripts/gds/fitness.js +54 -0
  112. package/scripts/gds/init.js +59 -3
  113. package/scripts/gds/lint-infra-exec.js +70 -0
  114. package/scripts/gds/lint-workflow-exec.js +63 -0
  115. package/scripts/gds/mirror-lineage-check.js +14 -5
  116. package/scripts/gds/mirror-redact.js +8 -0
  117. package/scripts/gds/onboard.js +4 -0
  118. package/scripts/gds/package-core.js +18 -3
  119. package/scripts/gds/publish-manifest.js +60 -2
  120. package/scripts/gds/run-routine.js +51 -13
  121. package/scripts/gds/run-unit-tests.js +4 -0
  122. package/scripts/gds/update-channel.js +186 -0
  123. package/scripts/gds/upgrade.js +116 -9
  124. package/scripts/hall-preview/README.md +103 -0
  125. package/scripts/hall-preview/fixtures/access-requests.json +1 -0
  126. package/scripts/hall-preview/fixtures/achievements.json +76 -0
  127. package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
  128. package/scripts/hall-preview/fixtures/audit-log.json +1 -0
  129. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +1 -0
  130. package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
  131. package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
  132. package/scripts/hall-preview/fixtures/builders__roster.json +1 -0
  133. package/scripts/hall-preview/fixtures/goals.json +502 -0
  134. package/scripts/hall-preview/fixtures/goals__25.json +96 -0
  135. package/scripts/hall-preview/fixtures/goals__26.json +67 -0
  136. package/scripts/hall-preview/fixtures/goals__3.json +103 -0
  137. package/scripts/hall-preview/fixtures/goals__35.json +49 -0
  138. package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
  139. package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
  140. package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
  141. package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
  142. package/scripts/hall-preview/fixtures/grades__by-builder.json +1 -0
  143. package/scripts/hall-preview/fixtures/inbox.json +330 -0
  144. package/scripts/hall-preview/fixtures/instance.json +124 -0
  145. package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
  146. package/scripts/hall-preview/fixtures/me.json +476 -0
  147. package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
  148. package/scripts/hall-preview/fixtures/override-requests.json +1 -0
  149. package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
  150. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
  151. package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
  152. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
  153. package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
  154. package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
  155. package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
  156. package/scripts/hall-preview/fixtures/public__grades.json +50 -0
  157. package/scripts/hall-preview/fixtures/public__progress.json +124 -0
  158. package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
  159. package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
  160. package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
  161. package/scripts/hall-preview/fixtures/security__docs.json +1 -0
  162. package/scripts/hall-preview/fixtures/security__reports.json +1 -0
  163. package/scripts/hall-preview/fixtures/sessions__search.json +1 -0
  164. package/scripts/hall-preview/fixtures/tasks.json +2420 -0
  165. package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
  166. package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
  167. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
  168. package/scripts/hall-preview/fixtures/versions.json +89 -0
  169. package/scripts/hall-preview/refresh-fixtures.js +77 -0
  170. package/scripts/hall-preview/server.js +178 -0
  171. package/scripts/migrate.sh +29 -4
  172. package/src/bongos/auth.js +14 -1
  173. package/src/bongos/optimizer.js +37 -2
  174. package/src/bongos/serve-internal.js +10 -1
  175. package/src/branding.js +1 -0
  176. package/src/module-api.js +12 -1
  177. package/src/module-loader/loader.js +36 -4
  178. package/src/module-loader/manifest-schema.js +16 -1
  179. package/tests/branding.mjs +11 -0
  180. package/tests/claim_error_surface.mjs +107 -0
  181. package/tests/claude_materialize.mjs +20 -0
  182. package/tests/consumer_layout_boot.mjs +302 -0
  183. package/tests/criterion_suggest.mjs +106 -0
  184. package/tests/cutover_codemod.mjs +184 -0
  185. package/tests/device_flow_start.mjs +25 -2
  186. package/tests/hook_core_resolve.mjs +125 -0
  187. package/tests/infra_exec_paths.mjs +104 -0
  188. package/tests/init.mjs +47 -0
  189. package/tests/migrate_root_resolution.mjs +97 -0
  190. package/tests/mirror_lineage_proof.mjs +14 -0
  191. package/tests/module_loader.mjs +42 -0
  192. package/tests/module_manifest.mjs +19 -0
  193. package/tests/onboard.mjs +13 -0
  194. package/tests/package_core.mjs +8 -0
  195. package/tests/provisioning_github_app_routes.mjs +15 -0
  196. package/tests/publish_manifest.mjs +52 -1
  197. package/tests/run_routine.mjs +26 -1
  198. package/tests/run_routine_two_root.mjs +77 -0
  199. package/tests/scrubber_corpus.mjs +17 -0
  200. package/tests/update_channel.mjs +186 -0
  201. package/tests/upgrade.mjs +131 -0
  202. package/tests/workflow_exec_paths.mjs +98 -0
  203. package/docs/design/container-exterior-tiles.md +0 -49
  204. package/docs/design/container-exterior.md +0 -81
  205. package/docs/design/container-interior.md +0 -77
  206. package/docs/design/world-map-scroll.md +0 -41
  207. package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
  208. package/docs/recipes/cloudbongos-staging-gate.md +0 -139
  209. package/docs/recipes/cloudbongos-standup.md +0 -111
  210. package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
  211. package/docs/recipes/parallel-load-test.md +0 -32
  212. package/docs/recipes/pixel-art-pipeline.md +0 -411
  213. package/docs/recipes/staging-data.md +0 -50
  214. package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
  215. package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
  216. package/migrations/instance/110_medusa_planning_seed.sql +0 -70
  217. package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
  218. package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
  219. package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
  220. package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
  221. package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
  222. package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
  223. package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
  224. package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
  225. package/scripts/gds/art-stage.js +0 -152
  226. package/server.js +0 -212
  227. package/tests/art_key.mjs +0 -121
  228. package/tests/day_cycle.mjs +0 -197
  229. package/tests/snapshot_picker.mjs +0 -233
  230. package/tests/sprite_facing.mjs +0 -35
  231. package/tests/terrain_picker.mjs +0 -298
  232. package/tests/turn_in_place.mjs +0 -280
  233. package/tests/walkability.mjs +0 -188
@@ -1,233 +0,0 @@
1
- // tests/snapshot_picker.mjs
2
- //
3
- // Parity test: verifies that the snapshot renderer's tile-picking logic
4
- // agrees with the JS runtime autotile pickers for a representative sample
5
- // of cells in INITIAL_MAP.
6
- //
7
- // The snapshot renderer (scripts/snapshots/render-world.mjs) calls the
8
- // SAME selectBlob47/selectPath16 and neighborsAt functions that the browser
9
- // client uses — so this test validates that the renderer correctly:
10
- // 1. Passes the ground grid to ground-layer pickers.
11
- // 2. Passes the object grid to object-layer pickers.
12
- // 3. Produces the same tile index as a direct call with the same inputs.
13
- //
14
- // If these diverge the in-game render and the snapshot would show different
15
- // tile choices for the same cell.
16
- //
17
- // Usage:
18
- // node tests/snapshot_picker.mjs
19
-
20
- import { createRequire } from 'node:module';
21
- import { selectBlob47, selectPath16 } from '../modules/game/public/game/world/autotile.js';
22
- import {
23
- TERRAIN_RENDER,
24
- OBJECT_RENDER,
25
- neighborsAt,
26
- } from '../modules/game/public/game/world/terrain.js';
27
-
28
- const require = createRequire(import.meta.url);
29
- const { INITIAL_MAP, WORLD_COLS, WORLD_ROWS, TERRAIN, OBJECT } = require('../modules/game/world/map.js');
30
-
31
- let passed = 0;
32
- let failed = 0;
33
-
34
- function assert(condition, msg) {
35
- if (condition) {
36
- passed++;
37
- } else {
38
- console.error(` FAIL: ${msg}`);
39
- failed++;
40
- }
41
- }
42
-
43
- // ---------- helpers mirrored from render-world.mjs -------------------------
44
-
45
- function groundIdx(x, y) {
46
- const terrainId = INITIAL_MAP.ground[y][x];
47
- const cfg = TERRAIN_RENDER[terrainId];
48
- if (!cfg || cfg.strategy === 'single') return null;
49
- const neighbors = neighborsAt(INITIAL_MAP.ground, x, y);
50
- if (cfg.strategy === 'blob47') return selectBlob47(terrainId, neighbors);
51
- if (cfg.strategy === 'path16') {
52
- return selectPath16({
53
- N: neighbors.N === terrainId ? 1 : 0,
54
- W: neighbors.W === terrainId ? 1 : 0,
55
- E: neighbors.E === terrainId ? 1 : 0,
56
- S: neighbors.S === terrainId ? 1 : 0,
57
- });
58
- }
59
- return null;
60
- }
61
-
62
- function objectIdx(x, y) {
63
- const objId = INITIAL_MAP.objects[y][x];
64
- if (!objId) return null;
65
- const cfg = OBJECT_RENDER[objId];
66
- if (!cfg || cfg.strategy === 'single') return null;
67
- const neighbors = neighborsAt(INITIAL_MAP.objects, x, y);
68
- if (cfg.strategy === 'blob47') return selectBlob47(objId, neighbors);
69
- if (cfg.strategy === 'path16') {
70
- return selectPath16({
71
- N: neighbors.N === objId ? 1 : 0,
72
- W: neighbors.W === objId ? 1 : 0,
73
- E: neighbors.E === objId ? 1 : 0,
74
- S: neighbors.S === objId ? 1 : 0,
75
- });
76
- }
77
- return null;
78
- }
79
-
80
- // ---------- tests -----------------------------------------------------------
81
-
82
- console.log('[snapshot_picker] running parity tests…\n');
83
-
84
- // 1. All-grass interior cell (0,0 — surrounded by grass on all sides).
85
- {
86
- const idx = groundIdx(1, 1);
87
- assert(idx !== null, 'grass interior: should return a blob47 index');
88
- assert(idx >= 0 && idx <= 46, `grass interior: index ${idx} out of range 0..46`);
89
-
90
- // Direct call with the same neighbors
91
- const neighbors = neighborsAt(INITIAL_MAP.ground, 1, 1);
92
- const direct = selectBlob47(INITIAL_MAP.ground[1][1], neighbors);
93
- assert(idx === direct, `grass interior: picker returned ${idx}, direct call returned ${direct}`);
94
- console.log(` PASS grass interior (1,1): blob47 idx=${idx}`);
95
- }
96
-
97
- // 2. SEA cell — single-tile strategy, no autotile index.
98
- {
99
- const seaRow = WORLD_ROWS - 1;
100
- const terrainId = INITIAL_MAP.ground[seaRow][0];
101
- assert(terrainId === TERRAIN.SEA, `row ${seaRow} col 0 should be SEA, got ${terrainId}`);
102
- const cfg = TERRAIN_RENDER[terrainId];
103
- assert(cfg.strategy === 'single', `sea: expected single strategy, got ${cfg.strategy}`);
104
- const idx = groundIdx(0, seaRow);
105
- assert(idx === null, `sea: single-tile strategy should return null, got ${idx}`);
106
- console.log(` PASS sea (0,${seaRow}): single tile, no autotile index`);
107
- }
108
-
109
- // 3. Grass→sand boundary cell (first SAND row, leftmost column).
110
- // At y=6, x=0 the ground is SAND. Its N neighbor is GRASS. The blob47 picker
111
- // should see a non-all-interior byte and return an edge tile index.
112
- {
113
- const x = 0, y = 6;
114
- const terrainId = INITIAL_MAP.ground[y][x];
115
- assert(terrainId === TERRAIN.SAND, `(0,6) should be SAND, got ${terrainId}`);
116
- const idx = groundIdx(x, y);
117
- assert(idx !== null, 'sand boundary: should have autotile index');
118
-
119
- const neighbors = neighborsAt(INITIAL_MAP.ground, x, y);
120
- const direct = selectBlob47(terrainId, neighbors);
121
- assert(idx === direct, `sand boundary: picker=${idx}, direct=${direct}`);
122
- // Sand at (0,6) has grass to the north and sea below, so it's not all-interior.
123
- // We just check it produces a valid range.
124
- assert(idx >= 0 && idx <= 46, `sand boundary idx ${idx} out of range`);
125
- console.log(` PASS sand boundary (0,6): blob47 idx=${idx}`);
126
- }
127
-
128
- // 4. Rock object — single tile, no autotile index.
129
- {
130
- // Rocks are placed in the grass band (y<6) where the hash hits.
131
- // Find the first rock in the map.
132
- let rockX = -1, rockY = -1;
133
- outer: for (let y = 0; y < 6; y++) {
134
- for (let x = 0; x < WORLD_COLS; x++) {
135
- if (INITIAL_MAP.objects[y][x] === OBJECT.ROCK) { rockX = x; rockY = y; break outer; }
136
- }
137
- }
138
- assert(rockX >= 0, 'should find at least one rock in the grass band');
139
- if (rockX >= 0) {
140
- const cfg = OBJECT_RENDER[OBJECT.ROCK];
141
- assert(cfg.strategy === 'single', `rock: expected single strategy, got ${cfg.strategy}`);
142
- const idx = objectIdx(rockX, rockY);
143
- assert(idx === null, `rock (${rockX},${rockY}): single-tile → null, got ${idx}`);
144
- console.log(` PASS rock (${rockX},${rockY}): single tile, no autotile index`);
145
- }
146
- }
147
-
148
- // 5. Container panel — single tile, no autotile index.
149
- {
150
- const cx = 13, cy = 8;
151
- const panel = INITIAL_MAP.objects[cy][cx];
152
- assert(panel && panel.startsWith('container_panel_'), `(13,8) should be a container panel, got ${panel}`);
153
- const idx = objectIdx(cx, cy);
154
- assert(idx === null, `container panel: single-tile → null, got ${idx}`);
155
- console.log(` PASS container panel (${cx},${cy}): single tile, no autotile index`);
156
- }
157
-
158
- // 6. Marble path — path16 autotile. Cells at (13, 10..13).
159
- // The path runs N→S at x=13, y=10-13. Middle cells should connect north+south.
160
- {
161
- const x = 13, y = 11; // interior path cell connects N (y=10) and S (y=12)
162
- const objId = INITIAL_MAP.objects[y][x];
163
- assert(objId === OBJECT.PATH_MARBLE, `(13,11) should be PATH_MARBLE, got ${objId}`);
164
-
165
- const idx = objectIdx(x, y);
166
- assert(idx !== null, 'path interior: should have path16 index');
167
-
168
- // Direct call
169
- const neighbors = neighborsAt(INITIAL_MAP.objects, x, y);
170
- const direct = selectPath16({
171
- N: neighbors.N === objId ? 1 : 0,
172
- W: neighbors.W === objId ? 1 : 0,
173
- E: neighbors.E === objId ? 1 : 0,
174
- S: neighbors.S === objId ? 1 : 0,
175
- });
176
- assert(idx === direct, `path interior: picker=${idx}, direct=${direct}`);
177
-
178
- // N and S neighbors are also PATH_MARBLE at (13,10) and (13,12)
179
- assert(neighbors.N === OBJECT.PATH_MARBLE, `path (13,11): N should be PATH_MARBLE`);
180
- assert(neighbors.S === OBJECT.PATH_MARBLE, `path (13,11): S should be PATH_MARBLE`);
181
- // Expected: N+S connected, W+E not → path16 = 0b1001 = 9
182
- const expected = selectPath16({ N: 1, W: 0, E: 0, S: 1 });
183
- assert(idx === expected, `path interior: expected N+S connected index ${expected}, got ${idx}`);
184
- console.log(` PASS marble path interior (13,11): path16 idx=${idx} (N+S connected)`);
185
- }
186
-
187
- // 7. Marble path endpoint — top of the path (13,10): connects S only.
188
- {
189
- const x = 13, y = 10;
190
- const objId = INITIAL_MAP.objects[y][x];
191
- assert(objId === OBJECT.PATH_MARBLE, `(13,10) should be PATH_MARBLE, got ${objId}`);
192
-
193
- const neighbors = neighborsAt(INITIAL_MAP.objects, x, y);
194
- // (13,9) is the container door panel — NOT a path marble
195
- assert(neighbors.N !== OBJECT.PATH_MARBLE, `path top (13,10): N should not be PATH_MARBLE`);
196
- assert(neighbors.S === OBJECT.PATH_MARBLE, `path top (13,10): S should be PATH_MARBLE`);
197
-
198
- const idx = objectIdx(x, y);
199
- const expected = selectPath16({ N: 0, W: 0, E: 0, S: 1 });
200
- assert(idx === expected, `path top (13,10): expected S-only idx ${expected}, got ${idx}`);
201
- console.log(` PASS marble path top (13,10): path16 idx=${idx} (S only)`);
202
- }
203
-
204
- // 8. Ground layer: no object cell pollutes ground autotile neighbors.
205
- // The rock at (rockX, rockY) must not affect the ground tile choice for
206
- // any of its neighbors — picker sees the ground grid only.
207
- {
208
- let rockX = -1, rockY = -1;
209
- outer2: for (let y = 0; y < 6; y++) {
210
- for (let x = 0; x < WORLD_COLS; x++) {
211
- if (INITIAL_MAP.objects[y][x] === OBJECT.ROCK) { rockX = x; rockY = y; break outer2; }
212
- }
213
- }
214
- if (rockX >= 0 && rockX + 1 < WORLD_COLS) {
215
- const rightX = rockX + 1;
216
- const rightY = rockY;
217
- const terrainId = INITIAL_MAP.ground[rightY][rightX];
218
- const groundNeighbors = neighborsAt(INITIAL_MAP.ground, rightX, rightY);
219
- // The W neighbor of (rightX, rightY) should be a terrain id (grass), not a rock object id.
220
- assert(
221
- groundNeighbors.W === TERRAIN.GRASS,
222
- `ground picks should see ground only: W neighbor of (${rightX},${rightY}) should be GRASS, got ${groundNeighbors.W}`
223
- );
224
- console.log(` PASS ground isolation (${rightX},${rightY}): W ground neighbor is ${groundNeighbors.W}, not an object id`);
225
- }
226
- }
227
-
228
- // ---------- summary ---------------------------------------------------------
229
-
230
- console.log(`\n[snapshot_picker] ${passed} passed, ${failed} failed`);
231
- if (failed > 0) {
232
- process.exit(1);
233
- }
@@ -1,35 +0,0 @@
1
- // tests/sprite_facing.mjs
2
- //
3
- // Behavior-preserving refactor check (task 1968, ADR 0113): resolveFacing
4
- // was extracted out of Player.js's _applyIdleTexture/_applyWalkTexture so
5
- // the sprite Studio can resolve the right-facing mirror identically to the
6
- // live game. Pure, Phaser-free — pins the exact mapping both callers rely on.
7
- //
8
- // Run: node tests/sprite_facing.mjs
9
-
10
- import { strict as assert } from 'node:assert';
11
- import { resolveFacing } from '../modules/game/public/game/player/spriteFacing.js';
12
-
13
- let passed = 0;
14
- function test(name, fn) {
15
- try {
16
- fn();
17
- console.log(` ok ${name}`);
18
- passed++;
19
- } catch (e) {
20
- console.error(` FAIL ${name}: ${e.message}`);
21
- process.exitCode = 1;
22
- }
23
- }
24
-
25
- test('down/up/left pass through unmirrored', () => {
26
- assert.deepEqual(resolveFacing('down'), { sourceDir: 'down', flipX: false });
27
- assert.deepEqual(resolveFacing('up'), { sourceDir: 'up', flipX: false });
28
- assert.deepEqual(resolveFacing('left'), { sourceDir: 'left', flipX: false });
29
- });
30
-
31
- test('right resolves to the left-facing art, flipped', () => {
32
- assert.deepEqual(resolveFacing('right'), { sourceDir: 'left', flipX: true });
33
- });
34
-
35
- console.log(`${passed} passed`);
@@ -1,298 +0,0 @@
1
- // tests/terrain_picker.mjs
2
- //
3
- // Verify the autotile picker only sees the ground grid — objects on top
4
- // of a cell must NOT change which tile the picker selects for that cell
5
- // or its neighbors. This is the core invariant of the ground-object
6
- // split (ADR 0012): an object overlay never pollutes ground rendering.
7
- //
8
- // Also exercises the basic dispatch shape: each render strategy
9
- // (single / blob47 / path16) returns the expected kind of result.
10
- //
11
- // Run: node tests/terrain_picker.mjs
12
-
13
- import { strict as assert } from 'node:assert';
14
- import {
15
- TERRAIN,
16
- OBJECT,
17
- TERRAIN_RENDER,
18
- OBJECT_RENDER,
19
- pickAutotileIndex,
20
- pickObjectAutotileIndex,
21
- neighborsAt,
22
- } from '../modules/game/public/game/world/terrain.js';
23
- import { cellTransform } from '../modules/game/public/game/world/tilePalette.js';
24
-
25
- let passed = 0;
26
- function test(name, fn) {
27
- try {
28
- fn();
29
- console.log(` ok ${name}`);
30
- passed++;
31
- } catch (e) {
32
- console.error(` FAIL ${name}: ${e.message}`);
33
- process.exitCode = 1;
34
- }
35
- }
36
-
37
- // Build a tiny ground grid for picker tests. 5×5 with grass center,
38
- // sand on the south edge — same shape as the V1 grass↔sand transition.
39
- //
40
- // g g g g g
41
- // g g g g g
42
- // g g g g g
43
- // s s s s s
44
- // s s s s s
45
- function makeGroundGrid() {
46
- const G = TERRAIN.GRASS, S = TERRAIN.SAND;
47
- return [
48
- [G, G, G, G, G],
49
- [G, G, G, G, G],
50
- [G, G, G, G, G],
51
- [S, S, S, S, S],
52
- [S, S, S, S, S],
53
- ];
54
- }
55
-
56
- // ---------- dispatch shape ------------------------------------------------------
57
-
58
- test('TERRAIN_RENDER covers all 4 ground terrains', () => {
59
- for (const t of Object.values(TERRAIN)) {
60
- assert.ok(TERRAIN_RENDER[t], `missing TERRAIN_RENDER for ${t}`);
61
- }
62
- });
63
-
64
- test('grass dispatches blob47, sea dispatches single, marble_path dispatches path16', () => {
65
- assert.equal(TERRAIN_RENDER[TERRAIN.GRASS].strategy, 'blob47');
66
- assert.equal(TERRAIN_RENDER[TERRAIN.SAND].strategy, 'blob47');
67
- assert.equal(TERRAIN_RENDER[TERRAIN.SEA].strategy, 'single');
68
- assert.equal(TERRAIN_RENDER[TERRAIN.MARBLE_PATH].strategy, 'path16');
69
- });
70
-
71
- test('every blob47/path16 entry declares family + fallbackTile', () => {
72
- for (const [t, cfg] of Object.entries(TERRAIN_RENDER)) {
73
- if (cfg.strategy === 'blob47' || cfg.strategy === 'path16') {
74
- assert.ok(cfg.family, `${t} missing family`);
75
- assert.ok(cfg.fallbackTile, `${t} missing fallbackTile`);
76
- }
77
- }
78
- });
79
-
80
- // ---------- ground-only invariant ----------------------------------------------
81
-
82
- test('neighborsAt returns 8 neighbors from the ground grid', () => {
83
- const g = makeGroundGrid();
84
- // (2, 1) is deep interior of the grass band — rows 0..2 are grass.
85
- const n = neighborsAt(g, 2, 1);
86
- for (const dir of ['NW','N','NE','W','E','SW','S','SE']) {
87
- assert.equal(n[dir], TERRAIN.GRASS, `neighbor ${dir} should be grass`);
88
- }
89
- });
90
-
91
- test('neighborsAt returns null for off-grid neighbors', () => {
92
- const g = makeGroundGrid();
93
- const n = neighborsAt(g, 0, 0);
94
- assert.equal(n.NW, null);
95
- assert.equal(n.N, null);
96
- assert.equal(n.W, null);
97
- assert.equal(n.NE, null); // off-top
98
- assert.equal(n.SW, null); // off-left
99
- assert.equal(n.E, TERRAIN.GRASS);
100
- assert.equal(n.S, TERRAIN.GRASS);
101
- assert.equal(n.SE, TERRAIN.GRASS);
102
- });
103
-
104
- test('grass cell deep in interior picks the all-interior tile (max byte)', () => {
105
- const g = makeGroundGrid();
106
- const idx = pickAutotileIndex(TERRAIN.GRASS, neighborsAt(g, 2, 1));
107
- // All 8 grass neighbors → byte 0xFF → maps to the canonical tile for
108
- // "all neighbors same." For the standard 47-blob lookup this is one
109
- // specific index; we just assert it's a valid 0..46 value and it
110
- // matches what we get for OTHER fully-interior grass cells.
111
- const idx2 = pickAutotileIndex(TERRAIN.GRASS, neighborsAt(g, 2, 0));
112
- // (2, 0) has off-grid N/NW/NE — those count as not-grass. So idx
113
- // and idx2 should DIFFER (different boundary configs).
114
- assert.ok(Number.isInteger(idx) && idx >= 0 && idx < 47, `idx out of range: ${idx}`);
115
- assert.notEqual(idx, idx2, 'interior cell should differ from top-edge cell');
116
- });
117
-
118
- test('grass cell at south edge of grass band picks a south-boundary tile', () => {
119
- const g = makeGroundGrid();
120
- // (2, 2) — last row of grass; south neighbors are sand
121
- const idx = pickAutotileIndex(TERRAIN.GRASS, neighborsAt(g, 2, 2));
122
- // (2, 1) — interior of grass band
123
- const idxInterior = pickAutotileIndex(TERRAIN.GRASS, neighborsAt(g, 2, 1));
124
- assert.notEqual(idx, idxInterior, 'south-edge cell must differ from interior');
125
- });
126
-
127
- test('marble_path picker is path16 (returns 0..15)', () => {
128
- const path = TERRAIN.MARBLE_PATH;
129
- // Lone path cell, no neighbors are also path
130
- const idxLone = pickAutotileIndex(path, { N: TERRAIN.GRASS, W: TERRAIN.GRASS, E: TERRAIN.GRASS, S: TERRAIN.GRASS });
131
- assert.equal(idxLone, 0, 'isolated path node = 0');
132
- // Cross junction
133
- const idxCross = pickAutotileIndex(path, { N: path, W: path, E: path, S: path });
134
- assert.equal(idxCross, 15, 'cross junction = 0b1111 = 15');
135
- });
136
-
137
- test('sea cell (single strategy) returns null from picker', () => {
138
- // 'single' terrains don't go through the autotile picker — caller
139
- // dispatches them directly to the legacy single-tile path. The
140
- // picker returns null to signal "not my problem."
141
- const idx = pickAutotileIndex(TERRAIN.SEA, {
142
- NW: TERRAIN.SEA, N: TERRAIN.SEA, NE: TERRAIN.SEA,
143
- W: TERRAIN.SEA, E: TERRAIN.SEA,
144
- SW: TERRAIN.SEA, S: TERRAIN.SEA, SE: TERRAIN.SEA,
145
- });
146
- assert.equal(idx, null, 'single-strategy terrain returns null');
147
- });
148
-
149
- // ---------- the headline invariant: objects do not affect picker --------------
150
-
151
- test('OBJECT-LAYER INVARIANT: picker only sees ground; objects must be passed via a separate grid', () => {
152
- // The picker takes the GROUND grid. We can't reach inside and verify
153
- // it ignores objects (they're not part of its API). Instead, verify
154
- // that two ground configs with IDENTICAL ground but different object
155
- // overlays produce identical picker output — by construction.
156
- //
157
- // (This test really enforces the invariant by demonstrating the
158
- // CALLING PATTERN: the renderer must pass `map.ground`, never
159
- // `map.objects`. The picker has no object-layer knob.)
160
- const g = makeGroundGrid();
161
- const idxA = pickAutotileIndex(TERRAIN.GRASS, neighborsAt(g, 2, 2));
162
-
163
- // Imagine an alternate world where every cell of g has a rock OBJECT
164
- // on top. The picker still receives only the ground grid → identical
165
- // tile selection.
166
- const idxB = pickAutotileIndex(TERRAIN.GRASS, neighborsAt(g, 2, 2));
167
-
168
- assert.equal(idxA, idxB, 'object overlays must NOT change picker output');
169
- });
170
-
171
- // ---------- ADR 0013: object-layer autotile dispatch --------------------------
172
-
173
- test('OBJECT_RENDER supports single + path16 strategies; PATH_MARBLE is path16', () => {
174
- // Every legacy entry must declare strategy='single' explicitly OR
175
- // accept the short-form `{ tile: '...' }` (back-compat).
176
- for (const [oid, cfg] of Object.entries(OBJECT_RENDER)) {
177
- const strat = cfg.strategy || 'single';
178
- assert.ok(['single', 'path16', 'blob47'].includes(strat),
179
- `unknown strategy ${strat} for ${oid}`);
180
- if (strat === 'path16' || strat === 'blob47') {
181
- assert.ok(cfg.family, `${oid} missing family`);
182
- assert.ok(cfg.fallbackTile, `${oid} missing fallbackTile`);
183
- }
184
- }
185
- assert.equal(OBJECT_RENDER[OBJECT.PATH_MARBLE].strategy, 'path16');
186
- assert.equal(OBJECT_RENDER[OBJECT.PATH_MARBLE].family, 'marble_path');
187
- });
188
-
189
- test('pickObjectAutotileIndex: empty cell + single-strategy objects return null', () => {
190
- // Empty cell.
191
- assert.equal(pickObjectAutotileIndex(null, {}), null);
192
- // ROCK is single-strategy → no autotile index.
193
- assert.equal(
194
- pickObjectAutotileIndex(OBJECT.ROCK,
195
- { N: null, W: null, E: null, S: null }), null,
196
- );
197
- });
198
-
199
- test('pickObjectAutotileIndex: PATH_MARBLE neighbors → path16 dispatch', () => {
200
- const P = OBJECT.PATH_MARBLE;
201
- // Isolated path = byte 0 (no connections).
202
- const lone = pickObjectAutotileIndex(P, { N: null, W: null, E: null, S: null });
203
- assert.equal(lone, 0, 'isolated path object = path16 index 0');
204
- // Cross junction = 0b1111 = 15.
205
- const cross = pickObjectAutotileIndex(P, { N: P, W: P, E: P, S: P });
206
- assert.equal(cross, 15, '4-way path object = path16 index 15');
207
- // Vertical strip (N+S) = 0b1001 = 9.
208
- const vert = pickObjectAutotileIndex(P, { N: P, W: null, E: null, S: P });
209
- assert.equal(vert, 0b1001, 'N+S vertical strip = 9');
210
- });
211
-
212
- test('OBJECT-LAYER AUTOTILE INVARIANT: object autotile sees only objects', () => {
213
- // Same calling pattern enforcement as the ground invariant: caller
214
- // must pass `map.objects` neighbors, not `map.ground`. A grass-on-sand
215
- // boundary in the ground grid has no effect on a path object's tile.
216
- const P = OBJECT.PATH_MARBLE;
217
- // Build a 5×5 objects grid: vertical path strip down column 2.
218
- const _ = null;
219
- const objects = [
220
- [_, _, P, _, _],
221
- [_, _, P, _, _],
222
- [_, _, P, _, _],
223
- [_, _, P, _, _],
224
- [_, _, P, _, _],
225
- ];
226
- // Cell (2, 2) — middle of the strip. N and S are PATH_MARBLE; W, E, corners empty.
227
- const n = neighborsAt(objects, 2, 2);
228
- assert.equal(n.N, P);
229
- assert.equal(n.S, P);
230
- assert.equal(n.W, null);
231
- assert.equal(n.E, null);
232
- const idx = pickObjectAutotileIndex(P, n);
233
- assert.equal(idx, 0b1001, 'middle of N-S strip should pick vertical-straight');
234
- // Cell (2, 0) — top of strip. Only S is path; N is off-grid.
235
- const idxTop = pickObjectAutotileIndex(P, neighborsAt(objects, 2, 0));
236
- assert.equal(idxTop, 0b0001, 'top end-cap = S only');
237
- // Cell (2, 4) — bottom of strip. Only N is path.
238
- const idxBot = pickObjectAutotileIndex(P, neighborsAt(objects, 2, 4));
239
- assert.equal(idxBot, 0b1000, 'bottom end-cap = N only');
240
- });
241
-
242
- // ---------- D.4-T2: cellTransform determinism -----------------------------------
243
-
244
- test('cellTransform returns an object with flipX, flipY, angle fields', () => {
245
- const t = cellTransform(0, 0);
246
- assert.ok('flipX' in t, 'missing flipX');
247
- assert.ok('flipY' in t, 'missing flipY');
248
- assert.ok('angle' in t, 'missing angle');
249
- assert.equal(typeof t.flipX, 'boolean');
250
- assert.equal(typeof t.flipY, 'boolean');
251
- assert.equal(typeof t.angle, 'number');
252
- });
253
-
254
- test('cellTransform is deterministic: same (x,y) always returns the same values', () => {
255
- const coords = [[0,0],[1,0],[0,1],[5,7],[12,3],[0,15],[7,7],[3,11]];
256
- for (const [x, y] of coords) {
257
- const a = cellTransform(x, y);
258
- const b = cellTransform(x, y);
259
- assert.equal(a.flipX, b.flipX, `flipX mismatch at (${x},${y})`);
260
- assert.equal(a.flipY, b.flipY, `flipY mismatch at (${x},${y})`);
261
- assert.equal(a.angle, b.angle, `angle mismatch at (${x},${y})`);
262
- }
263
- });
264
-
265
- test('cellTransform produces variation across cells (not all identical)', () => {
266
- const signatures = new Set();
267
- for (let x = 0; x < 8; x++) {
268
- for (let y = 0; y < 8; y++) {
269
- const t = cellTransform(x, y);
270
- signatures.add(`${t.flipX}|${t.flipY}|${t.angle}`);
271
- }
272
- }
273
- assert.ok(signatures.size >= 2, `expected variation across cells, got ${signatures.size} distinct transforms`);
274
- });
275
-
276
- test('cellTransform uses a different hash from interior-variant selection (cells differ)', () => {
277
- // The two hashes should produce different distributions so that flip/rotation
278
- // and variant selection don't always correlate. Verify they produce different
279
- // orderings for the same 4x4 patch.
280
- const transformSigs = [];
281
- for (let x = 0; x < 4; x++) {
282
- for (let y = 0; y < 4; y++) {
283
- const t = cellTransform(x, y);
284
- transformSigs.push(`${x},${y}:${Number(t.flipX)}${Number(t.flipY)}`);
285
- }
286
- }
287
- // Just verify the function ran without error and produced a non-trivial result.
288
- assert.equal(transformSigs.length, 16, '4x4 patch has 16 cells');
289
- });
290
-
291
- // ---------- summary -------------------------------------------------------------
292
-
293
- if (process.exitCode !== 1) {
294
- console.log(`\n[terrain_picker] OK — ${passed} tests passed`);
295
- } else {
296
- console.error(`\n[terrain_picker] FAIL — see output above`);
297
- process.exit(1);
298
- }