Skip to content

Instantly share code, notes, and snippets.

@itsjohncs
Created February 8, 2025 02:25
Show Gist options
  • Save itsjohncs/70d3625a3239897afdece812b2a830dd to your computer and use it in GitHub Desktop.
Save itsjohncs/70d3625a3239897afdece812b2a830dd to your computer and use it in GitHub Desktop.
Dice roller parsing in Shmeppy
type Token =
| {type: "slash"; text: "/"}
| {type: "whitespace"; text: string}
| {type: "reason-delimiter"; text: ":"}
| {type: "reason"; text: string}
| {type: "operator"; text: "+" | "-"}
| {type: "roll"; text: string; sides: number; count: number};
export function lex(raw: string): Token[] | undefined {
if (!raw.startsWith("/")) {
return undefined;
}
const tokens: Token[] = [
{
type: "slash",
text: "/",
},
];
let index = 1;
while (index < raw.length) {
const head = raw.substring(index);
const whitespaceMatch = head.match(/^\s+/);
if (whitespaceMatch) {
tokens.push({
type: "whitespace",
text: whitespaceMatch[0],
});
index += whitespaceMatch[0].length;
continue;
}
const reasonMatch = head.match(/^:(\s*.*)/);
if (reasonMatch) {
tokens.push({
type: "reason-delimiter",
text: ":",
});
tokens.push({
type: "reason",
text: reasonMatch[1],
});
index += reasonMatch[0].length;
continue;
}
const operatorMatch = head.match(/^[+-]/);
if (operatorMatch) {
tokens.push({
type: "operator",
text: operatorMatch[0] as "+" | "-",
});
index += operatorMatch[0].length;
continue;
}
const rollMatch = head.match(/^(?:([1-9][0-9]*)?(d))?([1-9][0-9]*)/);
if (rollMatch) {
const rawCount = rollMatch[1];
const rollDelimiter = rollMatch[2];
const rawSides = rollMatch[3];
if (rollDelimiter) {
tokens.push({
type: "roll",
text: rollMatch[0],
sides: parseInt(rawSides, 10),
count: rawCount ? parseInt(rawCount, 10) : 1,
});
} else {
tokens.push({
type: "roll",
text: rollMatch[0],
sides: 1,
count: parseInt(rawSides, 10),
});
}
index += rollMatch[0].length;
continue;
}
return undefined;
}
return tokens;
}
interface ParsedRoll {
diceCounts: Record<number, number>;
reason: string;
}
export function parse(tokens: Token[]): ParsedRoll | undefined {
const result: ParsedRoll = {diceCounts: {}, reason: ""};
let seenFirstRoll = false;
let pendingOperator: "+" | "-" | undefined;
for (const token of tokens) {
if (token.type === "reason") {
result.reason = token.text;
} else if (token.type === "operator") {
pendingOperator = token.text;
} else if (token.type === "roll") {
if (!pendingOperator && !seenFirstRoll) {
pendingOperator = "+";
} else if (!pendingOperator) {
return undefined;
}
result.diceCounts[token.sides] ??= 0;
result.diceCounts[token.sides] +=
token.count * (pendingOperator === "+" ? 1 : -1);
pendingOperator = undefined;
seenFirstRoll = true;
}
}
return result;
}
function addPart(str: string, part: string, sign: number): string {
if (sign >= 0 && str.length === 0) {
return part;
} else {
return `${str}${sign >= 0 ? "+" : "-"}${part}`;
}
}
export function toString(roll: ParsedRoll): string {
const rolls = Object.entries(roll.diceCounts).sort(function (a, b) {
return parseInt(b[0], 10) - parseInt(a[0], 10);
});
let result = "";
for (const [rawSides, count] of rolls) {
const sides = parseInt(rawSides, 10);
if (sides === 1) {
result = addPart(result, `${Math.abs(count)}`, count);
} else if (Math.abs(count) === 1) {
result = addPart(result, `d${rawSides}`, count);
} else {
result = addPart(result, `${Math.abs(count)}d${rawSides}`, count);
}
}
if (roll.reason) {
return `/${result}:${roll.reason}`;
} else {
return `/${result}`;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment