mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-08 11:30:12 +08:00
feat: combine task with stage
This commit is contained in:
1 parent
de8d49aea3
commit
dd7829b3e9
8 files changed
+127
-54
No files matched your search
@@ -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()
|
||||||
|
|||||||
@@ -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,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
@@ -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()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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`,
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
|
|||||||
@@ -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
@@ -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],
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in new issue
Block a user