@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,280 +0,0 @@
1
- // tests/turn_in_place.mjs
2
- //
3
- // Turn-in-place (#1057): quick-tap a direction to face it without stepping;
4
- // hold past the threshold to walk as before. Exercises the pure movement
5
- // helpers (Phaser-free, DB-free) that drive Player.update:
6
- // - decideMove(dir, facing, heldMs, threshold) — the tap-vs-hold rule.
7
- // - readDirectionFromInput(moveKeys) — WASD + arrow key resolution.
8
- // Plus a frame-by-frame simulation proving a real tap never steps and a
9
- // real hold does.
10
- //
11
- // Run: node tests/turn_in_place.mjs
12
-
13
- import { strict as assert } from 'node:assert';
14
- import {
15
- decideMove,
16
- readDirectionFromInput,
17
- TURN_TAP_MS,
18
- DIRS,
19
- } from '../modules/game/public/game/player/movement.js';
20
-
21
- let passed = 0;
22
- function test(name, fn) {
23
- try {
24
- fn();
25
- console.log(` ok ${name}`);
26
- passed++;
27
- } catch (e) {
28
- console.error(` FAIL ${name}: ${e.message}`);
29
- process.exitCode = 1;
30
- }
31
- }
32
-
33
- // A Phaser Key stand-in — readDirectionFromInput only reads these two props.
34
- const key = (isDown, timeDown = 0) => ({ isDown, timeDown });
35
- const NONE = key(false);
36
-
37
- // ---------- decideMove: the tap-vs-hold rule -----------------------------
38
-
39
- test('quick tap of a NEW direction turns, never steps (even if heldMs is large)', () => {
40
- // A fresh press in a new direction always pivots first and never steps the
41
- // same frame — so a quick tap of a new direction can only ever face it.
42
- assert.deepEqual(decideMove('left', 'down', 5, TURN_TAP_MS), { turn: true, step: false });
43
- assert.deepEqual(decideMove('up', 'down', 9999, TURN_TAP_MS), { turn: true, step: false });
44
- });
45
-
46
- test('already facing + held below threshold = no-op (a quick tap is just a look)', () => {
47
- assert.deepEqual(decideMove('down', 'down', 0, TURN_TAP_MS), { turn: false, step: false });
48
- assert.deepEqual(decideMove('down', 'down', TURN_TAP_MS - 1, TURN_TAP_MS), { turn: false, step: false });
49
- });
50
-
51
- test('already facing + held at/over threshold = step (walk)', () => {
52
- assert.deepEqual(decideMove('down', 'down', TURN_TAP_MS, TURN_TAP_MS), { turn: false, step: true });
53
- assert.deepEqual(decideMove('right', 'right', TURN_TAP_MS + 500, TURN_TAP_MS), { turn: false, step: true });
54
- });
55
-
56
- test('no direction held = nothing', () => {
57
- assert.deepEqual(decideMove(null, 'down', 9999, TURN_TAP_MS), { turn: false, step: false });
58
- });
59
-
60
- test('threshold sits in the task-specified 120-150ms band', () => {
61
- assert.ok(TURN_TAP_MS >= 120 && TURN_TAP_MS <= 150, `TURN_TAP_MS=${TURN_TAP_MS}`);
62
- });
63
-
64
- // ---------- readDirectionFromInput: WASD + arrow bindings ----------------
65
-
66
- const dirKeys = (over) => ({
67
- up: [NONE, NONE], down: [NONE, NONE], left: [NONE, NONE], right: [NONE, NONE], ...over,
68
- });
69
-
70
- test('nothing held resolves to null', () => {
71
- assert.equal(readDirectionFromInput(dirKeys({})), null);
72
- });
73
-
74
- test('WASD drives direction (W = up)', () => {
75
- const sel = readDirectionFromInput(dirKeys({ up: [key(true, 100), NONE] }));
76
- assert.equal(sel.dir, 'up');
77
- assert.equal(sel.t, 100);
78
- });
79
-
80
- test('arrow keys drive the same direction (#1057 added arrow bindings)', () => {
81
- // second slot in each direction array is the cursor (arrow) key
82
- const sel = readDirectionFromInput(dirKeys({ right: [NONE, key(true, 250)] }));
83
- assert.equal(sel.dir, 'right');
84
- assert.equal(sel.t, 250);
85
- });
86
-
87
- test('WASD + arrow for one direction use the most-recent press time', () => {
88
- const sel = readDirectionFromInput(dirKeys({ down: [key(true, 100), key(true, 400)] }));
89
- assert.equal(sel.dir, 'down');
90
- assert.equal(sel.t, 400);
91
- });
92
-
93
- test('most-recently-pressed direction wins', () => {
94
- const sel = readDirectionFromInput(dirKeys({
95
- up: [key(true, 100), NONE],
96
- left: [key(true, 200), NONE],
97
- }));
98
- assert.equal(sel.dir, 'left');
99
- });
100
-
101
- test('opposing keys cancel (W+S held = no direction)', () => {
102
- assert.equal(readDirectionFromInput(dirKeys({
103
- up: [key(true, 100), NONE],
104
- down: [key(true, 110), NONE],
105
- })), null);
106
- });
107
-
108
- test('opposing pair cancels but a third direction still resolves', () => {
109
- const sel = readDirectionFromInput(dirKeys({
110
- left: [key(true, 100), NONE],
111
- right: [key(true, 110), NONE],
112
- up: [key(true, 90), NONE],
113
- }));
114
- assert.equal(sel.dir, 'up');
115
- });
116
-
117
- test('equal timeDown breaks ties deterministically (up before left)', () => {
118
- const sel = readDirectionFromInput(dirKeys({
119
- up: [key(true, 100), NONE],
120
- left: [key(true, 100), NONE],
121
- }));
122
- assert.equal(sel.dir, 'up');
123
- });
124
-
125
- // ---------- frame-by-frame: a real tap vs a real hold --------------------
126
-
127
- // Drive the same decision loop Player.update runs, over a sequence of
128
- // frames. Each frame: resolve the held direction, compute heldMs = now - t,
129
- // decide, and apply (turn updates facing; step counts a tile move).
130
- function simulate(frames, startFacing = 'down') {
131
- let facing = startFacing;
132
- let steps = 0;
133
- let turns = 0;
134
- for (const f of frames) {
135
- const sel = readDirectionFromInput(f.keys);
136
- if (!sel) continue;
137
- const { turn, step } = decideMove(sel.dir, facing, f.now - sel.t, TURN_TAP_MS);
138
- if (turn) { facing = sel.dir; turns++; continue; }
139
- if (step) { steps++; }
140
- }
141
- return { facing, steps, turns };
142
- }
143
-
144
- test('a quick tap of a new direction turns once and never steps', () => {
145
- const pressedAt = 1000;
146
- const held = (now) => dirKeys({ left: [key(true, pressedAt), NONE] });
147
- // Press at 1000, sampled at 1000 and 1050, then released before 130ms.
148
- const r = simulate([
149
- { now: 1000, keys: held(1000) },
150
- { now: 1050, keys: held(1050) },
151
- { now: 1100, keys: dirKeys({}) }, // released (~100ms tap)
152
- ]);
153
- assert.equal(r.facing, 'left');
154
- assert.equal(r.turns, 1);
155
- assert.equal(r.steps, 0); // pure turn-in-place — no tile change
156
- });
157
-
158
- test('a quick tap while already facing that way is a no-op (no step)', () => {
159
- const r = simulate([
160
- { now: 1000, keys: dirKeys({ down: [key(true, 1000), NONE] }) },
161
- { now: 1060, keys: dirKeys({ down: [key(true, 1060), NONE] }) },
162
- { now: 1090, keys: dirKeys({}) }, // released (~90ms tap)
163
- ], 'down');
164
- assert.equal(r.facing, 'down');
165
- assert.equal(r.turns, 0);
166
- assert.equal(r.steps, 0);
167
- });
168
-
169
- test('holding a direction walks (turn then step past the threshold)', () => {
170
- const pressedAt = 1000;
171
- const r = simulate([
172
- { now: 1000, keys: dirKeys({ left: [key(true, pressedAt), NONE] }) }, // turn
173
- { now: 1050, keys: dirKeys({ left: [key(true, pressedAt), NONE] }) }, // still in tap window
174
- { now: 1000 + TURN_TAP_MS + 10, keys: dirKeys({ left: [key(true, pressedAt), NONE] }) }, // step
175
- ]);
176
- assert.equal(r.facing, 'left');
177
- assert.equal(r.turns, 1);
178
- assert.equal(r.steps, 1);
179
- });
180
-
181
- test('holding the direction you already face walks after the threshold (no extra turn)', () => {
182
- const pressedAt = 2000;
183
- const r = simulate([
184
- { now: 2000, keys: dirKeys({ up: [key(true, pressedAt), NONE] }) }, // within tap window: nothing
185
- { now: 2000 + TURN_TAP_MS + 5, keys: dirKeys({ up: [key(true, pressedAt), NONE] }) }, // step
186
- ], 'up');
187
- assert.equal(r.turns, 0);
188
- assert.equal(r.steps, 1);
189
- });
190
-
191
- // DIRS sanity — Player.update derives the target tile from these.
192
- test('DIRS deltas are the four cardinal unit steps', () => {
193
- assert.deepEqual(DIRS.up, { dx: 0, dy: -1 });
194
- assert.deepEqual(DIRS.down, { dx: 0, dy: 1 });
195
- assert.deepEqual(DIRS.left, { dx: -1, dy: 0 });
196
- assert.deepEqual(DIRS.right, { dx: 1, dy: 0 });
197
- });
198
-
199
- // ---------- integration: the real Player class with Phaser/DB mocks ------
200
- //
201
- // Player.js is Phaser-free at module load (it only touches the API at
202
- // runtime), so we can drive the real update() loop with light mocks and
203
- // assert the wiring: a pure turn sends 'turn' and changes NO position; a
204
- // held press sends 'move' and steps exactly one tile.
205
-
206
- const { Player } = await import('../modules/game/public/game/player/Player.js');
207
-
208
- function makePlayer() {
209
- const sprite = {
210
- x: 0, y: 0,
211
- setOrigin() { return this; },
212
- setDepth() { return this; },
213
- setTexture() { return this; },
214
- setFlipX() { return this; },
215
- setPosition(x, y) { this.x = x; this.y = y; return this; },
216
- };
217
- const sent = [];
218
- const scene = {
219
- add: { image: () => sprite },
220
- textures: { exists: () => false }, // canvas-fallback path in spriteTextureFor
221
- isBlocked: () => false,
222
- // Synchronous tween: fire onComplete immediately so the step settles
223
- // within the same call (real Phaser tweens take STEP_MS).
224
- tweens: { add: (cfg) => { if (cfg.onComplete) cfg.onComplete(); } },
225
- };
226
- const player = new Player(scene, 10, 10);
227
- player.attachRoom({ send: (type, msg) => sent.push({ type, msg }) });
228
- return { player, sent };
229
- }
230
-
231
- const keysFor = (dir, t) => dirKeys({ [dir]: [key(true, t), NONE] });
232
-
233
- test('integration: quick tap of a new direction → faces it, sends turn, NO position change', () => {
234
- const { player, sent } = makePlayer();
235
- assert.equal(player.facing, 'down');
236
- player.update(keysFor('left', 1000), 1000); // press; dir != facing → turn
237
- assert.equal(player.facing, 'left');
238
- assert.equal(player.gridX, 10);
239
- assert.equal(player.gridY, 10); // zero position change — the headline acceptance
240
- assert.deepEqual(sent, [{ type: 'turn', msg: { dir: 'left' } }]);
241
- });
242
-
243
- test('integration: tap while already facing that way → no message, no move', () => {
244
- const { player, sent } = makePlayer(); // starts facing down
245
- player.update(keysFor('down', 2000), 2000 + 50); // held 50ms < threshold
246
- assert.equal(player.gridX, 10);
247
- assert.equal(player.gridY, 10);
248
- assert.deepEqual(sent, []);
249
- });
250
-
251
- test('integration: holding past the threshold → sends move and steps one tile', () => {
252
- const { player, sent } = makePlayer(); // facing down
253
- // Already facing 'down'; hold past threshold → one step down (+1 y).
254
- player.update(keysFor('down', 3000), 3000 + TURN_TAP_MS + 10);
255
- assert.equal(player.gridY, 11);
256
- assert.equal(player.gridX, 10);
257
- assert.deepEqual(sent, [{ type: 'move', msg: { dir: 'down' } }]);
258
- });
259
-
260
- test('integration: turn frame never steps even when the key is already held long', () => {
261
- const { player, sent } = makePlayer(); // facing down
262
- // First time we see 'left' it differs from facing → must turn only, never
263
- // step, regardless of how long the (newly-resolved) key reads as held.
264
- player.update(keysFor('left', 100), 100 + 5000);
265
- assert.equal(player.facing, 'left');
266
- assert.equal(player.gridX, 10);
267
- assert.equal(player.gridY, 10);
268
- assert.deepEqual(sent, [{ type: 'turn', msg: { dir: 'left' } }]);
269
- });
270
-
271
- test('integration: blocked tile → faces direction but does not step', () => {
272
- const { player, sent } = makePlayer();
273
- player.scene.isBlocked = () => true;
274
- player.facing = 'up'; // pretend already facing up
275
- player.update(keysFor('up', 4000), 4000 + TURN_TAP_MS + 10); // would step, but blocked
276
- assert.equal(player.gridY, 10); // unchanged
277
- assert.deepEqual(sent, []); // beginWalk (which sends 'move') never reached
278
- });
279
-
280
- console.log(`\nturn_in_place: ${passed} passed`);
@@ -1,188 +0,0 @@
1
- // tests/walkability.mjs
2
- //
3
- // Verify the ground-object split's walkability semantics:
4
- // - Ground SEA blocks (terrain-level).
5
- // - Ground GRASS / SAND walkable on their own.
6
- // - Object ROCK on grass blocks (object-level).
7
- // - Object CONTAINER_PANEL_DOOR on sand blocks (object-level).
8
- // - Both also blocks (object wins, same outcome).
9
- // - Out-of-bounds blocks.
10
- //
11
- // Exercises the SERVER-side isBlocked() — that's the source of truth
12
- // for movement validation. Loads it via require() since the server
13
- // module is CJS.
14
- //
15
- // Run: node tests/walkability.mjs
16
-
17
- import { strict as assert } from 'node:assert';
18
- import { createRequire } from 'node:module';
19
-
20
- const require = createRequire(import.meta.url);
21
- const serverMap = require('../modules/game/world/map.js');
22
- const serverTerrain = require('../modules/game/world/terrain.js');
23
-
24
- const { isBlocked, INITIAL_MAP, WORLD_COLS, WORLD_ROWS } = serverMap;
25
- const { TERRAIN, OBJECT, isBlockingTerrain, isBlockingObject } = serverTerrain;
26
-
27
- let passed = 0;
28
- function test(name, fn) {
29
- try {
30
- fn();
31
- console.log(` ok ${name}`);
32
- passed++;
33
- } catch (e) {
34
- console.error(` FAIL ${name}: ${e.message}`);
35
- process.exitCode = 1;
36
- }
37
- }
38
-
39
- // ---------- helpers ------------------------------------------------------------
40
-
41
- // Find a sample cell of each kind in the actual INITIAL_MAP.
42
- function findCell(predicate) {
43
- for (let y = 0; y < WORLD_ROWS; y++) {
44
- for (let x = 0; x < WORLD_COLS; x++) {
45
- if (predicate(INITIAL_MAP.ground[y][x], INITIAL_MAP.objects[y][x], x, y)) {
46
- return { x, y };
47
- }
48
- }
49
- }
50
- return null;
51
- }
52
-
53
- const seaCell = findCell((g, _o) => g === TERRAIN.SEA);
54
- const sandCell = findCell((g, o) => g === TERRAIN.SAND && !o);
55
- const grassCell = findCell((g, o) => g === TERRAIN.GRASS && !o);
56
- const rockCell = findCell((_g, o) => o === OBJECT.ROCK);
57
- const doorCell = findCell((_g, o) => o === OBJECT.CONTAINER_PANEL_DOOR);
58
- const panelCell = findCell((_g, o) => o === OBJECT.CONTAINER_PANEL_TL);
59
-
60
- // ---------- registry shape ------------------------------------------------------
61
-
62
- test('TERRAIN + OBJECT enums export expected ids', () => {
63
- assert.equal(TERRAIN.GRASS, 'grass');
64
- assert.equal(TERRAIN.SAND, 'sand');
65
- assert.equal(TERRAIN.SEA, 'sea');
66
- assert.equal(OBJECT.ROCK, 'rock');
67
- assert.equal(OBJECT.CONTAINER_PANEL_DOOR, 'container_panel_door');
68
- });
69
-
70
- test('isBlockingTerrain: SEA blocks; GRASS, SAND, MARBLE_PATH walkable', () => {
71
- assert.equal(isBlockingTerrain(TERRAIN.SEA), true);
72
- assert.equal(isBlockingTerrain(TERRAIN.GRASS), false);
73
- assert.equal(isBlockingTerrain(TERRAIN.SAND), false);
74
- assert.equal(isBlockingTerrain(TERRAIN.MARBLE_PATH), false);
75
- });
76
-
77
- test('isBlockingObject: ROCK + all 8 container panels block; null + PATH_MARBLE walkable', () => {
78
- assert.equal(isBlockingObject(null), false);
79
- assert.equal(isBlockingObject(undefined), false);
80
- assert.equal(isBlockingObject(OBJECT.ROCK), true);
81
- assert.equal(isBlockingObject(OBJECT.CONTAINER_PANEL_TL), true);
82
- assert.equal(isBlockingObject(OBJECT.CONTAINER_PANEL_T1), true);
83
- assert.equal(isBlockingObject(OBJECT.CONTAINER_PANEL_T2), true);
84
- assert.equal(isBlockingObject(OBJECT.CONTAINER_PANEL_TR), true);
85
- assert.equal(isBlockingObject(OBJECT.CONTAINER_PANEL_DOOR), true);
86
- assert.equal(isBlockingObject(OBJECT.CONTAINER_PANEL_B1), true);
87
- assert.equal(isBlockingObject(OBJECT.CONTAINER_PANEL_B2), true);
88
- assert.equal(isBlockingObject(OBJECT.CONTAINER_PANEL_BR), true);
89
- // ADR 0013: paths are non-blocking objects (you walk on the path).
90
- assert.equal(isBlockingObject(OBJECT.PATH_MARBLE), false);
91
- });
92
-
93
- // ---------- isBlocked over the actual INITIAL_MAP ------------------------------
94
-
95
- test('INITIAL_MAP contains the expected cell kinds', () => {
96
- assert.ok(seaCell, 'no sea cell in INITIAL_MAP');
97
- assert.ok(sandCell, 'no plain-sand cell in INITIAL_MAP');
98
- assert.ok(grassCell, 'no plain-grass cell in INITIAL_MAP');
99
- assert.ok(rockCell, 'no rock cell in INITIAL_MAP');
100
- assert.ok(doorCell, 'no container-door cell in INITIAL_MAP');
101
- assert.ok(panelCell, 'no container_panel_tl cell in INITIAL_MAP');
102
- });
103
-
104
- test('sea cells are blocked', () => {
105
- assert.equal(isBlocked(seaCell.x, seaCell.y), true);
106
- });
107
-
108
- test('plain sand cells are walkable', () => {
109
- assert.equal(isBlocked(sandCell.x, sandCell.y), false);
110
- });
111
-
112
- test('plain grass cells are walkable', () => {
113
- assert.equal(isBlocked(grassCell.x, grassCell.y), false);
114
- });
115
-
116
- test('rock-on-grass cells are blocked (object wins)', () => {
117
- // The ground at the rock cell is GRASS (walkable). The object is ROCK
118
- // (blocking). isBlocked must return true.
119
- assert.equal(INITIAL_MAP.ground[rockCell.y][rockCell.x], TERRAIN.GRASS);
120
- assert.equal(INITIAL_MAP.objects[rockCell.y][rockCell.x], OBJECT.ROCK);
121
- assert.equal(isBlocked(rockCell.x, rockCell.y), true);
122
- });
123
-
124
- test('container-door cells are blocked (object wins; ground is sand)', () => {
125
- assert.equal(INITIAL_MAP.ground[doorCell.y][doorCell.x], TERRAIN.SAND);
126
- assert.equal(INITIAL_MAP.objects[doorCell.y][doorCell.x], OBJECT.CONTAINER_PANEL_DOOR);
127
- assert.equal(isBlocked(doorCell.x, doorCell.y), true);
128
- });
129
-
130
- test('all 8 container-panel cells share GROUND=sand and have an OBJECT', () => {
131
- // Container is at anchor (13, 8), 2 rows x 4 cols.
132
- for (let r = 0; r < 2; r++) {
133
- for (let c = 0; c < 4; c++) {
134
- const x = 13 + c, y = 8 + r;
135
- assert.equal(INITIAL_MAP.ground[y][x], TERRAIN.SAND, `(${x},${y}) ground should be sand`);
136
- assert.ok(INITIAL_MAP.objects[y][x], `(${x},${y}) object should not be null`);
137
- assert.equal(isBlocked(x, y), true, `(${x},${y}) should be blocked`);
138
- }
139
- }
140
- });
141
-
142
- test('vendor cell (13, 9) ground=sand, object=container_panel_door, blocked', () => {
143
- // Door MUST be at bottom-LEFT to preserve vendor placement.
144
- assert.equal(INITIAL_MAP.ground[9][13], TERRAIN.SAND);
145
- assert.equal(INITIAL_MAP.objects[9][13], OBJECT.CONTAINER_PANEL_DOOR);
146
- assert.equal(isBlocked(13, 9), true);
147
- });
148
-
149
- test('cell south of vendor (13, 10) is walkable (path-as-object on sand)', () => {
150
- // ADR 0013: marble path is now an OBJECT overlay over SAND ground.
151
- // The player approaches the vendor from here (faces up + presses E),
152
- // so the cell must remain walkable: sand is non-blocking ground +
153
- // path_marble is a non-blocking object.
154
- assert.equal(INITIAL_MAP.ground[10][13], TERRAIN.SAND);
155
- assert.equal(INITIAL_MAP.objects[10][13], OBJECT.PATH_MARBLE);
156
- assert.equal(isBlockingObject(OBJECT.PATH_MARBLE), false,
157
- 'PATH_MARBLE must be non-blocking (you walk on a path)');
158
- assert.equal(isBlocked(13, 10), false);
159
- });
160
-
161
- test('marble path strip (rows 10-13, col 13) all walkable, all path-on-sand', () => {
162
- // ADR 0013: the entire path strip is OBJECT.PATH_MARBLE over SAND.
163
- // No cell should block; ground should be uniform sand.
164
- for (let y = 10; y <= 13; y++) {
165
- assert.equal(INITIAL_MAP.ground[y][13], TERRAIN.SAND,
166
- `(13,${y}) ground should be SAND under the path overlay`);
167
- assert.equal(INITIAL_MAP.objects[y][13], OBJECT.PATH_MARBLE,
168
- `(13,${y}) object should be PATH_MARBLE`);
169
- assert.equal(isBlocked(13, y), false,
170
- `(13,${y}) should be walkable (sand + non-blocking path)`);
171
- }
172
- });
173
-
174
- test('out-of-bounds is blocked', () => {
175
- assert.equal(isBlocked(-1, 0), true);
176
- assert.equal(isBlocked(0, -1), true);
177
- assert.equal(isBlocked(WORLD_COLS, 0), true);
178
- assert.equal(isBlocked(0, WORLD_ROWS), true);
179
- });
180
-
181
- // ---------- summary -------------------------------------------------------------
182
-
183
- if (process.exitCode !== 1) {
184
- console.log(`\n[walkability] OK — ${passed} tests passed`);
185
- } else {
186
- console.error(`\n[walkability] FAIL — see output above`);
187
- process.exit(1);
188
- }