refactor(dice): extract engine from md-commander
The dice engine lived under md-commander/hooks but is shared by :md-dice, spark tables, the journal /roll command, and dice inside variable expressions. Move it to src/components/dice so the md-commander widget can be retired without losing rolling. Also promote the browser-console harness to a real jest suite; the file was named test.ts, so jest never ran it.
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
import type { Token, DicePool } from "./types";
|
||||
|
||||
/**
|
||||
* 词法分析器 - 将输入字符串转换为令牌流
|
||||
*/
|
||||
export function tokenize(input: string): Token[] {
|
||||
const tokens: Token[] = [];
|
||||
let i = 0;
|
||||
const str = input.replace(/\s+/g, ""); // 移除所有空格
|
||||
|
||||
while (i < str.length) {
|
||||
const char = str[i];
|
||||
|
||||
// 修饰符 kh/kl/dh/dl - 必须在检查 'd' 骰子之前检查
|
||||
if (char === "k" || char === "d") {
|
||||
const rest = str.slice(i).toLowerCase();
|
||||
if (rest.startsWith("kh")) {
|
||||
i += 2;
|
||||
let countStr = "";
|
||||
while (i < str.length && /\d/.test(str[i])) {
|
||||
countStr += str[i];
|
||||
i++;
|
||||
}
|
||||
tokens.push({ type: "modifier", modType: "kh", count: parseInt(countStr) || 1 });
|
||||
continue;
|
||||
} else if (rest.startsWith("kl")) {
|
||||
i += 2;
|
||||
let countStr = "";
|
||||
while (i < str.length && /\d/.test(str[i])) {
|
||||
countStr += str[i];
|
||||
i++;
|
||||
}
|
||||
tokens.push({ type: "modifier", modType: "kl", count: parseInt(countStr) || 1 });
|
||||
continue;
|
||||
} else if (rest.startsWith("dh")) {
|
||||
i += 2;
|
||||
let countStr = "";
|
||||
while (i < str.length && /\d/.test(str[i])) {
|
||||
countStr += str[i];
|
||||
i++;
|
||||
}
|
||||
tokens.push({ type: "modifier", modType: "dh", count: parseInt(countStr) || 1 });
|
||||
continue;
|
||||
} else if (rest.startsWith("dl")) {
|
||||
i += 2;
|
||||
let countStr = "";
|
||||
while (i < str.length && /\d/.test(str[i])) {
|
||||
countStr += str[i];
|
||||
i++;
|
||||
}
|
||||
tokens.push({ type: "modifier", modType: "dl", count: parseInt(countStr) || 1 });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 数字字面量
|
||||
if (/\d/.test(char)) {
|
||||
let numStr = "";
|
||||
while (i < str.length && /\d/.test(str[i])) {
|
||||
numStr += str[i];
|
||||
i++;
|
||||
}
|
||||
const num = parseInt(numStr);
|
||||
|
||||
// 检查是否是 NdX 格式
|
||||
if (str[i] === "d" || str[i] === "D") {
|
||||
i++; // 跳过 'd'
|
||||
let sidesStr = "";
|
||||
while (i < str.length && /\d/.test(str[i])) {
|
||||
sidesStr += str[i];
|
||||
i++;
|
||||
}
|
||||
const sides = parseInt(sidesStr);
|
||||
tokens.push({ type: "dice", count: num, sides });
|
||||
} else {
|
||||
tokens.push({ type: "number", value: num });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 单独的 dX 格式(等同于 1dX)
|
||||
if (char === "d" || char === "D") {
|
||||
i++;
|
||||
let sidesStr = "";
|
||||
while (i < str.length && /\d/.test(str[i])) {
|
||||
sidesStr += str[i];
|
||||
i++;
|
||||
}
|
||||
if (sidesStr.length > 0) {
|
||||
const sides = parseInt(sidesStr);
|
||||
tokens.push({ type: "dice", count: 1, sides });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 骰池开始
|
||||
if (char === "{") {
|
||||
tokens.push({ type: "pool_start" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 骰池结束
|
||||
if (char === "}") {
|
||||
tokens.push({ type: "pool_end" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 逗号
|
||||
if (char === ",") {
|
||||
tokens.push({ type: "comma" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 加号
|
||||
if (char === "+") {
|
||||
tokens.push({ type: "plus" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 减号
|
||||
if (char === "-") {
|
||||
tokens.push({ type: "minus" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 未知字符,跳过
|
||||
i++;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* 语法分析器 - 将令牌流转换为骰池列表
|
||||
*/
|
||||
export function parse(tokens: Token[]): DicePool[] {
|
||||
const pools: DicePool[] = [];
|
||||
let i = 0;
|
||||
let currentSign = 1; // 当前符号,1 为正,-1 为负
|
||||
|
||||
while (i < tokens.length) {
|
||||
const token = tokens[i];
|
||||
|
||||
// 处理运算符
|
||||
if (token.type === "plus") {
|
||||
currentSign = 1;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token.type === "minus") {
|
||||
currentSign = -1;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析骰池
|
||||
const pool = parsePool(tokens, i, currentSign);
|
||||
if (pool) {
|
||||
pools.push(pool.pool);
|
||||
i = pool.nextIndex;
|
||||
currentSign = 1; // 重置符号
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return pools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个骰池
|
||||
*/
|
||||
function parsePool(
|
||||
tokens: Token[],
|
||||
startIndex: number,
|
||||
sign: number
|
||||
): { pool: DicePool; nextIndex: number } | null {
|
||||
const dice: DicePool["dice"] = [];
|
||||
let i = startIndex;
|
||||
let modifier: DicePool["modifier"] = undefined;
|
||||
|
||||
const token = tokens[i];
|
||||
if (!token) return null;
|
||||
|
||||
// 骰池字面量 {d4, d6, ...}
|
||||
if (token.type === "pool_start") {
|
||||
i++; // 跳过 {
|
||||
while (i < tokens.length && tokens[i].type !== "pool_end") {
|
||||
if (tokens[i].type === "comma") {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const token = tokens[i];
|
||||
if (token.type === "dice") {
|
||||
for (let j = 0; j < token.count; j++) {
|
||||
dice.push({
|
||||
sides: token.sides,
|
||||
value: 0,
|
||||
isNegative: sign < 0,
|
||||
});
|
||||
}
|
||||
i++;
|
||||
} else if (token.type === "number") {
|
||||
// 数字作为 0 面骰子(固定值)
|
||||
dice.push({
|
||||
sides: 0,
|
||||
value: token.value,
|
||||
isNegative: sign < 0,
|
||||
});
|
||||
i++;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
i++; // 跳过 }
|
||||
}
|
||||
// 单个骰子或数字
|
||||
else if (token.type === "dice") {
|
||||
for (let j = 0; j < token.count; j++) {
|
||||
dice.push({
|
||||
sides: token.sides,
|
||||
value: 0,
|
||||
isNegative: sign < 0,
|
||||
});
|
||||
}
|
||||
i++;
|
||||
} else if (token.type === "number") {
|
||||
dice.push({
|
||||
sides: 0,
|
||||
value: token.value,
|
||||
isNegative: sign < 0,
|
||||
});
|
||||
i++;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查修饰符
|
||||
if (i < tokens.length && tokens[i].type === "modifier") {
|
||||
const mod = tokens[i] as Extract<Token, { type: "modifier" }>;
|
||||
modifier = { type: mod.modType, count: mod.count };
|
||||
i++;
|
||||
}
|
||||
|
||||
return {
|
||||
pool: { dice, modifier, isNegative: sign < 0 },
|
||||
nextIndex: i,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user