SOURCE / PINNED RELEASE
Made of little things.
Sickman Survival
- Release
- 7335c9fdf0a8…
- Author-recorded commit
- cbd2eeafa70d…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-8804151df7f
Archive hash verified: 591bab4addcbe5fc…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import assert from 'node:assert/strict';
import test from 'node:test';
import {
Game,
NO_CONTROLS,
PLAYER_H,
KILL_REACH,
KNOCK_REACH,
spawnInterval,
blastRadius,
} from '../src/game.ts';
import { AIR, BEDROCK, CELL, COLLIDE_NONE, COLLIDE_SOLID, PLANK, STONE, CAP, generateWorld } from '../src/terrain.ts';
import { createMole, MOLE_BLAST, MOLE_FUSE } from '../src/moles.ts';
import { createRng } from '../src/rng.ts';
import { BONES } from '../src/stickman.ts';
const STEP = 1 / 120;
const FLOOR_ROW = 120;
/** Replace the generated world with open air above a flat stone floor, and no moles. */
function arena(seed = 7) {
const game = new Game(seed);
game.moles.length = 0;
const t = game.terrain;
t.fg.fill(AIR);
t.bg.fill(0);
for (let y = FLOOR_ROW; y < t.rows; y++) for (let x = 0; x < t.cols; x++) t.set(x, y, STONE);
game.players[0].body.x = 1200;
game.players[0].body.y = FLOOR_ROW * CELL;
return game;
}
function run(game, seconds, controls = NO_CONTROLS) {
for (let i = 0; i < Math.round(seconds / STEP); i++) {
if (typeof controls === 'function') {
game.update(STEP, (slot) => (i === 0 ? controls(slot) : { ...controls(slot), jumpPressed: false }));
} else {
game.update(STEP, i === 0 ? controls : { ...controls, jumpPressed: false });
}
}
}
test('world generation is deterministic and bounded by bedrock', () => {
const a = generateWorld(42);
const b = generateWorld(42);
assert.deepEqual(a.terrain.fg, b.terrain.fg);
assert.notDeepEqual(a.terrain.fg, generateWorld(43).terrain.fg);
const t = a.terrain;
for (let x = 0; x < t.cols; x++) assert.equal(t.fgAt(x, t.rows - 1), BEDROCK);
const kinds = new Set(a.landmarks.map((l) => l.kind));
for (const kind of ['building', 'tree', 'mushroom']) assert.ok(kinds.has(kind), `world has a ${kind}`);
});
test('the stickman spawns on open ground and stays standing', () => {
for (const seed of [1, 2, 3, 99, 12345, 2024, 777, 31337, 4242, 8]) {
const game = new Game(seed);
assert.equal(game.bodyBlocked(game.players[0].body.x, game.players[0].body.y), false, `seed ${seed} spawn is clear`);
const col = Math.floor(game.players[0].body.x / CELL);
const row = game.players[0].body.y / CELL;
for (let y = row; y < row + 6; y++) {
assert.notEqual(game.terrain.fgAt(col, y), AIR, `seed ${seed} spawns on the ground, not a floating island`);
}
const startY = game.players[0].body.y;
run(game, 2);
assert.ok(game.players[0].body.grounded, `seed ${seed} grounded`);
assert.ok(Math.abs(game.players[0].body.y - startY) < CELL * 3, `seed ${seed} did not fall away`);
}
});
test('houses are big enough to walk in through the door and jump up a storey', () => {
let upstairs = 0;
for (const seed of [1, 2, 3, 42, 777]) {
const buildings = generateWorld(seed).landmarks.filter((l) => l.kind === 'building');
assert.ok(buildings.length >= 3, `seed ${seed} has several houses`);
for (const { x0, x1 } of buildings) {
assert.ok(x1 - x0 + 1 >= 28, 'houses are wide');
const game = new Game(seed);
game.moles.length = 0;
const t = game.terrain;
const base = t.surface[x0];
game.players[0].body.x = (x0 - 0.5) * CELL;
game.players[0].body.y = base * CELL;
game.players[0].body.vx = game.players[0].body.vy = 0;
assert.equal(game.bodyBlocked(game.players[0].body.x, game.players[0].body.y), false, `seed ${seed} doorstep is clear`);
run(game, 0.45, { ...NO_CONTROLS, right: true });
assert.ok(game.players[0].body.x > (x0 + 3) * CELL, `seed ${seed} house at ${x0}: walked in the door`);
assert.equal(game.players[0].body.y, base * CELL, 'standing on the ground floor');
// Jump up through the plank ceiling where nothing solid sits above it.
const upper = base - 14;
let col = -1;
for (let c = x0 + 4; c < x1 - 4 && col < 0; c++) {
let ok = true;
for (let dx = -1; dx <= 1; dx++) {
if (t.fgAt(c + dx, upper) !== PLANK) ok = false;
// Nothing to land on short of the upper floor, and nothing solid to bump a head on.
for (let y = upper - 7; y < base; y++) if (y !== upper && t.collisionAt(c + dx, y) !== COLLIDE_NONE) ok = false;
for (let y = upper - 15; y < upper - 7; y++) if (t.collisionAt(c + dx, y) === COLLIDE_SOLID) ok = false;
}
if (ok) col = c;
}
if (col < 0) continue;
game.players[0].body.x = (col + 0.5) * CELL;
game.players[0].body.vx = 0;
run(game, 0.2);
game.update(STEP, { ...NO_CONTROLS, jump: true, jumpPressed: true });
run(game, 1.2, { ...NO_CONTROLS, jump: true });
assert.equal(game.players[0].body.y, upper * CELL, `seed ${seed} house at ${x0}: landed on the upper floor`);
upstairs++;
}
}
assert.ok(upstairs >= 3, 'several multi-storey houses were climbed');
});
test('blasts carve terrain but never bedrock', () => {
const world = generateWorld(5);
const t = world.terrain;
const x = 300 * CELL;
const y = (t.rows - 2) * CELL;
const result = t.carve(x, y, 60);
assert.ok(result.removed > 50);
assert.equal(t.fgAt(300, t.rows - 1), BEDROCK);
assert.equal(t.fgAt(300, t.rows - 10), AIR);
});
test('running, jumping and landing on flat ground', () => {
const game = arena();
run(game, 0.2);
assert.ok(game.players[0].body.grounded);
const x0 = game.players[0].body.x;
run(game, 1, { ...NO_CONTROLS, right: true });
assert.ok(game.players[0].body.x - x0 > 120, 'ran right');
assert.equal(game.players[0].body.facing, 1);
let peak = game.players[0].body.y;
game.update(STEP, { ...NO_CONTROLS, jump: true, jumpPressed: true });
for (let i = 0; i < 120; i++) {
game.update(STEP, { ...NO_CONTROLS, jump: true });
peak = Math.min(peak, game.players[0].body.y);
}
assert.ok(FLOOR_ROW * CELL - peak > 60, 'jump clears a storey');
assert.ok(game.players[0].body.grounded, 'landed again');
assert.equal(game.players[0].body.y, FLOOR_ROW * CELL);
});
test('walks up small steps but is blocked by walls', () => {
const game = arena();
const col = Math.floor(game.players[0].body.x / CELL) + 4;
for (let x = col; x < col + 20; x++) game.terrain.set(x, FLOOR_ROW - 1, STONE);
run(game, 0.2);
run(game, 0.6, { ...NO_CONTROLS, right: true });
assert.equal(game.players[0].body.y, (FLOOR_ROW - 1) * CELL, 'stepped up one cell');
const wall = Math.floor(game.players[0].body.x / CELL) + 4;
for (let y = FLOOR_ROW - 20; y < FLOOR_ROW; y++) game.terrain.set(wall, y, STONE);
run(game, 1, { ...NO_CONTROLS, right: true });
assert.ok(game.players[0].body.x < wall * CELL, 'stopped by the wall');
});
test('one-way planks: jump up through, land on top, drop down through', () => {
const game = arena();
const plankRow = FLOOR_ROW - 12;
const col = Math.floor(game.players[0].body.x / CELL);
for (let x = col - 6; x <= col + 6; x++) game.terrain.set(x, plankRow, PLANK);
run(game, 0.2);
run(game, 1.2, { ...NO_CONTROLS, jump: true, jumpPressed: true });
assert.equal(game.players[0].body.y, plankRow * CELL, 'standing on the plank');
assert.ok(game.players[0].body.grounded);
run(game, 0.6, { ...NO_CONTROLS, down: true });
assert.equal(game.players[0].body.y, FLOOR_ROW * CELL, 'dropped back to the floor');
});
test('crouching crawls slowly through a gap too low to walk through', () => {
const game = arena();
const col = Math.floor(game.players[0].body.x / CELL);
// A 4-cell-high tunnel: a low ceiling from 6 to 30 cells right of the player.
for (let x = col + 6; x <= col + 30; x++) for (let y = FLOOR_ROW - 20; y < FLOOR_ROW - 4; y++) game.terrain.set(x, y, STONE);
run(game, 0.2);
run(game, 1, { ...NO_CONTROLS, right: true });
assert.ok(game.players[0].body.x < (col + 6) * CELL, 'standing, the tunnel blocks');
const before = game.players[0].body.x;
run(game, 0.5, { ...NO_CONTROLS, right: true, crouch: true });
assert.ok(game.players[0].body.ducked);
assert.equal(game.players[0].body.state, 'crawl');
const crawled = game.players[0].body.x - before;
assert.ok(crawled > 10 && crawled < 0.5 * 165 * 0.5, `crawls in, slower than running (${crawled})`);
// Letting go inside the tunnel keeps the stickman down until there is headroom.
run(game, 0.3, NO_CONTROLS);
assert.ok(game.players[0].body.ducked, 'no room to stand');
run(game, 4, { ...NO_CONTROLS, right: true });
assert.ok(game.players[0].body.x > (col + 31) * CELL, 'crawled out the far side');
run(game, 0.2, { ...NO_CONTROLS, right: true });
assert.equal(game.players[0].body.ducked, false, 'stands up again in the open');
});
test('mushroom caps bounce the stickman', () => {
const game = arena();
const col = Math.floor(game.players[0].body.x / CELL);
for (let x = col - 3; x <= col + 3; x++) game.terrain.set(x, FLOOR_ROW - 3, CAP);
game.players[0].body.y = (FLOOR_ROW - 30) * CELL;
game.players[0].body.vy = 300;
let rose = false;
for (let i = 0; i < 120; i++) {
game.update(STEP, NO_CONTROLS);
if (game.players[0].body.vy < -400) rose = true;
}
assert.ok(rose, 'bounced upward');
});
test('a direct hit kills and ragdolls; a near miss throws the stickman back', () => {
const near = arena();
run(near, 0.2);
const radius = 24;
const px = near.players[0].body.x;
const blastX = px - 10 - radius * ((KILL_REACH + KNOCK_REACH) / 2);
near.explode(blastX, near.players[0].body.y - PLAYER_H / 2, radius);
assert.ok(near.alive, 'survived the near miss');
assert.ok(near.players[0].body.vx > 150, 'pushed away from the blast');
assert.ok(near.players[0].body.vy < 0, 'lifted off the ground');
const direct = arena();
run(direct, 0.2);
let died = 0;
direct.listeners.death = () => died++;
direct.explode(direct.players[0].body.x + 3, direct.players[0].body.y - 10, radius);
assert.equal(direct.alive, false);
assert.equal(died, 1);
assert.ok(direct.players[0].ragdoll);
// A blast centred on the body must still throw it, not cancel out.
const x0 = direct.players[0].ragdoll.centerX();
const y0 = direct.players[0].ragdoll.centerY();
let travelled = 0;
for (let i = 0; i < 60; i++) {
direct.update(STEP, NO_CONTROLS);
travelled = Math.max(travelled, Math.hypot(direct.players[0].ragdoll.centerX() - x0, direct.players[0].ragdoll.centerY() - y0));
}
assert.ok(travelled > 80, `body was flung (${travelled.toFixed(0)} units)`);
});
test('ragdoll stays in one piece and comes to rest on the ground', () => {
const game = arena();
run(game, 0.2);
game.explode(game.players[0].body.x - 4, game.players[0].body.y - 4, 30);
run(game, 6);
const r = game.players[0].ragdoll;
for (const [a, b] of BONES) assert.ok(r.distance(a, b) < 12, `bone ${a}-${b} intact`);
for (let j = 0; j < r.pos.length; j += 2) {
assert.ok(r.pos[j + 1] <= game.terrain.height, 'joint above the world floor');
assert.notEqual(game.terrain.collisionAtPoint(r.pos[j], r.pos[j + 1]), 1, 'joint not inside solid');
}
});
test('the storm escalates', () => {
assert.ok(spawnInterval(0) > spawnInterval(60));
assert.ok(spawnInterval(60) > spawnInterval(120));
assert.equal(spawnInterval(10_000), 0.16);
assert.ok(blastRadius(120, 0.5) > blastRadius(0, 0.5));
const game = new Game(11);
game.meteorsEnabled = true;
let blasts = 0;
game.listeners.explosion = () => blasts++;
run(game, 12);
assert.ok(blasts >= 3, `meteors landed (${blasts})`);
});
/** Arena where everything counts as underground, with one mole placed in it. */
function moleArena(x, y, state) {
const game = arena();
game.terrain.surface.fill(0);
const mole = createMole(x, y, createRng(3));
mole.state = state;
game.moles.push(mole);
return { game, mole };
}
test('worlds have caves under the landscape, reachable from the surface', () => {
for (const seed of [1, 2, 3, 42, 777]) {
const { terrain: t, landmarks } = generateWorld(seed);
assert.ok(landmarks.some((l) => l.kind === 'cave-mouth'), `seed ${seed} has a cave mouth`);
// Flood the open air from the sky and see how deep it reaches.
const seen = new Uint8Array(t.cols * t.rows);
const queue = [];
for (let x = 0; x < t.cols; x++) {
seen[x] = 1;
queue.push(x);
}
let deepest = 0;
let reachedCave = 0;
while (queue.length) {
const i = queue.pop();
const x = i % t.cols;
const y = Math.floor(i / t.cols);
const depth = y - t.surface[x];
deepest = Math.max(deepest, depth);
if (depth > 20) reachedCave++;
for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) {
if (!t.inBounds(nx, ny)) continue;
const j = ny * t.cols + nx;
if (seen[j] || t.collisionAt(nx, ny) === COLLIDE_SOLID) continue;
seen[j] = 1;
queue.push(j);
}
}
assert.ok(deepest > 60, `seed ${seed} caves reach ${deepest} cells deep`);
assert.ok(reachedCave > 2000, `seed ${seed} has a sizeable connected cave network (${reachedCave} cells)`);
for (let x = 0; x < t.cols; x++) assert.equal(t.fgAt(x, t.rows - 1), BEDROCK);
}
});
test('moles start buried away from the stickman and tunnel without changing the world', () => {
const game = new Game(21);
assert.ok(game.moles.length >= 10);
for (const m of game.moles) {
assert.equal(m.state, 'burrow');
assert.equal(game.terrain.collisionAtPoint(m.x, m.y), COLLIDE_SOLID, 'spawned inside the earth');
assert.ok(game.terrain.depthAt(m.x, m.y) >= 10, 'spawned underground');
assert.ok(Math.hypot(m.x - game.players[0].body.x, m.y - game.players[0].body.y) >= 300, 'spawned away from the stickman');
}
const before = Uint8Array.from(game.terrain.fg);
const states = new Set();
for (let i = 0; i < 30 / STEP; i++) {
game.update(STEP, NO_CONTROLS);
for (const m of game.moles) {
states.add(m.state);
assert.ok(game.terrain.depthAt(m.x, m.y) > 2, 'moles stay underground');
}
}
assert.ok(states.has('walk'), 'some moles surfaced into the caves');
assert.deepEqual(game.terrain.fg, before, 'burrowing left the terrain untouched');
assert.ok(game.alive);
});
test('a burrowing mole pops out into a cave pocket', () => {
const x = 600;
const y = (FLOOR_ROW + 20) * CELL;
const { game, mole } = moleArena(x, y, 'burrow');
const t = game.terrain;
for (let cy = FLOOR_ROW + 10; cy < FLOOR_ROW + 30; cy++) {
for (let cx = Math.floor(x / CELL) - 14; cx < Math.floor(x / CELL) + 14; cx++) {
if (Math.hypot(cx * CELL - x, cy * CELL - y) > 18) t.set(cx, cy, AIR);
}
}
mole.minBurrow = 0;
run(game, 4);
assert.equal(mole.state, 'walk');
assert.equal(t.collisionAtPoint(mole.x, mole.y), COLLIDE_NONE);
});
test('getting close to a surfaced mole arms it, and it blows the stickman up', () => {
const { game, mole } = moleArena(1260, FLOOR_ROW * CELL - 4, 'walk');
run(game, 0.2);
assert.equal(mole.state, 'walk', 'not armed while the stickman is far');
let blasts = 0;
game.listeners.explosion = (b) => {
blasts++;
assert.equal(b.radius, MOLE_BLAST);
};
// Walk up to it, then stand still: it shuffles in and goes off.
for (let i = 0; i < 1.5 / STEP && game.moles.length; i++) {
game.update(STEP, { ...NO_CONTROLS, right: mole.state === 'walk' });
}
assert.equal(game.moles.length, 0, 'the mole detonated');
assert.equal(blasts, 1);
assert.equal(game.alive, false);
assert.equal(game.deathCause, 'mole');
});
test('a mole fuse leaves time to run, and the blast still throws the stickman', () => {
const { game, mole } = moleArena(1225, FLOOR_ROW * CELL - 4, 'walk');
run(game, 0.05);
assert.equal(mole.state, 'armed');
run(game, 0.35, { ...NO_CONTROLS, left: true });
let thrown = 0;
for (let i = 0; i < MOLE_FUSE / STEP; i++) {
game.update(STEP, NO_CONTROLS);
thrown = Math.min(thrown, game.players[0].body.vy);
}
assert.equal(game.moles.length, 0, 'the mole detonated');
assert.ok(game.alive, 'got clear of the kill zone');
assert.ok(thrown < -100, 'but was thrown by the blast');
});
test('explosions set off nearby moles, even buried ones', () => {
const game = arena();
const rng = createRng(5);
const buried = createMole(600, (FLOOR_ROW + 8) * CELL, rng);
const far = createMole(2000, (FLOOR_ROW + 8) * CELL, rng);
game.moles.push(buried, far);
let blasts = 0;
game.listeners.explosion = () => blasts++;
game.explode(600, FLOOR_ROW * CELL, 28);
assert.equal(buried.state, 'armed');
assert.notEqual(far.state, 'armed');
run(game, 0.5);
assert.equal(blasts, 2, 'chain reaction');
assert.ok(game.moles.includes(far));
assert.equal(game.alive, true);
});
// Local multiplayer ───────────────────────────────────────────────────────
/** Arena with several players standing apart on the floor. */
function squad(slots, spacing = 300) {
const game = arena();
for (const slot of slots) if (slot !== 0) game.addPlayer(slot);
game.players.forEach((p, i) => {
p.body.x = 1200 + i * spacing;
p.body.y = FLOOR_ROW * CELL;
p.shield = 0;
});
run(game, 0.05);
return game;
}
test('a blast kills only the player inside it, and the run goes on', () => {
const game = squad([0, 1]);
run(game, 0.2);
const deaths = [];
let over = 0;
game.listeners.death = (slot) => deaths.push(slot);
game.listeners.runOver = () => over++;
const p1 = game.player(1).body;
game.explode(p1.x + 3, p1.y - 10, 24);
assert.deepEqual(deaths, [1]);
assert.equal(game.player(0).alive, true);
assert.equal(game.alive, true, 'the run continues while anyone stands');
assert.equal(over, 0);
const p0 = game.player(0).body;
game.explode(p0.x + 3, p0.y - 10, 24);
assert.equal(game.alive, false);
assert.equal(over, 1, 'the run ends with the last player');
});
test('the survival clock runs until the last player is down', () => {
const game = squad([0, 1]);
game.meteorsEnabled = false;
run(game, 0.2);
game.meteorsEnabled = true;
const p0 = game.player(0).body;
game.explode(p0.x + 3, p0.y - 10, 24);
const before = game.survived;
run(game, 1);
assert.ok(game.survived > before + 0.9, 'clock keeps running for the survivor');
});
test('players are controlled independently', () => {
const game = squad([0, 1]);
run(game, 0.2);
const x0 = game.player(0).body.x;
const x1 = game.player(1).body.x;
run(game, 0.5, (slot) => (slot === 1 ? { ...NO_CONTROLS, right: true } : NO_CONTROLS));
assert.equal(game.player(0).body.x, x0, 'P1 stood still');
assert.ok(game.player(1).body.x - x1 > 50, 'P2 ran right');
});
test('a joining player drops in beside a living teammate, shielded', () => {
const game = arena();
game.meteorsEnabled = true;
run(game, 0.2);
const joined = game.addPlayer(2);
const anchor = game.player(0).body;
assert.ok(Math.abs(joined.body.x - anchor.x) <= 14 && joined.body.x !== anchor.x, "drops in just beside the teammate");
assert.equal(joined.body.y, anchor.y);
assert.equal(game.bodyBlocked(joined.body.x, joined.body.y), false);
assert.ok(joined.shield > 0);
game.explode(joined.body.x + 3, joined.body.y - 10, 24);
assert.equal(joined.alive, true, 'spawn shield blocks a lethal blast');
assert.equal(game.player(0).alive, false, 'the unshielded teammate still dies');
assert.equal(game.addPlayer(2), joined, 'adding a slot twice is a no-op');
});
test('unplugging a player keeps the run going unless they were the last one standing', () => {
const game = squad([0, 1, 2]);
let over = 0;
game.listeners.runOver = () => over++;
game.removePlayer(1);
assert.deepEqual(game.players.map((p) => p.slot), [0, 2]);
assert.equal(over, 0);
const p0 = game.player(0).body;
game.explode(p0.x + 3, p0.y - 10, 24);
assert.equal(over, 0);
game.removePlayer(2);
assert.equal(over, 1, 'removing the last survivor ends the run');
});
test('a dead player spectates their body, then the nearest living teammate', () => {
const game = squad([0, 1, 2], 200);
game.player(2).body.x = 3000;
run(game, 0.05);
const p0 = game.player(0).body;
game.explode(p0.x + 3, p0.y - 10, 24);
assert.equal(game.viewTarget(0).slot, 0, 'follows own body at first');
run(game, 2.5);
assert.equal(game.viewTarget(0).slot, 1, 'then the nearest survivor');
});
test('moles hunt the nearest living player', () => {
const game = squad([0, 1], 400);
const target = game.player(1).body;
const mole = createMole(target.x - 60, FLOOR_ROW * CELL - 6, createRng(3));
mole.state = 'walk';
mole.grounded = true;
mole.facing = -1;
game.moles.push(mole);
run(game, 1.5);
assert.ok(mole.state === 'armed' || mole.x > target.x - 50, 'went after P2, the closer player');
});
