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) }, }