diff --git a/packages/client/index.js b/packages/client/index.js index 486e5e0..f7988ed 100644 --- a/packages/client/index.js +++ b/packages/client/index.js @@ -82,6 +82,10 @@ async function connect(sessionId) { debug('receive graph:', data) }) + socket.on('tasks', (data) => { + debug('receive tasks:', data) + }) + socket.on('close', function close() { console.log('Disconnected from the server.') exit() diff --git a/quests/helloworld/game.yml b/quests/helloworld/game.yml index 6f99af0..c474213 100644 --- a/quests/helloworld/game.yml +++ b/quests/helloworld/game.yml @@ -4,8 +4,11 @@ instruction: | 你身在一個漆黑的房間裡,沒有任何的光線或聲音。你想不起來為什麼自己會來到這裡。但你的腦中隱約感到某個聲音跟你說: 「輸入 echo Hello World!」 + stages: - id: helloworld + task: 使用 `echo Hello World!` 召喚精靈 + repeatable: false condition: command: - echo Hello World @@ -26,7 +29,10 @@ stages: - 現在,請你輸入 `ls` 來查看目前的目錄下有哪些檔案。 hints: - 使用 `ls` 指令來查看目前的目錄下有哪些檔案。 + - id: ls + task: 搜索卷軸 + repeatable: false requirements: - helloworld condition: @@ -48,7 +54,25 @@ stages: hints: - 使用 `cat [檔案名稱]` 指令來查看檔案內容。 - 使用 `Tab` 鍵來取得指令提示。 + + - id: escape + task: 移動到下一個房間 + repeatable: false + requirements: + - ls + condition: + pwd: + - '^/home/commander/jail$' + responses: + - type: narrative + content: + - 你進入了一個牢房,牢房裡有一個門,門上有一個鎖。 + - 突然,你聽到門上鎖的聲音。 + - 轉身發現,自己被關在了牢房裡。 + - id: cat + task: 查看卷軸 + repeatable: false requirements: - ls condition: @@ -60,7 +84,10 @@ stages: content: - 你找到的是一個被遺忘的卷軸,上面寫著解除封印的咒語。 - 使用你已經知道的 `echo` 指令來詠唱咒語。 + - id: END + task: 解除封印 + repeatable: false requirements: - cat condition: diff --git a/quests/helloworld/story.md b/quests/helloworld/story.md index 66a2a21..528cbf8 100644 --- a/quests/helloworld/story.md +++ b/quests/helloworld/story.md @@ -6,7 +6,10 @@ 1. echo 朗誦魔術 2. ls 搜索魔術 -3. cat 檢察魔術 +3. cat 檢查魔術 +4. cd 移動魔術 +5. mv 搬運魔術 +6. rm 消滅魔術 ## 關卡設計 @@ -14,8 +17,8 @@ 2. 學習到了 ls 3. 使用 ls 找到地上放置了一個卷軸 4. 學習 cat -5. 使用 cat 查看卷軸內容 -6. 用 echo 念出卷軸上的咒語 +5. 使用 cat 查看卷軸內容,發現 +6. 請幫我設計: diff --git a/server/src/api/socket.js b/server/src/api/socket.js index 72f60e9..d3dd797 100644 --- a/server/src/api/socket.js +++ b/server/src/api/socket.js @@ -5,9 +5,9 @@ import { defaultUser, genSessionJWT } from '../utils/auth.js' const sessions = new Map() -function listenToSession(sessionId, event, callback) { - const callbacks = sessions.get((sessionId, event)) || [] - sessions.set((sessionId, event), callbacks.concat(callback)) +function listenToSession(sessionId, callback) { + const callbacks = sessions.get(sessionId) || [] + sessions.set(sessionId, callbacks.concat(callback)) } function removeFromSession(sessionId, event, callback) { @@ -18,10 +18,10 @@ function removeFromSession(sessionId, event, callback) { ) } -export function pushToSession(sessionId, event, data) { - const callbacks = sessions.get((sessionId, event)) +export function pushToSession(sessionId, ...args) { + const callbacks = sessions.get(sessionId) if (callbacks) { - callbacks.forEach((callback) => callback(data)) + callbacks.forEach((callback) => callback(...args)) } } @@ -68,16 +68,16 @@ async function connectClient(socket) { stream.socket.write(message) }) - const graphCallback = (data) => { - socket.emit('graph', data) + const socketCallback = (event, data) => { + socket.emit(event, data) } - listenToSession(session.id, 'graph', graphCallback) + listenToSession(session.id, socketCallback) socket.on('disconnect', () => { console.log('Disconnected from the client.') stream.socket.write('exit\n') - removeFromSession(session.id, 'graph', graphCallback) + removeFromSession(session.id, socketCallback) stream.destroy() }) diff --git a/server/src/containers/docker.js b/server/src/containers/docker.js index 74e2d86..a4c768f 100644 --- a/server/src/containers/docker.js +++ b/server/src/containers/docker.js @@ -15,7 +15,7 @@ const containerOptions = { Binds: !config.isProduction && config.hostPwd ? [ - `${config.hostPwd}/quests/helloworld/home:/home/rudeus`, + `${config.hostPwd}/quests/helloworld/home:/home/commander`, `${config.hostPwd}/packages/container:/usr/local/lib/container`, ] : [], diff --git a/server/src/game/commandHandler.js b/server/src/game/commandHandler.js index c5e6555..a166d6f 100644 --- a/server/src/game/commandHandler.js +++ b/server/src/game/commandHandler.js @@ -5,7 +5,7 @@ import { pushToSession } from '../api/socket.js' // 檢查 pattern 是否符合 input const checkMatch = (pattern, input) => { - if (pattern.length === 0) return true + if (!pattern || pattern.length === 0) return true return pattern.some((p) => { const regex = new RegExp(p) return regex.test(input) @@ -52,37 +52,53 @@ export default class CommandHandler { this.session.graph = graph } + getTask(stage) { + return this.session.tasks.find((t) => t.id === stage.id) + } + + isCompleted(stage) { + const task = this.getTask(stage) + return task ? task.completed : false + } + getStages() { - return this.quest.stages.filter((s) => - s.requirements.every((r) => this.session.completed.includes(r)) + return this.quest.stages.filter( + (stage) => + stage.requirements.every((r) => this.isCompleted({ id: r })) && + (stage.repeatable || !this.isCompleted(stage)) ) } - matchStage(stage) { - const commandMatch = checkMatch( - stage.condition.command, - this.commandInput.command - ) - const outputMatch = checkMatch( - stage.condition.output, - this.commandInput.output - ) - const errorMatch = checkMatch( - stage.condition.error, - this.commandInput.output + isMatch(stage) { + const keys = ['command', 'output', 'error', 'pwd'] + return keys.every((k) => + checkMatch(stage.condition[k], this.commandInput[k]) ) + } - if (!commandMatch || !outputMatch || !errorMatch) return {} - - this.session.completed.push(stage.id) + execute(stage) { + this.session.tasks + .filter((t) => t.id === stage.id) + .forEach((t) => { + t.completed = true + }) this.session.hints.push(...stage.hints) if (stage.id === 'END') { this.session.status = 'finished' this.session.finishedAt = new Date() } + const stages = this.getStages() + console.debug(stages) + const newTasks = stages + .filter((s) => s.task) + .map((s) => ({ id: s.id, name: s.task, completed: false })) + + this.session.tasks.push(...newTasks) + pushToSession(this.session.id, 'tasks', this.session.tasks) return { responses: stage.responses, hints: stage.hints, + tasks: newTasks, } } @@ -90,18 +106,18 @@ export default class CommandHandler { this.quest = await Quest.findById(this.session.quest) const stages = this.getStages() if (stages.length === 0) { - console.error('stage not found', this.session.progress) + console.error('stage not found', this.session.tasks) return {} } this.handleEvent() this.handleCommand() - const response = stages.reduce( - (r, s) => ({ ...r, ...this.matchStage(s) }), - {} - ) + const response = stages + .filter((s) => this.isMatch(s)) + .reduce((r, s) => ({ ...r, ...this.execute(s) }), {}) + console.log(this.session) return { end: this.session.status === 'finished', ...response, diff --git a/server/src/models/quest.js b/server/src/models/quest.js index 434a9e5..84c1966 100644 --- a/server/src/models/quest.js +++ b/server/src/models/quest.js @@ -15,25 +15,25 @@ const stageSchema = new Schema({ type: String, required: true, }, + task: { + type: String, + }, requirements: [ { type: String, required: true, }, ], + repeatable: { + type: Boolean, + required: true, + default: false, + }, condition: { - command: { - type: [String], - required: false, - }, - output: { - type: [String], - required: false, - }, - error: { - type: [String], - required: false, - }, + command: [String], + output: [String], + error: [String], + pwd: [String], }, responses: { type: [responseSchema], @@ -43,10 +43,6 @@ const stageSchema = new Schema({ type: [String], required: true, }, - next: { - type: String, - required: true, - }, }) const Quest = model( @@ -56,6 +52,18 @@ const Quest = model( title: String, order: Number, instruction: String, + tasks: { + id: String, + content: String, + display: { + type: Boolean, + default: false, + }, + completed: { + type: Boolean, + default: false, + }, + }, stages: [stageSchema], }) ) diff --git a/server/src/models/session.js b/server/src/models/session.js index 5aeac2f..ddb4fad 100644 --- a/server/src/models/session.js +++ b/server/src/models/session.js @@ -14,6 +14,21 @@ nodeSchema.add({ children: [nodeSchema], }) +const taskSchema = new Schema({ + id: { + type: String, + required: true, + }, + name: { + type: String, + required: true, + }, + completed: { + type: Boolean, + default: false, + }, +}) + const Session = model( 'Session', new Schema( @@ -39,7 +54,7 @@ const Session = model( default: Date.now, required: true, }, - completed: [String], + tasks: [taskSchema], hints: [String], graph: { type: nodeSchema,