Back to Powder Tool V600Billion
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.

source/test/save.test.mjs
/**
 * 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');
});