# 用 RegExp 實作骰子運算 Elantris [TOC] --- ## 前言 遊玩桌遊時很常使用骰子公式(Dice Expression)的形式表達需要丟擲的骰子種類數量以及對應的運算,2021 年我有幸玩到一場經典角色扮演桌遊龍與地下城(Dungeon and Dragon),當時的 DM(Dungeon Master)使用線上軟體 MapTool 當作載體,讓玩家可以操作自己的角色、DM 即時處理場景互動。 電子化的好處是丟擲骰子能夠透過簡單的語法直接在聊天室裡輸入並即時得到計算結果,大幅降低人工丟骰子、處理四則運算的時間成本,讓參與的玩家和主持人更能沉浸在劇情故事中。 MapTool 的聊天室支援基本的 HTML 語法以及特殊指令,例如輸入的文字中包含 `[r: 1d6]` 則會在發送之後被替換成丟擲一顆六面骰的結果。另外還可以寫成 macro 以按鈕的形式包裝這些指令,即使角色施放的技能是一串複雜的計算過程玩家也只需要按下按鈕即可快速得到結果,讓遊戲主持人可以快速判斷角色與世界的互動。 我的目標是寫一隻 [Discord 聊天室機器人](#Discord-Bot),讓跑團的使用者可以直接在文字頻道裡擲骰運算。 ## 骰子公式 最基本的骰子公式為 `XdY`,意即「丟擲 `X` 顆 `Y` 面骰並加總」,常見的正多面體骰子有 `d4`、`d6`、`d8`、`d12`、`d20` 五種。 但是在程式計算的領域內並不受限於幾何圖形規則,只要是隨機結果是公正的,`Y` 可以是任意正整數,`dY` 用來表示值域落在 1 ~ Y 之間的函數。 ### 完整的骰子公式 > MapTool 支援的骰子公式表:[Dice Expressions - RPTools Wiki](https://wiki.rptools.info/index.php/Dice_Expressions) 本文只參考 General Dice Expressions 的段落,完整的骰子公式有三種形式: - `XdY` - `XdYaZ` - `XdYaZbW` 其中大寫的 XYZW 是數字參數、小寫的 ab 則是指定運算函數。 ### 常見的玩法規則 即使是不同的遊戲也很常會有類似的遊戲規則: - 丟擲四顆骰子,捨棄數字最低的骰子並加總 - `XdYdZ` (drop) 丟擲 X 顆 Y 面骰,捨棄 Z 顆點數最小的骰子 - 丟擲兩顆骰子取較高的數值 - `XdYkZ` (keep) 丟擲 X 顆 Y 面骰,保留 Z 顆點數最大的骰子,其實就是 drop 的相對版本 - 數值低於 3 時重擲一次 - `XdYrZ` (reroll) 丟擲 X 顆 Y 面骰,每當點數低於 Z 時重擲,直到結果大於等於 3 為止 - 數值低於 3 時重擲一次並保留最後一次丟擲的結果 - `XdYrkZ` (reroll and keep) 丟擲 X 顆 Y 面骰,當點數低於 Z 時重擲一次,保留重丟的結果 ### Regular Expression 骰子公式寫成 RegExp 時可以分為兩個部分: - 第一部分:`\d*d\d+` 用來匹配最基本的幾顆多面骰 `XdY`,其中 X 省略時預設為 1 - 第二部分:`([a-z]+\d*){0,2}` 小寫英文字串 + 數字字串,用來匹配擲完骰子後要執行的計算公式 組合起來: ```typescript const DICE_REGEXP = /\d*d\d+([a-z]+\d*){0,2}/gi ``` ## 四則運算 原本我打算直接使用 eval 函數搭配 try catch 讓 js 計算,但直接把使用者的輸入拿來用衍生的安全問題不容忽視,而使用 regexp 來正確匹配四則運算的規則又有一點複雜,最後決定使用簡化版的規則來稍微限制使用者的輸入。 ### 需求 - 加減乘除、小括號 - 數字可以有小數點 - 數字可以替換成骰子公式 - 支援 js Math 函數 ### Regular Expression 寫成 regexp 最難處理的地方在於 Math 函數以及括號的匹配,由於這階段只需要對使用者的輸入做基本驗證防範非數字以外的輸入就好,實際上判斷是否能夠運算的檢查在於 eval 的 try-catch 區塊,於是先用字串 `.replace` 直接把它們省略: ```typescript input .replace(/Math\.\w+\(/g, '(') .replace(/[\(\)]+/gi, '') ``` 過濾後的算式應該只剩下運算子跟運算元: - 運算子:`[+\-*/,]`(減號需要跳脫) 作為兩個運算元之間的連接字元,包含加減乘除以及讓 Math 函數也能用的逗號 - 運算元: 數字本身、可能有小數點、可能是骰子公式 - `\d+(\.\d+)?` - `/\d*d\d+([a-z]+\d*){0,2}/` [(DICE_REGEXP)](#Regular-Expression) 用 template string 組合起來: ```typescript const EXPRESSION_REGEXP = new RegExp(`^([+\\-*/,]?(\\d+(\\.\\d+)?|${DICE_REGEXP.source}))*$`, 'gi') ``` ## 計算流程 1. 使用上述 `EXPRESSION_REGEXP` 驗證使用者輸入的字串是否符合四則運算的規則 2. 找出算式中的骰子公式 3. 辨別骰子公式的運算規則 4. 將原式子中的骰子公式替換為運算後的數字 5. 用 eval 計算替換後的式子 ## Discord 機器人 最後我把骰子運算的功能寫了聊天室機器人服務,使用者可以透過指令輸入算式並且得到隨機擲骰後的結果,另外新增了幾個隨機抽選、排序的功能。目前已加入超過兩百個伺服器,幫助眾多遊戲主持人帶團、省下了不少計算負擔。 說明文件:[eeDice - DC 擲骰機器人](https://hackmd.io/@eelayntris/eedice)