@erpg/dicecore
v3.6.0
Published
ERPG dice core with ergonomic RPG notation, grouped rolls, structured UI data, deterministic replay, and safe parse-only inspection.
Maintainers
Readme
@erpg/dicecore 3.5.0
Núcleo de dados do ERPG para compilar, inspecionar e resolver notações de dados. A V3 é escrita em TypeScript estrito, não mantém estado global de RNG e entrega resultados readonly, JSON-safe e próprios para frontend, backend, automações e visualização 3D.
Requisitos e formatos
- Node.js 22 ou mais recente;
- browsers com ES2022 e
globalThis.crypto.getRandomValues; - ESM, CommonJS e declarações TypeScript;
- zero dependências de runtime no pacote publicado.
npm install @erpg/dicecoreimport { rollRpgDice } from '@erpg/dicecore';
const result = rollRpgDice('2d6+3', { seed: 'encontro-42' });
console.log(result.total, result.replay);const { rollRpgDice } = require('@erpg/dicecore');
const result = rollRpgDice('1d20+5', { seed: 'ataque-1' });Para reduzir o código carregado, use um entrypoint suportado em vez do pacote completo:
import { createDiceEngine, rollRpgDice } from '@erpg/dicecore/core';
import { rollFateDice } from '@erpg/dicecore/systems/fate';Também estão disponíveis @erpg/dicecore/systems,
@erpg/dicecore/systems/assimilation, /daggerheart, /mixed e
/vampire-v5. Outros imports profundos, global UMD e as classes internas da V2
não fazem parte da API pública.
API pública
import {
compileRpgDice,
createDiceEngine,
createSystemRoller,
DICE_LIMIT_PRESETS,
evaluateAssimilationSelection,
inspectRpgDiceNotation,
isDiceRollError,
isDiceRollErrorData,
normalizeRpgDiceNotation,
rollAssimilation,
rollDaggerheart,
rollFateDice,
rollMixedDice,
rollRpgDice,
rollRpgDiceDetails,
rollRpgDiceSummary,
rollVampireV5,
verifyRpgDiceNotation,
} from '@erpg/dicecore';
const plan = compileRpgDice('2#4d6kh3 [atributos]');
const result = rollRpgDice(plan, { seed: 'personagem-42' });
const inspection = inspectRpgDiceNotation('5d10>=8f=1');
if (inspection.isValid && inspection.plan) {
console.log(inspection.cost, rollRpgDice(inspection.plan).pool);
}
console.log(verifyRpgDiceNotation('d20+5')); // true
console.log(normalizeRpgDiceNotation('d + 2f')); // d20+2dF
try {
rollRpgDice('10001d6');
} catch (error: unknown) {
if (isDiceRollError(error)) {
console.error(error.code, error.span, error.details);
}
}As funções públicas são:
compileRpgDice(input, options?): normaliza, valida e compila uma vez para umRollPlanimutável;createDiceEngine(options?): cria limites, cache de planos e política de congelamento isolados;inspectRpgDiceNotation(input, options?): valida sem rolar e retorna plano, grupos e estimativa de custo;rollRpgDice(inputOrPlan, options?): resolve a fórmula e sempre inclui um descritor de replay;rollRpgDiceDetails(inputOrPlan, options?): preserva rolls e dados resolvidos sem materializar grupos, eventos ou output;rollRpgDiceSummary(inputOrPlan, options?): resolve total, rolls, pool, replay e stats sem materializar dados, grupos, eventos ou output;rollVampireV5(input, options?): rola e avalia um pool de Vampiro V5 com dados normais e de Fome;rollAssimilation(input, options?): rola os dados especiais de Assimilação sem escolher faces automaticamente;evaluateAssimilationSelection(roll, selectedIds): seleciona atékeepIDs únicos e agrega seus símbolos;rollFateDice(input?, options?): rola quatro dados Fate por padrão, preserva as faces físicas e soma-1,0e+1;rollDaggerheart(input?, options?): rola os dois d12 de Duality Dice, aplica modificador e resolve Esperança/Medo, sucesso, falha ou crítico;rollMixedDice(notation, options?): executa fórmulas genéricas e sistemas diferentes em um único lote 2D/3D;createSystemRoller(engine): vincula todas as APIs de sistema ao engine configurado;verifyRpgDiceNotation(input, options?): atalho booleano de validação;normalizeRpgDiceNotation(input): aplica os atalhos de escrita do ERPG;isDiceRollError(error): type guard para os erros estruturados da V3.isDiceRollErrorData(value): valida erros transportados por JSON; restaure-os comDiceRollError.fromJSON().
Um engine é indicado quando a aplicação precisa definir tetos próprios ou reutilizar planos com frequência:
const dice = createDiceEngine({
limits: DICE_LIMIT_PRESETS.untrustedServer,
cache: {
maxInputEntries: 300,
maxProgramEntries: 100,
maxProgramNodes: 50_000,
},
randomAlgorithm: 'xoshiro128ss',
freezeResults: 'development',
});
const systems = createSystemRoller(dice);
const fate = systems.rollFateDice(undefined, {
seed: 'combate-17:fate',
detail: 'compact',
});
const plan = dice.compile('3#2d20kh1+5');
const result = dice.roll(plan, {
seed: 'combate-17',
limits: { maxRolls: 3 },
});Limites informados em uma chamada podem apenas reduzir os tetos do engine. O plano original reutiliza a representação compilada; cópias via JSON ou spread são validadas e recompiladas pelo engine de destino antes da execução. DICE_LIMIT_PRESETS oferece políticas para browser, servidor confiável e entrada não confiável; copie e sobrescreva somente os tetos que sua aplicação precisa reduzir.
Notação mista para 2D e 3D
Use ; para separar rolagens independentes que devem acontecer e aparecer
juntas. O + continua sendo aritmética dentro de uma fórmula:
const mixed = rollMixedDice(
'2d20+5; '
+ 'v5(pool=7,hunger=3,difficulty=4); '
+ 'fate(4); '
+ 'assim(d6=2,d10=1,d12=1,keep=1); '
+ 'daggerheart(modifier=2,difficulty=15)',
{ seed: 'sessao-42', detail: 'compact', randomAlgorithm: 'xoshiro128ss' },
);
console.log(mixed.rolls); // resultados completos de cada sistema
console.log(mixed.dice); // lista plana para UI 2D/3D
console.log(mixed.output); // resumo legível do loteNo modo compact, apenas os segmentos de sistema substituem o baseRoll
completo por um DiceRollSummary; os dados semânticos e os segmentos genéricos
continuam completos para animação. Use detail: 'full' quando a UI realmente
precisar dos grupos, eventos e output internos de cada sistema.
Chamadas aceitas:
| Sistema | Posicional | Nomeada |
| --- | --- | --- |
| Vampiro V5 | v5(7,3,4) | v5(pool=7,hunger=3,difficulty=4) |
| Fate | fate(4) ou fate() | fate(dice=4) |
| Assimilação | AS(2,1,1,1) ou assim(2,1,1,1) | AS(d6=2,d10=1,d12=1,keep=1) |
| Daggerheart | dh() ou dagger(2,15) | daggerheart(modifier=2,difficulty=15) |
Também são reconhecidos AS, vampiro, vampire, fatedice, assimilacao e
assimilation. Cada trecho genérico aceita a notação V3 completa, inclusive
modificadores, pools, funções e multi-roll.
O lote não possui um total geral: sucessos de Vampiro, valor Fate, símbolos
de Assimilação e totais numéricos não são grandezas equivalentes. Cada valor
permanece em mixed.rolls; mixed.dice contém IDs únicos, physicalValue e
os perfis necessários para desenhar todos os dados juntos. O replay do lote
restaura todos os sub-resultados:
const replayed = rollMixedDice(mixed.input, { replay: mixed.replay });Sistemas com faces simbólicas
As APIs de sistema usam o mesmo RNG, limites e replay da V3, mas projetam cada
ResolvedDie num SystemDieResult. O core entrega apenas IDs semânticos; SVGs,
texturas e materiais continuam sob responsabilidade do visualizador 3D.
const fate = rollFateDice(undefined, { seed: 'fate-42' });
console.log(fate.total);
console.log(fate.dice.map(({ rawValue, faceKey, fateValue }) => ({
rawValue,
faceKey,
fateValue,
})));Fate usa o perfil fate-df. O core rola d6 físicos para manter rawValue e
value entre 1 e 6: faces 1–2 são minus, 3–4 são blank e 5–6 são plus.
fateValue contém -1, 0 ou 1; total soma esses valores. A rolagem
genérica dF continua disponível, mas sua face física já é convertida para
-1, 0 ou 1, por isso a API semântica usa d6 no baseRoll.
const vampire = rollVampireV5(
{ pool: 7, hunger: 3, difficulty: 4 },
{ seed: 'sessao-12' },
);
console.log(vampire.successes, vampire.outcome);
console.log(vampire.dice.map(({ id, sourceDieId, profileId, faceKey }) => ({
id,
sourceDieId,
profileId,
faceKey,
})));Vampiro V5 usa os perfis vampire-v5-normal-d10 e
vampire-v5-hunger-d10. Dados 6–9 valem um sucesso; cada 10 também vale um,
e cada par de 10 acrescenta dois sucessos. Sem difficulty, o desfecho é
pending. Com dificuldade, o resultado distingue sucesso, sucesso crítico,
crítico bagunçado, falha e falha bestial.
const assimilation = rollAssimilation(
{ d6: 2, d10: 1, d12: 1, keep: 2 },
{ seed: 'teste-isolado' },
);
// A rolagem nunca escolhe dados automaticamente.
const chosen = evaluateAssimilationSelection(
assimilation,
[assimilation.dice[3].id, assimilation.dice[1].id],
);
console.log(chosen.success, chosen.adaptation, chosen.pressure);Assimilação usa assimilation-d6, assimilation-d10 e assimilation-d12.
Cada dado semântico inclui id, sourceDieId, sides, value, rawValue,
profileId, dieKind, faceKey e symbols. sourceDieId referencia
diretamente um item de baseRoll.dice; id é a chave estável que deve ser
usada na seleção e no visualizador.
O @erpg/dice3dview reconhece os perfis de Vampiro V5 e Assimilação a partir
da versão 2.3.0 e o perfil Fate a partir da 2.4.0, sem depender do core em
runtime:
import { createSystemDisplayRequest } from '@erpg/dice3dview';
await viewer.display(createSystemDisplayRequest({
id: 'resultado-42',
dice: assimilation.dice,
keptIds: chosen.selectedIds,
}));Lotes mistos usam @erpg/dice3dview 2.5.0 ou posterior:
import { createMixedDisplayRequest } from '@erpg/dice3dview';
await viewer.display(createMixedDisplayRequest({
id: 'misto-42',
seed: 'misto-42',
dice: mixed.dice,
}));Daggerheart
const daggerheart = rollDaggerheart(
{ modifier: 2, difficulty: 15 },
{ seed: 'duality-42' },
);
console.log(daggerheart.hopeDie.rawValue, daggerheart.fearDie.rawValue);
console.log(daggerheart.total, daggerheart.outcome);rollDaggerheart() sempre produz um d12 de Esperança com perfil
daggerheart-hope-d12 e um d12 de Medo com perfil daggerheart-fear-d12.
O total é a soma das duas faces e do modificador. Se os valores forem iguais,
o resultado é critical-success; caso contrário, a face maior define se a
ação foi feita com Esperança ou Medo. Sem difficulty, o resultado fica
pendente para Dificuldades secretas.
Apresentação 3D de Daggerheart
O @erpg/dice3dview com estes perfis reconhece os dois resultados como d12
default-v2. A aplicação pode passar themeColor por dado para usar a cor da
skin em Esperança e a inversa RGB em Medo, sem alterar os valores autoritativos.
Limites de segurança
As opções usam limits, tanto no engine quanto por chamada:
const result = rollRpgDice('3#2d6!', {
seed: 'teste-de-carga',
limits: {
maxInputLength: 4_096,
maxAstDepth: 64,
maxAstNodes: 10_000,
maxRolls: 3,
maxInitialDice: 100,
maxGeneratedDice: 500,
maxRandomCalls: 2_000,
maxEvents: 2_000,
maxSides: 4_294_967_296,
maxSeedLength: 1_024,
maxModifierSteps: 10_000,
maxResolvedGroups: 10_000,
maxResultItems: 25_000,
maxOutputLength: 100_000,
},
});Os padrões completos estão em DEFAULT_DICE_LIMITS; políticas prontas estão em
DICE_LIMIT_PRESETS. A inspeção calcula custo estático e pior caso; parser e
executor também aplicam orçamentos duros durante a construção e a execução.
Resultado V3
interface DiceRollResult {
readonly type: 'dice-roll';
readonly schemaVersion: 3;
readonly input: string;
readonly notation: string;
readonly normalizedNotation: string;
readonly comment: string;
readonly total: number;
readonly output: string;
readonly replay: ReplayDescriptor;
readonly stats: ExecutionStats;
readonly rolls: readonly ResolvedRoll[];
readonly groups: readonly ResolvedGroup[];
readonly dice: readonly ResolvedDie[];
readonly events: readonly DiceEvent[];
readonly pool: PoolSummary | null;
}rollssepara cada execução deN#formulapor ranges contíguos (diceRange,groupRange,eventRange); ototalraiz é a soma de seus totais;groupsresolve dados, expressões, funções e grupos com IDs eSourceSpanestáveis;dicesepara face inicial (rawValue), valor final (value), contribuição e inclusão;eventsregistra rolls, rerolls, explosões, transformações, inclusão, exclusão e classificação de dados e grupos na ordem do executor. Consumidores visuais devem reconstruir dependências por ID: orollde um filho explosivo é registrado antes do eventoexplodeque o liga ao pai;poolénullsem target e agrega sucessos/falhas quando a notação usa um target.
Veja o contrato completo em docs/API_V3.md e a atualização de consumidores em docs/MIGRATION_V3.md.
Seed e replay
Toda rolagem retorna:
interface ReplayDescriptor {
readonly schemaVersion: 2;
readonly algorithm: 'mt19937' | 'xoshiro128ss';
readonly algorithmVersion: 1;
readonly executionVersion: 1;
readonly mathProfile: 'decimal12-v1';
readonly origin: 'provided-number' | 'provided-string' | 'crypto';
readonly seedMaterial: string; // 32 hex, sem a seed textual
readonly planFingerprint: string; // 32 hex
}Com seed: string | number, a mesma fórmula, seed de entrada e versão do core produzem a mesma sequência. Sem seed, o core gera 128 bits com crypto.getRandomValues; não há fallback para Math.random. O descritor retornado pode reproduzir exatamente qualquer uma das duas origens:
const first = rollRpgDice('2d20kh1');
const repeated = rollRpgDice('2d20kh1', { replay: first.replay });seed e replay são mutuamente exclusivos. O replay já é vinculado ao fingerprint da fórmula; outra fórmula falha com REPLAY_PLAN_MISMATCH. MT19937 permanece o padrão e xoshiro128ss é opt-in.
Notação ERPG
A normalização preserva atalhos comuns:
d→d20,2d→2d20;f→4dF,2f→2dF,df→dF;ei6→!>=6;km→kl, ek,khouklsem quantidade recebem1;-pool(N)e+pool(N)→pool(-N)epool(+N);pulltambém é aceito como alias de entrada;- combinações simples como
+-,-+,++e--são limpas; N#formulaexecuta rolagens independentes;Ntambém pode ser uma expressão matemática determinística, como(3-1)#1d20(com(),{}ou[]para agrupamento);- comentários podem usar
[texto],//,#ou/* ... */conforme o contexto.
A sintaxe inclui dados padrão, percentuais e Fudge; aritmética e funções; grupos; keep/drop; reroll/unique; explode/compound/penetrate; min/max; critical; sort; e targets de sucesso/falha.
Condições estruturais de pool
Os sufixos pool(±N), adv e dis são resolvidos antes da rolagem. Quantidade compilada, limites, custo, RNG, replay e stats.initialDice refletem os dados realmente rolados.
rollRpgDice('2d8-pool(1)'); // 1d8 físico
rollRpgDice('1d20adv+5'); // 2d20, mantém o maior, depois soma 5
rollRpgDice('1d20-pool(1)'); // saldo 0: 2d20, mantém o menor
rollRpgDice('1d20-pool(2)'); // saldo -1: 3d20, mantém o menor
rollRpgDice('1d20-pool(1)adv'); // a vantagem cancela a desvantagem: 1d20
rollRpgDice('1d20+2-pool(1)'); // desvantagem no d20 e +2 no totalO saldo começa na quantidade escrita e recebe os ajustes pool. Se for positivo, vira a nova quantidade física. Zero é a primeira desvantagem; abaixo disso, um saldo P <= 0 rola 2 - P dados e mantém o menor. Cada adv soma um nível e cada dis subtrai um; condições repetidas acumulam e opostas se cancelam. Um saldo de seleção não nulo não pode ser combinado com keep/drop explícito.
O sufixo pode acompanhar o dado (1d20adv+5) ou ficar no fim de uma expressão que contenha exatamente um dado (1d20+5adv, 1d20+2-pool(1)). Em expressões com vários dados, ele precisa ficar junto do dado-alvo para evitar ambiguidade. O modificador pool(...) trata a quantidade física; a propriedade result.pool continua sendo exclusivamente o resumo de sucessos e falhas criado por targets.
Passos de dado
O modificador estrutural step(±N) promove ou rebaixa os lados antes da rolagem pela escada d2 → d4 → d6 → d8 → d10 → d12 → d20 → d100. Um dado que esteja entre dois degraus segue para o primeiro estritamente maior ou menor na direção escolhida.
rollRpgDice('1d5step(+1)'); // 1d6
rollRpgDice('1d5step(+2)'); // 1d8
rollRpgDice('1d5step(-1)'); // 1d4
rollRpgDice('1d5+2+step(1)'); // 1d6 + 2
rollRpgDice('1d5step(+2)step(-1)'); // saldo +1: 1d6As formas amigáveis +step(N) e -step(N) são normalizadas para step(+N) e step(-N). Todos os passos do mesmo dado são somados antes da transformação, portanto passos opostos se cancelam. O sufixo terminal segue a mesma regra de alvo único de pool; com vários dados deve acompanhar o dado alterado. A primeira versão aceita apenas dados numéricos padrão. d%, dF e movimentos além das extremidades da escada falham explicitamente, sem saturação silenciosa.
Explosões aceitam um teto inteiro positivo opcional logo após !, !!, !p ou !!p. d6!2 gera no máximo dois dados adicionais por dado original; o teto também pode ser combinado com um gatilho (d6!2>=5), compound (d6!!2) e penetrate (d6!p2 ou d6!!p2). Sem o número, as explosões continuam encadeando normalmente e permanecem sujeitas aos limites globais do engine. Para manter a compilação limitada, interações excepcionalmente amplas entre um teto e reroll/unique contínuos falham de forma segura com UNSUPPORTED_NOTATION quando excedem o domínio de análise semântica.
rollRpgDice('4d6kh3');
rollRpgDice('1d%+2dF');
rollRpgDice('5d10>=8f=1');
rollRpgDice('{1d8,1d10}kh1');
rollRpgDice('ceil(1d6/2)+pow(2,3)');Integração com dice3dview
O dicecore decide o resultado; o dice3dview apenas o apresenta. A API de timeline recebe definições visuais e o journal resolvido sem interpretar a notação novamente:
const result = rollRpgDice('2d6!kh2', { seed: 'cena-9' });
const supportedSides = new Set([2, 4, 6, 8, 10, 12, 20, 100]);
const visualDice = result.dice.filter(
(die) => typeof die.sides === 'number' && supportedSides.has(die.sides),
);
const visualIds = new Set(visualDice.map((die) => die.id));
await dice3dview.displayTimeline({
id: 'cena-9',
seed: 'cena-9',
dice: visualDice.map((die) => ({ id: die.id, sides: die.sides })),
events: result.events.filter(
(event) => event.subject === 'die' && visualIds.has(event.dieId),
),
});
const scoring = result.dice.map((die) => ({
id: die.id,
parentDieId: die.parentDieId,
value: die.value,
included: die.included,
contribution: die.contribution,
}));O viewer pré-compila o journal inteiro porque o roll de um filho aparece antes do explode correspondente. Dados excluídos ainda podem ser animados, mas included e contribution governam a pontuação. parentDieId liga uma explosão ao dado que a originou. transform representa valor semântico: compound pode ultrapassar o número de faces e penetrate pode chegar a zero, portanto esses valores nunca devem ser usados como face física.
Desenvolvimento da V3
O parser e o MT19937 da V3 são implementações TypeScript do próprio pacote. Vitest, o perfil TypeScript estrito e as verificações do tarball fazem parte da toolchain. dist/ é sempre gerado e não é versionado.
O núcleo V2 não é exportado pelo pacote V3. Ele permanece no repositório apenas como corpus de compatibilidade durante o desenvolvimento e pode ser removido quando a migração for encerrada.
Atribuição e licença
Este pacote é um derivado mantido pelo ERPG a partir do projeto open source @dice-roller/rpg-dice-roller, de GreenImp. A V3 substitui o parser e o runtime publicados por implementações próprias em TypeScript, mantendo o crédito e o aviso original em licence.txt.
Licença MIT.
