Back to Pickle Rustlers
SOURCE / PINNED RELEASE

Made of little things.

Pickle Rustlers

Release
039e51f55e34…
Author-recorded commit
40608586a8a1…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-bad44e7ab35

Archive hash verified: f38e99afa0992052…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

tests/gamepad.test.mjs
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFile } from 'node:fs/promises';
import { runInNewContext } from 'node:vm';
import ts from 'typescript';

// Execute the real TypeScript modules with only their shell boundary replaced.
async function load(path, imports = {}, globals = {}) {
  const source = await readFile(new URL(path, import.meta.url), 'utf8');
  const { outputText } = ts.transpileModule(source, {
    compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
  });
  const exports = {};
  runInNewContext(outputText, { exports, require: (name) => imports[name], structuredClone, Error, ...globals });
  return exports;
}
const { createGamepadInput } = await load('../src/game/gamepad.ts');
const { createShellGamepadSource } = await load('../src/game/gamepad-source.ts', {
  '@napplet/sdk': {}, '../domain-availability.js': {},
});
const bindings = { steer: [{ axis: 0 }], shoot: [{ button: 0 }], pause: [{ button: 9 }] };
const pad = (value = 0, axis = 0, index = 2, mapping = 'standard') => ({
  index, id: 'controller', mapping, connected: true, timestamp: 0,
  axes: [axis], buttons: Array.from({ length: 10 }, (_, i) => ({
    value: i === 0 ? value : 0, pressed: i === 0 && value > 0.5, touched: false,
  })),
});
function setup() {
  let handler, closed = 0, reads = 0;
  let mapper;
  const source = createShellGamepadSource(() => mapper?.reset(), true, {
    available: true, focused: false,
    getGamepads() { reads++; return []; },
    onChange(fn) { handler = fn; return { close() { closed++; } }; },
  });
  mapper = createGamepadInput(bindings, source);
  return {
    mapper, source, get closed() { return closed; }, get reads() { return reads; },
    send(pads, focused = true, available = true, reason) { handler({ pads, focused, available, reason }); },
  };
}

test('cached snapshots preserve sparse controller indices, analog dead zone and one-shot edges', () => {
  const s = setup();
  s.send([null, null, pad(0, 0.1)]);
  let frame = s.mapper.poll();
  assert.equal(frame.players[0].index, 2);
  assert.equal(frame.players[0].actions.steer.value, 0);
  s.send([null, null, pad(1, 0.59)]);
  frame = s.mapper.poll();
  assert.ok(Math.abs(frame.players[0].actions.steer.value - 0.5) < 0.0001);
  assert.equal(frame.players[0].actions.shoot.pressed, true);
  assert.equal(s.mapper.poll().players[0].actions.shoot.pressed, false);
  assert.equal(s.reads, 1, 'rendering reads only the cached state');
});

test('focus loss is idle, and a held button on regain cannot create a press', () => {
  const s = setup();
  s.send([pad()]); s.mapper.poll();
  s.send([pad(1)]); assert.equal(s.mapper.poll().players[0].actions.shoot.pressed, true);
  s.send([pad()], false);
  const idle = s.mapper.poll().players[0].actions.shoot;
  assert.equal(idle.value, 0); assert.equal(idle.released, false);
  s.send([pad(1)]);
  assert.equal(s.mapper.poll().players[0].actions.shoot.pressed, false);
  s.send([pad()]); s.mapper.poll();
  s.send([pad(1)]); assert.equal(s.mapper.poll().players[0].actions.shoot.pressed, true);
});

test('focus loss and regain between frames still suppress a new held press', () => {
  const s = setup();
  s.send([pad()]); s.mapper.poll();
  s.send([pad()], false); s.send([pad(1)]);
  assert.equal(s.mapper.poll().players[0].actions.shoot.pressed, false);
});

