├── assets ├── Zork.jpg ├── BigBang.jpg ├── BotHarryPotter.png ├── GamerChatCover.jpg └── BotHarryPotter2.png ├── .npmignore ├── .devcontainer ├── devcontainer.json └── Dockerfile ├── .vscode └── launch.json ├── save_offline.js ├── script_loader.js ├── package.json ├── .gitignore ├── game_offline.js ├── README.md ├── READHER.md ├── play.js └── scripts ├── harrypotter.yaml └── holmes.yaml /assets/Zork.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YicongCao/MarkdownGame/HEAD/assets/Zork.jpg -------------------------------------------------------------------------------- /assets/BigBang.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YicongCao/MarkdownGame/HEAD/assets/BigBang.jpg -------------------------------------------------------------------------------- /assets/BotHarryPotter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YicongCao/MarkdownGame/HEAD/assets/BotHarryPotter.png -------------------------------------------------------------------------------- /assets/GamerChatCover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YicongCao/MarkdownGame/HEAD/assets/GamerChatCover.jpg -------------------------------------------------------------------------------- /assets/BotHarryPotter2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YicongCao/MarkdownGame/HEAD/assets/BotHarryPotter2.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | *.tgz 3 | .DS_STORE 4 | scripts/ 5 | 6 | READHER.md 7 | assets/ 8 | .eslintrc.yml 9 | game_online.js 10 | save_online.js -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node.js 8", 3 | "dockerFile": "Dockerfile", 4 | 5 | // Uncomment the next line if you want to publish any ports. 6 | // "appPort": [], 7 | 8 | // Uncomment the next line if you want to add in default container specific settings.json values 9 | // "settings": { "workbench.colorTheme": "Quiet Light" }, 10 | 11 | // Uncomment the next line to run commands after the container is created. 12 | // "postCreateCommand": "yarn install", 13 | 14 | "extensions": [ 15 | "dbaeumer.vscode-eslint" 16 | ] 17 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | // "program": "${workspaceFolder}/test-project/server.js", 12 | "cwd": "${workspaceFolder}", 13 | "preLaunchTask": "npm: install" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /save_offline.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const yaml = require('yamljs') 3 | const path = require('path') 4 | 5 | function loadFromDisk(fileName) { 6 | var filename = path.join(path.resolve('./'), fileName) 7 | if (!fs.existsSync(filename)) { 8 | return undefined 9 | } 10 | var sve = yaml.parse(fs.readFileSync(filename).toString()) 11 | return sve 12 | } 13 | 14 | function saveToDisk(fileName, progress) { 15 | // console.log("[DEBUG] saving to:", fileName) 16 | fs.writeFileSync(path.join(path.resolve('./'), fileName), yaml.stringify(progress)) 17 | } 18 | 19 | module.exports = { 20 | loadFromDisk: loadFromDisk, 21 | saveToDisk: saveToDisk 22 | } -------------------------------------------------------------------------------- /script_loader.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const yaml = require('yamljs') 3 | const path = require('path') 4 | 5 | function loadScript(fileName) { 6 | var fullPath = path.join(path.resolve('./'), fileName) 7 | // console.log("[DEBUG] script full path:", fullPath) 8 | if (!fs.existsSync(fullPath)) { 9 | console.error("[DEBUG] script file not found") 10 | return undefined 11 | } 12 | try { 13 | var script = yaml.parse(fs.readFileSync(fullPath).toString()) 14 | return script 15 | } catch (e) { 16 | console.error("[DEBUG] script parse failed", e) 17 | return undefined 18 | } 19 | } 20 | 21 | module.exports = loadScript -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mdgame", 3 | "version": "1.1.7", 4 | "description": "Markdown文字冒险游戏", 5 | "main": "play.js", 6 | "bin": { 7 | "textgame": "game_offline.js", 8 | "textadv": "game_offline.js", 9 | "mdgame": "game_offline.js", 10 | "mdadv": "game_offline.js" 11 | }, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "bot", 17 | "game", 18 | "markdown" 19 | ], 20 | "author": "yicongcao yicongcao@gmail.com", 21 | "license": "MIT", 22 | "dependencies": { 23 | "commander": "^2.20.0", 24 | "yamljs": "^0.3.0" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^7.32.0" 28 | } 29 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | save.yaml 3 | .eslintrc.yml 4 | *.zip 5 | *.tgz 6 | *.save 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # Diagnostic reports (https://nodejs.org/api/report.html) 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # next.js build output 76 | .next 77 | 78 | # nuxt.js build output 79 | .nuxt 80 | 81 | # vuepress build output 82 | .vuepress/dist 83 | 84 | # Serverless directories 85 | .serverless/ 86 | 87 | # FuseBox cache 88 | .fusebox/ 89 | 90 | # DynamoDB Local files 91 | .dynamodb/ 92 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------------------- 2 | # Copyright (c) Microsoft Corporation. All rights reserved. 3 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 4 | #------------------------------------------------------------------------------------------------------------- 5 | 6 | FROM node:8 7 | 8 | # Avoid warnings by switching to noninteractive 9 | ENV DEBIAN_FRONTEND=noninteractive 10 | 11 | # Configure apt and install packages 12 | RUN apt-get update \ 13 | && apt-get -y install --no-install-recommends apt-utils 2>&1 \ 14 | # 15 | # Verify git and needed tools are installed 16 | && apt-get install -y git procps \ 17 | # 18 | # Remove outdated yarn from /opt and install via package 19 | # so it can be easily updated via apt-get upgrade yarn 20 | && rm -rf /opt/yarn-* \ 21 | && rm -f /usr/local/bin/yarn \ 22 | && rm -f /usr/local/bin/yarnpkg \ 23 | && apt-get install -y curl apt-transport-https lsb-release \ 24 | && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ 25 | && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ 26 | && apt-get update \ 27 | && apt-get -y install --no-install-recommends yarn \ 28 | # 29 | # Install eslint globally 30 | && npm install -g eslint \ 31 | # 32 | # Clean up 33 | && apt-get autoremove -y \ 34 | && apt-get clean -y \ 35 | && rm -rf /var/lib/apt/lists/* 36 | 37 | # Switch back to dialog for any ad-hoc use of apt-get 38 | ENV DEBIAN_FRONTEND=dialog 39 | -------------------------------------------------------------------------------- /game_offline.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const readline = require("readline") 4 | const question = question => { 5 | const rl = readline.createInterface({ 6 | input: process.stdin, 7 | output: process.stdout 8 | }) 9 | 10 | return new Promise(resolve => { 11 | rl.question(question, answer => { 12 | rl.close() 13 | return resolve(answer) 14 | }) 15 | }) 16 | } 17 | const program = require("commander") 18 | const path = require("path") 19 | const loadScript = require('./script_loader') // 使用 loader 20 | const save = require("./save_offline") 21 | const play = require("./play") 22 | const scriptHub = "https://github.com/YicongCao/MarkdownGame/tree/master/scripts" 23 | 24 | program 25 | .version("0.0.1", "-v, --version") 26 | .option("-s, --script <剧本文件>", "指定剧本文件(剧本下载:<" + scriptHub + ">)") 27 | .option("-p, --profile <存档文件>", "指定存档文件(非必须,会自动创建)", "") 28 | .parse(process.argv); 29 | 30 | var scriptFileName = program.script 31 | var profileFileName = program.profile 32 | if (!scriptFileName || scriptFileName == "") { 33 | console.error("必须指定一个剧本\n剧本获取: " + scriptHub + "\n") 34 | return -1 35 | } 36 | 37 | function endsWith(str, suffix) { 38 | return str.indexOf(suffix, str.length - suffix.length) !== -1; 39 | } 40 | 41 | if (!profileFileName || profileFileName == "") { 42 | if (endsWith(scriptFileName, ".yaml")) { 43 | profileFileName = scriptFileName.replace(".yaml", ".save") 44 | } else { 45 | profileFileName = scriptFileName + ".save" 46 | } 47 | } 48 | // console.log("[DEBUG] args, script:", scriptFileName, "profile:", profileFileName) 49 | var script = loadScript(scriptFileName) 50 | if (!script) { 51 | console.error("剧本加载失败") 52 | return -1 53 | } 54 | var profile = save.loadFromDisk(profileFileName) 55 | 56 | const startgame = async () => { 57 | // 新开存档 58 | if (profile == undefined) { 59 | var player = await question("\n> 请输入您的大名: ") 60 | if (player == undefined || String(player).trim() == "") { 61 | console.log("那就用默认名称吧~") 62 | player = "玩家" 63 | } 64 | profile = { 65 | player: player, 66 | chapter: "1.1", 67 | variables: {}, 68 | inputs: [] 69 | } 70 | save.saveToDisk(profileFileName, profile) 71 | } 72 | // 继续游戏 73 | var scene = play("", profile, script) 74 | console.log(scene.output) 75 | while (true) { 76 | var input = await question("\n> 请输入您的操作: ") 77 | if (input == "exit" || input == "quit" || input == "退出") { 78 | break 79 | } 80 | // 对局 81 | profile.inputs.push(input) 82 | save.saveToDisk(profileFileName, profile) 83 | scene = play(input, profile, script) 84 | // 存档 85 | profile.chapter = scene.chapter 86 | profile.variables = scene.variables 87 | save.saveToDisk(profileFileName, profile) 88 | // 展示剧情 89 | console.log("-----------------") 90 | console.log(scene.output) 91 | } 92 | } 93 | 94 | startgame() 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Markdown 文字冒险 2 | 3 | 这是一个**文字冒险游戏**引擎,可以通过输入**动作**或**指令**来推进剧情、获取帮助。 4 | 5 | 游戏剧本完全使用`yaml`格式编写,你也可以定制自己的游戏剧情,尽情`fork`,创作更好玩的文字冒险游戏。 6 | 7 | ## 使用方法 8 | 9 | 本包不包含剧本文件,示例剧本请从这里下载: 10 | 11 | ```bash 12 | npm install -g mdgame 13 | mdgame -s <剧本文件路径> 14 | # 下载剧本后即可游玩 15 | mdgame -s scripts/harrypotter.yaml # 玩哈利波特与魔法石(第一章) 16 | mdgame -s scripts/holmes.yaml # 玩福尔摩斯(斑点带子案) 17 | ``` 18 | 19 | ## 代码结构 20 | 21 | ```bash 22 | ├── game_offline.js # 离线游玩入口 23 | ├── package.json # 工程文件 24 | ├── play.js # 引擎 25 | ├── save_offline.js # 离线游玩存档 26 | ├── script_loader.js # 剧本加载器 27 | └── scripts # 剧本存档目录 28 | ├── 996adv.yaml 29 | ├── harrypotter.yaml 30 | └── holmes.yaml 31 | ``` 32 | 33 | ## 游戏机制 34 | 35 | 游戏机制基于舞台、章节、选择、行为和动态条件。 36 | 37 | 舞台是由章节组成的,每节都有剧情描述和几种选择,引擎会把用户输入的指令进行关键词匹配,来命中一种选择。这种选择会有对应的行为,行为包括: 38 | 39 | - 章节跳转 40 | - 变量增减 41 | - 流程控制 42 | 43 | 除了显示声明章节跳转,还可以编写动态条件。动态条件的表达式通常跟变量有关,比如 `fubao > 5`,每当触发变量增减的行为后,引擎就会检查动态条件,如果`fubao(福报)`值满足条件,就会触发该动态条件所对应的行为,该行为依然可以是章节跳转、变量增减、流程控制等。 44 | 45 | 文字冒险游戏的精髓,就在于让用户对千差万别的输入文字给出的不同效果感到惊奇,通过作出开放式的选择,来推进剧情。就像 Sheldon 所说: 46 | 47 | > 文字冒险游戏使用了世界上最强大的 GPU:想象力。 48 | 49 | ## 剧本格式 50 | 51 | 游戏剧本是 `script.yaml` ,其 loader 是 `script_loader.js`,每个回合的处理引擎是 `play.js`,最终呈现出来的交互入口是 `game_offline.js`,很明显,与之对应的还应该有一个 `game_online.js`(提供 bot 形式的游玩界面),但目前还没提交上来。 52 | 53 | 目前你能在剧本中配置如下内容: 54 | 55 | - 舞台:该字段名为`stages`,由一系列**章节**`chapters`构成 56 | - 章节:由**故事** `story`、**选项**`choices`组成,每个选项都包含**关键词**`keywords`、**描述**`description`、**行为**`action`、**参数**`param` 57 | - 变量:该字段名为 `variables`,是一个列表,可以在此处声明变量,然后: 58 | - 根据玩家的输入,编辑剧情内的分支选项`choice`,设置行为`action`,来改变变量的值 59 | - 根据变量的值,编辑动态条件`dynamics`,设置条件`conditions`,来触发一定的行为`action` 60 | - 动态条件:该字段名为 `dynamics`,提供与变量相关的动态功能 61 | - 默认区:该字段名为 `defaults`,当玩家输入没有匹配任何章节内的分支选项时,流程会走到默认区,默认区一般提供如下功能: 62 | - 帮助信息:帮助信息没必要在每个章节中配置成选项,可以放到默认区来配置 63 | - 特殊指令:全局流程性的指令,可以放到这里,比如开始游戏、重置进度等 64 | - 默认回复:当玩家随意输入内容时,也需要有一个友好回复,就在此处配置 65 | 66 | ### 剧情示例 67 | 68 | 以哈利波特与魔法石为例,剧情配置如下,变量区声明了6个变量: 69 | 70 | - rounds,表示回合数,这是个特殊变量,即便不做配置,引擎也会每个回合自动给这个变量+1 71 | - health,健康值,剧情选项会操作该变量,当健康值归零时,可以触发动态条件(Game Over) 72 | - qsnake,这个变量用来记录某个情节中,哈利与巨蟒对话的次数,当此数值达到 4 时,可以触发动态条件(章节跳转) 73 | 74 | 在分支选项中,由如下字段需要解释: 75 | 76 | - keywords,该选项要匹配的关键字 77 | - description,如果该选项触发了分支跳转,则显示下一章节的剧情;如果没触发,则显示这个字段配置的内容 78 | - action,动作类型: 79 | - none,什么也不做,直接给用户显示 description 字段的内容 80 | - goto,章节跳转,跳转后给用户显示新章节的剧情,param 填章节序号 81 | - incr,变量自增,param 填变量名 82 | - decr,变量自减,param 填变量名 83 | - calc,复杂变量运算,param 填变量修改后的表达式(如`(qsnake>=4)?0:qsnake`) 84 | - reset,重置剧情到 “1.1”,一般不会使用该动作 85 | 86 | ```yaml 87 | title: 哈利波特与魔法石 88 | msgtype: markdown 89 | # 变量: 可以记录某些变量的值、动态触发某些章节 90 | variables: [rounds, credit, study, love, health, qsnake] 91 | # 舞台: 由不定数量的章节组成游戏本体 92 | stages: 93 | "1.1": 94 | # 章节名称,会显示在消息标题的位置 95 | chapter: 序章 大难不死的男孩 96 | # 本节剧情 97 | story: |- 98 | > @sender,欢迎回来魔法世界。自从德思礼夫妇一觉醒来在大门口台阶上发现他们的外甥,已经快十年过去了,女贞路却几乎没有变化。湛蓝的天空上悬着几片云朵,太阳依旧升到整洁的花园上,☀️阳光洒满他们的起居室,只有壁炉台上的照片显示出流失了多少时光。照片上的大头娃娃骑着一辆🚴自行车、乘坐🎠旋转木马、和母亲拥吻,他们的儿子达力显然已经不再是个小婴儿了。这栋房子里,🙈没有任何迹象表明这儿还住着另一个男孩。 99 | 100 | 请@bot,输入“继续”阅读下一节剧情: 101 | - **继续** 102 | # 输入选项: 关键词匹配,用户输入若含有该词,视为选择了此选项 103 | choices: 104 | # 关键词 105 | - keywords: [继续, 下一步, continue, next] 106 | # 描述: 若选项行为没有触发章节跳转,则显示描述消息 107 | description: "" 108 | # 行为: goto、none、incr、decr、calc,分别是章节跳转、无、变量自增、变量自减、变量运算 109 | action: [goto, calc] 110 | param: ["1.2", "health+7"] 111 | ``` 112 | 113 | ### 动态条件示例 114 | 115 | 以**健康值减到0时,自动GameOver章节: 12.1**为例,需要配置动态条件如下: 116 | 117 | ```yaml 118 | # 动态条件: 输入选项结算后,会检查动态条件,若条件成立,则执行操作 119 | dynamics: 120 | - conditions: 121 | chapter: "1.*" 122 | expression: health <= 0 123 | action: goto 124 | param: "12.1" 125 | ``` 126 | 127 | ### 默认回复示例 128 | 129 | 以帮助文档、重置章节、和 fallback 回复为例,需要配置默认区如下: 130 | 131 | ```yaml 132 | # 默认区: 当用户输入没有命中章节内所覆盖的选项时,走到这里 133 | defaults: 134 | # 条件: 满足章节条件或关键词条件,则触发该默认行为 135 | - conditions: 136 | chapter: "*" 137 | keywords: [help, man, 帮助, 怎么玩, 你是谁] 138 | action: none 139 | description: |- 140 | > @sender,欢迎来到`@title`,这是一个`文字冒险游戏`,你通过输入`动作`或`指令`来推进剧情、获取帮助。 141 | > 几乎每个90后,都曾梦想过作为一名🔯魔法师,进入霍格沃茨的校园;都曾经想象自己置身哈利波特的剧情中,或是作出🎲改变魔法世界的决策、或是体会⛳魁地奇的欢乐、或是去充满危险的🐸禁林里冒险。 142 | > 现在,让我们以bot的形式,梦回童年的魔法世界,自己就是哈利,试试看你的决策,会书写出怎样的故事。 143 | > 每个人都有`独立的进度和存档`,建议拉到小群中调戏和游玩呢😄 144 | 145 | 游戏基本操作如下: 146 | - **继续游戏**: 直接@我即可 147 | - **开始游戏**: start 148 | - **帮助**: help、man 149 | - **提示**: hint 150 | - **重置**: reset 151 | - conditions: 152 | chapter: "*" 153 | keywords: [hint, 提示] 154 | action: none 155 | description: "@sender,本小节没有提示😂" 156 | - conditions: 157 | chapter: "*" 158 | keywords: [reset, start, 重置, 回到开始, 重新开始, 开始游戏] 159 | action: reset 160 | description: "重置章节" 161 | - conditions: 162 | chapter: "*" 163 | keywords: [] 164 | action: none 165 | description: "> 先生实在抱歉,可是你说话好像一个麻瓜🌚🌝" 166 | ``` 167 | 168 | -------------------------------------------------------------------------------- /READHER.md: -------------------------------------------------------------------------------- 1 | # 用 Bot 复刻经典——哈利波特与机器人 2 | 3 | [TOC] 4 | 5 | ## 楔子 6 | 7 | 几乎每个90后,都曾梦想过作为一名🔯魔法师,进入霍格沃茨的校园;都曾经想象自己置身哈利波特的剧情中,或是作出🎲改变魔法世界的决策、或是体会⛳魁地奇的欢乐、或是去充满危险的🐸禁林里冒险。 8 | 9 | 现在,让我们以 `Bot` 的形式,梦回童年的魔法世界,自己就是哈利,试试看你的决策,会书写出怎样的故事。 10 | 11 | ![哈利波特与魔法石](assets/BotHarryPotter.png) 12 | 13 | ![哈利波特与魔法石](assets/BotHarryPotter2.png) 14 | 15 | ## 缘起 16 | 17 | 记得《生活大爆炸》里两次提到了文字冒险游戏:Zork。 18 | 19 | 第一幕是 Penny 在 Sheldon 他们公寓门口邂逅时: 20 | 21 | ![生活大爆炸](assets/BigBang.jpg) 22 | 23 | 第二幕是 Leonard 准备和 Raj 的妹妹共度良宵时,Sheldon 一直在客厅玩 Zork 卡关了不肯睡觉,Leonard 糊弄他一直往东走,告诉他迷路了。 24 | 25 | ![Zork](assets/Zork.jpg) 26 | 27 | 这个就是 Zork 的游戏界面,生于上世界 80 年代,在 GUI 还没诞生的时候人们就在终端上,开发出这种文字冒险类的游戏。 28 | 29 | **文字冒险游戏**的精髓,就在于让用户对千差万别的输入文字给出的不同效果感到惊奇,通过作出**开放式的选择**,来**推进剧情**。就像 Sheldon 所说: 30 | 31 | > 文字冒险游戏使用了世界上最强大的 GPU:**想象力**。 32 | 33 | 一直到今天,二葱着手构建 Bot 开发平台的工作时,忽然想到,有了 Bot 之后,这些文字冒险游戏又可以复活了。 34 | 35 | 不同于当年的文字冒险游戏,用 Bot 来承载的话,会有这些好处: 36 | 37 | - 传播性:在群聊中通过 @bot 的方式玩游戏,能起到示范作用,有种文字直播的感觉,大家都能看得到 38 | - 富文本:这个时代有 `markdown✍️` 和 `emoji😄`,纯文本不再是 `poor style` 的代名词,你可以用纯文本编织出丰富的画面,图、文、表皆备 39 | - Any time,Any where:就像“云游戏”所承诺给大家的一样,你可以随时打开企业微信,只不过这次不是**聊工作**,而是**玩游戏**,而且,你的存档会保存在云端,**任何设备**、**任何时候**都能随时继续游戏 40 | 41 | 二葱沉思很久,感觉自己并没有编写剧本的才能,决定复刻一些经典作品:哈利波特、福尔摩斯探案、逆转裁判…… 42 | 43 | 回想童年,霍格沃茨、对角巷、厄里斯魔镜、魁地奇的画面历历在目,决定首先复刻哈利波特系列。而且既然源代码是开源的,大家也能根据喜好创作自己喜欢的剧本。 44 | 45 | ## 创作 46 | 47 | 为了拟定游戏引擎的需求,二葱周末云通关了前阵子火热的游戏《隐形守护者》。边看,边想,我现在实现的游戏引擎能不能满足剧情需要。 48 | 49 | 经过一些来来回回的修改,敲定的需求单如下: 50 | 51 | - 引擎要支持章节跳转 52 | - 要提供变量机制,变量能动态的触发剧情 53 | - 要抽离出引擎和剧本的关联,剧情元素均通过剧本文件来配置 54 | - 每个用户输入,要能触发一个或一组行为,这些行为包括但不限于 55 | - 章节跳转 56 | - 关卡提示 57 | - 变量修改 58 | - 游玩形式至少两种 59 | - 直接在控制台中游玩(面向开发者) 60 | - 在 IM 产品中游玩(面向多个平台的用户) 61 | 62 | ### 1. 代码结构 63 | 64 | Github链接🔗: 65 | 66 | ```bash 67 | ├── game_offline.js # 进行游戏 68 | ├── package.json 69 | ├── play.js # 对局逻辑 70 | ├── save_offline.js # 存档逻辑 71 | ├── script.yaml # 剧本(YAML 是最好的配置文件!!!) 72 | └── script_loader.js # 加载 yaml 剧本的工具(默认) 73 | ``` 74 | 75 | 拉到代码后,准备一个 `node.js` 的运行环境,然后运行 `game_offline.js` 即可: 76 | 77 | ```bash 78 | npm install 79 | node game_offline.js 80 | ``` 81 | 82 | ### 2. 游戏机制 83 | 84 | 游戏机制基于舞台、章节、选择、行为和动态条件。 85 | 86 | 舞台是由章节组成的,每节都有剧情描述和几种选择,引擎会把用户输入的指令进行关键词匹配,来命中一种选择。这种选择会有对应的行为,行为包括: 87 | 88 | - 章节跳转 89 | - 变量增减 90 | - 流程控制 91 | 92 | 除了显示声明章节跳转,还可以编写动态条件。动态条件的表达式通常跟变量有关,比如 `fubao > 5`,每当触发变量增减的行为后,引擎就会检查动态条件,如果`fubao(福报)`值满足条件,就会触发该动态条件所对应的行为,该行为依然可以是章节跳转、变量增减、流程控制等。 93 | 94 | 文字冒险游戏的精髓,就在于让用户对千差万别的输入文字给出的不同效果感到惊奇,通过作出开放式的选择,来推进剧情。就像 Sheldon 所说: 95 | 96 | > 文字冒险游戏使用了世界上最强大的 GPU:想象力。 97 | 98 | ### 3. 剧本格式 99 | 100 | 游戏剧本是 `script.yaml` ,其 loader 是 `script_loader.js`,每个回合的处理引擎是 `play.js`,最终呈现出来的交互入口是 `game_offline.js`,很明显,与之对应的还应该有一个 `game_online.js`(提供 bot 形式的游玩界面),但目前还没提交上来。 101 | 102 | 目前你能在剧本中配置如下内容: 103 | 104 | - 舞台:该字段名为`stages`,由一系列**章节**`chapters`构成 105 | - 章节:由**故事** `story`、**选项**`choices`组成,每个选项都包含**关键词**`keywords`、**描述**`description`、**行为**`action`、**参数**`param` 106 | - 变量:该字段名为 `variables`,是一个列表,可以在此处声明变量,然后: 107 | - 根据玩家的输入,编辑剧情内的分支选项`choice`,设置行为`action`,来改变变量的值 108 | - 根据变量的值,编辑动态条件`dynamics`,设置条件`conditions`,来触发一定的行为`action` 109 | - 动态条件:该字段名为 `dynamics`,提供与变量相关的动态功能 110 | - 默认区:该字段名为 `defaults`,当玩家输入没有匹配任何章节内的分支选项时,流程会走到默认区,默认区一般提供如下功能: 111 | - 帮助信息:帮助信息没必要在每个章节中配置成选项,可以放到默认区来配置 112 | - 特殊指令:全局流程性的指令,可以放到这里,比如开始游戏、重置进度等 113 | - 默认回复:当玩家随意输入内容时,也需要有一个友好回复,就在此处配置 114 | 115 | #### 3.1 剧情示例 116 | 117 | 以哈利波特与魔法石为例,剧情配置如下,变量区声明了6个变量: 118 | 119 | - rounds,表示回合数,这是个特殊变量,即便不做配置,引擎也会每个回合自动给这个变量+1 120 | - health,健康值,剧情选项会操作该变量,当健康值归零时,可以触发动态条件(Game Over) 121 | - qsnake,这个变量用来记录某个情节中,哈利与巨蟒对话的次数,当此数值达到 4 时,可以触发动态条件(章节跳转) 122 | 123 | 在分支选项中,由如下字段需要解释: 124 | 125 | - keywords,该选项要匹配的关键字 126 | - description,如果该选项触发了分支跳转,则显示下一章节的剧情;如果没触发,则显示这个字段配置的内容 127 | - action,动作类型: 128 | - none,什么也不做,直接给用户显示 description 字段的内容 129 | - goto,章节跳转,跳转后给用户显示新章节的剧情,param 填章节序号 130 | - incr,变量自增,param 填变量名 131 | - decr,变量自减,param 填变量名 132 | - calc,复杂变量运算,param 填变量修改后的表达式(如`(qsnake>=4)?0:qsnake`) 133 | - reset,重置剧情到 “1.1”,一般不会使用该动作 134 | 135 | ```yaml 136 | title: 哈利波特与魔法石 137 | msgtype: markdown 138 | # 变量: 可以记录某些变量的值、动态触发某些章节 139 | variables: [rounds, credit, study, love, health, qsnake] 140 | # 舞台: 由不定数量的章节组成游戏本体 141 | stages: 142 | "1.1": 143 | # 章节名称,会显示在消息标题的位置 144 | chapter: 序章 大难不死的男孩 145 | # 本节剧情 146 | story: |- 147 | > @sender,欢迎回来魔法世界。自从德思礼夫妇一觉醒来在大门口台阶上发现他们的外甥,已经快十年过去了,女贞路却几乎没有变化。湛蓝的天空上悬着几片云朵,太阳依旧升到整洁的花园上,☀️阳光洒满他们的起居室,只有壁炉台上的照片显示出流失了多少时光。照片上的大头娃娃骑着一辆🚴自行车、乘坐🎠旋转木马、和母亲拥吻,他们的儿子达力显然已经不再是个小婴儿了。这栋房子里,🙈没有任何迹象表明这儿还住着另一个男孩。 148 | 149 | 请@bot,输入“继续”阅读下一节剧情: 150 | - **继续** 151 | # 输入选项: 关键词匹配,用户输入若含有该词,视为选择了此选项 152 | choices: 153 | # 关键词 154 | - keywords: [继续, 下一步, continue, next] 155 | # 描述: 若选项行为没有触发章节跳转,则显示描述消息 156 | description: "" 157 | # 行为: goto、none、incr、decr、calc,分别是章节跳转、无、变量自增、变量自减、变量运算 158 | action: [goto, calc] 159 | param: ["1.2", "health+7"] 160 | ``` 161 | 162 | #### 3.2 动态条件示例 163 | 164 | 以**健康值减到0时,自动GameOver章节: 12.1**为例,需要配置动态条件如下: 165 | 166 | ```yaml 167 | # 动态条件: 输入选项结算后,会检查动态条件,若条件成立,则执行操作 168 | dynamics: 169 | - conditions: 170 | chapter: "1.*" 171 | expression: health <= 0 172 | action: goto 173 | param: "12.1" 174 | ``` 175 | 176 | #### 3.3 默认回复示例 177 | 178 | 以帮助文档、重置章节、和 fallback 回复为例,需要配置默认区如下: 179 | 180 | ```yaml 181 | # 默认区: 当用户输入没有命中章节内所覆盖的选项时,走到这里 182 | defaults: 183 | # 条件: 满足章节条件或关键词条件,则触发该默认行为 184 | - conditions: 185 | chapter: "*" 186 | keywords: [help, man, 帮助, 怎么玩, 你是谁] 187 | action: none 188 | description: |- 189 | > @sender,欢迎来到`@title`,这是一个`文字冒险游戏`,你通过输入`动作`或`指令`来推进剧情、获取帮助。 190 | > 几乎每个90后,都曾梦想过作为一名🔯魔法师,进入霍格沃茨的校园;都曾经想象自己置身哈利波特的剧情中,或是作出🎲改变魔法世界的决策、或是体会⛳魁地奇的欢乐、或是去充满危险的🐸禁林里冒险。 191 | > 现在,让我们以bot的形式,梦回童年的魔法世界,自己就是哈利,试试看你的决策,会书写出怎样的故事。 192 | > 每个人都有`独立的进度和存档`,建议拉到小群中调戏和游玩呢😄 193 | 194 | 游戏基本操作如下: 195 | - **继续游戏**: 直接@我即可 196 | - **开始游戏**: start 197 | - **帮助**: help、man 198 | - **提示**: hint 199 | - **重置**: reset 200 | - conditions: 201 | chapter: "*" 202 | keywords: [hint, 提示] 203 | action: none 204 | description: "@sender,本小节没有提示😂" 205 | - conditions: 206 | chapter: "*" 207 | keywords: [reset, start, 重置, 回到开始, 重新开始, 开始游戏] 208 | action: reset 209 | description: "重置章节" 210 | - conditions: 211 | chapter: "*" 212 | keywords: [] 213 | action: none 214 | description: "> 先生实在抱歉,可是你说话好像一个麻瓜🌚🌝" 215 | ``` 216 | 217 | ## 承载 218 | 219 | 虽然引入了存档的概念,但 Bot 的实现其实是 **无状态** 的。 220 | 221 | 无状态也就意味着,只要存档集中存储在一个地方,Bot 可以自动扩容、缩容。既然如此,那我为什么还要自己准备服务器?有没有一个地方,能托管运行我的 Bot 代码? 222 | 223 | 再次提问🙋,其实众多 IM 产品的 Bot 接口,都只是提供一个你可以主动调用的 CGI 接口(**推送消息**)、还有一个平台方调用你的 CGI 接口(**接收消息**)。每个平台都有自己的协议约定、加密方式,有没有一种方法,可以让 Bot 实现一次开发、在多个 IM 产品上运行? 224 | 225 | 这就是本小节的主题:**承载**。 226 | 227 | 但单是这一小节的主题,就足以开一个专栏写很多长篇文章了。在这里二葱只做一个预告,来自 IEG 游戏平台部 的产品:**呱聊**,即将上线~ 228 | 229 | 呱聊产品的大杀器之一,就是开放、自由的 Bot 平台。一次编写、随处运行的梦想,我们能帮你实现🌝 230 | 231 | > excited! 232 | 233 | ![天若有情天亦老](assets/GamerChatCover.jpg) 234 | 235 | ## 尾声 236 | 237 | 目前 Bot 的剧本已经完成了第一部分:大难不死的男孩、悄悄消失的玻璃。 238 | 239 | 大家可以私聊我,建个小群、我把 Bot 拉到里面给大家体验。 240 | 241 | 谨献给每个童年时代的哈迷~ -------------------------------------------------------------------------------- /play.js: -------------------------------------------------------------------------------- 1 | var script = undefined 2 | 3 | // 显示章节剧情 4 | function displayStage(stage, player, vars) { 5 | var story = stage == undefined ? "该小节没有故事" : stage.story 6 | return displayCustom(stage, story, player, vars) 7 | } 8 | 9 | // 显示自定内容 10 | function displayCustom(stage, defmsg, player, vars) { 11 | defmsg = defmsg.replace("@sender", "@" + player).replace("@title", script.title) 12 | Object.keys(vars).forEach(function (key) { 13 | defmsg = defmsg.replace("@" + key, vars[key]) 14 | }) 15 | Object.keys(script.constants).forEach(function (key) { 16 | defmsg = defmsg.replace("@" + key, script.constants[key]) 17 | }) 18 | var template = '#### [title]\n\n[story]\n' 19 | var output = template.replace("[title]", stage == undefined ? "未知章节" : stage.chapter).replace("[story]", defmsg) 20 | return output 21 | } 22 | 23 | // 判定相等 24 | // "1.1" == "1.1" 25 | // "2.2" == "2.*" 26 | function chapterMatch(template, compare) { 27 | if (template == undefined || template.trim() == "*") { 28 | return true 29 | } else if (template.indexOf(".*") != -1) { 30 | template = template.replace(".*", "").trim() 31 | var templateNum = Number(template) 32 | var compareNum = Number(compare) 33 | return (templateNum - compareNum) * (templateNum - compareNum) < 1 34 | } else { 35 | return template.trim() == compare.trim() 36 | } 37 | } 38 | 39 | // 根据输入推进剧情 40 | function proceed(stage, input, chapter, vars) { 41 | var defaults = script.defaults 42 | var dynamics = script.dynamics 43 | var variables = script.variables 44 | input = String(input).toLowerCase() 45 | // 处理剧情选项/默认回复 46 | var process = function (choice) { 47 | var ret = { 48 | chapter: chapter, 49 | output: [], 50 | variables: vars 51 | } 52 | // 记录回合数: rounds 53 | var roundsVar = "rounds" 54 | if (variables.indexOf(roundsVar) != -1) { 55 | vars[roundsVar] = vars[roundsVar] == undefined ? 0 : vars[roundsVar] + 1 56 | ret.variables = vars 57 | } 58 | // 动态执行代码 59 | var evalEx = function (cmd, savechg = false) { 60 | var cmdLines = [] 61 | // 因为需要初始化所有变量,所以要遍历整个变量声明列表 62 | variables.forEach(element => { 63 | cmdLines.push("var " + element + " = " + JSON.stringify(ret.variables[element])) 64 | }) 65 | // 常量 66 | Object.keys(script.constants).forEach((key) => { 67 | cmdLines.push("var " + key + " = " + JSON.stringify(script.constants[key])) 68 | }) 69 | cmdLines.push(cmd) 70 | var cmdCode = cmdLines.join(";\n") 71 | // console.log("\n[evalex begin]\n", cmdCode, "\n[evalex end]\n") 72 | var evalRet = eval(cmdCode) 73 | // 原地保存修改 74 | if (savechg) { 75 | variables.forEach(element => { 76 | if (element != undefined && element != "") { 77 | vars[element] = eval(element) 78 | } 79 | }) 80 | } 81 | return evalRet 82 | } 83 | // 执行该选项的行动 84 | var execute = function (choice) { 85 | var varChanged = false 86 | // action 可以是 list(一组动作)、string(单个动作) 87 | // param 的类型和长度要和 action 保持一致 88 | var actionSet, paramSet 89 | if (typeof choice.action == "string") { 90 | actionSet = [choice.action] 91 | paramSet = [String(choice.param)] 92 | } else if (typeof choice.action == "object" && 93 | choice.action instanceof Array == true) { 94 | actionSet = choice.action 95 | paramSet = choice.param 96 | } else { 97 | console.log("choice action exception") 98 | return varChanged 99 | } 100 | // 处理输出 101 | if (choice.description != "") { 102 | ret.output.push(choice.description) 103 | } 104 | // 执行 105 | actionSet.forEach((action, index) => { 106 | if (action == "goto") { 107 | // 章节推进 108 | ret.chapter = String(paramSet[index]) 109 | } else if (action == "gotox") { 110 | var chapterNext = evalEx(paramSet[index]) 111 | ret.chapter = String(chapterNext) 112 | } else if (action == "none") { 113 | // 章节不变 114 | } else if (action == "incr") { 115 | // 变量增加,章节不变 116 | varChanged = true 117 | vars[paramSet[index]] = vars[paramSet[index]] == undefined ? 1 : vars[paramSet[index]] + 1 118 | ret.variables = vars 119 | } else if (action == "decr") { 120 | // 变量减少,章节不变 121 | varChanged = true 122 | vars[paramSet[index]] = vars[paramSet[index]] == undefined ? 0 : vars[paramSet[index]] - 1 123 | ret.variables = vars 124 | } else if (action == "calc") { 125 | // 变量运算,章节不变 126 | varChanged = true 127 | // 要对哪个变量做运算 128 | var varName = "" 129 | variables.forEach(element => { 130 | if (paramSet[index].indexOf(element) != -1) { 131 | varName = element 132 | } 133 | }) 134 | if (varName != "") { 135 | vars[varName] = evalEx(paramSet[index]) 136 | } 137 | ret.variables = vars 138 | } else if (action == "eval") { 139 | // 变量运算,章节不变 140 | varChanged = true 141 | // 原地保存结果 142 | evalEx(paramSet[index], true) 143 | ret.variables = vars 144 | } else if (action == "reset") { 145 | // 重置章节到开头,清空变量环境 146 | ret.chapter = "1.1" 147 | ret.variables = {} 148 | } else { 149 | console.log("choice action exception") 150 | ret.output.push("行为配置异常,游戏树崩塌") 151 | } 152 | }) 153 | return varChanged 154 | } 155 | // 执行选项 156 | execute(choice) 157 | // 匹配动态条件 158 | // phase 0: 检查章节条件 159 | var found = false 160 | dynamics.forEach((dynamic) => { 161 | if (chapterMatch(dynamic.conditions.chapter, ret.chapter)) { 162 | found = true 163 | } 164 | }) 165 | if (!found) { 166 | return ret 167 | } 168 | // phase 1: 检查动态条件 169 | var targetDynamic = -1 170 | dynamics.forEach((dynamic, i) => { 171 | var bool = evalEx(dynamic.conditions.expression) 172 | // 确保最后选中最先匹配到的条件 173 | if (bool && targetDynamic == -1) { 174 | targetDynamic = i 175 | } 176 | }) 177 | if (targetDynamic == -1) { 178 | return ret 179 | } 180 | // 执行动态条件 181 | // 注意: 执行 incr、decr、calc 这两种反过来又影响了变量的条件行为时,可以改写代码,来允许再次推导动态条件。但这可能引起死循环。 182 | execute(dynamics[targetDynamic]) 183 | return ret 184 | } 185 | // 查找剧情选项 186 | var target = -1 187 | loop1: 188 | for (var i = 0; stage != undefined && i < stage.choices.length; i++) { 189 | if (stage.choices[i].keywords.length == 0) { 190 | target = i 191 | break loop1 192 | } 193 | for (var j = 0; j < stage.choices[i].keywords.length; j++) { 194 | if (input.indexOf(stage.choices[i].keywords[j]) != -1) { 195 | target = i 196 | break loop1 197 | } 198 | } 199 | } 200 | // 遍历缺省选项 201 | if (target == -1) { 202 | // 查找缺省回复 203 | loop2: for (var x = 0; x < defaults.length; x++) { 204 | if (chapterMatch(defaults[x].conditions.chapter, chapter)) { 205 | if (defaults[x].conditions.keywords.length == 0) { 206 | target = x 207 | break loop2 208 | } 209 | for (var y = 0; y < defaults[x].conditions.keywords.length; y++) { 210 | if (input.indexOf(defaults[x].conditions.keywords[y]) != -1) { 211 | target = x 212 | break loop2 213 | } 214 | } 215 | } 216 | } 217 | if (target == -1) { 218 | return { 219 | chapter: chapter, 220 | output: "无匹配分支,游戏树崩塌", 221 | variables: vars 222 | } 223 | } 224 | // 执行缺省回复 225 | return process(defaults[target]) 226 | } 227 | // 处理章节选项 228 | else { 229 | // 执行选择 230 | return process(stage.choices[target]) 231 | } 232 | } 233 | 234 | // 玩 235 | function play(input, profile, scriptObj) { 236 | script = scriptObj 237 | var chapter = profile.chapter 238 | var player = profile.player 239 | var vars = profile.variables 240 | // console.log("玩家:", player, "当前章节:", chapter, "输入:", input) 241 | var chapterAfter = chapter 242 | var outputText = [] 243 | var stage = script.stages[chapter] 244 | if (String(input).trim() == "") { 245 | // 播放当前剧情 246 | // console.log("用户无输入,播放当前剧情") 247 | outputText.push(displayStage(stage, player, vars)) 248 | } else { 249 | // 处理用户输入 250 | var result = proceed(stage, input, chapter, vars) 251 | // 处理结果 252 | chapterAfter = result.chapter 253 | vars = result.variables 254 | var stageToShow = result.chapter == chapter?stage:script.stages[result.chapter] 255 | // 处理内容显示 256 | if (result.output.length > 0) { 257 | outputText.push(displayCustom(stage, result.output.join('\n'), player, vars)) 258 | } 259 | if (result.output.length == 0 || result.chapter != chapter) { 260 | outputText.push(displayStage(stageToShow, player, vars)) 261 | } 262 | } 263 | 264 | return { 265 | chapter: chapterAfter, 266 | output: outputText.join('\n'), 267 | variables: vars 268 | } 269 | } 270 | 271 | module.exports = play -------------------------------------------------------------------------------- /scripts/harrypotter.yaml: -------------------------------------------------------------------------------- 1 | title: 哈利波特与魔法石 2 | msgtype: markdown 3 | # 变量: 可以记录某些变量的值、动态触发某些章节 4 | variables: [rounds, credit, study, love, health, qsnake] 5 | # 常量 6 | constants: {} 7 | # 舞台: 由不定数量的章节组成游戏本体 8 | stages: 9 | "1.1": 10 | # 章节名称,会显示在消息标题的位置 11 | chapter: 序章 大难不死的男孩 12 | # 本节剧情 13 | story: |- 14 | > @sender,欢迎回来魔法世界。自从德思礼夫妇一觉醒来在大门口台阶上发现他们的外甥,已经快十年过去了,女贞路却几乎没有变化。湛蓝的天空上悬着几片云朵,太阳依旧升到整洁的花园上,☀️阳光洒满他们的起居室,只有壁炉台上的照片显示出流失了多少时光。照片上的大头娃娃骑着一辆🚴自行车、乘坐🎠旋转木马、和母亲拥吻,他们的儿子达力显然已经不再是个小婴儿了。这栋房子里,🙈没有任何迹象表明这儿还住着另一个男孩。 15 | 16 | 请@bot,输入“继续”阅读下一节剧情: 17 | - **继续** 18 | # 输入选项: 关键词匹配,用户输入若含有该词,视为选择了此选项 19 | choices: 20 | # 关键词 21 | - keywords: [继续, 下一步, continue, next] 22 | # 描述: 若选项行为没有触发章节跳转,则显示描述消息 23 | description: "" 24 | # 行为: goto、none、incr、decr、calc,分别是章节跳转、无、变量自增、变量自减、变量运算 25 | action: [goto, calc] 26 | param: ["1.2", "health?7:7"] 27 | "1.2": 28 | chapter: 序章 悄悄消失的玻璃 29 | story: |- 30 | > 哈利波特还住在这里,住在楼梯间的壁橱下面,此刻他正在😪睡觉,但不会太久了。他的佩妮姨妈正在尖叫着叫他起床。 31 | 32 | 请问@sender,哈利该⏰立即起床还是💤继续睡懒觉呢? 33 | - **立即起床** 34 | - **继续赖床** 35 | choices: 36 | - keywords: [起床, 立即] 37 | description: |- 38 | > @sender,哈利被惊醒了,姨妈又在拍着他的房门。哈利睡的并不好,还做了很多梦,虽然很想起床,但疲惫的身体好想并不听使唤… 39 | 40 | 看起来倒不如: 41 | - **继续赖床** 42 | action: none 43 | - keywords: [继续, 赖床, 睡觉, 懒, 忽略, 梦] 44 | description: "" 45 | action: goto 46 | param: "1.3" 47 | - keywords: [提示, hint] 48 | description: |- 49 | 提示: @sender,你可以作出如下选择: 50 | - **起床**: 既然佩尼姨妈已经在叫你起床了,为什么不顺从她呢? 51 | - **赖床**: 也许,当一个叛逆的孩子,也没有什么不好 52 | action: none 53 | "1.3": 54 | chapter: 序章 悄悄消失的玻璃 55 | story: |- 56 | > 哈利躺在床上,意犹未尽的回味着刚才的梦: 那是一个好梦,梦里有一辆会飞的摩托车,似乎,以前也做过同样的梦。 57 | > “你起来了吗?”佩妮姨妈又来到门外。 58 | > “快了。”哈利说。 59 | > “快了,那就赶紧,我要你看着🍗熏咸肉。你敢把它煎糊了试试。我要达力生日这一天都顺顺当当” 60 | 61 | 请问@sender,接到了任务的哈利,现在要不要起床了呢? 62 | - **立即起床** 63 | - **继续赖床** 64 | choices: 65 | - keywords: [起床, 立即] 66 | description: "" 67 | action: goto 68 | param: "1.4" 69 | - keywords: [继续, 赖床, 睡觉, 懒, 忽略, 梦] 70 | description: |- 71 | > @sender,过生日的达力显然十分高兴,故意在楼梯上使劲地跳,震的哈利睡的壁橱里一阵落灰。 72 | > 哈利的🏥健康值-1,再不起床可不行了… 73 | > 当前健康值: @health 74 | 75 | 看起来只有一个选择了: 76 | - **立即起床** 77 | action: calc 78 | param: health-1 79 | - keywords: [提示, hint] 80 | description: |- 81 | 提示: @sender,你可以作出如下选择: 82 | - **起床**: 佩妮姨妈已经两次叫你起床了,再不起床可不妙 83 | - **赖床**: 也许,就算当一个叛逆的孩子,也没有什么不好 84 | action: none 85 | "1.4": 86 | chapter: 序章 悄悄消失的玻璃 87 | story: |- 88 | > @sender,哈利把一盘盘煎蛋和熏咸肉放到餐桌上,这可不容易,因为桌上已经满是礼物🎁,没有多余的地方了。 89 | > 但此刻正在清点礼物的达力却大发雷霆,“36,比去年少两件。” 90 | > 佩妮姨妈显然嗅出了危险的信号,连忙说:“你还没算上玛姬姑妈送你的礼物呢,你看,在桌子底下。今天上街我们再买两件,怎么样,宝贝,这样好了吧?” 91 | > 达力想了一会儿,这似乎是件困难的工作,他那胖胖的大脑袋仿佛要挤出汗珠来了:“那我就有36…37…” 92 | 93 | 哈利显然知道答案,请问,这样达力就有多少件礼物了呢? 94 | choices: 95 | - keywords: ["36", "三十六"] 96 | description: "@sender,佩妮姨妈说上街再买两件,你显然没有算进来" 97 | action: none 98 | - keywords: ["38", "三十八"] 99 | description: "@sender,你算错了,要不要再重新数数看?" 100 | action: none 101 | - keywords: ["39", "三十九", "不要"] 102 | description: "" 103 | action: goto 104 | param: "1.5" 105 | - keywords: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "三", "十"] 106 | description: |- 107 | > “笨蛋,这都不会算!我再给你一次机会”佩妮姨妈说到,她显然感觉到骄傲,因为就智商而言,虽然达力不算高,但哈利也远远比不上他。 108 | 109 | 请问@sender,要不要再想想? 110 | action: none 111 | "1.5": 112 | chapter: 序章 悄悄消失的玻璃 113 | story: |- 114 | > “哦,”达力重重地坐下来,抓起离他最近的一只🎄礼包,“那好吧。” 115 | > 此时,弗农姨夫决定开车带达力去🐒动物园了。 116 | > “我警告你,”弗农把红得发紫的大脸凑到哈利跟前说,“⚠️小子,只要你干出一点点蠢事,那就在碗柜里一直呆到圣诞节吧!” 117 | 118 | 哈利皱了皱眉,说: 119 | - **我答应你**: 毕竟碗柜里可没有好滋味 120 | - **去你的吧**: 不去动物园又怎样,哈利本就不抱希望 121 | choices: 122 | - keywords: [答应, 好, 可以, ok, yes] 123 | description: "" 124 | action: goto 125 | param: "1.6" 126 | - keywords: [去你的, 不, 不行, no, fuck] 127 | description: |- 128 | > “你说什么?”弗农姨夫往哈利头上重重地捶了一拳,哈利健康值-5 129 | > 由于佩妮姨妈不放心哈利一个人在家,哈利一行人还是一起去了动物园 130 | > 当前健康值: @health 131 | 132 | @sender,请输入“继续”阅读下一小节的剧情: 133 | - **继续** 134 | action: calc 135 | param: health-5 136 | - keywords: [继续, 下一步, continue, next] 137 | description: "" 138 | action: goto 139 | param: "1.6" 140 | "1.6": 141 | chapter: 序章 悄悄消失的玻璃 142 | story: |- 143 | > 即使哈利答应弗农姨夫,他也不会相信的。问题是哈利周围常常会发生一些怪事,即使你磨破嘴皮子来解释,也是白费口舌。 144 | > 比如每次哈利理发回来都像没理过一样,有一次佩妮姨妈一口气把他的头发都剪光了,只留下一绺头发来“盖住他那道⚡️可怕的伤疤”。 145 | > 可是第二天起床的时候,他的头发又悉数长了出来,尽快他拼命辩白,可依然阻挡不了被佩妮姨妈关了一周禁闭的事实。 146 | 147 | @sender,可怜的哈利霉运什么时候才到头,请输入“继续”阅读下一小节: 148 | - **继续** 149 | choices: 150 | - keywords: [继续, 下一步, continue, next] 151 | description: "" 152 | action: goto 153 | param: "1.7" 154 | "1.7": 155 | chapter: 序章 悄悄消失的玻璃 156 | story: |- 157 | > 去🐒动物园的路上,弗农姨夫边开车边抱怨工作的事情。开会、银行、同事,都是他抱怨的对象。哈利说自己梦见过一辆会飞的摩托车。 158 | > “摩托车不会飞!”这次哈利成了他抱怨的对象,达力和佩妮姨妈哧哧的嘲笑起来。 159 | > 弗农一家来到了动物园的🐍爬虫馆,达力隔着玻璃、鼻子紧贴着看这盘亮闪闪的棕色Python(巨蟒)。 160 | > “让它动呀!”达力哼哼唧唧地央求他父亲。弗农姨夫敲了敲玻璃,可是巨蟒却纹丝不动。 161 | > “再敲一遍!”达力命令他父亲说。 162 | 163 | @sender,前方高能,请输入“继续”阅读下一小节: 164 | - **继续** 165 | choices: 166 | - keywords: [继续, 下一步, continue, next] 167 | description: "" 168 | action: goto 169 | param: "1.8" 170 | "1.8": 171 | chapter: 序章 悄悄消失的玻璃 172 | story: |- 173 | > 可是巨蟒依然继续打盹💤 174 | > “真烦人”,达力抱怨了一句,拖着脚慢慢走开了。 175 | > 哈利在后面盯着🐍巨蟒的👀眼睛,它那眼神仿佛在对哈利说:“我总是碰到像他们这样的人。” 176 | 177 | @sender,哈利隔着玻璃小声对巨蟒说: 178 | - **我理解…** 179 | - **你是谁?** 180 | - **你来自哪里?** 181 | - **你幸福吗?** 182 | choices: 183 | - keywords: [理解, 知道, 是啊, "i understand", "used to it"] 184 | description: |- 185 | > “那一定让你也很烦。”巨蟒用力点点头。 186 | 187 | @sender,哈利心里一惊,巨蟒好像听得懂他的话,于是哈利接着问道… 188 | - **我理解…** 189 | - **你是谁?** 190 | - **你来自哪里?** 191 | - **你幸福吗?** 192 | action: incr 193 | param: qsnake 194 | - keywords: [你是, 是谁, 你谁, who] 195 | description: |- 196 | > “这还不够明显嘛,我是一条巨蟒🐍,这一带的程序员跟我很熟。”巨蟒回答说。 197 | 198 | @sender,哈利心里一惊,巨蟒好像懂得幽默,于是哈利接着问道… 199 | - **我理解…** 200 | - **你是谁?** 201 | - **你来自哪里?** 202 | - **你幸福吗?** 203 | action: incr 204 | param: qsnake 205 | - keywords: [哪里, 地方, 来自, 来的, 这里, 家, where, home, from] 206 | description: |- 207 | > 巨蟒甩着尾巴猛地拍了一下玻璃窗上的一块小牌子📜。哈利仔细看了一下: 208 | > `蟒蛇,巴西🇧🇷` 209 | 210 | @sender,哈利很好奇巨蟒平时的生活,于是继续问道… 211 | - **我理解…** 212 | - **你是谁?** 213 | - **你来自哪里?** 214 | - **你幸福吗?** 215 | action: incr 216 | param: qsnake 217 | - keywords: 218 | [幸福, 生活, 快乐, 开心, 动物园, 喜欢, 您, happy, life, love, like] 219 | description: |- 220 | > “emmm,你来自中国🇨🇳吗?”巨蟒友善地讽刺哈利说。哈利仿佛能看到空气中地一个白眼🙄️ 221 | 222 | @sender,哈利耐心的继续问道… 223 | - **我理解…** 224 | - **你是谁?** 225 | - **你来自哪里?** 226 | - **你幸福吗?** 227 | action: incr 228 | param: qsnake 229 | - keywords: [] 230 | description: |- 231 | > “你在胡言乱语些什么?”巨蟒友善地讽刺哈利说。哈利仿佛能看到空气中地一个白眼🙄️ 232 | 233 | @sender,哈利耐心的继续问道… 234 | - **我理解…** 235 | - **你是谁?** 236 | - **你来自哪里?** 237 | - **你幸福吗?** 238 | action: incr 239 | param: qsnake 240 | "1.9": 241 | chapter: 序章 悄悄消失的玻璃 242 | story: |- 243 | > “天呐!瞧瞧他在做什么!” 244 | > 正当巨蟒准备回答哈利时,达力发现了巨蟒和哈利在对话,“别挡道!”他说着,朝哈利胸口就是一拳。哈利惊讶不已,重重地摔在水泥地上。 245 | > 达力紧贴在玻璃上,突然,只见他惊恐万状,大喊大叫 246 | 247 | @sender,达力究竟发生了什么?输入“继续”进入下一小节: 248 | - **继续** 249 | choices: 250 | - keywords: [继续, 下一步, continue, next] 251 | description: "" 252 | action: goto 253 | param: "1.10" 254 | "1.10": 255 | chapter: 序章 悄悄消失的玻璃 256 | story: |- 257 | > `蟒蛇柜前的玻璃不见了。` 258 | > 巨蟒🐍迅速展开盘着的身体,溜到地板上。整个爬虫馆的人都在尖叫着跑向门口。 259 | > “我从巴西来,多谢你🌹,我先走了”巨蟒经过哈利时,他能清清楚楚地听到丝丝的声音。 260 | > 此时,天花板上仿佛布满阴云,弗农姨夫暴跳如雷,怒目圆睁地看向哈利。 261 | 262 | @sender,请问哈利要怎么办: 263 | - **逃跑**: 三十六计走为上💨 264 | - **趴下**: 虽然这很奇怪,但似乎抑制不住趴下的冲动🐾 265 | - **什么也不做**: 哈利吓呆了❓ 266 | choices: 267 | - keywords: [逃跑, 快, 逃, 走, 跑, run] 268 | description: |- 269 | > “看我不揍死你!”弗农姨夫一把抓住想要逃跑的哈利,一顿胖揍。哈利健康值-2 270 | > 当前健康值: @health 271 | 272 | @sender,请输入“继续”进入下一小节: 273 | - **继续** 274 | action: calc 275 | param: "health-2" 276 | - keywords: [趴下, 趴, 蹲, 躺, lay] 277 | description: |- 278 | > “你竟然还在做奇怪的事!”弗农姨夫一把抓住想要逃跑的哈利,一顿胖揍。哈利健康值-3 279 | > 当前健康值: @health 280 | 281 | @sender,请输入“继续”进入下一小节: 282 | - **继续** 283 | action: calc 284 | param: "health-2" 285 | - keywords: [睡觉, 睡, 打盹, sleep] 286 | description: |- 287 | > “你竟然能睡得着!”弗农姨夫一把抓住想要逃跑的哈利,一顿胖揍。哈利健康值-4 288 | > 当前健康值: @health 289 | 290 | @sender,请输入“继续”进入下一小节: 291 | - **继续** 292 | action: calc 293 | param: "health-2" 294 | - keywords: 295 | [什么也不, 不做, 呆, 坐, 站, stay, 继续, 下一步, continue, next] 296 | description: "" 297 | action: goto 298 | param: "1.11" 299 | "1.11": 300 | chapter: 序章 悄悄消失的玻璃 301 | story: |- 302 | > 由于哈利机智的行动,躲过一劫,但还是没能躲过在碗橱禁闭的惩罚。 303 | > 哈利在黑洞般的碗橱理躺了好久,他不知道现在几点钟,等弗农姨夫一家睡了,他就能偷偷溜到厨房找点东西吃。 304 | > 他睡着了,梦到一道耀眼的绿光,前额上的伤疤一阵火辣辣的疼痛。他猜想,那道绿光,一定和他的父母有关。 305 | > 梦里的哈利,见到了很多戴着兜帽长相奇怪的人跟他打招呼,梦到他有很多朋友,这跟现实中的哈利完全不同。 306 | 307 | @sender,恭喜你,完成了第一章的故事。请输入“继续”进入下一章节~ 308 | - **继续** 309 | choices: 310 | - keywords: [继续, 下一步, continue, next] 311 | description: "" 312 | action: goto 313 | param: "2.1" 314 | "2.1": 315 | chapter: 猫头鹰传书 316 | story: |- 317 | > 施工中👷 318 | > 感谢您的游玩,请联系`@pixelcao`反馈游玩体验,欢迎拍砖🧱 319 | 320 | 试玩版只编写了第一章的剧本,您可以输入“重置”重新游玩,或者期待完整版本~ 321 | - **重置** 322 | choices: 323 | - keywords: [继续, 下一步, continue, next] 324 | description: "> 试玩版只有第一章哟~" 325 | action: none 326 | "12.1": 327 | chapter: Game Over 328 | story: |- 329 | > 由于💊身负重伤,哈利在去往🏫霍格沃茨之前,很不幸地提前倒在了女贞路15号。 330 | > 哈利一家人,在天堂团聚了👪 331 | > 健康值: @health 332 | 333 | @sender,请输入“重置”来重新开始吧🌝 334 | - **重置** 335 | choices: {} 336 | # 动态条件: 输入选项结算后,会检查动态条件,若条件成立,则执行操作 337 | dynamics: 338 | - conditions: 339 | chapter: "1.*" 340 | expression: health <= 0 341 | action: goto 342 | param: "12.1" 343 | - conditions: 344 | chapter: "1.8" 345 | expression: qsnake >= 4 346 | action: [goto, calc] 347 | param: ["1.9", "(qsnake>=4)?0:qsnake"] 348 | # 默认区: 当用户输入没有命中章节内所覆盖的选项时,走到这里 349 | defaults: 350 | # 条件: 满足章节条件或关键词条件,则触发该默认行为 351 | - conditions: 352 | chapter: "*" 353 | keywords: [help, man, 帮助, 怎么玩, 你是谁] 354 | action: none 355 | description: |- 356 | > @sender,欢迎来到`@title`,这是一个`文字冒险游戏`,你通过输入`动作`或`指令`来推进剧情、获取帮助。 357 | > 几乎每个90后,都曾梦想过作为一名🔯魔法师,进入霍格沃茨的校园;都曾经想象自己置身哈利波特的剧情中,或是作出🎲改变魔法世界的决策、或是体会⛳魁地奇的欢乐、或是去充满危险的🐸禁林里冒险。 358 | > 现在,让我们以bot的形式,梦回童年的魔法世界,自己就是哈利,试试看你的决策,会书写出怎样的故事。 359 | > 每个人都有`独立的进度和存档`,建议拉到小群中调戏和游玩呢😄 360 | 361 | 游戏基本操作如下: 362 | - **继续游戏**: 直接@我即可 363 | - **开始游戏**: start 364 | - **帮助**: help、man 365 | - **提示**: hint 366 | - **重置**: reset 367 | - conditions: 368 | chapter: "*" 369 | keywords: [hint, 提示] 370 | action: none 371 | description: "@sender,本小节没有提示😂" 372 | - conditions: 373 | chapter: "*" 374 | keywords: [reset, start, 重置, 回到开始, 重新开始, 开始游戏] 375 | action: reset 376 | description: "重置章节" 377 | - conditions: 378 | chapter: "*" 379 | keywords: [] 380 | action: none 381 | description: "> 先生实在抱歉,可是你说话好像一个麻瓜🌚🌝" 382 | -------------------------------------------------------------------------------- /scripts/holmes.yaml: -------------------------------------------------------------------------------- 1 | title: 福尔摩斯探案之斑点带子案 2 | msgtype: markdown 3 | variables: 4 | [ 5 | rounds, 6 | health, 7 | love, 8 | proof1, 9 | round1, 10 | proof2, 11 | round2, 12 | used1, 13 | used2, 14 | used3, 15 | clues1, 16 | clues2, 17 | ] 18 | constants: 19 | img1_1: "![PIC](https://www.baidu.com/img/baidu_resultlogo@2.png)" 20 | clue1_1: 宅子的布局:[继父的、姐姐的、你以前的] 21 | clue1_2: 你姐姐遇害前听到了口哨声和金属哐啷声 22 | clue1_3: 暴风雨夜你姐姐遇害时看到的那条带斑点的带子 23 | clue2_1: 悬着铃绳但是不响的铃铛 24 | clue2_2: 朝向继父房间的通气孔 25 | clue2_3: 质感厚重的毯子 26 | clue2_4: 被钉死在铃绳旁边的床 27 | clue2_5: 金属保险柜 28 | clue2_6: 放在保险柜上的一小碟奶 29 | clue2_7: 角落里打结的鞭子 30 | stages: 31 | "1.1": 32 | chapter: 楔子 33 | story: |- 34 | > 1883年4月初的一天早晨,华生一觉醒来,便发现夏洛克·福尔摩斯穿的整整齐齐的站在床边。🌁窗外天色湛蓝,🐧几只麻雀上下翻飞。 35 | > “抱歉,吵醒你了,华生,”他说,“但你我今早注定这样,先是赫德森太太被敲门声吵醒,接着她报复似的来吵醒我,现在轮到我来吵醒你了。” 36 | > “出什么事了?🔥失火了吗?” 37 | > “来了一位委托人,是个年轻的女士。你不是对有(漂)趣(亮)的(的)案(女)子(人)很感兴趣吗,不要错过这个机会。” 38 | 39 | 请@bot,输入“继续”阅读下一节剧情: 40 | - **继续** 41 | choices: 42 | - keywords: [继续, 下一步, jixu, continue, next] 43 | description: "" 44 | action: [goto, calc, calc] 45 | param: ["1.2", "health?10:10", "love?10:10"] 46 | "1.2": 47 | chapter: 楔子 48 | story: |- 49 | > @sender,对华生来说,他最大的乐趣就是观察福尔摩斯进行调查工作✍️,欣赏他迅速地作出推论。他作出推论之迅速,就好像是单凭直觉;但事实上,他的推论总是建立在逻辑基础之上。 50 | > 他匆忙地穿上衣服,然后和福尔摩斯一起来到了楼下的起居室。 51 | > 一位女士👩端坐窗前,身穿黑色衣服、蒙着厚厚的头纱。 52 | > “早上好,小姐姐。”福尔摩斯虽然母胎单身,但撩妹功夫了得,他叫人端了一杯热咖啡,往炉火里扔了几根柴火。 53 | 54 | 请@bot,输入“继续”阅读下一节剧情: 55 | - **继续** 56 | choices: 57 | - keywords: [继续, 下一步, jixu, continue, next] 58 | description: "" 59 | action: [goto] 60 | param: ["1.3"] 61 | "1.3": 62 | chapter: 初见 63 | story: |- 64 | > 那位女士出于害怕和惊恐,脸色苍白、神情沮丧。她的双眼惊慌不安,仿佛正在被🐯狮子追逐的铃鹿。 65 | > 她的身材和样貌最多不过30岁,但头发却未老先衰、夹杂着几缕银丝。她的左手袖口溅了些许`泥点`,口袋里有一张露出半截的`车票`。 66 | 67 | @sender,福尔摩斯作出推理,说道: 68 | - **A**: 你昨晚就到了贝克街,怎么隔了一晚才来找我? 69 | - **B**: 我知道,你是今天早上坐火车来的 70 | - **C**: 小姐,你的眼睛是我见过最美的星空 71 | choices: 72 | - keywords: [A, a, 昨晚, 昨天] 73 | description: |- 74 | > “可我是今天早上才来的啊,”这位女士一脸看待傻瓜的眼神,“福尔摩斯先生是不是有点走神了…” 75 | > `好感度`-3 76 | > 当前好感度: @love 77 | 78 | @sender,福尔摩斯连忙改口说道: 79 | - **A**: *你昨晚就到了贝克街,怎么隔了一晚才来找我?* 80 | - **B**: 我知道,你是今天早上坐火车来的 81 | - **C**: 小姐,你的眼睛是我见过最美的星空 82 | action: calc 83 | param: "love-3" 84 | - keywords: [B, b, 早上, 今天, 火车] 85 | description: "" 86 | action: [goto, calc] 87 | param: ["1.4", "love+1"] 88 | - keywords: [C, c, 星空, 小姐, 爱情, 美, 眼睛, 漂亮] 89 | description: |- 90 | > “死变态!”这位女士又害怕又生气,转身朝门口走去。 91 | > “小姐留步,刚刚那句话是华生说的!” 92 | > 虽然很窘迫,但小姐勉强留在了房间里,加上点怒气,脸色反而变得红润些了。 93 | > `好感度`-5 94 | > 当前好感度: @love 95 | 96 | @sender,为了救场,福尔摩斯接着说: 97 | - **A**: 你昨晚就到了贝克街,怎么隔了一晚才来找我? 98 | - **B**: 我知道,你是今天早上坐火车来的 99 | - **C**: *小姐,你的眼睛是我见过最美的星空* 100 | action: calc 101 | param: "love-5" 102 | "1.4": 103 | chapter: 初见 104 | story: |- 105 | > “这么说,你认识我?”这位女士大为吃惊,她的眼睛困惑而迷恋地注视着福尔摩斯。 106 | > `好感度`+1 107 | > 当前好感度: @love 108 | > “不认识,但我注意到你口袋里有一张💳回程车票的后半截。你一定是很早就动身的,还乘坐过🏇双轮单马车,在崎岖泥泞的路上行驶了一段路程。除了单马车外,其他车辆根本不会这样甩起泥巴。” 109 | 110 | @sender,此外,这位小姐一定坐在车夫的: 111 | - **左边👈** 112 | - **右边👉** 113 | - **车底🚗** 114 | choices: 115 | - keywords: [左, left] 116 | description: |- 117 | > “您真的好厉害。”这位女士显得更加佩服了。 118 | > 福尔摩斯解释道:“这没什么,因为只有坐在车夫左边,你才会在左手袖口沾到泥巴。” 119 | > “下面,请跟我们讲述一下你的遭遇吧。”他说。 120 | > `好感度`+2 121 | > 当前好感度: @love 122 | 123 | @sender,请输入“继续”阅读下一节剧情: 124 | - **继续** 125 | action: calc 126 | param: "love+2" 127 | - keywords: [右, right] 128 | description: |- 129 | > “不好意思,我是坐在车夫左边的。”这位女士反驳说。 130 | > 福尔摩斯解释道:“那好吧,请你跟我们讲述一下你的遭遇” 131 | > `好感度`-1 132 | > 当前好感度: @love 133 | 134 | @sender,请输入“继续”阅读下一节剧情: 135 | - **继续** 136 | action: calc 137 | param: "love-1" 138 | - keywords: [底, 上, 下, 东, 西, 南, 北, 前, 后] 139 | description: |- 140 | > “你说什么!?”这位女士很惊讶,“您疯了吧……” 141 | > 福尔摩斯连忙解释,“请别误会,女士,这只是个玩笑……不如请跟我们讲述一下你的遭遇吧。” 142 | > `好感度`-2.5 143 | > 当前好感度: @love 144 | 145 | @sender,请输入“继续”阅读下一节剧情: 146 | - **继续** 147 | action: calc 148 | param: "love-2.5" 149 | - keywords: [继续, 下一步, jixu, continue, next] 150 | description: "" 151 | action: goto 152 | param: "1.5" 153 | "1.5": 154 | chapter: 讲述 155 | story: |- 156 | > “我叫海伦·斯托纳,和继父住在一起,他是英国古老的罗伊洛特家族最后一个还活着的人。”她说。 157 | > 福尔摩斯点点头,“我很熟悉这个家族,曾经是英国最富有的家族之一,北至伯克郡,西至汉普郡。可是连续四代子嗣都荒淫浪荡、挥霍无度,几乎倾家荡产了。” 158 | > “继父在`印度`时娶了我的母亲,她是孟加拉炮兵司令斯托纳少将的年轻遗孀,斯托纳太太。我和`我的姐姐`朱莉亚是孪生姐妹,母亲再婚时,我们才两岁。母亲家里很富有,去世前立下`遗嘱`,那就是在我们结婚后,必须每年给我们每人至少300英镑。” 159 | 160 | @sender,请输入“继续”阅读下一节剧情: 161 | - **继续** 162 | choices: 163 | - keywords: [继续, 下一步, jixu, continue, next] 164 | description: "" 165 | action: goto 166 | param: "1.6" 167 | "1.6": 168 | chapter: 讲述 169 | story: |- 170 | > “继父喜欢和流浪的`吉卜赛人`混在一起,经常在庄园里接待他们。而且还非常喜欢`印度动物`,目前他有一只🐨印度猎豹和一只🐒狒狒,是一个记者送他的。村民们既害怕它们,也怕它们的主人。” 171 | > “我的姐姐死时才30岁,可她早已经两鬓斑白了。朱莉亚订婚后两天,继父知道了这一婚约,但没有表示反对。但婚礼前不到两周时,🕯️她便惨死家中。” 172 | 173 | @sender,请输入“继续”阅读下一节剧情: 174 | - **继续** 175 | choices: 176 | - keywords: [继续, 下一步, jixu, continue, next] 177 | description: "" 178 | action: [goto, calc, calc, calc] 179 | param: ["1.7", "round1?0:0", "proof1?0:0", 'clues1?"":""'] 180 | "1.7": 181 | chapter: 讲述 182 | story: |- 183 | > “福尔摩斯先生,请问您还想知道哪方面的细节?”这位女士问道。 184 | 185 | @sender,请为福尔摩斯做出选择(注意,时间有限,请在`4`个回合内获知至少`2`条有效线索): 186 | - **母亲遗产的数额** 187 | - **宅子的布局** 188 | - **姐姐死前做的事** 189 | - **警察的结论** 190 | 同时,你还能查看已收集的线索: 191 | - **查看线索** 192 | choices: 193 | - keywords: [母亲, 遗产, 数额, 遗嘱, 英镑, 钱] 194 | description: |- 195 | > “我的母亲有一笔相当可观的财产,由于前夫作为🚀炮兵司令的缘故,每年进账不少于💰1000英镑。”斯托纳小姐慢慢说道。 196 | > 这则信息`不是`有效线索。 197 | > 当前回合数: @round1,当前线索数: @proof1 198 | 199 | @sender,请继续提问: 200 | - **母亲遗产的数额** 201 | - **宅子的布局** 202 | - **姐姐死前做的事** 203 | - **警察的结论** 204 | 同时,你还能查看已收集的线索: 205 | - **查看线索** 206 | action: [incr] 207 | param: [round1] 208 | - keywords: [宅, 房, 屋, 布局, 位置, 方位] 209 | description: |- 210 | > “我们住的宅子极其古老,只有一侧的耳房还住着人。在这些卧室里,`第一间`时我继父的,`第二间`是我姐姐的,`第三间`时我自己的。这些房间之间彼此互不相通,但房门都朝向一条共同的过道。”斯托纳小姐说,“我讲清楚了吗?” 211 | > “十分清楚。” 212 | > 这则信息`是`有效线索。 213 | > 当前回合数: @round1,当前线索数: @proof1 214 | 215 | @sender,请继续提问: 216 | - **母亲遗产的数额** 217 | - **宅子的布局** 218 | - **姐姐死前做的事** 219 | - **警察的结论** 220 | 同时,你还能查看已收集的线索: 221 | - **查看线索** 222 | action: [incr, incr, calc] 223 | param: [round1, proof1, 'clues1==""?clue1_1:clues1+"、"+clue1_1'] 224 | - keywords: [姐姐, 死] 225 | description: |- 226 | > “姐姐遇害那天晚上,她来我房间讲述了即将举行的婚礼。到了11点钟,她起身回自己房间,但走到门口时她突然担心地问我”,斯托纳小姐说,“她说,‘海伦,夜深人静的时候,你听到过有人吹口哨吗?’” 227 | > “‘从来没有。’”斯托纳回答她姐姐。 228 | > “‘怎么可能?这几天深夜3点左右,我总能听到轻轻的口哨声,搞不清那声音是哪里来的。’” 229 | > 这则信息`是`有效线索。 230 | > 当前回合数: @round1,当前线索数: @proof1 231 | 232 | @sender,请继续提问: 233 | - **母亲遗产的数额** 234 | - **宅子的布局** 235 | - **姐姐死前做的事** 236 | - **警察的结论** 237 | 你有一个`新的`提问选项: 238 | - **有关口哨声** 239 | action: [incr, incr, calc] 240 | param: [round1, proof1, 'clues1==""?clue1_2:clues1+"、"+clue1_2'] 241 | - keywords: [警, 验尸官, 调查, 疑点, 结论] 242 | description: |- 243 | > “由于我继父在郡里臭名昭著,警察和验尸官都非常认真的调查了这个案子。但是他们找不到任何能让人信服的死因。”斯托纳小姐无奈的说道,“警察敲过墙壁,四周很坚固,发生意外时姐姐也确认独自在房间里,身上也没有暴力痕迹。” 244 | > 这则信息`不是`有效线索。 245 | > 当前回合数: @round1,当前线索数: @proof1 246 | 247 | @sender,请继续提问: 248 | - **母亲遗产的数额** 249 | - **宅子的布局** 250 | - **姐姐死前做的事** 251 | - **警察的结论** 252 | 同时,你还能查看已收集的线索: 253 | - **查看线索** 254 | action: [incr] 255 | param: [round1] 256 | - keywords: [口哨, 声, 吹, 哨] 257 | description: |- 258 | > “那天晚上,我就一直有大难临头的感觉。那是一个暴风雨之夜,狂风怒吼,雨点噼啪噼啪地打在窗户上。突然,在嘈杂的风雨声中,传来一声女人惊恐的尖叫。那是姐姐的声音,就在我开启房门的时候,我仿佛听到了那样的`口哨声`。停了一会儿,我又听到金属的`哐啷`一声。”斯托纳小姐回忆说。 259 | > “姐姐这时正在剧痛地抽搐着,躺在地板上,发出凄厉的呐喊,‘啊,海伦,是那条`带斑点的带子`!’”斯托纳小姐脸色发白地说,“这时继父穿着睡衣匆忙出来,叫来了医生,但此时已经徒劳无功了……” 260 | > 这则信息`是`有效线索。 261 | > 当前回合数: @round1,当前线索数: @proof1 262 | 263 | @sender,请继续提问: 264 | - **母亲遗产的数额** 265 | - **宅子的布局** 266 | - **姐姐死前做的事** 267 | - **警察的结论** 268 | 同时,你还能查看已收集的线索: 269 | - **查看线索** 270 | action: [incr, incr, calc] 271 | param: [round1, proof1, 'clues1==""?clue1_3:clues1+"、"+clue1_3'] 272 | - keywords: [查看, 检查, 查, 收集, 线索, view, inspect] 273 | description: |- 274 | > 已收集的线索: `@clues1` 275 | > 当前回合数: @round1,当前线索数: @proof1 276 | 277 | @sender,请继续提问: 278 | - **母亲遗产的数额** 279 | - **宅子的布局** 280 | - **姐姐死前做的事** 281 | - **警察的结论** 282 | action: none 283 | "1.8": 284 | chapter: 讲述 285 | story: |- 286 | > 福尔摩斯总结道:“我们梳理一下关键线索: @clues1。你的思路很清楚,请继续讲述。” 287 | > 斯托纳小姐继续说:“最近我也要结婚了,两天前这所房子突然开始修缮。我的卧室被钻出破洞,不得不搬到姐姐丧命的房间里居住。就在昨晚,在我回想姐姐可怕的遭遇时,突然听到了相同的口哨声。我吓得魂不附体,天一亮就来找您了。” 288 | 289 | @sender,请输入“继续”阅读下一节剧情: 290 | - **继续** 291 | choices: 292 | - keywords: [继续, 下一步, jixu, continue, next] 293 | description: "" 294 | action: goto 295 | param: "1.9" 296 | "1.9": 297 | chapter: 讲述 298 | story: |- 299 | > 好长一阵子,谁都没有说话。福尔摩斯用手托着下巴,凝视着噼啪作响的炉火🔥 300 | > 最后,他打破了沉默,“现在虽然细节不多,但却已经刻不容缓。你先回家,我们随后就到。不知能否在你继父不知道的情况下,查看一下这些房间?” 301 | > 获得了肯定的答复后,斯托纳小姐放心地离开了,福尔摩斯目送她登上了马车。 302 | > 过了半晌,突然响起敲门声。 303 | 304 | @sender,请作出选择: 305 | - **开门** 306 | - **假装没听到** 307 | choices: 308 | - keywords: [开门, 打开, open] 309 | description: "" 310 | action: goto 311 | param: "2.1" 312 | - keywords: [没, 不, 假装, 闭, close] 313 | description: "" 314 | action: goto 315 | param: "2.2" 316 | "2.1": 317 | chapter: 闯入 318 | story: |- 319 | > 福尔摩斯打开🚪门,一个👮彪形大汉堵在门口。穿着古怪,像一个专家,又像个庄稼汉。他凶光毕露的深陷的眼睛和那细长的鹰钩鼻子,看起来活像一只苍老、残忍的猛禽🦅 320 | > “哪一位是福尔摩斯?”那位彪形大汉对着华生和福尔摩斯两人问道。 321 | 322 | @sender,福尔摩斯说: 323 | - **是我** 324 | - **是他** 325 | choices: 326 | - keywords: [是, 不, i, am, me, you, he, him] 327 | description: "" 328 | action: goto 329 | param: "2.3" 330 | "2.2": 331 | chapter: 闯入 332 | story: |- 333 | > 突然,🚪门被撞开了,一个👮彪形大汉堵在门口。穿着古怪,像一个专家,又像个庄稼汉。他凶光毕露的深陷的眼睛和那细长的鹰钩鼻子,看起来活像一只苍老、残忍的猛禽🦅 334 | > “你俩谁他娘的是福尔摩斯?”那位彪形大汉对着华生和福尔摩斯两人吼道。 335 | 336 | @sender,福尔摩斯说: 337 | - **是我** 338 | - **是他** 339 | choices: 340 | - keywords: [是, 不, i, am, me, you, he, him] 341 | description: "" 342 | action: goto 343 | param: "2.3" 344 | "2.3": 345 | chapter: 闯入 346 | story: |- 347 | > “我就是罗伊洛特医生,别来这套,我知道我继女刚来过你这,我在跟踪她,她对你们说什么了!” 348 | > 福尔摩斯微微一笑。 349 | > “我警告你,不要多管闲事。” 350 | 351 | @sender,福尔摩斯接下来: 352 | - **继续微笑** 353 | - **打他一拳** 354 | choices: 355 | - keywords: [继续, 微笑, smile] 356 | description: "" 357 | action: goto 358 | param: "2.4" 359 | - keywords: [打, 拳, 他, hit] 360 | description: "" 361 | action: goto 362 | param: "2.5" 363 | "2.4": 364 | chapter: 闯入 365 | story: |- 366 | > 福尔摩斯更加笑容可掬。 367 | > “你这个苏格兰场自命不凡的芝麻官!”看到福尔摩斯不为所动,这个彪形大汉突然害怕了。他甩出一句垃圾话,转身离开了。 368 | > “刚刚斯托纳女士提到她继父进城有事要办,我们趁这个机会赶紧去一趟斯托纳女士家里调查一下。”福尔摩斯对华生说。 369 | 370 | @sender,请输入“继续”阅读下一节剧情: 371 | - **继续** 372 | choices: 373 | - keywords: [继续, 下一步, jixu, continue, next] 374 | description: "" 375 | action: goto 376 | param: "3.1" 377 | "2.5": 378 | chapter: 闯入 379 | story: |- 380 | > 这个彪形大汉狠狠地挨了福尔摩斯一拳,重重地摔倒在地。 381 | > 福尔摩斯掏出手枪,对准了他的额头。 382 | 383 | @sender,请决定: 384 | - **开枪**: 为民除害,替天行道 385 | - **放走**: 饶人一命,查案要紧 386 | choices: 387 | - keywords: [开, 枪, 打死, shot] 388 | description: "" 389 | action: goto 390 | param: "10.3" 391 | - keywords: [放, 走, leave] 392 | description: "" 393 | action: goto 394 | param: "3.1" 395 | "3.1": 396 | chapter: 调查 397 | story: |- 398 | > 那里天气很好,阳光明媚,晴空中白云直飘。这春意盎然的景色和不详事件的调查,形成了鲜明的对照。 399 | > “对面就是罗伊洛特医生的房子了,斯托纳小姐就在前面,她继父还没回来,我们快过去吧。”福尔摩斯说道。 400 | 401 | @sender,请输入“继续”阅读下一节剧情: 402 | - **继续** 403 | choices: 404 | - keywords: [继续, 下一步, jixu, continue, next] 405 | description: "> 目前只做完了前两章哟~" 406 | action: [goto, calc, calc, calc, calc, calc, calc] 407 | param: 408 | [ 409 | "3.2", 410 | "used1?0:0", 411 | "used2?4:4", 412 | "used3?0:0", 413 | "proof2?0:0", 414 | "round2?0:0", 415 | 'clues2?"":""', 416 | ] 417 | "3.2": 418 | chapter: 调查 419 | story: |- 420 | > “现在我们必须抓紧时间,请马上带我们去需要检查的房间。”福尔摩斯对斯托纳小姐说。 421 | 422 | @sender,请输入“观察房间”: 423 | - **观察房间** 424 | choices: 425 | - keywords: [观察, 房间, 看, 继续, 下一步, jixu, continue, next, watch] 426 | description: "" 427 | action: goto 428 | param: "3.3" 429 | "3.3": 430 | chapter: 走廊 431 | story: |- 432 | > 这座🏠宅邸是灰色石头砌成的,石壁上布满青苔。两侧是弧形的边房,像一对蟹钳似的。一侧边房已经荒废残破,只有另一侧边房可住人了。 433 | > 福尔摩斯看向这排边房,“我想,这是⏳你过去的房间,当中是你👩姐姐的房间,挨着主楼的就是你🚑继父罗伊洛特医生的房间吧。” 434 | > 他看了看表,说:“罗伊洛特医生就快回来,线索应该只存在其中`两个房间`里。” 435 | 436 | @sender,请选择要检查的房间: 437 | - **A**: 斯托纳小姐过去的房间 438 | - **B**: 她姐姐死去的房间,也是她现在住的房间 439 | - **C**: 罗伊洛特医生的房间 440 | choices: 441 | - keywords: [A, a, 斯托纳, 小姐] 442 | description: "" 443 | action: goto 444 | param: "3.5" 445 | - keywords: [B, b, 姐姐, 现在, 死去] 446 | description: "" 447 | action: goto 448 | param: "3.6" 449 | - keywords: [C, c, 医生, 继父, 罗伊, 先生] 450 | description: "" 451 | action: goto 452 | param: "3.7" 453 | "3.4": 454 | chapter: 走廊 455 | story: |- 456 | > 站在走廊,“看来离真相还有段距离”,福尔摩斯沉思着,👀眼睛时而充满光芒,时而变得灰暗。 457 | > “大概还可以再🔍检查一下四周吧。”他想。 458 | > 线索数: `@proof2/7` 459 | > 行动数: `@round2/10` 460 | > (*检查物品*会增加行动数,但*切换房间*不增加行动数) 461 | 462 | @sender,请选择要检查的房间: 463 | - **A**: 斯托纳小姐过去的房间 464 | - **B**: 她姐姐死去的房间,也是她现在住的房间 465 | - **C**: 罗伊洛特医生的房间 466 | 同时,你还能查看已收集的线索: 467 | - **查看线索** 468 | choices: 469 | - keywords: [A, a, 斯托纳, 小姐] 470 | description: "" 471 | action: goto 472 | param: "3.5" 473 | - keywords: [B, b, 姐姐, 现在, 死去] 474 | description: "" 475 | action: goto 476 | param: "3.6" 477 | - keywords: [C, c, 医生, 继父, 罗伊, 先生] 478 | description: "" 479 | action: goto 480 | param: "3.7" 481 | - keywords: [查看, 检查, 查, 收集, 线索, view, inspect] 482 | description: |- 483 | > 已收集的线索: `@clues2` 484 | > 当前线索数: `@proof2/7`,当前回合数: `@round2/10` 485 | 486 | @sender,请选择要检查的房间: 487 | - **A**: 斯托纳小姐过去的房间 488 | - **B**: 她姐姐死去的房间,也是她现在住的房间 489 | - **C**: 罗伊洛特医生的房间 490 | action: none 491 | "3.5": 492 | chapter: 斯托纳的房间 493 | story: |- 494 | > 斯托纳小姐过去的房间,位于别墅耳房的最远端。进门之后,扑面而来装修的粉尘味。 495 | > 有一个开口式的壁炉,铁质的百叶窗,天花板上的吊灯看起来摇摇欲坠。 496 | > 线索数: `@proof2/7` 497 | > 回合数: `@round2/10` 498 | 499 | @sender,请选择要检查或进行的动作: 500 | - 检查**吊灯** 501 | - 检查**炉子** 502 | - 检查**百叶窗** 503 | - 查看**线索** 504 | - 返回**走廊** 505 | choices: 506 | - keywords: [返回, 后退, 走廊, 上一级, 离开, back, hall] 507 | description: "" 508 | action: goto 509 | param: "3.4" 510 | - keywords: [吊灯, 灯, lamp] 511 | description: |- 512 | > 一个很普通的吊灯 513 | > 线索数: `@proof2/7` 514 | > 回合数: `@round2/10` 515 | 516 | @sender,请选择要检查或进行的动作: 517 | - 检查**吊灯** 518 | - 检查**炉子** 519 | - 检查**百叶窗** 520 | - 查看**线索** 521 | - 返回**走廊** 522 | action: [incr] 523 | param: [round2] 524 | - keywords: [炉子, 炉, kindle] 525 | description: |- 526 | > 外面已然是春天,这个炉子许久不用了,沾了一层烟灰都没有清理。 527 | > 线索数: `@proof2/7` 528 | > 回合数: `@round2/10` 529 | 530 | @sender,请选择要检查或进行的动作: 531 | - 检查**吊灯** 532 | - 检查**炉子** 533 | - 检查**百叶窗** 534 | - 查看**线索** 535 | - 返回**走廊** 536 | action: [incr] 537 | param: [round2] 538 | - keywords: [百叶窗, 窗, window] 539 | description: |- 540 | > 福尔摩斯把百叶窗关上,试了各种办法都无法打开。合叶是铁质的,牢牢地嵌在坚硬的石墙上。 541 | > 线索数: `@proof2/7` 542 | > 回合数: `@round2/10` 543 | 544 | @sender,请选择要检查或进行的动作: 545 | - 检查**吊灯** 546 | - 检查**炉子** 547 | - 检查**百叶窗** 548 | - 查看**线索** 549 | - 返回**走廊** 550 | action: [incr] 551 | param: [round2] 552 | - keywords: [查看, 检查, 查, 收集, 线索, view, inspect] 553 | description: |- 554 | > 已收集的线索: `@clues2` 555 | > 当前线索数: `@proof2/7`,当前回合数: `@round2/10` 556 | 557 | @sender,请选择要检查或进行的动作: 558 | - 检查**吊灯** 559 | - 检查**炉子** 560 | - 检查**百叶窗** 561 | - 返回**走廊** 562 | action: none 563 | "3.6": 564 | chapter: 姐姐的房间 565 | story: |- 566 | > “斯托纳小姐原来的房间并不值得浪费🕙时间检查。”福尔摩斯径直走向姐姐的房间,也就是斯托纳小姐现在住的房间。 567 | > 这个房间有低低的天花板和四方形的威尔顿地毯。一角放着褐色橱柜,另一角摆着一张窄窄的🛏️床。四周的地板褪了色,显得十分古老。 568 | > 福尔摩斯搬了把💺椅子坐在墙角,默默地、前前后后、上上下下地不停看着。 569 | > 线索数: `@proof2/7` 570 | > 回合数: `@round2/10` 571 | 572 | @sender,请选择要检查或进行的动作: 573 | - 检查**铃铛** 574 | - 检查**地毯** 575 | - 检查**橱柜** 576 | - 检查**床** 577 | - 查看**线索** 578 | - 返回**走廊** 579 | choices: 580 | - keywords: [返回, 后退, 走廊, 上一级, 离开, back, hall] 581 | description: "" 582 | action: goto 583 | param: "3.4" 584 | - keywords: [铃铛, 铃, ring] 585 | description: |- 586 | > 悬挂在床边有一根粗粗的🔔铃拉绳,但福尔摩斯猛的一拉,却发现铃铛`不响`。 587 | > “没必要在这里安装这么好的一根铃绳。”福尔摩斯趴了下来,发现铃绳的尾端,有一个不起眼的`通气孔`。 588 | > 线索数: `@proof2/7` 589 | > 回合数: `@round2/10` 590 | 591 | @sender,请选择要检查或进行的动作: 592 | - 检查**铃铛** 593 | - 检查**地毯** 594 | - 检查**橱柜** 595 | - 检查**床** 596 | - 查看**线索** 597 | - 返回**走廊** 598 | 你有一个`新的`提问选项: 599 | - 检查**通气孔** 600 | action: [incr, incr, calc] 601 | param: [round2, proof2, 'clues2==""?clue2_1:clues2+"、"+clue2_1'] 602 | - keywords: [通气孔, 孔, 小孔, 洞, hool] 603 | description: |- 604 | > “非常奇怪!”福尔摩斯拉着铃绳喃喃地说,“这个房间有两个十分特别的地方,一个是不响的铃铛,另一个是朝向隔壁的`通气孔`;花费同样的功夫,就能让它通向户外的。” 605 | > “我记得铃铛和通气孔都是姐姐结婚前,继父定做的,你看,通气孔就通往他那边的房间。”斯托纳小姐说。 606 | > 线索数: `@proof2/7` 607 | > 回合数: `@round2/10` 608 | 609 | @sender,请选择要检查或进行的动作: 610 | - 检查**铃铛** 611 | - 检查**地毯** 612 | - 检查**橱柜** 613 | - 检查**床** 614 | - 查看**线索** 615 | - 返回**走廊** 616 | action: [incr, incr, calc] 617 | param: [round2, proof2, 'clues2==""?clue2_2:clues2+"、"+clue2_2'] 618 | - keywords: [地毯, 毯, 布, kindle] 619 | description: |- 620 | > 这块褐色的地毯毛绒很厚,踩在上面非常舒服。如果是细小的东西落在地毯上,或是一只小虫,都`很难发现`。 621 | > “大概是家族唯一遗留下来、功能还不减当年的物件了吧。”福尔摩斯环顾破旧的四周说道。 622 | > 线索数: `@proof2/7` 623 | > 回合数: `@round2/10` 624 | 625 | @sender,请选择要检查或进行的动作: 626 | - 检查**铃铛** 627 | - 检查**地毯** 628 | - 检查**橱柜** 629 | - 检查**床** 630 | - 查看**线索** 631 | - 返回**走廊** 632 | action: [incr, incr, calc] 633 | param: [round2, proof2, 'clues2==""?clue2_3:clues2+"、"+clue2_3'] 634 | - keywords: [床, 铺盖, 被子, bed] 635 | description: |- 636 | > 看起来是一张很普通的床,只是有点窄。但福尔摩斯尝试移动这张床,却发现推不动。仔细一看,这张床被螺钉`固定`再了地板上。 637 | > “福尔摩斯,我似乎隐约感到你正在揭露和暗示一项可怕的罪行。”华生说。 638 | > 线索数: `@proof2/7` 639 | > 回合数: `@round2/10` 640 | 641 | @sender,请选择要检查或进行的动作: 642 | - 检查**铃铛** 643 | - 检查**地毯** 644 | - 检查**橱柜** 645 | - 检查**床** 646 | - 查看**线索** 647 | - 返回**走廊** 648 | action: [incr, incr, calc] 649 | param: [round2, proof2, 'clues2==""?clue2_4:clues2+"、"+clue2_4'] 650 | - keywords: [橱柜, 柜, 橱, bed] 651 | description: |- 652 | > 福尔摩斯打开柜子,里面都是女人日常用的东西,看不出什么异常。 653 | > 线索数: `@proof2/7` 654 | > 回合数: `@round2/10` 655 | 656 | @sender,请选择要检查或进行的动作: 657 | - 检查**铃铛** 658 | - 检查**地毯** 659 | - 检查**橱柜** 660 | - 检查**床** 661 | - 查看**线索** 662 | - 返回**走廊** 663 | action: [incr] 664 | param: [round2] 665 | - keywords: [查看, 检查, 查, 收集, 线索, view, inspect] 666 | description: |- 667 | > 已收集的线索: `@clues2` 668 | > 当前线索数: `@proof2/7`,当前回合数: `@round2/10` 669 | 670 | @sender,请选择要检查或进行的动作: 671 | - 检查**铃铛** 672 | - 检查**地毯** 673 | - 检查**橱柜** 674 | - 检查**床** 675 | - 返回**走廊** 676 | action: none 677 | "3.7": 678 | chapter: 继父的房间 679 | story: |- 680 | > 与继女的房间相比,罗伊洛特医生的房间较为宽敞,但陈设同样简朴。一张行军床,一个摆满书的书架。一张圆桌,一个大铁保险柜,大概就是一眼能看到的全部了。 681 | > 线索数: `@proof2/7` 682 | > 回合数: `@round2/10` 683 | 684 | @sender,请选择要检查或进行的动作: 685 | - 检查**保险柜** 686 | - 检查**书架** 687 | - 检查**角落** 688 | - 查看**线索** 689 | - 返回**走廊** 690 | choices: 691 | - keywords: [返回, 后退, 走廊, 上一级, 离开, back, hall] 692 | description: "" 693 | action: goto 694 | param: "3.4" 695 | - keywords: [保险柜, 柜, 保险, ring] 696 | description: |- 697 | > 福尔摩斯敲敲保险柜问道:“你知道里面装的什么吗?” 698 | > “我继父业务上的文件。” 699 | > “这么说你看过?里面难道不会是一只猫什么的?” 700 | > “不会啊,你这想法可真奇怪” 701 | > 正说着,福尔摩斯从保险柜上拿起一个`盛奶的碟子`。 702 | > 线索数: `@proof2/7` 703 | > 回合数: `@round2/10` 704 | 705 | @sender,请选择要检查或进行的动作: 706 | - 检查**保险柜** 707 | - 检查**书架** 708 | - 检查**角落** 709 | - 查看**线索** 710 | - 返回**走廊** 711 | 你有一个`新的`提问选项: 712 | - 检查**盛奶的碟子** 713 | action: [incr, incr, calc] 714 | param: [round2, proof2, 'clues2==""?clue2_5:clues2+"、"+clue2_5'] 715 | - keywords: [奶, 碟, 盘, hool] 716 | description: |- 717 | > “可是,我们家没养猫啊,我继父倒是有一只印度猎豹和狒狒”斯托纳小姐说。 718 | > 福尔摩斯答道:“的确,以一只猎豹的`体型`,一碟奶恐怕不够的。” 719 | > 线索数: `@proof2/7` 720 | > 回合数: `@round2/10` 721 | 722 | @sender,请选择要检查或进行的动作: 723 | - 检查**保险柜** 724 | - 检查**书架** 725 | - 检查**角落** 726 | - 查看**线索** 727 | - 返回**走廊** 728 | action: [incr, incr, calc] 729 | param: [round2, proof2, 'clues2==""?clue2_6:clues2+"、"+clue2_6'] 730 | - keywords: [书, 架, shelf] 731 | description: |- 732 | > 继父的书架上,摆的大多是技术性的书籍。 733 | > 线索数: `@proof2/7` 734 | > 回合数: `@round2/10` 735 | 736 | @sender,请选择要检查或进行的动作: 737 | - 检查**保险柜** 738 | - 检查**书架** 739 | - 检查**角落** 740 | - 查看**线索** 741 | - 返回**走廊** 742 | action: [incr] 743 | param: [round2] 744 | - keywords: [角落, corner] 745 | description: |- 746 | > 福尔摩斯转过身,悬挂在床头的一根`小鞭子`引起了他的注意。这跟鞭子卷着,而且打成了结,似乎是为了让鞭绳盘成一个圈。 747 | > 华生问:“那只不过一根普通的鞭子,为什么要打结呢?” 748 | > 线索数: `@proof2/7` 749 | > 回合数: `@round2/10` 750 | 751 | @sender,请选择要检查或进行的动作: 752 | - 检查**保险柜** 753 | - 检查**书架** 754 | - 检查**角落** 755 | - 查看**线索** 756 | - 返回**走廊** 757 | action: [incr, incr, calc] 758 | param: [round2, proof2, 'clues2==""?clue2_7:clues2+"、"+clue2_7'] 759 | - keywords: [查看, 检查, 查, 收集, 线索, view, inspect] 760 | description: |- 761 | > 已收集的线索: `@clues2` 762 | > 当前线索数: `@proof2/7`,当前回合数: `@round2/10` 763 | 764 | @sender,请选择要检查或进行的动作: 765 | - 检查**保险柜** 766 | - 检查**书架** 767 | - 检查**角落** 768 | - 返回**走廊** 769 | action: none 770 | "3.8": 771 | chapter: 调查 772 | story: |- 773 | > 福尔摩斯拼命思考着这`@proof2`条线索,感觉🧩拼图就快完整起来了。 774 | > 当前线索: `@clues2` 775 | 776 | @sender,请问你知道真相了吗? 777 | - **我明白了**: 真相永远只有一个! 778 | - **我再看看**: 求稳,搜寻剩下的线索(推荐) 779 | choices: 780 | - keywords: 781 | [明白, 可以, 知道, 真相, 一个, knew, know, got, get, underst, yes] 782 | description: "" 783 | action: goto 784 | param: "3.9" 785 | - keywords: 786 | [ 787 | 再, 788 | 想想, 789 | 看看, 790 | 等, 791 | 算, 792 | 不, 793 | 没, 794 | 别, 795 | 待会, 796 | 剩下, 797 | 继续, 798 | 搜寻, 799 | 稳, 800 | wait, 801 | no, 802 | ] 803 | description: "" 804 | action: goto 805 | param: "3.4" 806 | "3.9": 807 | chapter: 调查 808 | story: |- 809 | > 有那么一瞬,福尔摩斯看起来似乎知道了真相。 810 | > 斯托纳女士问道:“果然我姐姐不是死于自杀的吗?” 811 | > 福尔摩斯回答:“你的姐姐一定是……” 812 | 813 | @sender,请回答: 814 | - **自杀** 815 | - **他杀** 816 | choices: 817 | - keywords: [自杀, 自己, self] 818 | description: |- 819 | > “怎么可能?”斯托纳女士质疑道,“我都(裤)请(子)你(都)来(脱)了(了)你给我看这个?” 820 | > “请原谅我的玩笑,其实你姐姐是…”福尔摩斯解释道。 821 | > 好感度-1 822 | > 当前好感度: @love 823 | 824 | @sender,请回答: 825 | - **他杀** 826 | action: decr 827 | param: love 828 | - keywords: [他杀, 她杀, other] 829 | description: "" 830 | action: goto 831 | param: "3.10" 832 | "3.10": 833 | chapter: 调查 834 | story: |- 835 | > 就像闪电⚡️划过一般,所有这`@proof2`条线索连成了一条线。 836 | > `“@clues2……”` 837 | > “那我姐姐是怎么死的?”斯托纳女士问道。 838 | 839 | @sender,福尔摩斯只答了一个字: 840 | ⚠️注意: 严肃的推理不是选择题,而是填空题。请仔细分析线索,然后谨慎作答。 841 | choices: 842 | - keywords: [蛇, 毛毛虫, snake] 843 | description: "" 844 | action: goto 845 | param: "3.11" 846 | - keywords: [豹, 狒, 人, 毒, 动物, 病, 咬, animal] 847 | description: |- 848 | > ❌答案错误,但已经很接近了,再多给一次机会,加油~ 849 | > 剩余尝试次数: `@used2` 850 | 851 | @sender,请问姐姐的`死因`的真相是(一个字): 852 | action: none 853 | - keywords: [作弊, 偷懒, 提示, 帮助, cheat, help] 854 | description: |- 855 | > 监测到作弊行为,作弊可耻,但有用! 856 | > 机会数➕1⃣️ 857 | > 剩余尝试次数: `@used2` 858 | > 提示: 是一种极其`危险`的动物,而且与一门`编程语言`相关 859 | 860 | @sender,请问姐姐的`死因`的真相是(一个字): 861 | action: incr 862 | param: used2 863 | - keywords: [一个字, one, word] 864 | description: |- 865 | > ❌答案错误,机会有限,请不要抖机灵啊喂~ 866 | > 剩余尝试次数: `@used2` 867 | 868 | @sender,请问姐姐的`死因`的真相是(一个字): 869 | action: decr 870 | param: used2 871 | - keywords: [] 872 | description: |- 873 | > ❌答案错误,请重新思考。 874 | > 剩余尝试次数: `@used2` 875 | 876 | @sender,请问姐姐的`死因`的真相是(一个字): 877 | action: decr 878 | param: used2 879 | "3.11": 880 | chapter: 调查 881 | story: |- 882 | > “天呐!真相竟然是这样。”斯托纳女士受到了惊吓。 883 | > “别怕,我教你如何去做。”福尔摩斯安抚她说。 884 | 885 | @sender,请输入“继续”阅读下一章剧情: 886 | - **继续** 887 | choices: 888 | - keywords: [继续, 下一步, jixu, continue, next] 889 | description: "" 890 | action: goto 891 | param: "4.1" 892 | "3.12": 893 | chapter: 调查 894 | story: |- 895 | > “我虽知道答案,`但是`为了你的安全,暂时先不告诉你。”福尔摩斯解释道,“不用怕,今晚你按我说的来做。” 896 | 897 | @sender,请输入“继续”阅读下一章剧情: 898 | - **继续** 899 | choices: 900 | - keywords: [继续, 下一步, jixu, continue, next] 901 | description: "" 902 | action: goto 903 | param: "4.1" 904 | "4.1": 905 | chapter: 至暗时刻 906 | story: |- 907 | > 福尔摩斯为斯托纳女士谋划说:“你继父回来时,一定要假装头疼,把自己关在房间里。夜深后,必须打开百叶窗,把灯摆在那儿作为信号。随后带上铺盖悄悄溜到你以前的房间,虽然在修缮,但过一夜应该问题不大。其余的事情你就不要管了。” 908 | > “可是,你们打算怎么办呢?” 909 | > “我们要在你的卧室过夜,调查打扰你的口哨声的来源。” 910 | 911 | @sender,请输入“继续”阅读下一节剧情: 912 | - **继续** 913 | choices: 914 | - keywords: [继续, 下一步, jixu, continue, next] 915 | description: "" 916 | action: goto 917 | param: "4.2" 918 | "4.2": 919 | chapter: 至暗时刻 920 | story: |- 921 | > 福尔摩斯和华生在一个旅馆中守候着,大约9点钟的时候,周遭的灯光熄灭了,庄园里一片漆黑。 922 | > 两个小时缓慢地过去了,就在时钟敲响11点的时候,他们的正前方出现了一盏明亮的灯。 923 | > “我们走,”福尔摩斯说,“华生,带上手枪。” 924 | 925 | @sender,请输入“继续”阅读下一节剧情: 926 | - **继续** 927 | choices: 928 | - keywords: [继续, 下一步, jixu, continue, next] 929 | description: "" 930 | action: goto 931 | param: "4.3" 932 | "4.3": 933 | chapter: 至暗时刻 934 | story: |- 935 | > 两人在漆黑如铁的房间里,一直缄默地等候着。每刻钟都仿佛无限漫长。 936 | > 突然,从通气孔方向闪来一道转瞬即逝的亮光。接着,是另一种声音,就像烧开水壶的嘶嘶声。 937 | > 福尔摩斯在听到口哨声的瞬间,拿起了他带来的藤编,猛烈的抽打起来… 938 | 939 | @sender,请选择动作(注意,这一选择`至关重要`): 940 | - 抽打**铃绳**: 🔔悬在床边的绳子 941 | - 抽打**门把手**: 🚪光滑的门把手 942 | - 抽打**地毯**: 📜柔软的地毯 943 | choices: 944 | - keywords: [铃, 绳, 床, ring] 945 | description: "" 946 | action: goto 947 | param: "4.4" 948 | - keywords: [门, 把, 手, 毯, door] 949 | description: "" 950 | action: goto 951 | param: "10.4" 952 | "4.4": 953 | chapter: 至暗时刻 954 | story: |- 955 | > “看见了没有,华生?”他大声地嚷着,“看见了没有!” 956 | > 我看不清福尔摩斯拼命抽打的是什么东西,但我却看到,他的脸死一样的苍白,满脸恐怖和憎恶的表情。 957 | > 他停下了抽打,眼睛怒视着通气孔。 958 | 959 | @sender,请输入“继续”阅读下一节剧情: 960 | - **继续** 961 | choices: 962 | - keywords: [继续, 下一步, jixu, continue, next] 963 | description: "" 964 | action: goto 965 | param: "4.5" 966 | "4.5": 967 | chapter: 至暗时刻 968 | story: |- 969 | > 紧接着,在黑夜的寂静中,突然爆发出一声我此前从未听到过的凄厉尖叫。那叫声越来越高,交织着痛苦、恐惧和愤怒,让人不寒而栗。 970 | > “一切都结束了。”福尔摩斯说,“该走了,我们先去哪个房间?” 971 | 972 | @sender,请决定要去的房间: 973 | - **继父**的房间 974 | - **斯托纳小姐**的房间 975 | choices: 976 | - keywords: [继父, 父, father, jifu] 977 | description: "" 978 | action: goto 979 | param: "4.6" 980 | - keywords: [斯托纳, 姐, 小, sister, girl, sit] 981 | description: "" 982 | action: goto 983 | param: "4.7" 984 | "4.6": 985 | chapter: 光明 986 | story: |- 987 | > 福尔摩斯转动门把手,进入继父的房间。 988 | > 罗伊洛特医生披着一件长长的睡衣,横躺在木椅上,他的下巴向上翘起,眼睛恐怖地、僵直地盯着天花板角落的通气孔。 989 | > 他的头上缠着一条异样的、带有褐色斑点的带子,这是一条印度蝰蛇。 990 | 991 | @sender,请输入“继续”阅读下一节剧情: 992 | - **继续** 993 | choices: 994 | - keywords: [继续, 下一步, jixu, continue, next] 995 | description: "" 996 | action: goto 997 | param: "4.8" 998 | "4.7": 999 | chapter: 光明 1000 | story: |- 1001 | > 福尔摩斯对斯托纳小姐说:“到这来吧,一切都结束了。” 1002 | > 三人一起来到她继父的房间,罗伊洛特医生披着一件长长的睡衣,横躺在木椅上,他的下巴向上翘起,眼睛恐怖地、僵直地盯着天花板角落的通气孔。 1003 | > 他的头上缠着一条异样的、带有褐色斑点的带子,这是一条印度蝰蛇。 1004 | 1005 | @sender,请输入“继续”阅读下一节剧情: 1006 | - **继续** 1007 | choices: 1008 | - keywords: [继续, 下一步, jixu, continue, next] 1009 | description: "" 1010 | action: goto 1011 | param: "4.9" 1012 | "4.8": 1013 | chapter: 光明 1014 | story: |- 1015 | > 此时斯托纳小姐也闻声进来,看到眼前的一幕,一切都真相大白。 1016 | 1017 | @sender,请输入“继续”阅读下一节剧情: 1018 | - **继续** 1019 | choices: 1020 | - keywords: [继续, 下一步, jixu, continue, next] 1021 | description: "" 1022 | action: goto 1023 | param: "4.9" 1024 | "4.9": 1025 | chapter: 谢幕 1026 | story: |- 1027 | > 第二天早上,安顿好斯托纳小姐、跟警方交待完情况后,福尔摩斯和华生二人踏上了回城的路。 1028 | > “多么狠毒的继父,我在看到绳铃和通气孔的时候,就不免怀疑。当发现📦保险柜和那碟🥛牛奶,并和口哨和金属哐啷声结合起来之后,才发现这个可怕的事实。原来医生一直在训练🐍毒蛇。” 1029 | > “所以你狠狠的抽打绳铃,其实是在抽打这条蛇,于是它从通气孔钻了回去。” 1030 | 1031 | @sender,请输入“继续”阅读下一节剧情: 1032 | - **继续** 1033 | choices: 1034 | - keywords: [继续, 下一步, jixu, continue, next] 1035 | description: "" 1036 | action: goto 1037 | param: "4.10" 1038 | "4.10": 1039 | chapter: 谢幕 1040 | story: |- 1041 | > “对,结果还引起它💢愤怒地咬死了它看到的第一个人。这样看来,我无疑要对罗伊洛特医生的死负间接责任。但凭良心说,我是不大可能因此而感到内疚的。” 1042 | 1043 | @sender,请输入“继续”阅读下一节剧情: 1044 | - **继续** 1045 | choices: 1046 | - keywords: [继续, 下一步, jixu, continue, next] 1047 | description: "" 1048 | action: goto 1049 | param: "4.11" 1050 | "4.11": 1051 | chapter: 致谢 1052 | story: |- 1053 | > 感谢您的游玩,请联系`@pixelcao`反馈游玩体验,欢迎拍砖🧱 1054 | > 回合数总计: `@rounds` 1055 | 1056 | @sender,您可以输入“重置”重新游玩: 1057 | - **重置** 1058 | choices: {} 1059 | "10.1": 1060 | chapter: Game Over 1061 | story: |- 1062 | > 由于❤️好感度降到了 `@love`,这位女士决定不委托福尔摩斯先生来解决案子了。 1063 | > 并且由于你的所作所为,福尔摩斯先生的声望在贝克街每况愈下,最终决定搬离贝克街13号…… 1064 | > 回合数总计: `@rounds` 1065 | 1066 | @sender,请输入“重置”来重新开始吧🌝 1067 | - **重置** 1068 | choices: {} 1069 | "10.2": 1070 | chapter: Game Over 1071 | story: |- 1072 | > 就在谈话间,斯托纳女士的继父突然破门而入,看到房间里的继女、夏洛克和华生,连开三枪🔫 1073 | > 调查中止,当前回合数: @round1,当前线索数: @proof1 1074 | > 回合数总计: `@rounds` 1075 | 1076 | @sender,请输入“重置”来重新开始吧🌝 1077 | - **重置** 1078 | choices: {} 1079 | "10.3": 1080 | chapter: Game Over 1081 | story: |- 1082 | > 砰地一声,罗伊洛特的脑袋炸开了花。此时,贝克街上过往的路人慌张的找来警察👮‍♀️ 1083 | > 福尔摩斯和华生一起被抓了起来,虽然斯托纳小姐安然无恙,但福尔摩斯再也不能继续探案之路了…… 1084 | > 回合数总计: `@rounds` 1085 | 1086 | @sender,请输入“重置”来重新开始吧🌝 1087 | - **重置** 1088 | choices: {} 1089 | "10.4": 1090 | chapter: Game Over 1091 | story: |- 1092 | > 嘶嘶声变强了,福尔摩斯突然发出惨叫,被什么东西咬中了胳膊。不到10秒,就躺在地上、一动不动了。 1093 | > 面对破门而入的继父,华生扣下了扳机,救下了隔壁的斯托纳小姐。 1094 | > 此时,他才发现,地上游走的斑点带子,实际上是一条狠毒的印度蝰蛇。 1095 | > 回合数总计: `@rounds` 1096 | 1097 | @sender,请输入“重置”来重新开始吧🌝 1098 | - **重置** 1099 | choices: {} 1100 | dynamics: 1101 | - conditions: 1102 | chapter: "*" 1103 | expression: love <= 0 1104 | action: goto 1105 | param: "10.1" 1106 | - conditions: 1107 | chapter: "1.*" 1108 | expression: round1 > 3 && proof1 < 2 1109 | action: goto 1110 | param: "10.2" 1111 | - conditions: 1112 | chapter: "1.*" 1113 | expression: round1 > 3 && proof1 >= 2 1114 | action: [goto, calc, calc] 1115 | param: ["1.8", "round1?0:0", "proof1?0:0"] 1116 | - conditions: 1117 | chapter: "3.*" 1118 | expression: proof2 >= 5 && round2 < 10 && used1 == 0 1119 | action: [goto, incr] 1120 | param: ["3.8", "used1"] 1121 | - conditions: 1122 | chapter: "3.*" 1123 | expression: (proof2 >= 7 || round2 >= 10) && used3 == 0 1124 | action: [goto, incr] 1125 | param: ["3.10", "used3"] 1126 | - conditions: 1127 | chapter: "3.*" 1128 | expression: used2 <= 0 1129 | action: [goto, calc] 1130 | param: ["3.12", "used2?999:999"] 1131 | defaults: 1132 | - conditions: 1133 | chapter: "*" 1134 | keywords: [help, man, 帮助, 怎么玩, 你是谁] 1135 | action: none 1136 | description: |- 1137 | > @sender,欢迎来到`@title`,这是一个`文字冒险游戏`,你通过输入`动作`或`指令`来推进剧情、获取帮助。 1138 | > 一百多年来,《福尔摩斯探案集》📖被翻译成57种文字,畅销不衰,🎬衍生电影超过200部。 1139 | > 《斑点带子案》作为二葱记忆深刻的一则短篇,有着离奇的剧情、阴森的氛围、意料之外的结局。 1140 | > 现在,让我们以bot的形式,进入19世纪的英国,自己就是福尔摩斯,试试看你的决策,会把案子带到怎样的🧭方向。 1141 | > 每个人都有`独立的进度和存档`,建议拉到小群中调戏和游玩呢😄 1142 | 1143 | 游戏基本操作如下: 1144 | - **继续游戏**: 直接@我即可 1145 | - **开始游戏**: start 1146 | - **帮助**: help、man 1147 | - **提示**: hint 1148 | - **重置**: reset 1149 | - conditions: 1150 | chapter: "*" 1151 | keywords: [hint, 提示] 1152 | action: none 1153 | description: "@sender,本小节没有提示😂" 1154 | - conditions: 1155 | chapter: "*" 1156 | keywords: [reset, start, 重置, 回到开始, 重新开始, 开始游戏] 1157 | action: reset 1158 | description: "重置章节" 1159 | - conditions: 1160 | chapter: "*" 1161 | keywords: [] 1162 | action: none 1163 | description: |- 1164 | > 福尔摩斯愣了一下,感觉自己像个被傻几操控的提线布偶🌚🌝 1165 | 1166 | @sender,请@我,输入“帮助”以获取游玩方法: 1167 | - **帮助** 1168 | --------------------------------------------------------------------------------