import {
type ASTNode, isBinaryOp, isDice, isFateDice, isGroup, isGrouped,
isFunctionCall, isLiteral, isUnaryOp, isVersus, parse,
} from 'roll-parser';
function countPools(node: ASTNode): number {
if (isDice(node) || isFateDice(node)) return 1;
if (isLiteral(node)) return 0;
if (isBinaryOp(node)) return countPools(node.left) + countPools(node.right);
if (isUnaryOp(node)) return countPools(node.operand);
if (isGrouped(node)) return countPools(node.expression);
if (isVersus(node)) return countPools(node.roll) + countPools(node.dc);
if (isGroup(node)) return node.expressions.reduce((n, e) => n + countPools(e), 0);
if (isFunctionCall(node)) return node.args.reduce((n, a) => n + countPools(a), 0);
// Every remaining variant is a postfix modifier wrapping `target`.
return 'target' in node ? countPools(node.target) : 0;
}
countPools(parse('2d6+3')); // 1
countPools(parse('{2d20kh1+5, 3d8!}kh1')); // 2
countPools(parse('1+2')); // 0
Discriminated union of all 17 AST node types — what parse returns and what evaluate consumes.
Narrow it either by switching on
node.type(PascalCase discriminants, as opposed to the camelCase ones on RollPart) or with the exported type guards: isLiteral, isDice, isFateDice, isBinaryOp, isUnaryOp, isKeepDrop, isExplode, isReroll, isDieBound, isSuccessCount, isVersus, isFunctionCall, isGrouped, isVariable, isGroup, isSort, isCritThreshold.Nodes are plain data with no methods, so they are structurally clonable and safe to cache. Every parser-produced node carries a NodeSpan.