test('disconnect, slot replacement and reset never synthesize presses', () => {
  const s = setup();
  s.send([pad()]); s.mapper.poll();
  s.send([]); assert.equal(s.mapper.poll().disconnected[0], 2);
  s.send([pad(1)]); assert.equal(s.mapper.poll().players[0].actions.shoot.pressed, false);
  s.send([{ ...pad(1), id: 'replacement' }]);
  const replacement = s.mapper.poll();
  assert.equal(replacement.disconnected[0], 2);
  assert.equal(replacement.players[0].actions.shoot.pressed, false);
  s.mapper.reset();
  assert.equal(s.mapper.poll().players[0].actions.shoot.pressed, false);
});

test('unknown layouts stay neutral; missing, blocked and failed optional domains remain safe', () => {
  const s = setup();
  s.send([pad(1, 1, 2, '')]); s.mapper.poll();
  assert.equal(s.mapper.poll().players[0].actions.steer.value, 0);
  s.send([], false, false, 'blocked'); assert.equal(s.mapper.poll().status, 'blocked');
  // No shell domain and no browser Gamepad API either: nothing to read, still safe.
  const absent = createShellGamepadSource(() => {}, false);
  assert.equal(createGamepadInput(bindings, absent).poll().status, 'unavailable');
  const broken = createShellGamepadSource(() => {}, true, {
    get available() { throw new Error('denied'); },
  });
  assert.equal(createGamepadInput(bindings, broken).poll().status, 'unavailable');
});

test('teardown closes subscription once and ignores later snapshots', () => {
  const s = setup();
  s.source.dispose(); s.source.dispose();
  assert.equal(s.closed, 1);
  s.send([pad(1)]);
  assert.equal(s.source.active(), false);
  s.mapper.dispose(); assert.equal(s.mapper.poll().status, 'closed');
});

test('disconnect and reconnect in one slot between frames rebaseline held input', () => {
  const s = setup();
  s.send([pad()]); s.mapper.poll();
  s.send([]); s.send([pad(1)]);
  assert.equal(s.mapper.poll().players[0].actions.shoot.pressed, false);
});

test('hosts without the gamepad domain fall back to the browser Gamepad API', async () => {
  let reads = 0;
  const browser = {
    navigator: { getGamepads() { reads++; return [pad(1)]; } },
    document: { hidden: false, hasFocus: () => true },
  };
  const native = await load('../src/game/gamepad-source.ts', { '@napplet/sdk': {}, '../domain-availability.js': {} }, browser);
  const source = native.createShellGamepadSource(() => {}, false);
  assert.equal(source.active(), true);
  assert.equal(source.read()[0].id, 'controller');
  assert.equal(reads, 1);
  source.dispose();
});

const { Input } = await load('../src/game/input.ts', {
  './gamepad.js': { createGamepadInput }, './gamepad-source.js': {}, './keyboard.js': {},
});
test('keyboard and touch still work alongside controllers and after domain loss', () => {
  // Exercise Input.read without constructing DOM listeners.
  const input = Object.create(Input.prototype);
  Object.assign(input, {
    keyboard: (() => {
      let pending = new Set(['lasso']);
      return {
        down: (...names) => names.includes('w'),
        read: () => { const result = pending; pending = new Set(); return result; },
      };
    })(),
    stick: { id: -1, x: 0, y: 0 },
    touchShoot: false, touchLasso: false,
    gamepad: { poll: () => ({ players: [{ actions: {
      moveX: { value: 0.6 }, moveY: { value: 0 }, gas: { value: 0.2 }, brake: { value: 0 },
      shoot: { pressed: false }, lasso: { pressed: false }, pause: { pressed: true },
    } }] }) },
  });
  let frame = input.read();
  assert.equal(frame.throttle, 1); assert.equal(frame.steer, 0.6);
  assert.equal(frame.lasso, true); assert.equal(frame.pause, true);
  input.gamepad = { poll: () => ({ players: [] }) };
  input.touchShoot = true;
  frame = input.read();
  assert.equal(frame.throttle, 1); assert.equal(frame.shoot, true); assert.equal(frame.confirm, true);
  assert.equal(input.read().shoot, false);
});