From 4ebf8d3994298012ecb8c9aa304fcd557dd57ee2 Mon Sep 17 00:00:00 2001 From: Tim Evans Date: Mon, 20 Jul 2026 16:15:02 +0100 Subject: [PATCH] feat: rule-pack contract v2 (params + summary) and registry listing --- src/components/dice-tray.js | 7 ++++--- src/dice/rulepacks/index.js | 3 +++ src/dice/rulepacks/year-zero.js | 7 ++++++- tests/dice/rulepacks-registry.test.js | 9 +++++++++ tests/dice/year-zero.test.js | 12 ++++++++++++ 5 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 tests/dice/rulepacks-registry.test.js diff --git a/src/components/dice-tray.js b/src/components/dice-tray.js index 141ff22..f080334 100644 --- a/src/components/dice-tray.js +++ b/src/components/dice-tray.js @@ -11,6 +11,7 @@ export class DiceTray extends HTMLElement { this._systemId = 'year-zero'; this.rng = Math.random; this.last = null; + this.params = {}; } get sides() { return this._sides; } set sides(v) { this._sides = Number(v); this.render(); } @@ -24,7 +25,7 @@ export class DiceTray extends HTMLElement { roll() { const { results, total } = rollPool([{ sides: this._sides, count: this._count }], this.rng); const pack = getRulePack(this._systemId); - const verdict = pack ? pack.interpret(results) : null; + const verdict = pack ? pack.interpret(results, this.params) : null; this.last = { results, total, verdict }; this.render(); this.dispatchEvent(new CustomEvent('rolled', { detail: this.last, bubbles: true })); @@ -36,9 +37,9 @@ export class DiceTray extends HTMLElement { const felt = this.last ? this.last.results.map((r) => `${r.value}`).join('') : 'Tap Roll'; + const pack = getRulePack(this._systemId); const verdict = this.last?.verdict - ? `${this.last.verdict.successes} success${this.last.verdict.successes === 1 ? '' : 'es'}` + - (this.last.verdict.ones ? ` · ${this.last.verdict.ones} stress` : '') + ? (pack && pack.summary ? pack.summary(this.last.verdict) : '') : ''; this.innerHTML = ` diff --git a/src/dice/rulepacks/index.js b/src/dice/rulepacks/index.js index 32c6973..8432605 100644 --- a/src/dice/rulepacks/index.js +++ b/src/dice/rulepacks/index.js @@ -4,3 +4,6 @@ const packs = new Map([[yearZero.id, yearZero]]); export function getRulePack(id) { return packs.get(id) || null; } export function registerRulePack(pack) { packs.set(pack.id, pack); } +export function listRulePacks() { + return [...packs.values()].map((p) => ({ id: p.id, label: p.label })); +} diff --git a/src/dice/rulepacks/year-zero.js b/src/dice/rulepacks/year-zero.js index 246a085..3c9e5fd 100644 --- a/src/dice/rulepacks/year-zero.js +++ b/src/dice/rulepacks/year-zero.js @@ -1,7 +1,8 @@ export const yearZero = { id: 'year-zero', label: 'Year-Zero', - interpret(results) { + params: [], + interpret(results, params = {}) { let successes = 0, ones = 0; for (const r of results) { if (r.value >= 10) successes += 2; @@ -10,4 +11,8 @@ export const yearZero = { } return { successes, ones, canPush: true, isCritical: successes >= 2 }; }, + summary(v) { + const base = `${v.successes} success${v.successes === 1 ? '' : 'es'}`; + return v.ones ? `${base} · ${v.ones} stress` : base; + }, }; diff --git a/tests/dice/rulepacks-registry.test.js b/tests/dice/rulepacks-registry.test.js new file mode 100644 index 0000000..f84adc8 --- /dev/null +++ b/tests/dice/rulepacks-registry.test.js @@ -0,0 +1,9 @@ +import { describe, it, expect } from 'vitest'; +import { getRulePack, listRulePacks } from '../../src/dice/rulepacks/index.js'; + +describe('rule-pack registry', () => { + it('lists registered packs as {id,label}', () => { + const packs = listRulePacks(); + expect(packs.some((p) => p.id === 'year-zero' && typeof p.label === 'string')).toBe(true); + }); +}); diff --git a/tests/dice/year-zero.test.js b/tests/dice/year-zero.test.js index 7d0971a..9c5b2e7 100644 --- a/tests/dice/year-zero.test.js +++ b/tests/dice/year-zero.test.js @@ -24,3 +24,15 @@ describe('year-zero rule-pack', () => { expect(getRulePack('x')).toBe(custom); }); }); + +describe('year-zero contract v2', () => { + it('exposes an empty params list and a summary string', () => { + expect(yearZero.params).toEqual([]); + expect(yearZero.summary({ successes: 2, ones: 1 })).toBe('2 successes · 1 stress'); + expect(yearZero.summary({ successes: 1, ones: 0 })).toBe('1 success'); + }); + it('interpret ignores a params argument', () => { + const v = yearZero.interpret([{ sides: 8, value: 8 }], { anything: true }); + expect(v.successes).toBe(1); + }); +});