SOURCE / PINNED RELEASE
Made of little things.
Powder Tool V600Billion
- Release
- 142767edcab8…
- Author-recorded commit
- 6d92971effd0…
- License
- LICENSE
- Author’s source reference
- nostr://npub1fllw8kw0thjj55wds0uugcnp5kej2nfxd36eruq39d56wwz8r44q5q78wj/wss%3A%2F%2Fgit.napplet.soy%2F/powder-toy
Archive hash verified: ed7d6a8ea7083197…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
/**
* Reading and drawing saves, against files the real game wrote
* (scripts/make-fixtures.mjs): positions, element types, decoration and
* walls come out exactly; the thumbnail shows them in the right colours;
* broken and hostile input ends in an exception, quickly.
*/
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import palette from '../src/save/elements.js';
import { BsonError, parseBson } from '../src/save/bson.js';
import { readSave, SaveError } from '../src/save/ops.js';
import { encodePng } from '../src/save/png.js';
import { fit, thumbnailPng } from '../src/save/thumbnail.js';
import { readPng } from './lib/png.mjs';
const fixture = (name) => new Uint8Array(readFileSync(new URL(`./fixtures/${name}`, import.meta.url)));
const small = fixture('stamp-small.cps');
const full = fixture('stamp-full.cps');
const colour = (name) => palette.elements.find((row) => row[1] === name)[2];
const rgbOf = (value) => [value >>> 16, (value >>> 8) & 0xff, value & 0xff];
test('a game-written stamp reads back exactly', () => {
const save = readSave(small);
assert.deepEqual([save.width, save.height, save.blockWidth, save.blockHeight], [100, 48, 25, 12]);
const seen = [];
const count = save.forEachParticle((x, y, type, dcolour) => seen.push({ x, y, type, dcolour }));
assert.equal(count, 41);
const metl = seen.filter((p) => p.type === 14).map((p) => [p.x, p.y]);
assert.deepEqual(metl, Array.from({ length: 20 }, (_, i) => [10 + i, 10]));
const watr = seen.filter((p) => p.type === 2).map((p) => [p.x, p.y]);
assert.deepEqual(watr, Array.from({ length: 20 }, (_, i) => [5, 20 + i]));
assert.deepEqual(seen.filter((p) => p.type === 1), [{ x: 50, y: 20, type: 1, dcolour: 0xffff0000 }]);
assert.equal(save.walls[5 * 25 + 20], 8);
assert.equal(save.walls.reduce((n, wall) => n + (wall ? 1 : 0), 0), 1);
});
test('a full-screen stamp reads back with every particle', () => {
const save = readSave(full);
assert.deepEqual([save.width, save.height], [612, 384]);
let sand = 0, lowest = Infinity;
const count = save.forEachParticle((x, y, type) => { if (type === 44) sand++; lowest = Math.min(lowest, y); });
assert.equal(count, 612 * 134);
assert.equal(sand, count);
assert.equal(lowest, 250);
assert.equal(save.walls.reduce((n, wall) => n + (wall === 8 ? 1 : 0), 0), 153 * 2);
});
test('the thumbnail shows elements, decoration and walls in their colours', async () => {
const { png, width, height } = await thumbnailPng(small);
assert.deepEqual([width, height], [100, 48]);
const image = readPng(png);
assert.deepEqual(image.at(15, 10).slice(0, 3), rgbOf(colour('METL')));
assert.deepEqual(image.at(5, 30).slice(0, 3), rgbOf(colour('WATR')));
assert.deepEqual(image.at(50, 20).slice(0, 3), [255, 0, 0], 'full-alpha decoration replaces the element colour');
const wall = palette.walls[8][2];
const wallPixels = [[80, 20], [81, 20], [80, 21], [81, 21]].map(([x, y]) => image.at(x, y).slice(0, 3).join());
assert.ok(wallPixels.includes(rgbOf(wall).join()), 'the wall cell is drawn');
assert.deepEqual(image.at(70, 40).slice(0, 3), [0, 0, 0], 'empty space stays black');
});
test('a full-screen save is shrunk to the browser button and keeps its layout', async () => {
const { png, width, height, size } = await thumbnailPng(full);
assert.deepEqual(size, [612, 384]);
assert.deepEqual([width, height], [108, 68]);
const image = readPng(png);
assert.deepEqual(image.at(54, 55).slice(0, 3), rgbOf(colour('SAND')), 'the sand floor keeps its colour');
assert.deepEqual(image.at(54, 30).slice(0, 3), [0, 0, 0], 'the sky above the sand is empty');
assert.deepEqual(image.at(54, 0).slice(0, 3), rgbOf(palette.walls[8][2]), 'the wall rows show at the top');
});
test('a one-pixel line survives the shrink at full brightness', () => {
const rgb = new Uint8Array(612 * 384 * 3);
for (let x = 0; x < 612; x++) { const i = (200 * 612 + x) * 3; rgb[i] = 255; rgb[i + 1] = 200; rgb[i + 2] = 50; }
const small = fit(rgb, 612, 384, 108, 68);
const row = Math.floor((200 * 68) / 384);
for (let x = 0; x < 108; x++) {
const i = (row * 108 + x) * 3;
assert.deepEqual([small.rgb[i], small.rgb[i + 1], small.rgb[i + 2]], [255, 200, 50], `column ${x}`);
}
});
test('PNG writer: compressed and stored output decode to the same pixels', async () => {
const rgb = new Uint8Array(7 * 5 * 3).map((_, i) => (i * 37) & 0xff);
const compressed = readPng(await encodePng(7, 5, rgb));
const saved = globalThis.CompressionStream;
globalThis.CompressionStream = undefined;
try {
const stored = readPng(await encodePng(7, 5, rgb));
for (let y = 0; y < 5; y++) for (let x = 0; x < 7; x++) {
const i = (y * 7 + x) * 3;
assert.deepEqual(compressed.at(x, y).slice(0, 3), [rgb[i], rgb[i + 1], rgb[i + 2]]);
assert.deepEqual(stored.at(x, y), compressed.at(x, y));
}
} finally { globalThis.CompressionStream = saved; }
});
test('BSON: types the game writes parse, broken documents throw', () => {
// {"a": int32 7, "s": "hi", "b": true, "bin": binary [1,2,3], "doc": {"x": 1.5}, "arr": [int32 1, int32 2]}
const parts = [];
const cstr = (s) => [...Buffer.from(s), 0];
const int32 = (n) => { const b = Buffer.alloc(4); b.writeInt32LE(n); return [...b]; };
const doc = (body) => { const inner = body.flat(); return [...int32(inner.length + 5), ...inner, 0]; };
const dbl = Buffer.alloc(8); dbl.writeDoubleLE(1.5);
parts.push([0x10, ...cstr('a'), ...int32(7)]);
parts.push([0x02, ...cstr('s'), ...int32(3), ...Buffer.from('hi'), 0]);
parts.push([0x08, ...cstr('b'), 1]);
parts.push([0x05, ...cstr('bin'), ...int32(3), 0x80, 1, 2, 3]);
parts.push([0x03, ...cstr('doc'), ...doc([[0x01, ...cstr('x'), ...dbl]])]);
parts.push([0x04, ...cstr('arr'), ...doc([[0x10, ...cstr('0'), ...int32(1)], [0x10, ...cstr('1'), ...int32(2)]])]);
const bytes = new Uint8Array(doc(parts));
const parsed = parseBson(bytes);
assert.equal(parsed.a, 7); assert.equal(parsed.s, 'hi'); assert.equal(parsed.b, true);
assert.deepEqual([...parsed.bin], [1, 2, 3]); assert.equal(parsed.doc.x, 1.5); assert.deepEqual(parsed.arr, [1, 2]);
assert.throws(() => parseBson(bytes.subarray(0, bytes.length - 3)), BsonError);
const lying = bytes.slice(); lying[0] = 0xff; // size beyond the buffer
assert.throws(() => parseBson(lying), BsonError);
let deep = [0x10, ...cstr('z'), ...int32(0)];
for (let i = 0; i < 40; i++) deep = [0x03, ...cstr('d'), ...doc([deep])];
assert.throws(() => parseBson(new Uint8Array(doc([deep]))), BsonError);
});
test('broken and hostile saves throw quickly instead of drawing garbage', () => {
const started = Date.now();
assert.throws(() => readSave(new Uint8Array([1, 2, 3])), SaveError);
const wrongCell = small.slice(); wrongCell[5] = 8;
assert.throws(() => readSave(wrongCell), SaveError);
const tooWide = small.slice(); tooWide[6] = 200;
assert.throws(() => readSave(tooWide), SaveError);
const hugeClaim = small.slice(); hugeClaim.set([0, 0, 0, 0x7f], 8);
assert.throws(() => readSave(hugeClaim), SaveError);
const smallClaim = small.slice(); smallClaim.set([16, 0, 0, 0], 8); // decompressed size said to be 16 bytes
assert.throws(() => readSave(smallClaim));
assert.throws(() => readSave(small.subarray(0, small.length - 20)));
for (let i = 12; i < small.length; i++) {
const damaged = small.slice();
damaged[i] ^= 0x5a;
try { readSave(damaged).forEachParticle(() => {}); } catch { /* an exception is the expected outcome */ }
}
assert.ok(Date.now() - started < 5000, 'hostile input took too long');
});
