163 lines
4.6 KiB
TypeScript
163 lines
4.6 KiB
TypeScript
type DiceType = 'f' | 'r' | 't';
|
|
|
|
interface RollResult {
|
|
input: string;
|
|
value: number;
|
|
rolls: number[]; // Kept dice
|
|
discarded: number[]; // Discarded dice
|
|
summary: string; // Formatted string of all dice
|
|
steps?: string[]; // Arithmetic operations performed
|
|
}
|
|
|
|
function rollDie(diceType: DiceType): number {
|
|
const roll = {
|
|
f: Math.floor(Math.random() * 12) + 1 - 6,
|
|
r: Math.floor(Math.random() * 18) + 1 - 9,
|
|
t: Math.floor(Math.random() * 24) + 1 - 12,
|
|
};
|
|
return roll[diceType];
|
|
}
|
|
|
|
function rollDice(count: number, diceType: DiceType): number[] {
|
|
return Array.from({ length: count }, () => rollDie(diceType));
|
|
}
|
|
|
|
function getDieRange(diceType: DiceType): { min: number; max: number } {
|
|
const ranges = {
|
|
f: { min: -5, max: 6 },
|
|
r: { min: -8, max: 9 },
|
|
t: { min: -11, max: 12 },
|
|
};
|
|
return ranges[diceType];
|
|
}
|
|
|
|
function formatSummary(rolls: number[], discarded: number[], diceType: DiceType): string {
|
|
const { min, max } = getDieRange(diceType);
|
|
const all = rolls.map(r => ({ value: r, kept: true }))
|
|
.concat(discarded.map(r => ({ value: r, kept: false })));
|
|
|
|
return all
|
|
.sort((a, b) => b.value - a.value)
|
|
.map(({ value, kept }) => {
|
|
const isMinOrMax = value === min || value === max;
|
|
let formatted = `${value}`;
|
|
if (isMinOrMax) formatted = `**${formatted}**`;
|
|
if (!kept) formatted = `~${formatted}~`;
|
|
return formatted;
|
|
})
|
|
.join(' ');
|
|
}
|
|
|
|
function parseExpression(input: string): RollResult | RollResult[] {
|
|
const batchMatch = input.match(/^(\d+)#(.+)$/);
|
|
if (batchMatch) {
|
|
const [, batchCountStr, innerExpr] = batchMatch;
|
|
const batchCount = parseInt(batchCountStr, 10);
|
|
return Array.from({ length: batchCount }, () => parseSingleExpression(innerExpr));
|
|
} else {
|
|
return parseSingleExpression(input);
|
|
}
|
|
}
|
|
|
|
function parseSingleExpression(expr: string): RollResult {
|
|
const baseMatch = expr.match(/^(\d+)?([frt])(?:(kh|kl)(\d+))?/);
|
|
if (!baseMatch) throw new Error(`Invalid expression: ${expr}`);
|
|
|
|
const [, countStr, diceType, keepType, keepCountStr] = baseMatch;
|
|
const diceExprLength = baseMatch[0].length;
|
|
const arithmeticExpr = expr.slice(diceExprLength);
|
|
|
|
const count = parseInt(countStr || '1', 10);
|
|
const dice = diceType as DiceType;
|
|
const keepCount = keepCountStr ? parseInt(keepCountStr, 10) : null;
|
|
|
|
const allRolls = rollDice(count, dice);
|
|
let rolls = [...allRolls];
|
|
let discarded: number[] = [];
|
|
|
|
if (keepType && keepCount !== null) {
|
|
const sorted = [...allRolls].sort((a, b) => keepType === 'kh' ? b - a : a - b);
|
|
rolls = sorted.slice(0, keepCount);
|
|
discarded = sorted.slice(keepCount);
|
|
}
|
|
|
|
const steps: string[] = [];
|
|
let result = rolls.reduce((acc, val) => acc + val, 0);
|
|
steps.push(`Initial sum: ${result}`);
|
|
|
|
const opRegex = /([+\-*/])\s*(-?\d+(?:\.\d+)?)/g;
|
|
let match;
|
|
while ((match = opRegex.exec(arithmeticExpr)) !== null) {
|
|
const [, op, valStr] = match;
|
|
const val = parseFloat(valStr);
|
|
const prev = result;
|
|
switch (op) {
|
|
case '+': result += val; break;
|
|
case '-': result -= val; break;
|
|
case '*': result *= val; break;
|
|
case '/': result /= val; break;
|
|
}
|
|
steps.push(`${prev} ${op} ${val} = ${result}`);
|
|
}
|
|
|
|
return {
|
|
input: expr,
|
|
value: result,
|
|
rolls,
|
|
discarded,
|
|
summary: formatSummary(rolls, discarded, dice),
|
|
steps
|
|
};
|
|
}
|
|
|
|
function safeParseInput(message: string): RollResult | RollResult[] | null {
|
|
const trimmed = message.trim();
|
|
const isSingleWord = !trimmed.includes(' ');
|
|
|
|
if (isSingleWord && !trimmed.startsWith('[') && !trimmed.endsWith(']')) {
|
|
try {
|
|
return parseExpression(trimmed);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (isSingleWord && trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
const inside = trimmed.slice(1, -1).trim();
|
|
try {
|
|
return parseExpression(inside);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const words = trimmed.split(/\s+/);
|
|
const expressions = words
|
|
.filter(word => word.startsWith('[') && word.endsWith(']'))
|
|
.map(word => word.slice(1, -1).trim());
|
|
|
|
if (expressions.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const results: RollResult[] = [];
|
|
|
|
for (const expr of expressions) {
|
|
try {
|
|
const result = parseExpression(expr);
|
|
if (Array.isArray(result)) {
|
|
results.push(...result);
|
|
} else {
|
|
results.push(result);
|
|
}
|
|
} catch {
|
|
// skip invalid
|
|
}
|
|
}
|
|
|
|
return results.length > 0 ? results : null;
|
|
}
|
|
|
|
console.log(safeParseInput('Hello I roll a 5rkh3+1'));
|
|
console.log(safeParseInput('I ROLL asdf[2#3fkl1*2]asdf'));
|
|
console.log(safeParseInput('[2#3fkl1*2] [2#3fkl1*2]')); |