Created
February 8, 2025 02:25
-
-
Save itsjohncs/70d3625a3239897afdece812b2a830dd to your computer and use it in GitHub Desktop.
Dice roller parsing in Shmeppy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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