@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.
- package/.bongos-core.json +521 -281
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +2 -0
- package/.claude/skills/builder-exit/SKILL.md +2 -2
- package/.claude/skills/builder-redteam/SKILL.md +3 -3
- package/.claude/skills/builder-setup/SKILL.md +2 -2
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/dev/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +2 -2
- package/.claude/skills/ideate/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/new-project/SKILL.md +3 -0
- package/.claude/skills/paint/SKILL.md +1 -1
- package/GOVERNANCE.md +1 -1
- package/README.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -2
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0001-stack-choice-single-vps.md +1 -1
- package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
- package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
- package/docs/adr/0030-builders-hall-live-updates.md +1 -1
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
- package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
- package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
- package/docs/adr/0057-container-cost-ledger.md +3 -3
- package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
- package/docs/adr/0096-require-reward-before-workable.md +9 -9
- package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
- package/docs/adr/0100-two-repo-instance-model.md +1 -1
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
- package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
- package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
- package/docs/adr/README.md +9 -6
- package/docs/api/openapi.json +65 -26
- package/docs/api-reference.md +3 -2
- package/docs/architecture.md +2 -2
- package/docs/branding-contract.md +5 -4
- package/docs/canonical-permissions.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +18 -17
- package/docs/handoff-template.md +2 -2
- package/docs/modules-contract.md +6 -1
- package/docs/onboarding/diagrams/README.md +7 -7
- package/docs/onboarding/drachmae-and-karma.md +18 -18
- package/docs/onboarding/primer.md +9 -9
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/builder-box-lifecycle.md +2 -2
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/ops-gotchas.md +1 -1
- package/docs/recipes/private-npm-distribution.md +17 -0
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/upgrading-the-core.md +55 -0
- package/docs/research/cursor-competitive-analysis.md +1 -1
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
- package/modules/discord/module.json +2 -1
- package/modules/economy/achievements.js +9 -1
- package/modules/economy/credits.js +10 -1
- package/modules/hall-ui/public/builders.js +137 -620
- package/modules/hall-ui/public/goal-inbox.js +232 -0
- package/modules/hall-ui/public/goals-page.js +4 -1
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/hall-kit.js +474 -0
- package/modules/hall-ui/public/hall-widgets.js +14 -5
- package/modules/hall-ui/public/index.html +3 -0
- package/modules/hall-ui/public/instances-panel.js +24 -3
- package/modules/hall-ui/public/new-project-wizard.js +7 -5
- package/modules/hall-ui/public/style.css +168 -0
- package/modules/hall-ui/public/work.css +194 -0
- package/modules/hall-ui/public/work.html +46 -374
- package/modules/hall-ui/public/work.js +137 -102
- package/modules/lifecycle/criterion-suggest.js +80 -0
- package/modules/lifecycle/db.js +42 -5
- package/modules/lifecycle/routes/goals.js +26 -0
- package/modules/lifecycle/routes/tasks.js +21 -0
- package/modules/provisioning/CLAUDE.md +15 -0
- package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
- package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
- package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
- package/modules/provisioning/module.json +17 -0
- package/modules/provisioning/onboard-plan.js +37 -0
- package/modules/provisioning/provisioning.js +539 -0
- package/modules/provisioning/rate-limit.js +35 -0
- package/modules/provisioning/routes/provisioning.js +607 -0
- package/modules/provisioning/tests/provisioning.mjs +232 -0
- package/modules/provisioning/ui/.gitkeep +0 -0
- package/modules/security/secret-scrub.js +10 -0
- package/package-lock.json +2 -2
- package/package.json +7 -2
- package/scripts/discord/README.md +1 -1
- package/scripts/gds/claim.js +98 -48
- package/scripts/gds/claude-materialize.js +30 -9
- package/scripts/gds/cutover-codemod.js +241 -0
- package/scripts/gds/fitness.js +54 -0
- package/scripts/gds/init.js +59 -3
- package/scripts/gds/lint-infra-exec.js +70 -0
- package/scripts/gds/lint-workflow-exec.js +63 -0
- package/scripts/gds/mirror-lineage-check.js +14 -5
- package/scripts/gds/mirror-redact.js +8 -0
- package/scripts/gds/onboard.js +4 -0
- package/scripts/gds/package-core.js +18 -3
- package/scripts/gds/publish-manifest.js +60 -2
- package/scripts/gds/run-routine.js +51 -13
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/update-channel.js +186 -0
- package/scripts/gds/upgrade.js +116 -9
- package/scripts/hall-preview/README.md +103 -0
- package/scripts/hall-preview/fixtures/access-requests.json +1 -0
- package/scripts/hall-preview/fixtures/achievements.json +76 -0
- package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
- package/scripts/hall-preview/fixtures/audit-log.json +1 -0
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
- package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
- package/scripts/hall-preview/fixtures/builders__roster.json +1 -0
- package/scripts/hall-preview/fixtures/goals.json +502 -0
- package/scripts/hall-preview/fixtures/goals__25.json +96 -0
- package/scripts/hall-preview/fixtures/goals__26.json +67 -0
- package/scripts/hall-preview/fixtures/goals__3.json +103 -0
- package/scripts/hall-preview/fixtures/goals__35.json +49 -0
- package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
- package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
- package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
- package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
- package/scripts/hall-preview/fixtures/grades__by-builder.json +1 -0
- package/scripts/hall-preview/fixtures/inbox.json +330 -0
- package/scripts/hall-preview/fixtures/instance.json +124 -0
- package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
- package/scripts/hall-preview/fixtures/me.json +476 -0
- package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
- package/scripts/hall-preview/fixtures/override-requests.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
- package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
- package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
- package/scripts/hall-preview/fixtures/public__grades.json +50 -0
- package/scripts/hall-preview/fixtures/public__progress.json +124 -0
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
- package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
- package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
- package/scripts/hall-preview/fixtures/security__docs.json +1 -0
- package/scripts/hall-preview/fixtures/security__reports.json +1 -0
- package/scripts/hall-preview/fixtures/sessions__search.json +1 -0
- package/scripts/hall-preview/fixtures/tasks.json +2420 -0
- package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
- package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
- package/scripts/hall-preview/fixtures/versions.json +89 -0
- package/scripts/hall-preview/refresh-fixtures.js +77 -0
- package/scripts/hall-preview/server.js +178 -0
- package/scripts/migrate.sh +29 -4
- package/src/bongos/auth.js +14 -1
- package/src/bongos/optimizer.js +37 -2
- package/src/bongos/serve-internal.js +10 -1
- package/src/branding.js +1 -0
- package/src/module-api.js +12 -1
- package/src/module-loader/loader.js +36 -4
- package/src/module-loader/manifest-schema.js +16 -1
- package/tests/branding.mjs +11 -0
- package/tests/claim_error_surface.mjs +107 -0
- package/tests/claude_materialize.mjs +20 -0
- package/tests/consumer_layout_boot.mjs +302 -0
- package/tests/criterion_suggest.mjs +106 -0
- package/tests/cutover_codemod.mjs +184 -0
- package/tests/device_flow_start.mjs +25 -2
- package/tests/hook_core_resolve.mjs +125 -0
- package/tests/infra_exec_paths.mjs +104 -0
- package/tests/init.mjs +47 -0
- package/tests/migrate_root_resolution.mjs +97 -0
- package/tests/mirror_lineage_proof.mjs +14 -0
- package/tests/module_loader.mjs +42 -0
- package/tests/module_manifest.mjs +19 -0
- package/tests/onboard.mjs +13 -0
- package/tests/package_core.mjs +8 -0
- package/tests/provisioning_github_app_routes.mjs +15 -0
- package/tests/publish_manifest.mjs +52 -1
- package/tests/run_routine.mjs +26 -1
- package/tests/run_routine_two_root.mjs +77 -0
- package/tests/scrubber_corpus.mjs +17 -0
- package/tests/update_channel.mjs +186 -0
- package/tests/upgrade.mjs +131 -0
- package/tests/workflow_exec_paths.mjs +98 -0
- package/docs/design/container-exterior-tiles.md +0 -49
- package/docs/design/container-exterior.md +0 -81
- package/docs/design/container-interior.md +0 -77
- package/docs/design/world-map-scroll.md +0 -41
- package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
- package/docs/recipes/cloudbongos-staging-gate.md +0 -139
- package/docs/recipes/cloudbongos-standup.md +0 -111
- package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
- package/docs/recipes/parallel-load-test.md +0 -32
- package/docs/recipes/pixel-art-pipeline.md +0 -411
- package/docs/recipes/staging-data.md +0 -50
- package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
- package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
- package/migrations/instance/110_medusa_planning_seed.sql +0 -70
- package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
- package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
- package/scripts/gds/art-stage.js +0 -152
- package/server.js +0 -212
- package/tests/art_key.mjs +0 -121
- package/tests/day_cycle.mjs +0 -197
- package/tests/snapshot_picker.mjs +0 -233
- package/tests/sprite_facing.mjs +0 -35
- package/tests/terrain_picker.mjs +0 -298
- package/tests/turn_in_place.mjs +0 -280
- package/tests/walkability.mjs +0 -188
package/tests/turn_in_place.mjs
DELETED
|
@@ -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`);
|
package/tests/walkability.mjs
DELETED
|
@@ -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
|
-
}
|