Compare commits

..
38 Commits
Author SHA1 Message Date
Lin, Hung-Shin dc1d3f6ce5 Feat-optimise-ui (#85)
Feat-optimise-ui
2023-11-27 06:26:13 +08:00
Lin, Hung-Shin 5cfe6823cc change xterm back to normal fontWeight 2023-11-27 00:10:52 +08:00
Lin, Hung-Shin 6cd348433a feat: display code block in terminal to different color 2023-11-27 00:10:41 +08:00
Lin, Hung-Shin 617bb56f5f Improve quest exceptions (#84) 2023-11-27 00:08:03 +08:00
Chen, Tai-Lin a859b4ff3e fix exceptions error 2023-11-26 23:48:55 +08:00
Lin, Hung-Shin 46fb8407a6 Feat markdown (#86) 2023-11-26 23:44:45 +08:00
Lin, Hung-Shin c7d18ad95e fix: stage re-execute 2023-11-26 23:43:44 +08:00
Lin, Hung-Shin 8f31e4f2cd feat: separate Markdown to a component 2023-11-26 23:32:37 +08:00
Chen, Tai-Lin d4446186cd improve exceptions 2023-11-26 23:32:31 +08:00
Annie Huang 8fb8af036d change color in content, bigger text in term 2023-11-26 23:31:38 +08:00
Chen, Tai-Lin 68492cd932 improve exceptions in quest path 2023-11-26 23:25:44 +08:00
Chen, Tai-Lin 3c1afb92e1 improve exceptions 2023-11-26 23:17:02 +08:00
Lin, Hung-Shin abaacf7ee7 feat: display markdown to html 2023-11-26 23:16:13 +08:00
Chen, Tai-Lin a22b898c0c improve quest exceptions 2023-11-26 23:12:36 +08:00
Chen, Tai-Lin d64a4ae81d improve quests 2023-11-26 23:06:14 +08:00
Lin, Hung-Shin ebd81f7cb4 fix: set content in response to be required 2023-11-26 22:36:10 +08:00
CHEN, TAI-LIN f657f633b4 Feat global exception (#83)
- add global exceptions
- refactor quest models to reusable parts

other improvements:
- improve error handling
2023-11-26 22:33:11 +08:00
Lin, Hung-Shin fb6411e923 feat: logger display error stack 2023-11-26 19:21:58 +08:00
Lin, Hung-Shin a7e646c28b feat: check global exceptions 2023-11-26 19:20:30 +08:00
Lin, Hung-Shin 98a226f3ff feat: load global exceptions 2023-11-26 18:40:39 +08:00
Lin, Hung-Shin 49fb5a6a61 fix: Remove exceptions from stages 2023-11-26 18:32:06 +08:00
Lin, Hung-Shin fe69841622 feat: improve parsing quest error message 2023-11-26 18:31:45 +08:00
Lin, Hung-Shin bb968706ad feat: refactor quest models
+ separate stage and exception
+ reuse commandResponder
2023-11-26 18:29:52 +08:00
Annie Huang 849e669147 Map init position, pwd display, instruct speed 2023-11-26 18:27:34 +08:00
Lin, Hung-Shin b41e67ee0b fix: error in quest read (#81) 2023-11-24 12:59:46 +08:00
Chen, Tai-Lin 50a0bbbab1 add warning 2023-11-24 12:59:09 +08:00
Chen, Tai-Lin d712925d0f modify path 2023-11-24 12:56:57 +08:00
Lin, Hung-Shin b78398680d fix: add error handling (#82) 2023-11-24 12:53:00 +08:00
Lin, Hung-Shin c7f52b1003 fix: add error handling 2023-11-24 12:52:01 +08:00
Chen, Tai-Lin f008fb9f60 modify quest map 2023-11-24 12:49:17 +08:00
Chen, Tai-Lin 76a39add29 fix: error in quest read 2023-11-24 11:54:30 +08:00
Lin, Hung-Shin eb9118d5b1 Quest update (#80) 2023-11-24 09:58:09 +08:00
Chen, Tai-Lin cfdf8730e8 fix error 2023-11-24 09:55:17 +08:00
Chen, Tai-Lin 6651071580 fix: error in quest read 2023-11-24 09:54:18 +08:00
Lin, Hung-Shin f30f6472f5 fix: error in quests (#79) 2023-11-24 09:51:33 +08:00
Chen, Tai-Lin 5ac3b3088d fix: error in create quest 2023-11-24 09:50:15 +08:00
Chen, Tai-Lin acc4d5acba fix: error in create quest 2023-11-24 09:48:41 +08:00
Chen, Tai-Lin 1fb43b78e3 fix: erro in quest explore 2023-11-24 09:46:06 +08:00
39 changed files with 555 additions and 276 deletions

No files matched your search

+2
View File
@@ -20,7 +20,9 @@
"@linux-odyssey/file-graph": "*",
"@linux-odyssey/utils": "*",
"axios": "^1.6.0",
"dompurify": "^3.0.6",
"echarts": "^5.4.3",
"marked": "^10.0.0",
"socket.io-client": "^4.7.1",
"validator": "^13.11.0",
"vue": "^3.3.4",
+1 -1
View File
@@ -90,7 +90,7 @@ const handleChange = () => {
class="bg-bg border-8 border-bg-primary flex flex-1 flex-col items-center justify-center rounded-3xl p-10"
>
<h1 class="text-text-primary text-3xl font-black mb-2">{{ title }}</h1>
<div class="text-error inline-flex justify-center items-center xl:hidden">
<div class="text-error inline-flex justify-center items-center md:hidden">
<font-awesome-icon
:icon="['fas', 'circle-exclamation']"
class="px-1 text-sm"
+2 -1
View File
@@ -1,6 +1,7 @@
<script setup>
import { ref, watch } from 'vue'
import sessionStore from '../store/session'
import MarkdownText from './MarkdownText.vue'
const current = ref(-1)
watch(
@@ -69,7 +70,7 @@ const right = () => {
:key="hint"
class="text-text font-xl whitespace-pre-wrap"
>
{{ hint }}
<MarkdownText :content="hint" />
</li>
</ul>
<br />
+38
View File
@@ -0,0 +1,38 @@
<script setup>
import { computed, defineProps } from 'vue'
import markdown2HTML from '../utils/markdown'
const props = defineProps({
content: {
type: String,
required: true,
},
})
const html = computed(() => {
return markdown2HTML(props.content)
})
</script>
<template>
<div class="markdown-content" v-html="html"></div>
</template>
<style scoped>
.markdown-content {
margin: 0;
padding: 0;
line-height: 1;
}
.markdown-content :deep(ol) {
list-style: decimal;
padding-left: 1em;
}
.markdown-content :deep(code) {
background-color: #282a33;
border-radius: 0.25rem;
padding: 0.25rem;
font-size: 0.875rem;
}
</style>
+6 -5
View File
@@ -15,8 +15,8 @@ import { DAG } from '@linux-odyssey/utils'
import api from '../utils/api'
import { NodeImage } from '../img/svg.js'
const marginX = 200
const marginY = 65
const marginX = 500
const marginY = 300
const chartContainer = ref(null)
let chartInstance = null
const fullwidth = window.screen.width
@@ -71,9 +71,10 @@ const genOption = (nodes, edges) => ({
symbol: () => {
return NodeImage
},
symbolSize: [fullwidth / 10, fullwidth / 25],
symbolSize: [fullwidth / 13, fullwidth / 30],
roam: 'move',
zoom: 1,
zoom: 1.5,
center: ['120%', '30%'],
label: {
show: true,
fontSize: fullwidth / 90,
@@ -103,7 +104,7 @@ const genOption = (nodes, edges) => ({
if (unlocked) {
return '#ADADB5'
}
return '454552'
return '#505050'
},
},
},
+20 -10
View File
@@ -1,5 +1,19 @@
<script setup>
import { computed } from 'vue'
import MarkdownText from './MarkdownText.vue'
import sessionStore from '../store/session'
const tasks = computed(() => {
return sessionStore.session.tasks.map((task) => {
const prefix = task.completed ? '✓' : '➤'
const color = task.completed ? 'text-text-primary' : 'text-text'
return {
...task,
class: `${color} markdown-content`,
content: `${prefix} ${task.name}`,
}
})
})
</script>
<template>
@@ -10,22 +24,18 @@ import sessionStore from '../store/session'
<br />
<div>
<div v-if="sessionStore.quest" class="whitespace-pre-wrap">
<p class="text-text">{{ sessionStore.quest.instruction }}</p>
<MarkdownText
class="text-text markdown-content"
:content="sessionStore.quest.instruction"
/>
<br />
<p id="tasks" class="text-text">
<!-- Tasks: -->
任務:
</p>
<ul v-if="sessionStore.session">
<li v-for="task in sessionStore.session.tasks" :key="task.id">
<p v-if="task.completed" class="text-text-primary">
<span class=""> ✓</span>
{{ task.name }}
</p>
<p v-else class="text-text">
<span class="">➤</span>
{{ task.name }}
</p>
<li v-for="task in tasks" :key="task.id">
<MarkdownText :class="task.class" :content="task.content" />
</li>
</ul>
</div>
+1 -1
View File
@@ -121,6 +121,6 @@ li.discovered {
}
a.pwd {
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
box-shadow: 0 0 40px rgba(255, 255, 0, 20);
}
</style>
+7
View File
@@ -0,0 +1,7 @@
import { marked } from 'marked'
import DOMPurify from 'dompurify'
export default function markdown2HTML(markdown) {
const html = marked.parse(markdown)
return DOMPurify.sanitize(html)
}
+5 -6
View File
@@ -13,12 +13,11 @@ class SocketTerminal {
disableStdin: false, // 是否应禁用输入
// cursorStyle: "underline", //光标样式
cursorBlink: true, // 光标闪烁
theme: {
foreground: '#ECECEC', // 字体
background: '#000000', // 背景色
cursor: 'help', // 设置光标
lineHeight: 20,
},
foreground: '#ECECEC', // 字体
background: '#000000', // 背景色
cursor: 'help', // 设置光标
fontSize: 18,
fontWeight: 'normal',
})
this.fitAddon = new FitAddon()
this.term.loadAddon(this.fitAddon)
+2 -2
View File
@@ -48,10 +48,10 @@ async function handleCommand(command) {
async function handleResponse({ responses, hints, end }) {
if (responses) {
await printResponses(responses, 60)
await printResponses(responses, 90)
}
if (hints) {
await printHints(hints, 60)
await printHints(hints, 90)
}
if (end) {
console.log(colorize('Quest completed!', 'blue'))
+25 -4
View File
@@ -6,6 +6,7 @@ const RETURN_SYMBOL = '\u21B5'
// ANSI color map
const ANSI_COLORS = {
default: 0,
black: 30,
red: 31,
green: 32,
@@ -16,10 +17,13 @@ const ANSI_COLORS = {
white: 37,
}
function colorCode(color) {
return `\x1b[${ANSI_COLORS[color]}m`
}
function colorize(content, color) {
const colorCode = ANSI_COLORS[color]
if (!colorCode) return content
return `\x1b[${colorCode}m${content}\x1b[0m`
if (!color) return content
return `${colorCode(color)}${content}${colorCode('default')}`
}
process.stdin.setRawMode(true)
@@ -36,8 +40,25 @@ function waitForEnter() {
})
}
function formatLine(content, color) {
let result = colorCode(color)
let codeBlock = false
for (const c of content) {
switch (c) {
case '`':
result += colorCode(codeBlock ? color : 'green')
codeBlock = !codeBlock
break
default:
result += c
}
}
return result + colorCode('default') + RETURN_SYMBOL
}
function printLine(content, delay, color) {
const printContent = `${colorize(content, color)}${RETURN_SYMBOL}`
const printContent = formatLine(content, color)
return new Promise((resolve) => {
let i = 0
const timer = setInterval(() => {
+19
View File
@@ -0,0 +1,19 @@
import { Schema } from 'mongoose'
import conditionSchema from './condition.js'
import responseSchema from './response.js'
import hintSchema from './hint.js'
const commandResponderSchema = new Schema({
condition: {
type: conditionSchema,
required: false,
},
responses: {
type: [responseSchema],
required: true,
},
})
commandResponderSchema.add(hintSchema)
export default commandResponderSchema
+28
View File
@@ -0,0 +1,28 @@
import { Schema } from 'mongoose'
const fileConditionSchema = new Schema({
path: {
type: String,
required: true,
},
type: String,
exists: {
type: Boolean,
default: true,
},
})
const conditionSchema = new Schema({
command: [String],
output: [String],
error: [String],
pwd: [String],
files: [fileConditionSchema],
})
conditionSchema.add({
or: [conditionSchema],
not: conditionSchema,
})
export default conditionSchema
+8
View File
@@ -0,0 +1,8 @@
import { Schema } from 'mongoose'
import commandResponderSchema from './commandResponder.js'
const exceptionSchema = new Schema({})
exceptionSchema.add(commandResponderSchema)
export default exceptionSchema
+10
View File
@@ -0,0 +1,10 @@
import { Schema } from 'mongoose'
import exceptionSchema from './exception.js'
import requirementSchema from './requirement.js'
const globalExceptionSchema = new Schema({})
globalExceptionSchema.add(exceptionSchema)
globalExceptionSchema.add(requirementSchema)
export default globalExceptionSchema
+10
View File
@@ -0,0 +1,10 @@
import { Schema } from 'mongoose'
const hintSchema = new Schema({
hints: {
type: [String],
required: true,
},
})
export default hintSchema
+6 -94
View File
@@ -1,98 +1,6 @@
import { model, Schema } from 'mongoose'
const responseSchema = new Schema({
type: {
type: String,
enum: ['narrative', 'dialogue'],
default: 'dialogue',
required: true,
},
content: [String],
speaker: { type: String, default: 'Ada' },
color: String,
})
const fileConditionSchema = new Schema({
path: {
type: String,
required: true,
},
type: String,
exists: {
type: Boolean,
default: true,
},
})
const conditionSchema = new Schema({
command: [String],
output: [String],
error: [String],
pwd: [String],
files: [fileConditionSchema],
})
conditionSchema.add({
or: [conditionSchema],
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,
required: true,
},
task: {
type: String,
},
requirements: [
{
type: String,
required: true,
},
],
repeatable: {
type: Boolean,
required: true,
default: false,
},
condition: {
type: conditionSchema,
required: true,
},
responses: {
type: [responseSchema],
required: true,
},
hints: {
type: [String],
required: true,
},
exceptions: {
type: [exceptionSchema],
required: true,
},
})
import stageSchema from './stage.js'
import globalExceptionSchema from './globalException.js'
const Quest = model(
'Quest',
@@ -126,6 +34,10 @@ const Quest = model(
type: [stageSchema],
required: true,
},
exceptions: {
type: [globalExceptionSchema],
required: true,
},
})
)
+12
View File
@@ -0,0 +1,12 @@
import { Schema } from 'mongoose'
const requirementSchema = new Schema({
requirements: [
{
type: String,
required: true,
},
],
})
export default requirementSchema
+18
View File
@@ -0,0 +1,18 @@
import { Schema } from 'mongoose'
const responseSchema = new Schema({
type: {
type: String,
enum: ['narrative', 'dialogue'],
default: 'dialogue',
required: true,
},
content: {
type: [String],
required: true,
},
speaker: { type: String, default: 'Ada' },
color: String,
})
export default responseSchema
+24
View File
@@ -0,0 +1,24 @@
import { Schema } from 'mongoose'
import commandResponderSchema from './commandResponder.js'
import stageExceptionSchema from './stageException.js'
import requirementSchema from './requirement.js'
const stageSchema = new Schema({
id: {
type: String,
required: true,
},
task: {
type: String,
required: true,
},
exceptions: {
type: [stageExceptionSchema],
required: true,
},
})
stageSchema.add(commandResponderSchema)
stageSchema.add(requirementSchema)
export default stageSchema
+14
View File
@@ -0,0 +1,14 @@
import { Schema } from 'mongoose'
import exceptionSchema from './exception.js'
const stageExceptionSchema = new Schema({
catchAll: {
type: Boolean,
required: true,
default: false,
},
})
stageExceptionSchema.add(exceptionSchema)
export default stageExceptionSchema
+8 -8
View File
@@ -9,14 +9,6 @@ requirements:
- get-started
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始教程
condition:
@@ -73,3 +65,11 @@ stages:
- 你成功的在終端機上顯示了 `How are you?`。
- 相信你已經了解如何使用 `echo` 指令來顯示文字了。
- 之後如果要在終端機顯示文字,記得可以使用 `echo` 指令。
exceptions:
- repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
-8
View File
@@ -9,14 +9,6 @@ requirements:
- create
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始
condition:
+2 -9
View File
@@ -10,14 +10,6 @@ requirements:
- read
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始
condition:
@@ -68,6 +60,7 @@ stages:
- ➤ 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
- id: cat
task: 使用 `cat` 指令來查看 `hello.txt` 檔案的內容
condition:
command:
- ^cat hello.txt$
@@ -159,7 +152,7 @@ stages:
- ls-mkdir
condition:
pwd:
- notes
- ^/home/commander/notes$
responses:
- type: narrative
content:
+142
View File
@@ -0,0 +1,142 @@
exceptions:
- condition:
command:
- '[\uff21-\uff3a\uff41-\uff5a]'
responses:
- type: narrative
content:
- '!! 請使用半形英文字母 !!'
- condition:
command:
- ' '
responses:
- type: narrative
content:
- '!! 請使用半形空格 !!'
- condition:
or:
- command:
- '`'
- command:
- \'
responses:
- type: narrative
content:
- '!! 不需要輸入上下引號 !!'
- condition:
command:
- ^echo\S$
responses:
- type: narrative
content:
- '!! `echo` 後面要加空格 !!'
- condition:
command:
- ^cd\S$
responses:
- type: narrative
content:
- '!! `cd` 後面要加空格 !!'
- condition:
command:
- ^ls\S$
responses:
- type: narrative
content:
- '!! `ls` 後面要加空格 !!'
- condition:
command:
- ^cat\S$
responses:
- type: narrative
content:
- '!! `cat` 後面要加空格 !!'
- condition:
command:
- ^touch\S$
responses:
- type: narrative
content:
- '!! `touch` 後面要加空格 !!'
- condition:
not:
command:
- ^rmdir$
command:
- ^rm\S$
responses:
- type: narrative
content:
- '!! `rm` 後面要加空格 !!'
- condition:
command:
- ^mv\S$
responses:
- type: narrative
content:
- '!! `mv` 後面要加空格 !!'
- condition:
command:
- ^mkdir\S$
responses:
- type: narrative
content:
- '!! `mkdir` 後面要加空格 !!'
- condition:
command:
- ^rmdir\S$
responses:
- type: narrative
content:
- '!! `rmdir` 後面要加空格 !!'
- condition:
command:
- ^cp\S$
responses:
- type: narrative
content:
- '!! `cp` 後面要加空格 !!'
- condition:
command:
- ^[Ii1]s$
responses:
- type: narrative
content:
- '!! `ls` 的首字母是小寫的 `L` !!'
- condition:
command:
- 嗨
responses:
- type: narrative
content:
- '嗨~'
- condition:
command:
- folderl
responses:
- type: narrative
content:
- '`folder1` 的 `1` 是數字 `1`,不是小寫的 `L` !!'
- condition:
command:
- filel
responses:
- type: narrative
content:
- '`file1` 的 `1` 是數字 `1`,不是小寫的 `L` !!'
+10 -25
View File
@@ -10,14 +10,6 @@ requirements:
- get-started
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始教程
condition:
@@ -44,19 +36,12 @@ stages:
- condition:
not:
pwd:
- /home/commander
- ^/home/commander$
responses:
- type: narrative
content:
- 請先回到 `/home/commander/` 資料夾
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
- condition:
command:
- ^[Ii]s$
responses:
- type: narrative
content:
- '!! `ls` 的首字母是小寫的 `L`,不是 `I` !!'
responses:
- content:
- 你應該能看到資料夾中的內容了。
@@ -78,11 +63,11 @@ stages:
- ls
condition:
pwd:
- /home/commander/folder1
- ^/home/commander/folder1$
exceptions:
- condition:
pwd:
- /home/commander
- ^/home/commander$
responses:
- type: narrative
content:
@@ -90,7 +75,7 @@ stages:
- condition:
not:
pwd:
- /home/commander
- ^/home/commander$
responses:
- type: narrative
content:
@@ -113,9 +98,9 @@ stages:
- cd-folder1
condition:
pwd:
- /home/commander/folder1
- ^/home/commander/folder1$
command:
- ls
- ^ls$
responses:
- content:
- 你現在可以看到 `folder1` 資料夾中有兩的檔案,分別是 `file1.txt` 跟 `file2.txt`。
@@ -131,7 +116,7 @@ stages:
- ls-folder1
condition:
pwd:
- /home/commander
- ^/home/commander$
responses:
- content:
- 你成功的回到上一層資料夾了。
@@ -146,7 +131,7 @@ stages:
- cd-..
condition:
pwd:
- /home/commander/folder2
- ^/home/commander/folder2$
responses:
- content:
- 你成功的進入 `folder2` 資料夾了。
@@ -160,7 +145,7 @@ stages:
- cd-folder2
condition:
pwd:
- /home/commander/folder2
- ^/home/commander/folder2$
command:
- ls
responses:
@@ -176,7 +161,7 @@ stages:
- ls-folder2
condition:
pwd:
- /home/commander
- ^/home/commander$
responses:
- content:
- 你成功的回到原本的資料夾了。
+2 -8
View File
@@ -7,16 +7,9 @@ instruction: |
在畫面的中間可以看到終端機和提示兩個區塊。
請你在終端機中的 `commander:~ $` 後面輸入 `echo start` 並按下 Enter 來開始教程。
!! 注意:不需要輸入上下引號 !!
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始教程
condition:
@@ -73,6 +66,7 @@ stages:
hints:
- ➤ 按下 `Ctrl + C` 能夠中斷指令
- ➤ 劇情進行時可以按下 `Enter` 來快速跳過
- ➤ 輸入 `echo finish` 來結束這一關
- id: END
task: 結束關卡
+8 -8
View File
@@ -12,9 +12,9 @@ stages:
task: 輸入 `echo help` 來朗誦咒語
condition:
pwd:
- '^/home/commander$'
- ^/home/commander$
command:
- echo [Hh]elp
- ^echo [Hh]elp$
responses:
- type: narrative
content:
@@ -39,9 +39,9 @@ stages:
- help
condition:
pwd:
- '^/home/commander$'
- ^/home/commander$
command:
- ls
- ^ls$
responses:
- type: narrative
content:
@@ -65,9 +65,9 @@ stages:
- find-scroll
condition:
pwd:
- '^/home/commander$'
- ^/home/commander$
command:
- cat forgotten_scroll.txt
- ^cat forgotten_scroll.txt$
responses:
- type: dialogue
speaker: Unix 精靈
@@ -83,9 +83,9 @@ stages:
- read-forgotten-scroll
condition:
pwd:
- '^/home/commander$'
- ^/home/commander$
command:
- echo [Hh]ello\s*[Ww]orld
- ^echo [Hh]ello\s*[Ww]orld$
responses:
- type: narrative
content:
-8
View File
@@ -9,14 +9,6 @@ requirements:
- create
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始
condition:
+7 -20
View File
@@ -11,14 +11,6 @@ requirements:
- explore
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始教程
condition:
@@ -64,23 +56,20 @@ stages:
- ➤ 請輸入 `cd ../folder2` 來進入 `folder2` 資料夾。
- id: cd-folder2
task: 用指定方式進入 `folder2` 資料夾
task: 進入 `folder2` 資料夾
requirements:
- cd-folder1
condition:
pwd:
- ^/home/commander/folder2$
command:
- ^cd \.\./folder2$
exceptions:
- condition:
not:
pwd:
- ^/home/commander/folder1$
pwd:
- ^/home/commander$
responses:
- type: narrative
content:
- 請先回到 `folder1` 資料夾,並用指定的方式進入 `folder2` 資料夾。
- 輸入 `cd folder2` 來進入 `folder2` 資料夾
responses:
- content:
- 你成功的用相對路徑進入了 `folder2` 資料夾。
@@ -95,10 +84,8 @@ stages:
requirements:
- cd-folder2
condition:
pwd:
- ^/home/commander/folder2$
command:
- ^ls \.\.\/folder1$
output:
- file1.txt
exceptions:
- condition:
not:
@@ -107,7 +94,7 @@ stages:
responses:
- type: narrative
content:
- 請先回到 `folder2` 資料夾,並用指定的方式查看 `folder1` 資料夾的內容。
- 進到 `folder1` 資料夾並用 `ls` 查看內容。
responses:
- content:
- 很好,你現在知道如何運用相對路徑了。
+6 -14
View File
@@ -9,14 +9,6 @@ requirements:
- path
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始教程
condition:
@@ -44,19 +36,19 @@ stages:
responses:
- content:
- 這個資料夾中有一個檔案和一個資料夾。
- 現在你可以使用 `cat` 指令來查看 `file.txt` 的內容。
- 現在你可以使用 `cat` 指令來查看 `file1.txt` 的內容。
hints:
- ➤ 請輸入 `cat file.txt` 來查看檔案的內容。
- ➤ 請輸入 `cat file1.txt` 來查看檔案的內容。
- id: cat-file
task: 查看 `file.txt` 的內容
- id: cat-file1
task: 查看 `file1.txt` 的內容
requirements:
- ls-~
condition:
pwd:
- ^/home/commander$
command:
- ^cat file.txt$
- ^cat file1.txt$
responses:
- content:
- 你應該能看到檔案的內容了。
@@ -68,7 +60,7 @@ stages:
- id: ls-folder
task: 查看 `folder` 資料夾
requirements:
- cat-file
- cat-file1
condition:
pwd:
- ^/home/commander$
-8
View File
@@ -13,14 +13,6 @@ requirements:
- move
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始
condition:
+2 -2
View File
@@ -25,8 +25,8 @@ export function createContainer(name, questId) {
Image: getQuestImage(questId),
}
const { hostPwd, mountQuest } = config.docker
if (!config.isProduction && hostPwd && mountQuest) {
logger.debug('Mounting quest folder', mountQuest)
if (!config.isProduction && hostPwd && mountQuest === questId) {
logger.info('Mounting quest folder', mountQuest)
option.HostConfig.Binds = [
`${hostPwd}/quests/${mountQuest}/home:/home/commander`,
`${hostPwd}/packages/container:/usr/local/lib/container`,
+58 -23
View File
@@ -50,9 +50,18 @@ export default class CommandHandler extends SessionHandler {
}
async discoverHandler() {
const graph = new FileGraph(this.session.graph)
graph.discover(this.params.discover)
this.session.graph = graph
try {
const graph = new FileGraph(this.session.graph)
graph.discover(this.params.discover)
this.session.graph = graph
} catch (e) {
logger.error('Update file graph failed', {
error: e.message,
session: this.session.id,
graph: this.session.graph,
discover: this.params.discover,
})
}
}
async isMatch(condition) {
@@ -111,22 +120,33 @@ export default class CommandHandler extends SessionHandler {
}
async run() {
this.quest = await Quest.findById(this.session.quest)
const stages = this.getStages()
if (stages.length === 0) {
logger.warn('stage not found', this.session.tasks)
return {}
}
try {
this.quest = await Quest.findById(this.session.quest)
const stages = this.getStages()
if (stages.length === 0) {
logger.warn('stage not found', { session: this.session.id })
return {}
}
this.handleEvent()
this.handleCommand()
this.handleEvent()
this.handleCommand()
const matches = await Promise.all(
stages.map((stage) => this.isMatch(stage.condition))
)
const matches = await Promise.all(
stages.map((stage) => this.isMatch(stage.condition))
)
const stage = stages.find((_, i) => matches[i])
if (!stage) {
const stage = stages.find((_, i) => matches[i])
if (stage) {
const response = this.execute(stage)
return {
stage: stage.id,
end: this.session.status === 'finished',
...response,
}
}
// stage exception
for (const s of stages) {
// eslint-disable-next-line no-await-in-loop
const exception = await this.checkException(s)
@@ -134,14 +154,29 @@ export default class CommandHandler extends SessionHandler {
return this.executeException(exception)
}
}
return {}
}
const response = this.execute(stage)
return {
stage: stage.id,
end: this.session.status === 'finished',
...response,
// global exception
if (this.quest.exceptions.length > 0) {
const globalExceptions = this.getUnlockedComponents(
this.quest.exceptions
)
for (const exception of globalExceptions) {
// eslint-disable-next-line no-await-in-loop
if (await this.isMatch(exception.condition)) {
return this.executeException(exception)
}
}
}
return {}
} catch (err) {
logger.error('CommandHandler error', {
error: err.message,
session: this.session._id,
quest: this.quest._id,
command: this.commandInput,
})
return {}
}
}
}
+8 -5
View File
@@ -19,14 +19,17 @@ export default class SessionHandler {
return task ? task.completed : false
}
getStages() {
return this.quest.stages.filter(
(stage) =>
stage.requirements.every((r) => this.isCompleted({ id: r })) &&
(stage.repeatable || !this.isCompleted(stage))
getUnlockedComponents(components) {
return components.filter((component) =>
component.requirements.every((r) => this.isCompleted({ id: r }))
)
}
getStages() {
const unlockedStages = this.getUnlockedComponents(this.quest.stages)
return unlockedStages.filter((s) => !this.isCompleted(s))
}
getNewTasks() {
const stages = this.getStages()
return stages
+6 -1
View File
@@ -38,7 +38,12 @@ async function main() {
await setupTest()
}
await loadAndUpdateQuests()
try {
await loadAndUpdateQuests()
} catch (err) {
logger.error('Failed to load quests', err)
return
}
expiryRemovalScheduler()
const file = await fs.readFile('./swagger.yaml', 'utf8')
+6 -2
View File
@@ -1,9 +1,13 @@
import winston from 'winston'
import winston, { format } from 'winston'
import config from '../config.js'
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
format: format.combine(
format.errors({ stack: true }),
format.timestamp(),
format.json()
),
defaultMeta: { service: 'backend', timestamp: new Date().toISOString() },
transports: [
//
+22 -3
View File
@@ -7,6 +7,15 @@ import logger from './logger.js'
const questDirectory = path.join(process.cwd(), '..', 'quests')
class QuestValidationError extends Error {
constructor(message, questId, error) {
super(message)
this.name = 'QuestValidationError'
this.questId = questId
this.error = error
}
}
async function mapQuests(callback) {
const questNames = await fs.readdir(questDirectory, {
withFileTypes: true,
@@ -23,12 +32,20 @@ async function mapQuests(callback) {
)
}
async function loadGlobalExceptions() {
const exceptionsPath = path.join(questDirectory, 'exceptions.yml')
const body = await fs.readFile(exceptionsPath, 'utf-8')
return yaml.parse(body)
}
export async function loadAndUpdateQuests() {
await Quest.deleteMany({})
const { exceptions } = await loadGlobalExceptions()
console.log(exceptions)
return mapQuests(async (id, questPath) => {
const files = await fs.readdir(questPath)
if (!files.includes('game.yml')) {
throw new Error(`Quest ${id} is missing game.yml`)
throw new QuestValidationError(`Missing game.yml`, id)
}
try {
@@ -44,10 +61,12 @@ export async function loadAndUpdateQuests() {
image,
...questData,
})
quest.exceptions.push(...exceptions)
await quest.save()
} catch (error) {
logger.error(`Error parsing quest ${id}:`, error)
throw error
console.error(error)
logger.error(`Error parsing quest`, { id, error })
throw new QuestValidationError(`Error parsing quest`, id, error)
}
})
}
+10
View File
@@ -2404,6 +2404,11 @@ dom-accessibility-api@^0.5.9:
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
dompurify@^3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.6.tgz#925ebd576d54a9531b5d76f0a5bef32548351dae"
integrity sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==
dotenv@^16.3.1:
version "16.3.1"
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
@@ -4691,6 +4696,11 @@ makeerror@1.0.12:
dependencies:
tmpl "1.0.5"
marked@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/marked/-/marked-10.0.0.tgz#7fe1805bb908433d760e2de0fcc8841a2b2d745c"
integrity sha512-YiGcYcWj50YrwBgNzFoYhQ1hT6GmQbFG8SksnYJX1z4BXTHSOrz1GB5/Jm2yQvMg4nN1FHP4M6r03R10KrVUiA==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"