Two testing switches: one-shot bodies, and the eaters off
"please make the zombie killable with a single hit for testing" and "remove the fake zombies for now". drillOneShot drops every body on a drill court to 1 hp, applied at spawn to the batch and the eaters alike. Worth knowing what it removes: the roster's HP is what makes the called ORDER matter — a roller is normally seven serves, so being told to take it third means starting on it first. At one hit that planning layer is gone and the sequence is pure recall. Flagged in PROJECT.md so it gets decided on feel rather than left on by drift. drillEaterMax 3 -> 0 switches the plain shamblers off. The mechanism is untouched — spawnEater, the wide entry away from the gang, the never-a-fault rule — so one number brings them back. The eater test now asserts whichever contract the knob describes (at 0: nothing but the puzzle ever appears; above 0: they arrive, capped, always plain shamblers) instead of being deleted and rotting. And the never-a-fault rule gets its own test with a hand-placed body, so the rule the eaters depend on stays covered while they are switched off. 31 tests, 15 seeds clean. Verified live: brute/steward/roller all at 1 hp where the roster says 5/5/7, zero strays over 40s, one aimed serve puts down the roller. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d34683bc68
commit
0f524a08c9
5 files changed
+70
-20
No files matched your search
+13
@@ -739,6 +739,19 @@ Watch: **a roller in the middle of a sequence is seven aimed serves.** Oliver as
|
|||||||
robustness to be kept, so it is — but if a round ever feels like grinding rather than reading, a
|
robustness to be kept, so it is — but if a round ever feels like grinding rather than reading, a
|
||||||
drill-only HP scale is the knob, not a change to the roster.
|
drill-only HP scale is the knob, not a change to the roster.
|
||||||
|
|
||||||
|
**TENTH PASS — two testing switches, and neither should be left on by drift.**
|
||||||
|
- **`TUNE.drillOneShot` (ON)**: every body on a drill court drops to 1 hp. Asked for to make testing
|
||||||
|
quick, but be aware what it removes — **the roster's HP is what makes the called ORDER matter**. A
|
||||||
|
roller is normally seven serves, so being told to take it third means starting on it first; at one
|
||||||
|
hit that whole layer of planning is gone and the sequence is pure recall. Decide it on feel.
|
||||||
|
- **`TUNE.drillEaterMax` 3 -> 0 (OFF)**: *"remove the fake zombies for now"*. The drill is the pure
|
||||||
|
puzzle with no second threat at the moment. The mechanism is untouched — `spawnEater`, the wide
|
||||||
|
entry away from the gang, the never-a-fault rule — so one number brings it back.
|
||||||
|
The test suite asserts **whichever contract the knob currently describes** (at 0: nothing but the
|
||||||
|
puzzle ever appears; above 0: they arrive, capped, always plain shamblers) rather than being
|
||||||
|
deleted and rotting, and the never-a-fault rule is now tested with a hand-placed body so it holds
|
||||||
|
either way. That is the shape to copy for any other switchable subsystem here.
|
||||||
|
|
||||||
**Open, and every one of them is Oliver's call after he plays it:**
|
**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
|
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
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="src/main.js?v=160"></script>
|
<script type="module" src="src/main.js?v=161"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+11
-1
@@ -367,9 +367,19 @@ export const TUNE = {
|
|||||||
// no business standing in front of the puzzle.
|
// no business standing in front of the puzzle.
|
||||||
drillEaterEvery: 12.0, // s between plain-shambler eaters at the bottom of the ramp...
|
drillEaterEvery: 12.0, // s between plain-shambler eaters at the bottom of the ramp...
|
||||||
drillEaterMin: 5.0, // ...and at the top. Still THE difficulty dial, just a slower one
|
drillEaterMin: 5.0, // ...and at the top. Still THE difficulty dial, just a slower one
|
||||||
drillEaterMax: 3, // eaters allowed on court at once — past this the puzzle is unreadable
|
drillEaterMax: 0, // eaters allowed on court at once. **0 = OFF, which is where it is now** —
|
||||||
|
// Oliver: "remove the fake zombies for now", so the drill is currently the
|
||||||
|
// pure puzzle with no second threat. The whole mechanism is intact
|
||||||
|
// (spawnEater, the wide entry, the never-a-fault rule); this one number
|
||||||
|
// brings it back. Past 3 the puzzle stops being readable.
|
||||||
drillEaterSpeed: 1.15,// they walk a touch faster than the batch: the batch is a list, this is a threat
|
drillEaterSpeed: 1.15,// they walk a touch faster than the batch: the batch is a list, this is a threat
|
||||||
drillRampT: 210, // s to go from the bottom of every curve above to the top
|
drillRampT: 210, // s to go from the bottom of every curve above to the top
|
||||||
|
// TESTING KNOB, currently ON. Every body in the drill dies to one splat-quality hit, whatever the
|
||||||
|
// roster says it should soak. Flip to 0 for the real thing — and note that this is not purely a
|
||||||
|
// convenience: the roster's HP is what makes the called ORDER matter (a roller is seven serves, so
|
||||||
|
// being told to take it third means starting on it first), and one-shot bodies delete that whole
|
||||||
|
// layer. Decide it on feel rather than leaving it on by drift.
|
||||||
|
drillOneShot: 1, // >= 0.5: batch bodies AND eaters all drop to 1 hp
|
||||||
drillCaughtBack: 45, // s of ramp handed back when one reaches you. A real setback, not a reset
|
drillCaughtBack: 45, // s of ramp handed back when one reaches you. A real setback, not a reset
|
||||||
fps: 60, // frame cap — 30 halves battery/heat but Oliver felt it; smoothness wins (0 = uncapped)
|
fps: 60, // frame cap — 30 halves battery/heat but Oliver felt it; smoothness wins (0 = uncapped)
|
||||||
lowPower: 1, // >= 0.5 caps RES at 3 (−44% of pixels painted per frame) — the battery saver,
|
lowPower: 1, // >= 0.5 caps RES at 3 (−44% of pixels painted per frame) — the battery saver,
|
||||||
|
|||||||
+6
-2
@@ -66,6 +66,10 @@ export const drillSpeed = d => lerp(TUNE.drillSpeed, TUNE.drillSpeedMax, ramp(d)
|
|||||||
const batchSize = d => Math.round(lerp(TUNE.drillBatch, TUNE.drillBatchMax, ramp(d)));
|
const batchSize = d => Math.round(lerp(TUNE.drillBatch, TUNE.drillBatchMax, ramp(d)));
|
||||||
const eaterGap = d => lerp(TUNE.drillEaterEvery, TUNE.drillEaterMin, ramp(d));
|
const eaterGap = d => lerp(TUNE.drillEaterEvery, TUNE.drillEaterMin, ramp(d));
|
||||||
|
|
||||||
|
// TUNE.drillOneShot: strip a body down to a single hit. Applied at spawn to the batch AND the
|
||||||
|
// eaters, so nothing on a drill court ever needs a second serve.
|
||||||
|
const oneShot = zb => { if (TUNE.drillOneShot >= 0.5) zb.hp = zb.hpMax = 1; return zb; };
|
||||||
|
|
||||||
const shuffle = a => { for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; };
|
const shuffle = a => { for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; };
|
||||||
const gang = () => zombies.filter(z => z.drill); // the batch: a member's identity IS its variant
|
const gang = () => zombies.filter(z => z.drill); // the batch: a member's identity IS its variant
|
||||||
|
|
||||||
@@ -93,7 +97,7 @@ function spawnBatch(d) {
|
|||||||
// rank walking in abreast — but every one still starts on the far side, which is the promise.
|
// rank walking in abreast — but every one still starts on the far side, which is the promise.
|
||||||
spawnZombie(lanes[i] + (Math.random() - 0.5) * 0.4,
|
spawnZombie(lanes[i] + (Math.random() - 0.5) * 0.4,
|
||||||
TUNE.drillSpawnZ + Math.random() * TUNE.drillBatchDepth, t.v);
|
TUNE.drillSpawnZ + Math.random() * TUNE.drillBatchDepth, t.v);
|
||||||
const zb = zombies[zombies.length - 1];
|
const zb = oneShot(zombies[zombies.length - 1]);
|
||||||
zb.drill = true; // marks it as the puzzle AND holds its lane (see updateZombies)
|
zb.drill = true; // marks it as the puzzle AND holds its lane (see updateZombies)
|
||||||
zb.v = drillSpeed(d) * t.vMul * (0.92 + Math.random() * 0.16);
|
zb.v = drillSpeed(d) * t.vMul * (0.92 + Math.random() * 0.16);
|
||||||
});
|
});
|
||||||
@@ -125,7 +129,7 @@ function spawnEater(d) {
|
|||||||
// ALWAYS A PLAIN SHAMBLER — rule 1 above doing its work: the alphabet never contains 'a'/'b', so
|
// ALWAYS A PLAIN SHAMBLER — rule 1 above doing its work: the alphabet never contains 'a'/'b', so
|
||||||
// an eater can never be mistaken for a body the sequence is asking for, with no marking on either.
|
// an eater can never be mistaken for a body the sequence is asking for, with no marking on either.
|
||||||
spawnZombie(bx, TUNE.drillSpawnZ + Math.random() * 0.8, Math.random() < 0.3 ? 'b' : 'a');
|
spawnZombie(bx, TUNE.drillSpawnZ + Math.random() * 0.8, Math.random() < 0.3 ? 'b' : 'a');
|
||||||
const zb = zombies[zombies.length - 1];
|
const zb = oneShot(zombies[zombies.length - 1]);
|
||||||
zb.v = drillSpeed(d) * TUNE.drillEaterSpeed * (0.9 + Math.random() * 0.2);
|
zb.v = drillSpeed(d) * TUNE.drillEaterSpeed * (0.9 + Math.random() * 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -358,13 +358,14 @@ test('the puzzle is WINNABLE: every called TYPE is on the far side, and nothing
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the eaters are the danger: plain shamblers, homing, and killing one is never a fault', () => {
|
test('the eaters obey drillEaterMax — including 0, which is OFF', () => {
|
||||||
|
// They are switched off right now ("remove the fake zombies for now") but the mechanism is intact,
|
||||||
|
// so this asserts whichever contract the knob currently describes rather than being deleted and
|
||||||
|
// rotting. The never-a-fault rule below is checked EITHER WAY, because that is the part that has
|
||||||
|
// to still be true on the day they are switched back on.
|
||||||
const eaten = TUNE.eatenR; TUNE.eatenR = 0; // the soak trick: don't get caught mid-measurement
|
const eaten = TUNE.eatenR; TUNE.eatenR = 0; // the soak trick: don't get caught mid-measurement
|
||||||
try {
|
try {
|
||||||
startDrill();
|
startDrill();
|
||||||
// COUNT ARRIVALS, NOT SURVIVORS. An eater crosses the whole court in about 24 s and then
|
|
||||||
// despawns, so a snapshot taken at 40 s can legitimately show an empty court — that read as
|
|
||||||
// "the eaters never came" and made this test fail about one run in five.
|
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
let peak = 0;
|
let peak = 0;
|
||||||
for (let i = 0; i < 40 * 60; i++) {
|
for (let i = 0; i < 40 * 60; i++) {
|
||||||
@@ -377,22 +378,31 @@ test('the eaters are the danger: plain shamblers, homing, and killing one is nev
|
|||||||
'an eater is ALWAYS a plain shambler — that is what keeps a called type unambiguous');
|
'an eater is ALWAYS a plain shambler — that is what keeps a called type unambiguous');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert.ok(seen.size > 0, 'plain shamblers arrive on their own clock');
|
assert.ok(peak <= TUNE.drillEaterMax, 'never more than ' + TUNE.drillEaterMax + ' at once, saw ' + peak);
|
||||||
assert.ok(peak <= TUNE.drillEaterMax, 'and never more than ' + TUNE.drillEaterMax + ' at once, saw ' + peak);
|
if (TUNE.drillEaterMax === 0)
|
||||||
const eaters = [...seen].filter(z => zombies.includes(z));
|
assert.equal(seen.size, 0, 'at 0 the court carries nothing but the puzzle');
|
||||||
if (!eaters.length) return; // all of them have walked off again: nothing left to shoot at
|
else
|
||||||
// killing one must not touch the combination
|
assert.ok(seen.size > 0, 'otherwise plain shamblers arrive on their own clock');
|
||||||
const seq = G.drill.seq.slice(), faults = G.drill.faults;
|
} finally { TUNE.eatenR = eaten; }
|
||||||
const e = eaters[0];
|
});
|
||||||
for (let i = 0; i < 40 && zombies.includes(e); i++) {
|
|
||||||
|
test('killing a body that is not in the gang is never a fault', () => {
|
||||||
|
// The rule the eaters depend on, tested with a hand-placed shambler so it holds whether or not
|
||||||
|
// the spawner is switched on.
|
||||||
|
drillPlaying();
|
||||||
|
const seq = G.drill.seq.slice(), faults = G.drill.faults, step = G.drill.step;
|
||||||
|
spawnZombie(P.x + 2, 6, 'a');
|
||||||
|
const stray = zombies[zombies.length - 1];
|
||||||
|
assert.ok(!stray.drill, 'setup: a hand-placed body is not part of the gang');
|
||||||
|
for (let i = 0; i < 40 && zombies.includes(stray); i++) {
|
||||||
ball.active = true; ball.lastHit = 'P'; ball.y = 1; ball.pow = 1; ball.bounces = 0;
|
ball.active = true; ball.lastHit = 'P'; ball.y = 1; ball.pow = 1; ball.bounces = 0;
|
||||||
ball.x = e.x; ball.z = e.z; ball.vx = ball.vy = ball.vz = 0;
|
ball.x = stray.x; ball.z = stray.z; ball.vx = ball.vy = ball.vz = 0;
|
||||||
update(DT);
|
update(DT);
|
||||||
}
|
}
|
||||||
assert.ok(!zombies.includes(e), 'setup: the eater is down');
|
assert.ok(!zombies.includes(stray), 'setup: it is down');
|
||||||
assert.deepEqual(G.drill.seq, seq, 'the combination is untouched');
|
assert.deepEqual(G.drill.seq, seq, 'the combination is untouched');
|
||||||
assert.equal(G.drill.faults, faults, 'and it costs no fault — it never wore a colour');
|
assert.equal(G.drill.faults, faults, 'and it costs no fault');
|
||||||
} finally { TUNE.eatenR = eaten; }
|
assert.equal(G.drill.step, step, 'and does not advance the sequence');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the ramp raises the batch, the walk and the eater rate', () => {
|
test('the ramp raises the batch, the walk and the eater rate', () => {
|
||||||
@@ -480,6 +490,19 @@ test('a wrong kill never leaves an un-winnable round', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('drillOneShot drops every body on the drill court to a single hit', () => {
|
||||||
|
startDrill();
|
||||||
|
assert.ok(TUNE.drillOneShot >= 0.5, 'setup: the testing knob is on');
|
||||||
|
for (const z of zombies) assert.equal(z.hp, 1, z.variant + ' should be one-shot, saw hp ' + z.hp);
|
||||||
|
// and it really dies to ONE splat-quality ball, roster HP notwithstanding
|
||||||
|
const tank = zombies.find(z => z.drill && z.variant === 'k')
|
||||||
|
|| zombies.find(z => z.drill && z.variant === 'g') || zombies.find(z => z.drill);
|
||||||
|
ball.active = true; ball.lastHit = 'P'; ball.y = 1; ball.pow = 1; ball.bounces = 0;
|
||||||
|
ball.x = tank.x; ball.z = tank.z; ball.vx = ball.vy = ball.vz = 0;
|
||||||
|
update(DT);
|
||||||
|
assert.ok(!zombies.includes(tank), 'one hit put down a ' + tank.variant);
|
||||||
|
});
|
||||||
|
|
||||||
test('a batch body that walks past you counts as caught, never as a free escape', () => {
|
test('a batch body that walks past you counts as caught, never as a free escape', () => {
|
||||||
// Types move at different speeds now, and drill bodies hold their lane — so one in a lane you are
|
// Types move at different speeds now, and drill bodies hold their lane — so one in a lane you are
|
||||||
// not standing in can cross the baseline untouched. Left as a plain despawn that stranded the
|
// not standing in can cross the baseline untouched. Left as a plain despawn that stranded the
|
||||||
|
|||||||
Reference in new issue
Block a user