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/ui.test.mjs
/**
 * The page's own controls, without a browser: the tool catalogue read from
 * the game's Lua, the scene files (headers, pictures, and that every element
 * they name exists in the pinned game), and the Lua a scene is loaded with.
 * The browser checks (scripts/smoke.mjs) run all of it against the game.
 */
import assert from 'node:assert/strict';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import test from 'node:test';

import palette from '../src/save/elements.js';
import { inkFor, readTools, SECTIONS } from '../src/ui/catalog.js';
import { ANCHOR, KIT, RESET, STOP, parseScene, sceneScript } from '../src/ui/scene-meta.js';
import { PUBLIC_SOURCE } from '../scripts/public-source.mjs';

const SCENES = new URL('../src/scenes/', import.meta.url);
const sceneFiles = readdirSync(SCENES).filter((name) => name.endsWith('.lua')).sort();
const scene = (name) => parseScene(`/src/scenes/${name}`, readFileSync(new URL(name, SCENES), 'utf8'));

/** What `tools.index` answers in the game, in the page's tab-separated form (index, id, section, sort, colour, name, description). */
const LISTING = [
  [2, 'DEFAULT_PT_WATR', 7, 0, 0x2030d0, 'WATR', 'Water. Conducts electricity, freezes, and extinguishes fires.'],
  [1, 'DEFAULT_PT_DUST', 8, 0, 0xffe0a0, 'DUST', 'Very light dust. Flammable.'],
  [5, 'DEFAULT_PT_STNE', 8, 0, 0xa0a0a0, 'STNE', 'Stone. Heavy particles, meltable.'],
  [300, 'DEFAULT_WL_CNDTW', 0, 0, 0xc0c0c0, 'CONDUCTIVE WALL', 'Blocks everything. Conductive.'],
  [400, 'DEFAULT_TOOL_HEAT', 13, 0, -0xff0000, 'HEAT', 'Heats the targeted element.'],
  [500, 'DEFAULT_PT_LIFE_GOL', 12, 0, 0x0cac00, 'GOL', 'Game Of Life: Begin 3/Stay 23'],
  [19, 'DEFAULT_PT_PLUT', 10, 0, 0x407020, 'PLUT', 'Plutonium. Heavy, fissile particles. Generates neutrons under pressure.'],
].map((row) => row.join('\t')).join('\n');

test('the tool catalogue comes from the game, in its sections and order, with readable names', () => {
  const asked = [];
  const tools = readTools({ lua: (code) => { asked.push(code); return LISTING; } });
  assert.match(asked[0], /tools\.index/);
  assert.deepEqual(tools.map((tool) => tool.id), [
    'DEFAULT_WL_CNDTW', 'DEFAULT_PT_WATR', 'DEFAULT_PT_DUST', 'DEFAULT_PT_STNE',
    'DEFAULT_PT_PLUT', 'DEFAULT_PT_LIFE_GOL', 'DEFAULT_TOOL_HEAT',
  ]);
  const byId = Object.fromEntries(tools.map((tool) => [tool.id, tool]));
  assert.equal(byId.DEFAULT_PT_WATR.label, 'Water');
  assert.equal(byId.DEFAULT_PT_DUST.label, 'Dust');
  assert.equal(byId.DEFAULT_PT_PLUT.label, 'Plutonium');
  assert.equal(byId.DEFAULT_WL_CNDTW.label, 'Conductive wall');
  assert.equal(byId.DEFAULT_TOOL_HEAT.label, 'Heat');
  assert.equal(byId.DEFAULT_PT_LIFE_GOL.label, 'GOL', 'Game of Life rules keep their codes');
  assert.equal(byId.DEFAULT_PT_WATR.colour, '#2030d0');
  // A colour the game packs with its alpha bits comes out as plain RGB.
  assert.equal(byId.DEFAULT_TOOL_HEAT.colour, '#010000');
  assert.equal(byId.DEFAULT_PT_WATR.code, 'WATR');
});

test('every section the palette names is one of the game\'s, favourites left out', () => {
  const ids = SECTIONS.map((section) => section.id).sort((a, b) => a - b);
  assert.deepEqual(ids, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15]);
});

test('tool labels get black or white ink, whichever reads on their colour', () => {
  assert.equal(inkFor('#ffe0a0'), '#111');
  assert.equal(inkFor('#2030d0'), '#fff');
  assert.equal(inkFor('#000000'), '#fff');
  assert.equal(inkFor('#ffffff'), '#111');
});

