257 lines
5.8 KiB
TypeScript
257 lines
5.8 KiB
TypeScript
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,
|
|
};
|
|
}
|