The marker stops lying, and the rule announces itself
"the marker flips all the time even during a ball being midair.. i am not sure that a player would get the mechanic intuitively let alone without the marker". The flip had a specific cause and it was a design bug, not jitter. With lanes held and speeds constant, arrival differences never change while bodies walk — the queue only reorders on a kill or an arrival. The marker flipped because it showed only q[0] while the rule accepts any near-tie within the slack: every kill made it jump between tied bodies, so the marker was stricter than the rule it was explaining. drillLegal() marks the whole slack set now, bright head and dim ties, which is stable by construction. Measured: zero head-changes across 12s of walking with no kills. Learnability, two answers. The rule gets one modal, once per run, on the mode's own calm court — CLEARANCE DRILL / DROP WHOEVER WOULD REACH YOU FIRST / FAST BEATS CLOSE / STREAKS PAY — because "most urgent first" is not guessable from a chevron alone. And the markers are training wheels that take themselves off: hidden at streak >= 8 (the streak is the evidence you can read the court), back on any fault. The teacher steps in exactly when you demonstrably need it. 31 tests, 15 seeds clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6a16e77480
commit
be191dba82
6 files changed
+94
-18
No files matched your search
+22
@@ -794,6 +794,28 @@ check the game's read against his own — and it answers the exact question the
|
||||
be off in anything shipped. If his read and the marker disagree often, tune `drillSlack` before
|
||||
touching `arrival`.
|
||||
|
||||
**TWELFTH PASS — the marker stops lying, and the rule announces itself.** Oliver: *"the marker
|
||||
flips all the time even during a ball being midair.. i am not sure that a player would get the
|
||||
mechanic intuitively let alone without the marker"*.
|
||||
|
||||
**The flip had a specific cause and it was a design bug, not jitter**: with lanes held and speeds
|
||||
constant, arrival DIFFERENCES never change while bodies walk — the queue only reorders on a kill or
|
||||
an arrival. So the marker was flipping because it showed ONLY `q[0]` while the rule accepts any
|
||||
near-tie within the slack: every kill made it jump between tied bodies, i.e. **the marker was
|
||||
stricter than the rule it was explaining**. `drillLegal()` now marks the whole slack set — bright
|
||||
head, dim ties — which is stable by construction. Measured: zero head-changes across 12 s of
|
||||
walking with no kills.
|
||||
|
||||
**Learnability, two answers:**
|
||||
- the rule gets ONE modal, once per run, on the mode's own calm court (the seeded bodies are all at
|
||||
the far baseline — this is the popup policy's sanctioned case): *CLEARANCE DRILL — DROP WHOEVER
|
||||
WOULD REACH YOU FIRST · FAST BEATS CLOSE · STREAKS PAY*. "Most urgent first" is not guessable
|
||||
from a chevron alone; one sentence makes it learnable in the first five seconds.
|
||||
- **the markers are training wheels that take themselves off**: they hide at streak >=
|
||||
`drillMarkOff` (8) — the streak IS the evidence you can read the court — and any fault brings
|
||||
them back. The teacher steps in exactly when you demonstrably need it and out when you don't,
|
||||
and `drillMark: 0` still kills them outright.
|
||||
|
||||
**Open, and every one of them is Oliver's call after he plays it:**
|
||||
1. ~~**A ball that carries through the lane can complete two steps at once.**~~ Largely CLOSED by
|
||||
the slice removal (seventh pass) — a cut ball picking its own next victim was the main way this
|
||||
|
||||
+1
-1
@@ -399,6 +399,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="src/main.js?v=163"></script>
|
||||
<script type="module" src="src/main.js?v=164"></script>
|
||||
</body>
|
||||
</html>
|
||||
+6
-3
@@ -369,9 +369,12 @@ export const TUNE = {
|
||||
drillSlack: 0.9,
|
||||
drillMultEvery: 4, // correct kills per multiplier step...
|
||||
drillMultCap: 5, // ...capped: a deep streak should feel valuable, not become the whole score
|
||||
drillMark: 1, // >= 0.5 draws a chevron over the queue head — the CALIBRATION AID, so a
|
||||
// playtest can check the game's read against the player's. Off for the real
|
||||
// thing: the marker answers the exact question the mode exists to ask
|
||||
drillMark: 1, // >= 0.5 draws chevrons over every LEGAL kill (the slack set, bright head,
|
||||
// dim ties). Training wheels, not a cheat: see drillMarkOff
|
||||
drillMarkOff: 8, // streak at which the markers hide — you have shown you can read the court, so
|
||||
// the teacher steps back. Any fault (streak to 0) brings them back. This is the
|
||||
// learnability answer: the marker teaches, the streak decides when you have
|
||||
// learned, unlearning re-teaches
|
||||
drillOneShot: 0, // >= 0.5: every drill body drops to 1 hp. OFF after one playtest: "way too easy"
|
||||
// AND a 1 hp body skips every ZWOUND stage, so the gore art never renders.
|
||||
// Also flattens triage — pre-softening a heavy IS the planning layer
|
||||
|
||||
+23
-2
@@ -22,10 +22,11 @@
|
||||
// THE MODE TRANSITIONS ARE NOT HERE — startDrill/endDrill live in beats.js with every other one
|
||||
// (CLAUDE.md rule 3). This file is the stream, the urgency rule and the scoring.
|
||||
|
||||
import { TUNE, COURT } from './config.js';
|
||||
import { TUNE, COURT, UI } from './config.js';
|
||||
import { G, ball } from './state.js';
|
||||
import { zombies, spawnZombie } from './zombies.js';
|
||||
import { voice } from './audio.js';
|
||||
import { showModal } from './gameplay.js'; // runtime-only cycle (gameplay imports drill), same pattern as beats
|
||||
|
||||
// The roster in the stream, weighted: shamblers are the crowd, heavies are events. vMul is the
|
||||
// drill's own speed character (deliberately narrower than the roster's 3.2x..0.45x so the queue
|
||||
@@ -52,8 +53,20 @@ const spawnGap = d => lerp(TUNE.drillSpawnEvery, TUNE.drillSpawnMin, ramp(d));
|
||||
// ---- THE URGENCY RULE, and it is the whole mode. Seconds until this body crosses your baseline at
|
||||
// its own pace down its own lane (drill bodies hold their lane, so this is honest — nothing swerves).
|
||||
export const arrival = zb => (COURT.L - zb.z) / Math.max(0.01, zb.v);
|
||||
// The queue, most urgent first. Exported: render marks the head, tests assert against it.
|
||||
// The queue, most urgent first. Exported: render marks the legal set, tests assert against it.
|
||||
export const drillQueue = () => zombies.filter(z => z.drill).sort((a, b) => arrival(a) - arrival(b));
|
||||
// EVERY legal kill right now — the head plus anything within the slack of it. THE MARKER MUST DRAW
|
||||
// THIS SET, not the bare head: the rule accepts any near-tie, so a marker showing only q[0] was
|
||||
// STRICTER than the rule it explained, and every kill made it jump between tied bodies in a way
|
||||
// that read as the rule changing mid-flight. (With lanes held and speeds constant, arrival
|
||||
// differences never change while bodies walk — the queue only reorders on a kill or an arrival, so
|
||||
// marking the whole legal set is stable by construction.)
|
||||
export const drillLegal = () => {
|
||||
const q = drillQueue();
|
||||
if (!q.length) return q;
|
||||
const t0 = arrival(q[0]);
|
||||
return q.filter(z => arrival(z) <= t0 + TUNE.drillSlack);
|
||||
};
|
||||
|
||||
function pickType() {
|
||||
const pool = DRILL_TYPES.flatMap(t => Array(t.w).fill(t));
|
||||
@@ -87,6 +100,14 @@ function spawnDrillBody(d) {
|
||||
export function startDrillStream() {
|
||||
const d = G.drill; if (!d) return;
|
||||
zombies.length = 0;
|
||||
// The rule gets ONE sentence, once per run, on the sanctioned calm court (the seeded bodies are
|
||||
// all at the far baseline — nothing is near). Oliver's worry is right: "most urgent first" is NOT
|
||||
// guessable from a chevron alone, and this is the modal policy's exact use case.
|
||||
if (!G.seen.drillRule) {
|
||||
G.seen.drillRule = true;
|
||||
showModal({ title: 'CLEARANCE DRILL', color: UI.pick,
|
||||
body: ['DROP WHOEVER WOULD REACH YOU FIRST', 'FAST BEATS CLOSE · STREAKS PAY'] });
|
||||
}
|
||||
d.t = 0; d.spawnT = 0; d.score = 0; d.streak = 0; d.kills = 0; d.faults = 0; d.lost = 0; d.msg = null;
|
||||
try { d.best = Number(localStorage.getItem('drillBest')) || 0; } catch { d.best = 0; }
|
||||
// seed a small spread so there is a queue to read from the first second
|
||||
|
||||
+17
-10
@@ -4,7 +4,7 @@ import { cv, ctx } from './dom.js';
|
||||
import { TUNE, UI, EATEN_BURST, EATEN_SUMMARY, EATEN_RESTART, COURT, P_BOUNDS, PLAYER_H, BOY_H, UMP_H, UMP_X, MACH_H, BOSS_ART, ZWOUND, woundStage, BOUNCE_IDLE, SPRITE_H, zombieH , WEAPONS, RACK_ROW, TOSS } from './config.js';
|
||||
import { P, O, ball, score, crowd, G, court, tour } from './state.js';
|
||||
import { predictLanding, previewServe, slotId, slotStock } from './gameplay.js';
|
||||
import { drillQueue, drillMult } from './drill.js';
|
||||
import { drillQueue, drillLegal, drillMult } from './drill.js';
|
||||
import { zombies, gore, piles, parts, pellets, extras, shots, pops } from './zombies.js';
|
||||
|
||||
// ---------------------------------------------------------------- palette ----
|
||||
@@ -696,21 +696,28 @@ function drawDrillHud() {
|
||||
ctx.font = F(8);
|
||||
shadowText('BEST ' + Math.max(d.best, d.score), W / 2, y + 44, UI.dim);
|
||||
if (d.msg && d.msg.t > 0) { ctx.font = B(20); shadowText(d.msg.text, W / 2, y + 60, d.msg.bad ? UI.bad : UI.pick); }
|
||||
// THE CALIBRATION MARKER (TUNE.drillMark): a chevron over the body the rule says is next. For the
|
||||
// playtest only — it exists so Oliver can check the game's read against his own, and it answers
|
||||
// the exact question the mode is asking, so it must be OFF in anything shipped.
|
||||
if (TUNE.drillMark >= 0.5) {
|
||||
const head = drillQueue()[0];
|
||||
if (head) {
|
||||
const p = proj(head.x, zombieH(head) + 0.5, head.z);
|
||||
if (p.s > 0) {
|
||||
// THE MARKERS ARE TRAINING WHEELS THAT TAKE THEMSELVES OFF. Every LEGAL kill is marked — the
|
||||
// whole slack set, not the bare head: the rule accepts any near-tie, so a single chevron was
|
||||
// stricter than the rule it explained and every kill made it jump between tied bodies. Marking
|
||||
// the set is also stable by construction (arrival differences never change while bodies walk;
|
||||
// the set only moves on a kill or an arrival).
|
||||
// They HIDE once the streak shows you have the read (drillMarkOff) and come back on any fault —
|
||||
// which answers "would a player get this without the marker": the marker teaches, the streak
|
||||
// decides when you have learned it, and unlearning (a fault) brings the teacher back.
|
||||
if (TUNE.drillMark >= 0.5 && d.streak < TUNE.drillMarkOff) {
|
||||
const legal = drillLegal();
|
||||
for (let i = 0; i < legal.length; i++) {
|
||||
const zb = legal[i];
|
||||
const p = proj(zb.x, zombieH(zb) + 0.5, zb.z);
|
||||
if (p.s <= 0) continue;
|
||||
const bob = Math.sin(G.idleT * 6) * 2;
|
||||
const px = Math.round(p.x), py = Math.round(p.y + bob);
|
||||
ctx.globalAlpha = i === 0 ? 1 : 0.45; // the head is bright; a tied body is a quiet "also fine"
|
||||
ctx.fillStyle = UI.warm;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px - 5, py - 7); ctx.lineTo(px + 5, py - 7); ctx.lineTo(px, py);
|
||||
ctx.closePath(); ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,10 +83,13 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
|
||||
// ---- game modules (real code) ----------------------------------------------
|
||||
const { G, P, score, ball, setupPoint } = await import('./src/state.js');
|
||||
const { update, forceZombies, splatPoint, restartRun, flickTarget } = await import('./src/gameplay.js');
|
||||
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill, endDrill } = await import('./src/beats.js');
|
||||
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill } = await import('./src/beats.js');
|
||||
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
|
||||
// player would, so the tests step a live court
|
||||
const startDrill = (...a) => { const r = _startDrill(...a); G.modal = null; G.modalQ.length = 0; return r; };
|
||||
const { zombies, spawnZombie, pellets } = await import('./src/zombies.js');
|
||||
const { killPlayer } = await import('./src/zombies.js');
|
||||
const { drillQueue, arrival, drillMult, DRILL_TYPES } = await import('./src/drill.js');
|
||||
const { drillQueue, drillLegal, arrival, drillMult, DRILL_TYPES } = await import('./src/drill.js');
|
||||
const { TUNE, bossArt, COURT } = await import('./src/config.js');
|
||||
|
||||
const DT = 1 / 60;
|
||||
@@ -467,6 +470,26 @@ test('drillOneShot is a switch, and OFF means the roster HP is intact', () => {
|
||||
else assert.ok(heavy.hp > 1, 'one-shot off: a ' + heavy.variant + ' keeps its ' + heavy.hp + ' hp');
|
||||
});
|
||||
|
||||
test('the rule announces itself once, and the markers are the legal SET', () => {
|
||||
_startDrill(); // the raw one: we WANT the modal this time
|
||||
assert.ok(G.modal, 'first drill of a run: the rule gets its one sentence');
|
||||
assert.equal(G.modal.title, 'CLEARANCE DRILL');
|
||||
G.modal = null;
|
||||
endDrill(); _startDrill();
|
||||
assert.equal(G.modal, null, 'second drill of the same run: no re-teaching');
|
||||
G.modal = null;
|
||||
// the legal set: everything within the slack of the head, and never anything beyond it
|
||||
zombies.length = 0; G.drill.spawnT = 999;
|
||||
for (const [dz, v] of [[6, 1.0], [6.5, 1.0], [16, 1.0]]) { // 6 s, 6.5 s, 16 s out
|
||||
spawnZombie(0, COURT.L - dz, 'a');
|
||||
const zb = zombies[zombies.length - 1];
|
||||
zb.drill = true; zb.pts = 20; zb.v = v; zb.hp = zb.hpMax = 1;
|
||||
}
|
||||
const legal = drillLegal();
|
||||
assert.equal(legal.length, 2, 'the 0.5 s tie is IN the marker set (slack ' + TUNE.drillSlack + ')');
|
||||
assert.ok(!legal.some(z => Math.abs(arrival(z) - 16) < 0.5), 'the 16 s body is not');
|
||||
});
|
||||
|
||||
test('endDrill hands the court back', () => {
|
||||
drillPlaying();
|
||||
endDrill();
|
||||
|
||||
Reference in new issue
Block a user