feat: combine task with stage

This commit is contained in:
wancat committed 2023-09-01 14:56:28 +08:00
1 parent de8d49aea3
commit dd7829b3e9
8 files changed
+127 -54

No files matched your search

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