test('every scene has a complete header and a picture', () => {
  assert.ok(sceneFiles.length >= 8, `${sceneFiles.length} scenes`);
  const ids = new Set();
  for (const name of sceneFiles) {
    assert.match(name, /^\d{2}-[a-z-]+\.lua$/, name);
    const source = readFileSync(new URL(name, SCENES), 'utf8');
    for (const key of ['title', 'icon', 'blurb', 'hint', 'view', 'thumb']) {
      assert.match(source, new RegExp(`^-- ${key}: \\S`, 'm'), `${name} has no ${key}`);
    }
    const parsed = scene(name);
    assert.ok(Number.isInteger(parsed.view) && parsed.view >= 0 && parsed.view <= 12, `${name}: view ${parsed.view}`);
    assert.ok(parsed.blurb.length <= 140, `${name}: the blurb is ${parsed.blurb.length} characters`);
    assert.ok(!ids.has(parsed.id), `${parsed.id} twice`);
    ids.add(parsed.id);
    assert.ok(existsSync(new URL(`thumbs/${parsed.id}.webp`, SCENES)), `no picture for ${parsed.id} (node scripts/scene-thumbs.mjs ${parsed.id})`);
  }
  assert.ok(ids.has('reactor'), 'the nuclear reactor is there');
});

test('every element a scene names exists in the pinned game', () => {
  const known = new Set(palette.elements.map((row) => row[1]));
  for (const name of sceneFiles) {
    const source = readFileSync(new URL(name, SCENES), 'utf8');
    for (const [, id] of source.matchAll(/(?:E|elem)\.DEFAULT_PT_([A-Z0-9]+)/g)) {
      assert.ok(known.has(id), `${name}: DEFAULT_PT_${id} is no element of The Powder Toy ${palette.version}`);
    }
  }
});

test('a scene loads after a reset and its helpers, and ends in its display mode', () => {
  const reactor = scene(sceneFiles.find((name) => name.endsWith('-reactor.lua')));
  assert.equal(reactor.id, 'reactor');
  assert.equal(reactor.title, 'Nuclear reactor');
  const script = sceneScript(reactor);
  assert.ok(script.startsWith(RESET));
  assert.ok(script.indexOf(KIT) > script.indexOf('sim.clearSim()'));
  assert.ok(script.indexOf(reactor.source) > script.indexOf(KIT));
  // The anchors come from what the scene built, so they follow it.
  assert.ok(script.indexOf(ANCHOR) > script.indexOf(reactor.source));
  assert.match(script, new RegExp(`ren\\.useDisplayPreset\\(${reactor.view}\\)$`));
  // The previous scene's scripts stop before anything else happens.
  assert.ok(RESET.startsWith(STOP));
  assert.match(STOP, /^if napplet_scene_step then evt\.unregister\(evt\.AFTERSIM/);
  assert.match(STOP, /evt\.unregister\(evt\.TICK, napplet_scene_watch\)/);
  assert.match(STOP, /napplet_scene_ended = nil$/);
});

test('a scene\'s scripts end with the scene: the step script only stops, the watch takes both off', () => {
  // Only the tick event may call evt.unregister (the game refuses it after a simulation step).
  const step = KIT.slice(KIT.indexOf('local function every'), KIT.indexOf('local function watch'));
  const watchFn = KIT.slice(KIT.indexOf('local function watch'));
  assert.doesNotMatch(step, /evt\.unregister/);
  assert.match(step, /napplet_scene_ended = true return end/);
  assert.match(watchFn, /evt\.unregister\(evt\.AFTERSIM, napplet_scene_step\)/);
  assert.match(watchFn, /evt\.unregister\(evt\.TICK, napplet_scene_watch\)/);
  // Every scene that runs scripts stands on ground walls, which become its anchors.
  for (const name of sceneFiles) {
    const source = readFileSync(new URL(name, SCENES), 'utf8');
    if (!/\b(every|watch)\(function/.test(source)) continue;
    assert.match(source, /walls\(0, 3\d\d, 611, 383\)/, `${name} runs scripts but has no ground walls to anchor them`);
  }
});

test('the page names its license and the way to its source (GPL-3.0 §6, and for whoever gets the file alone)', () => {
  const page = readFileSync(new URL('../index.html', import.meta.url), 'utf8');
  const about = page.slice(page.indexOf('<dialog id="about"'), page.indexOf('</dialog>', page.indexOf('<dialog id="about"')));
  assert.match(about, /GNU General Public License, version 3 \(GPL-3\.0\)/);
  assert.match(about, /no warranty/);
  // This repository is private: the page names napplet.soy's public Git (scripts/public-source.mjs).
  assert.ok(about.includes(`data-link="${PUBLIC_SOURCE}"`), 'the source button opens the public Git');
  assert.ok(about.includes(`<code>${PUBLIC_SOURCE}</code>`), 'and the address is there to copy');
  assert.doesNotMatch(page, /github\.com\/BIMbeamFLX/, 'no link to the private repository');
  assert.match(about, /data-link="https:\/\/www\.gnu\.org\/licenses\/gpl-3\.0\.html"/);
  assert.match(page, /data-action="about"/, 'the menu opens it');
});

test('a scene\'s picture is found by its id, whatever its file is numbered', () => {
  const parsed = parseScene('/src/scenes/07-storm.lua', '-- title: Storm\n-- view: 4\nlocal x = 1', { '/src/scenes/thumbs/storm.webp': 'data:image/webp;base64,AA' });
  assert.equal(parsed.id, 'storm');
  assert.equal(parsed.thumb, 'data:image/webp;base64,AA');
  assert.equal(parsed.view, 4);
  assert.equal(parsed.hint, '');
});