diff --git a/.eslintrc.js b/.eslintrc.js
index 7410a96..0d2e94d 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -28,5 +28,6 @@ module.exports = {
'no-plusplus': 'off',
'prettier/prettier': ['error', { endOfLine: 'auto', semi: false }],
'import/prefer-default-export': 'off',
+ 'no-restricted-syntax': ['off', 'ForOfStatement'],
},
}
diff --git a/app/src/components/HintPart.vue b/app/src/components/HintPart.vue
index dad0189..54b417f 100644
--- a/app/src/components/HintPart.vue
+++ b/app/src/components/HintPart.vue
@@ -60,9 +60,15 @@ const right = () => {
-
- {{ sessionStore.session.hints[current] }}
-
+
diff --git a/app/src/store/session.js b/app/src/store/session.js
index ac64e88..5464bd0 100644
--- a/app/src/store/session.js
+++ b/app/src/store/session.js
@@ -86,7 +86,7 @@ function updateGraph(event) {
}
function updateHints(hints) {
- store.session.hints.push(...hints)
+ store.session.hints.push(hints)
}
function updateStatus(status) {
diff --git a/packages/models/quest.js b/packages/models/quest.js
index 4c500bd..efdd130 100644
--- a/packages/models/quest.js
+++ b/packages/models/quest.js
@@ -35,6 +35,26 @@ conditionSchema.add({
not: conditionSchema,
})
+const exceptionSchema = new Schema({
+ condition: {
+ type: conditionSchema,
+ required: false,
+ },
+ catchAll: {
+ type: Boolean,
+ required: true,
+ default: false,
+ },
+ responses: {
+ type: [responseSchema],
+ required: true,
+ },
+ hints: {
+ type: [String],
+ required: true,
+ },
+})
+
const stageSchema = new Schema({
id: {
type: String,
@@ -66,6 +86,10 @@ const stageSchema = new Schema({
type: [String],
required: true,
},
+ exceptions: {
+ type: [exceptionSchema],
+ required: true,
+ },
})
const Quest = model(
diff --git a/packages/models/session.js b/packages/models/session.js
index 85568a1..50c62b5 100644
--- a/packages/models/session.js
+++ b/packages/models/session.js
@@ -54,7 +54,7 @@ const Session = model(
required: true,
},
tasks: [taskSchema],
- hints: [String],
+ hints: [[String]],
graph: {
type: nodeSchema,
default: {
diff --git a/quests/copy/Dockerfile b/quests/copy/Dockerfile
new file mode 100644
index 0000000..2a8e341
--- /dev/null
+++ b/quests/copy/Dockerfile
@@ -0,0 +1,3 @@
+FROM linuxodyssey/quest-base
+
+COPY home ${HOME}
\ No newline at end of file
diff --git a/quests/copy/game.yml b/quests/copy/game.yml
index f4a7260..43ae6e2 100644
--- a/quests/copy/game.yml
+++ b/quests/copy/game.yml
@@ -1,3 +1,247 @@
-title: Read the File
+title: 複製檔案
instruction: |
- 閱讀檔案
+ 在這個關卡中,你將學習如何複製檔案。
+
+requirements:
+ - create
+
+stages:
+ - id: start
+ task: 輸入 `echo start` 來開始
+ condition:
+ output:
+ - start
+ exceptions:
+ - condition:
+ command:
+ - ^echo$
+ responses:
+ - type: narrative
+ content:
+ - 你忘記輸入 `start` 了嗎?
+ - 請輸入 `echo start` 來開始
+ - catchAll: true
+ responses:
+ - type: narrative
+ content:
+ - 請輸入 `echo start` 來開始
+ responses:
+ - type: narrative
+ content:
+ - 今天我們要學習如何複製檔案。
+ - 現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,請使用 `ls` 與 `cat` 來查看它們的內容。
+ - 看完之後,使用 `echo continue` 來繼續吧!
+ hints:
+ - 使用 `ls` 來查看資料夾內的檔案
+ - 使用 `cat hello.txt` 以及 `cat world.txt` 來查看檔案內容
+ - 如果終端機卡在某個指令無法輸入,請按下 `Ctrl + C` 來取消該指令
+
+ - id: cat-hello
+ task: 查看 `hello.txt` 的內容
+ requirements:
+ - start
+ condition:
+ command:
+ - ^cat hello.txt$
+ output:
+ - Hello
+ exceptions:
+ - condition:
+ command:
+ - ^cat$
+ responses:
+ - type: narrative
+ content:
+ - 你忘記輸入 `cat` 了嗎?
+ - 請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
+ - condition:
+ command:
+ - ^cat hello$
+ responses:
+ - type: narrative
+ content:
+ - 你忘記輸入 `.txt` 了嗎?
+ - 請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
+ responses:
+ - type: narrative
+ content:
+ - 你成功查看到 `hello.txt` 的內容了!
+
+ - id: cat-world
+ task: 查看 `world.txt` 的內容
+ requirements:
+ - start
+ condition:
+ command:
+ - ^cat world.txt$
+ output:
+ - World
+ responses:
+ - type: narrative
+ content:
+ - 你成功查看到 `world.txt` 的內容了!
+
+ - id: cat-continue
+ task: 使用 `echo continue` 來繼續
+ requirements:
+ - cat-hello
+ - cat-world
+ condition:
+ command:
+ - echo
+ responses:
+ - type: narrative
+ content:
+ - 接下來我們要來學習複製檔案。
+ - '`cp` 這個指令,是 copy 的縮寫,就是複製的意思。'
+ - 我們可以用 `cp [來源檔案] [目的檔案]` 的方式來複製檔案。
+ - 請你使用 `cp` 來複製 `hello.txt` 到 `hello2.txt`。
+ hints:
+ - 使用 `cp hello.txt hello2.txt` 來複製 `hello.txt` 到 `hello2.txt`
+
+ - id: cp-hello
+ task: 複製 `hello.txt` 到 `hello2.txt`
+ requirements:
+ - cat-continue
+ condition:
+ command:
+ - ^cp hello.txt hello2.txt$
+ files:
+ - path: hello2.txt
+ type: file
+ responses:
+ - type: narrative
+ content:
+ - 你成功複製 `hello.txt` 到 `hello2.txt` 了!
+ - 我們打開來確認一下 `hello2.txt` 的內容吧!
+ hints:
+ - 使用 `cat hello2.txt` 來查看 `hello2.txt` 的內容
+
+ - id: cat-hello2
+ requirements:
+ - cp-hello
+ task: 查看 `hello2.txt` 的內容
+ condition:
+ command:
+ - ^cat hello2.txt$
+ output:
+ - Hello
+ responses:
+ - type: narrative
+ content:
+ - 你成功查看到 `hello2.txt` 的內容了!
+ - 正如我們所見,`hello2.txt` 的內容跟 `hello.txt` 一模一樣。
+ - 那我們再來試試看複製 `world.txt` 到 `world2.txt` 吧!
+ hints:
+ - 使用 `cp world.txt world2.txt` 來複製 `world.txt` 到 `world2.txt`
+
+ - id: cp-world
+ requirements:
+ - cat-hello2
+ task: 複製 `world.txt` 到 `world2.txt`
+ condition:
+ command:
+ - ^cp world.txt world2.txt$
+ files:
+ - path: world2.txt
+ type: file
+ responses:
+ - type: narrative
+ content:
+ - 你成功複製 `world.txt` 到 `world2.txt` 了!
+ - 我們打開來確認一下 `world2.txt` 的內容吧!
+ hints:
+ - 使用 `cat world2.txt` 來查看 `world2.txt` 的內容
+
+ - id: cat-world2
+ requirements:
+ - cp-world
+ task: 查看 `world2.txt` 的內容
+ condition:
+ command:
+ - ^cat world2.txt$
+ output:
+ - World
+ responses:
+ - type: narrative
+ content:
+ - 你成功查看到 `world2.txt` 的內容了!
+ - '`world2.txt` 的內容跟 `world.txt` 一模一樣。'
+ - 接下來我們要來學習如何複製資料夾。
+ - 現在目錄下有一個 `notes` 資料夾,裡面有一個 `note.txt` 的檔案。
+ - 我們要能整個資料夾包含裡面的檔案一起複製到 `notes2`。
+ - 跟複製單一檔案不一樣的是,複製資料夾時,我們要加上 `-r` 的參數,recursive,代表遞迴地將資料夾中的所有檔案都複製出來。
+ - 請你使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`。
+ hints:
+ - 使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
+ - 記得要加上 `-r` 的參數來複製所有檔案
+
+ - id: cp-notes
+ requirements:
+ - cat-world2
+ task: 複製 `notes` 資料夾到 `notes2`
+ condition:
+ command:
+ - ^cp -r notes notes2$
+ files:
+ - path: notes2
+ type: directory
+ - path: notes2/note.txt
+ type: file
+ responses:
+ - type: narrative
+ content:
+ - 你成功複製 `notes` 資料夾到 `notes2` 了!
+ - 我們打開來確認一下 `notes2` 資料夾的內容吧!
+ hints:
+ - 使用 `ls notes2` 來查看 `notes2` 資料夾的內容
+
+ - id: ls-notes2
+ requirements:
+ - cp-notes
+ task: 查看 `notes2` 資料夾的內容
+ condition:
+ command:
+ - ^ls notes2$
+ output:
+ - note.txt
+ responses:
+ - type: narrative
+ content:
+ - 你成功查看到 `notes2` 資料夾的內容了!
+ - 我們可以看到,整個 `notes` 資料夾包含裡面的檔案,都被複製到 `notes2` 資料夾了。
+ - 那麼最後我們要來試試看,如果複製一個檔案到已經存在的檔案會發生什麼事。
+ hints:
+ - 使用 `cp hello.txt world.txt` 來複製 `hello.txt` 到 `world.txt`
+
+ - id: cp-overwrite
+ requirements:
+ - ls-notes2
+ task: 複製 `hello.txt` 到 `world.txt`
+ condition:
+ command:
+ - ^cp hello.txt world.txt$
+ responses:
+ - type: narrative
+ content:
+ - 你成功複製 `hello.txt` 到 `world.txt` 了!
+ - 我們打開來確認一下 `world.txt` 的內容吧!
+ hints:
+ - 使用 `cat world.txt`
+
+ - id: END
+ requirements:
+ - cp-overwrite
+ task: 檢查 `world.txt` 的內容
+ condition:
+ command:
+ - ^cat world.txt$
+ output:
+ - Hello
+ responses:
+ - type: narrative
+ content:
+ - '`world.txt` 的內容變成 `Hello` 了!'
+ - 這是因為,當我們複製檔案時,如果目的檔案已經存在,那麼目的檔案的內容就會被覆蓋掉。
+ - 所以在複製檔案的時候,要特別注意目的檔案是否已經存在,否則可能會不小心把資料弄丟。
+ - 以上就是複製檔案的基本操作!
diff --git a/quests/copy/home/hello.txt b/quests/copy/home/hello.txt
new file mode 100644
index 0000000..5ab2f8a
--- /dev/null
+++ b/quests/copy/home/hello.txt
@@ -0,0 +1 @@
+Hello
\ No newline at end of file
diff --git a/quests/copy/home/notes/note.txt b/quests/copy/home/notes/note.txt
new file mode 100644
index 0000000..168015a
--- /dev/null
+++ b/quests/copy/home/notes/note.txt
@@ -0,0 +1 @@
+Note
\ No newline at end of file
diff --git a/quests/copy/home/world.txt b/quests/copy/home/world.txt
new file mode 100644
index 0000000..beef906
--- /dev/null
+++ b/quests/copy/home/world.txt
@@ -0,0 +1 @@
+World
\ No newline at end of file
diff --git a/quests/create/game.yml b/quests/create/game.yml
index 1c21ae7..fad30f0 100644
--- a/quests/create/game.yml
+++ b/quests/create/game.yml
@@ -1,3 +1,169 @@
title: 建立檔案與資料夾
instruction: |
- 資料夾
+ 在這個關卡中,你將學習如何建立檔案與資料夾。
+
+stages:
+ - id: start
+ task: 輸入 `echo start` 來開始
+ condition:
+ command:
+ - ^echo start
+ responses:
+ - type: narrative
+ content:
+ - 這次要學習的指令是 `touch` 指令。
+ - '`touch` 指令可以建立一個檔案。'
+ - 接下來,請你試著在終端機上建立一個檔案,檔名為 `hello.txt`。
+ hints:
+ - 輸入 `touch hello.txt` 來建立一個檔案
+
+ - id: touch
+ task: 使用 `touch` 指令來建立一個檔案
+ requirements:
+ - start
+ condition:
+ files:
+ - path: hello.txt
+ type: file
+ responses:
+ - type: narrative
+ content:
+ - 你應該可以看到終端機上沒有任何回應。
+ - 這表示 `touch` 指令已經成功建立了一個檔案。
+ - 接下來,請你用之前學過的 `ls` 指令來查看目前所在資料夾中有哪些檔案。
+ hints:
+ - 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
+
+ - id: ls-touch
+ task: 使用 `ls` 指令來查看新建立的檔案
+ requirements:
+ - touch
+ condition:
+ command:
+ - ^ls$
+ output:
+ - hello.txt
+ responses:
+ - type: narrative
+ content:
+ - 你應該可以看到終端機上顯示了 `hello.txt` 這個檔案。
+ - 我們再來試試建立其他的檔案吧!
+ - 請你試著在終端機上建立另一個檔案,檔名為 `world.txt`。
+ hints:
+ - 輸入 `touch world.txt` 來建立一個檔案
+
+ - id: touch-2
+ task: 使用 `touch` 指令來建立另一個檔案 `world.txt`
+ requirements:
+ - ls-touch
+ condition:
+ files:
+ - path: world.txt
+ type: file
+ responses:
+ - type: narrative
+ content:
+ - 非常好,你已經成功建立了第二個檔案。
+ - 接下來,請你用之前學過的 `ls` 指令來查看剛剛建立的兩個檔案。
+ hints:
+ - 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
+
+ - id: ls-2
+ task: 使用 `ls` 指令來查看剛剛建立的兩個檔案
+ requirements:
+ - touch-2
+ condition:
+ command:
+ - ^ls$
+ output:
+ - hello.txt
+ - world.txt
+ responses:
+ - type: narrative
+ content:
+ - 你應該可以看到終端機上顯示了 `hello.txt` 和 `world.txt` 這兩個檔案。
+ - 接下來,我們來試試看建立資料夾吧!
+ - '`mkdir` 指令可以建立一個空的資料夾。'
+ - 使用方式是 `mkdir [資料夾名稱]`。
+ - 請你試著在終端機上建立一個資料夾,名稱為 `notes`。
+ hints:
+ - 輸入 `mkdir notes` 來建立一個資料夾
+
+ - id: mkdir
+ task: 使用 `mkdir` 指令建立 `notes` 資料夾
+ requirements:
+ - ls-2
+ condition:
+ files:
+ - path: notes
+ type: folder
+ responses:
+ - type: narrative
+ content:
+ - 非常好,你已經成功建立了 `notes` 資料夾。
+ - 接下來,請你用 `ls` 指令來查看剛剛建立的資料夾。
+
+ - id: ls-mkdir
+ task: 使用 `ls` 查看剛建立的資料夾
+ requirements:
+ - mkdir
+ condition:
+ command:
+ - ^ls$
+ output:
+ - notes
+ responses:
+ - type: narrative
+ content:
+ - 你應該可以看到終端機上顯示了 `notes` 這個資料夾。
+ - 接下來,我們要在 `notes` 資料夾中建立一個 `note.txt` 的檔案。
+ - 首先,我們要先進入 `notes` 資料夾。
+ - 還記得之前的 `cd` 指令嗎?請你試著使用 `cd` 指令來進入 `notes` 資料夾。
+ hints:
+ - 輸入 `cd notes` 來進入 `notes` 資料夾
+ - 可以在輸入到 `cd n` 時按下 `Tab` 鍵來自動補齊資料夾名稱
+
+ - id: cd-notes
+ task: 移動到 `notes` 資料夾
+ requirements:
+ - ls-mkdir
+ condition:
+ pwd:
+ - notes
+ responses:
+ - type: narrative
+ content:
+ - 你已經成功進入了 `notes` 資料夾。
+ - 接下來,請你試著在 `notes` 資料夾裡建立一個檔案,檔名為 `note.txt`。
+ hints:
+ - 輸入 `touch note.txt` 來建立一個檔案
+
+ - id: touch-note
+ task: 在 `notes` 中建立 `note.txt` 檔案
+ requirements:
+ - cd-notes
+ condition:
+ files:
+ - path: notes/note.txt
+ type: file
+ responses:
+ - type: narrative
+ content:
+ - 非常好,你已經成功建立了 `note.txt` 檔案。
+ - 跟之前一樣,你可以使用 `ls` 指令來查看目前所在資料夾中有哪些檔案。
+
+ - id: END
+ task: 使用 `ls` 指令來查看目前所在資料夾中有哪些檔案
+ requirements:
+ - touch-note
+ condition:
+ command:
+ - ^ls
+ output:
+ - note.txt
+ responses:
+ - type: narrative
+ content:
+ - 你應該可以看到終端機上顯示了 `note.txt` 這個檔案。
+ - 恭喜你完成了這個關卡!
+ - 在下一個關卡中,你將學習如何複製檔案與資料夾。
diff --git a/quests/get-started/game.yml b/quests/get-started/game.yml
index 918b5a7..2203bbd 100644
--- a/quests/get-started/game.yml
+++ b/quests/get-started/game.yml
@@ -47,10 +47,10 @@ stages:
- 使用指令 `cat [ 檔案名稱 ]` (concatenate)來查看檔案內容。
- 我可以為你提供一些指令的提示,只要輸入指令的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
- 例如你只要輸入檔案名稱的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
- hints: |
- ➤ 使用 `cat [ 檔案名稱 ]` 指令來查看檔案內容。
- ➤ 使用 `Tab` 鍵來取得指令提示。
- ➤ 如果卡在未知的情況,按下 `Ctrl + C` 嘗試離開。
+ hints:
+ - 使用 `cat [ 檔案名稱 ]` 指令來查看檔案內容。
+ - 使用 `Tab` 鍵來取得指令提示。
+ - 如果卡在未知的情況,按下 `Ctrl + C` 嘗試離開。
- id: read-forgotten-scroll
task: 查看卷軸
diff --git a/quests/move/game.yml b/quests/move/game.yml
index f4a7260..0f3d187 100644
--- a/quests/move/game.yml
+++ b/quests/move/game.yml
@@ -1,3 +1,6 @@
-title: Read the File
+title: 移動檔案
instruction: |
- 閱讀檔案
+ 移動檔案
+
+requirements:
+ - create
diff --git a/quests/remove/game.yml b/quests/remove/game.yml
index f4a7260..b5964c4 100644
--- a/quests/remove/game.yml
+++ b/quests/remove/game.yml
@@ -1,3 +1,7 @@
-title: Read the File
+title: 刪除檔案與資料夾
instruction: |
- 閱讀檔案
+ 刪除檔案
+
+requirements:
+ - copy
+ - move
diff --git a/server/src/api/controllers/sessionController.js b/server/src/api/controllers/sessionController.js
index f21b6e0..a5c03db 100644
--- a/server/src/api/controllers/sessionController.js
+++ b/server/src/api/controllers/sessionController.js
@@ -66,7 +66,7 @@ export const getSessionById = asyncHandler(async (req, res) => {
export const getOrCreateSession = asyncHandler(async (req, res) => {
const { questId } = matchedData(req)
- if ((await isQuestUnlocked(req.user, questId)) || config.testing.enable) {
+ if ((await isQuestUnlocked(req.user, questId)) || config.testing.enabled) {
const session = await getOrCreateActiveSession(req.user, questId)
res.json(sessionDetail(session))
} else {
diff --git a/server/src/game/commandHandler.js b/server/src/game/commandHandler.js
index d03f6f1..26f4526 100644
--- a/server/src/game/commandHandler.js
+++ b/server/src/game/commandHandler.js
@@ -97,6 +97,19 @@ export default class CommandHandler extends SessionHandler {
return matches.some((m) => m === true)
}
+ async checkException(stage) {
+ const exceptions = stage.exceptions || []
+ for (const exception of exceptions) {
+ console.log(exception)
+ // eslint-disable-next-line no-await-in-loop
+ if (exception.condition && (await this.isMatch(exception.condition))) {
+ return exception
+ }
+ if (exception.catchAll) return exception
+ }
+ return null
+ }
+
async run() {
this.quest = await Quest.findById(this.session.quest)
const stages = this.getStages()
@@ -114,6 +127,14 @@ export default class CommandHandler extends SessionHandler {
const stage = stages.find((_, i) => matches[i])
if (!stage) {
+ for (const s of stages) {
+ // eslint-disable-next-line no-await-in-loop
+ const exception = await this.checkException(s)
+ if (exception) {
+ console.log('Got exception', exception)
+ return this.executeException(exception)
+ }
+ }
return {}
}
const response = this.execute(stage)
diff --git a/server/src/game/sessionHandler.js b/server/src/game/sessionHandler.js
index 053126c..2312c4c 100644
--- a/server/src/game/sessionHandler.js
+++ b/server/src/game/sessionHandler.js
@@ -47,7 +47,8 @@ export default class SessionHandler {
t.completed = true
})
- this.session.hints.push(...stage.hints)
+ if (stage.hints && stage.hints.length > 0)
+ this.session.hints.push(stage.hints)
this.addNewTasks()
@@ -56,7 +57,23 @@ export default class SessionHandler {
hints: stage.hints,
callback: () => {
pushToSession(this.session.id, 'tasks', this.session.tasks)
- pushToSession(this.session.id, 'hints', stage.hints)
+ if (stage.hints && stage.hints.length > 0)
+ pushToSession(this.session.id, 'hints', stage.hints)
+ pushToSession(this.session.id, 'status', this.session.status)
+ },
+ }
+ }
+
+ executeException(exception) {
+ const { hints, responses } = exception
+ if (hints && hints.length > 0) this.session.hints.push(hints)
+
+ return {
+ responses,
+ hints,
+ callback: () => {
+ if (hints && hints.length > 0)
+ pushToSession(this.session.id, 'hints', hints)
pushToSession(this.session.id, 'status', this.session.status)
},
}