mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
feat: add series of quests (#75)
This commit is contained in:
75 files changed
+1991
-517
No files matched your search
@@ -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'],
|
||||
},
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
|
||||
<h1 id="CommandBook" class="inline text-text-primary font-xl">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<section class="h-[10%] p-3 grid grid-cols-3 gap-4 place-content-between">
|
||||
<button id="solution" class="bg-bg-disabled rounded-lg p-2">
|
||||
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'circle-question']"
|
||||
class="text-text-disabled"
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
<template>
|
||||
<div id="box">
|
||||
<canvas ref="canvas"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
@@ -79,6 +73,12 @@ onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="box">
|
||||
<canvas ref="canvas"></canvas>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#box {
|
||||
position: relative;
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
<template>
|
||||
<a href="#" :class="nodeClass()">{{ node.discovered ? node.name : '???' }}</a>
|
||||
<ul :class="nodeClass()" v-if="node.children && node.children.length > 0">
|
||||
<li :class="nodeClass()" v-for="child in node.children" :key="child.path">
|
||||
<FileNode :node="child" :pwd="pwd" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
node: {
|
||||
@@ -27,3 +18,12 @@ function nodeClass() {
|
||||
return classes
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a href="#" :class="nodeClass()">{{ node.discovered ? node.name : '???' }}</a>
|
||||
<ul :class="nodeClass()" v-if="node.children && node.children.length > 0">
|
||||
<li :class="nodeClass()" v-for="child in node.children" :key="child.path">
|
||||
<FileNode :node="child" :pwd="pwd" />
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -66,7 +66,7 @@ onMounted(async () => {
|
||||
<button id="backward" class="flex items-center h-5 w-5">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'arrow-left']"
|
||||
class="text-text-tertiary h-full w-full"
|
||||
class="text-border h-full w-full"
|
||||
/>
|
||||
</button> -->
|
||||
<button
|
||||
|
||||
@@ -63,9 +63,15 @@ const right = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div id="hint" class="bg-bg flex flex-wrap p-8">
|
||||
<p class="text-text font-xl whitespace-pre-wrap">
|
||||
{{ sessionStore.session.hints[current] }}
|
||||
</p>
|
||||
<ul>
|
||||
<li
|
||||
v-for="hint in sessionStore.session.hints[current]"
|
||||
:key="hint"
|
||||
class="text-text font-xl whitespace-pre-wrap"
|
||||
>
|
||||
{{ hint }}
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
</div>
|
||||
</template>
|
||||
@@ -6,18 +6,28 @@ import { useToast } from 'vue-toastification'
|
||||
import { use, init } from 'echarts/core'
|
||||
import { GraphChart } from 'echarts/charts'
|
||||
import { SVGRenderer } from 'echarts/renderers'
|
||||
import { TitleComponent, TooltipComponent } from 'echarts/components'
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
DataZoomComponent,
|
||||
} from 'echarts/components'
|
||||
import { DAG } from '@linux-odyssey/utils'
|
||||
import api from '../utils/api'
|
||||
import { NodeImage } from '../img/svg.js'
|
||||
|
||||
const marginX = 100
|
||||
const marginX = 200
|
||||
const marginY = 65
|
||||
const chartContainer = ref(null)
|
||||
let chartInstance = null
|
||||
const fullwidth = window.screen.width
|
||||
|
||||
use([GraphChart, SVGRenderer, TitleComponent, TooltipComponent])
|
||||
use([
|
||||
GraphChart,
|
||||
SVGRenderer,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
DataZoomComponent,
|
||||
])
|
||||
|
||||
async function getQuests() {
|
||||
try {
|
||||
@@ -45,6 +55,15 @@ const genOption = (nodes, edges) => ({
|
||||
tooltip: {},
|
||||
animationDurationUpdate: 1500,
|
||||
animationEasingUpdate: 'quinticInOut',
|
||||
// dataZoom: [
|
||||
// {
|
||||
// type: 'slider', // This type means "slider data zoom" which provides a slider bar for zooming
|
||||
// orient: 'vertical', // This makes the data zoom component vertical
|
||||
// start: 0, // The starting percentage of the window out of the data extent
|
||||
// end: 100, // The ending percentage of the window out of the data extent
|
||||
// show: true,
|
||||
// },
|
||||
// ],
|
||||
series: [
|
||||
{
|
||||
type: 'graph',
|
||||
@@ -52,8 +71,9 @@ const genOption = (nodes, edges) => ({
|
||||
symbol: () => {
|
||||
return NodeImage
|
||||
},
|
||||
symbolSize: [fullwidth / 10, fullwidth / 20],
|
||||
roam: false,
|
||||
symbolSize: [fullwidth / 10, fullwidth / 25],
|
||||
roam: 'move',
|
||||
zoom: 1,
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: fullwidth / 90,
|
||||
@@ -96,8 +116,8 @@ function getOption(quests, progress) {
|
||||
const nodes = dag.getNodes().map((node) => ({
|
||||
id: node._id,
|
||||
name: node.title,
|
||||
x: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
|
||||
y: marginY * node.layer,
|
||||
y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
|
||||
x: marginY * node.layer * 2,
|
||||
completed: progress[node._id]?.completed || false,
|
||||
unlocked: node.requirements.every((req) => progress[req]?.completed),
|
||||
}))
|
||||
|
||||
@@ -4,10 +4,10 @@ import sessionStore from '../store/session'
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full max-h-full">
|
||||
<h1 id="topic" class="text-text" v-if="sessionStore.quest">
|
||||
<p id="topic" class="text-text font-black" v-if="sessionStore.quest">
|
||||
{{ sessionStore.quest.title }}
|
||||
</h1>
|
||||
<hr class="border-text-tertiary border-2 my-2" />
|
||||
</p>
|
||||
<br />
|
||||
<div>
|
||||
<div v-if="sessionStore.quest" class="whitespace-pre-wrap">
|
||||
<p class="text-text">{{ sessionStore.quest.instruction }}</p>
|
||||
@@ -19,7 +19,7 @@ import sessionStore from '../store/session'
|
||||
<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>
|
||||
<span class=""> ✓</span>
|
||||
{{ task.name }}
|
||||
</p>
|
||||
<p v-else class="text-text">
|
||||
|
||||
@@ -86,7 +86,7 @@ function updateGraph(event) {
|
||||
}
|
||||
|
||||
function updateHints(hints) {
|
||||
store.session.hints.push(...hints)
|
||||
store.session.hints.push(hints)
|
||||
}
|
||||
|
||||
function updateStatus(status) {
|
||||
|
||||
@@ -48,6 +48,7 @@ async function handleSubmit({ username, success, error }) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-screen h-screen">
|
||||
<Background class="w-full h-full" />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed, onMounted } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import sessionStore, { init } from '../store/session'
|
||||
import GameHeaderPart from '../components/GameHeaderPart.vue'
|
||||
import CommandlistPart from '../components/CommandlistPart.vue'
|
||||
// import CommandlistPart from '../components/CommandlistPart.vue'
|
||||
import HintPart from '../components/HintPart.vue'
|
||||
import QuestPart from '../components/QuestPart.vue'
|
||||
import TerminalPart from '../components/TerminalPart.vue'
|
||||
@@ -62,19 +62,20 @@ onMounted(async () => {
|
||||
<div id="main" class="h-[94vh] w-full flex p-3 space-x-3">
|
||||
<!-- Topic and Command List -->
|
||||
<div class="bg-bg-secondary h-full w-1/3 rounded-lg">
|
||||
<section id="quest" class="h-2/3 p-3">
|
||||
<section id="quest" class="h-full p-3 overflow-auto">
|
||||
<QuestPart />
|
||||
</section>
|
||||
<hr class="border-text-tertiary border" />
|
||||
<!-- <hr class="border-border border" />
|
||||
<section id="cmdlist" class="h-1/3">
|
||||
<CommandlistPart />
|
||||
</section>
|
||||
</section> -->
|
||||
</div>
|
||||
<!-- Terminal and Hint -->
|
||||
<div class="bg-bg h-full w-1/3 rounded-lg">
|
||||
<section id="terminal" class="h-3/5">
|
||||
<section id="terminal" class="h-3/5 overflow-hidden">
|
||||
<TerminalPart />
|
||||
</section>
|
||||
<hr class="border-border border" />
|
||||
<section id="hint" class="h-2/5">
|
||||
<HintPart />
|
||||
</section>
|
||||
@@ -93,4 +94,3 @@ onMounted(async () => {
|
||||
<CompleteModal v-if="completed" />
|
||||
</div>
|
||||
</template>
|
||||
<script></script>
|
||||
@@ -35,6 +35,7 @@ const handleLogin = async ({ username, password, success, error }) => {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-screen h-screen">
|
||||
<Background class="w-full h-full" />
|
||||
|
||||
@@ -72,6 +72,7 @@ async function check({ username, email, password, error }) {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-screen h-screen">
|
||||
<Background class="w-full h-full" />
|
||||
|
||||
@@ -18,7 +18,7 @@ export default {
|
||||
DEFAULT: '#FFFFFF',
|
||||
},
|
||||
border: {
|
||||
DEFAULT: '#526B4E',
|
||||
DEFAULT: '#4A4B63',
|
||||
},
|
||||
error: {
|
||||
DEFAULT: '#FF0000',
|
||||
|
||||
+4
-3
@@ -42,7 +42,6 @@ services:
|
||||
- TRUSTED_PROXIES=${TRUSTED_PROXIES}
|
||||
- SURVEY_URL=${SURVEY_URL}
|
||||
- BUG_REPORT_URL=${BUG_REPORT_URL}
|
||||
- HOST_PWD=${PWD}
|
||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
||||
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
|
||||
@@ -50,6 +49,8 @@ services:
|
||||
- TESTING=true
|
||||
- TESTING_USERNAME=alex
|
||||
- TESTING_PASSWORD=Alex1234
|
||||
- HOST_PWD=${PWD}
|
||||
- MOUNT_QUEST=${MOUNT_QUEST}
|
||||
|
||||
analytics:
|
||||
build:
|
||||
@@ -64,8 +65,8 @@ services:
|
||||
- '3001:3001'
|
||||
environment:
|
||||
- MONGO_URL=mongodb://db:27017/odyssey-test
|
||||
- USERNAME=user
|
||||
- PASSWORD=user
|
||||
- ADMIN_USERNAME=alex
|
||||
- ADMIN_PASSWORD=Alex1234
|
||||
volumes:
|
||||
- ./packages/analytics:/app/packages/analytics
|
||||
- ./packages/models:/app/packages/models
|
||||
|
||||
+43
-33
@@ -3,9 +3,26 @@ const fs = require('fs').promises
|
||||
const { exit } = require('process')
|
||||
const minimist = require('minimist')
|
||||
const axios = require('axios')
|
||||
const { colorize, printResponses } = require('./print.js')
|
||||
const { colorize, printResponses, printHints } = require('./print.js')
|
||||
const discoverFiles = require('./discover.js')
|
||||
|
||||
const {
|
||||
API_ENDPOINT,
|
||||
TOKEN,
|
||||
PWD,
|
||||
CMD_NAME,
|
||||
CMD_OUTPUT_FILE,
|
||||
CMD_ERROR_FILE,
|
||||
CMD_EXIT_CODE,
|
||||
} = process.env
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: `${API_ENDPOINT}/api/v1`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${TOKEN}`,
|
||||
},
|
||||
})
|
||||
|
||||
async function readOrNone(file) {
|
||||
try {
|
||||
return await fs.readFile(file, 'utf8')
|
||||
@@ -29,24 +46,21 @@ async function handleCommand(command) {
|
||||
}, {})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const {
|
||||
API_ENDPOINT,
|
||||
TOKEN,
|
||||
PWD,
|
||||
CMD_NAME,
|
||||
CMD_OUTPUT_FILE,
|
||||
CMD_ERROR_FILE,
|
||||
CMD_EXIT_CODE,
|
||||
} = process.env
|
||||
if (!CMD_NAME) exit(0)
|
||||
async function handleResponse({ responses, hints, end }) {
|
||||
if (responses) {
|
||||
await printResponses(responses, 60)
|
||||
}
|
||||
if (hints) {
|
||||
await printHints(hints, 60)
|
||||
}
|
||||
if (end) {
|
||||
console.log(colorize('Quest completed!', 'blue'))
|
||||
}
|
||||
await api.post('/commands/completed')
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: `${API_ENDPOINT}/api/v1`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${TOKEN}`,
|
||||
},
|
||||
})
|
||||
async function main() {
|
||||
if (!CMD_NAME) exit(0)
|
||||
|
||||
const params = await handleCommand(CMD_NAME)
|
||||
|
||||
@@ -62,20 +76,8 @@ async function main() {
|
||||
}
|
||||
try {
|
||||
const res = await api.post('/commands', payload)
|
||||
if (!res.data) return
|
||||
const { responses, hints, end } = res.data
|
||||
if (responses) {
|
||||
await printResponses(responses, 60)
|
||||
await api.post('/commands/completed')
|
||||
}
|
||||
if (hints) {
|
||||
for (const hint of hints) {
|
||||
console.log(colorize(hint, 'yellow'))
|
||||
}
|
||||
}
|
||||
if (end) {
|
||||
console.log(colorize('Quest completed!', 'blue'))
|
||||
}
|
||||
if (!res.data || !res.data.responses) return
|
||||
await handleResponse(res.data)
|
||||
} catch (err) {
|
||||
if (err.response) {
|
||||
console.error(err.response.data)
|
||||
@@ -85,4 +87,12 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main().then(exit)
|
||||
main()
|
||||
.then(exit)
|
||||
.catch((err) => {
|
||||
console.log(
|
||||
'發生預期外的錯誤,請嘗試重新啟動 Quest,如果問題持續發生,請幫我們填寫右上角的錯誤回報表單。'
|
||||
)
|
||||
console.error(err)
|
||||
exit(1)
|
||||
})
|
||||
@@ -34,12 +34,16 @@ async function collectFilesInfo(inputPath, level = 0, hiddenFiles = false) {
|
||||
|
||||
async function discoverFiles(argv) {
|
||||
const targetPath = argv._.length < 2 ? ['.'] : argv._.slice(1)
|
||||
const result = await Promise.all(
|
||||
targetPath
|
||||
.map((p) => path.resolve(process.cwd(), p))
|
||||
.map((p) => collectFilesInfo(p, 0, argv.a || argv.all))
|
||||
)
|
||||
return { discover: result.flat() }
|
||||
try {
|
||||
const result = await Promise.all(
|
||||
targetPath
|
||||
.map((p) => path.resolve(process.cwd(), p))
|
||||
.map((p) => collectFilesInfo(p, 0, argv.a || argv.all))
|
||||
)
|
||||
return { discover: result.flat() }
|
||||
} catch (err) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = discoverFiles
|
||||
@@ -52,6 +52,7 @@ function printLine(content, delay, color) {
|
||||
|
||||
waitForEnter().then(() => {
|
||||
clearInterval(timer)
|
||||
if (i >= printContent.length) return
|
||||
process.stdout.write(printContent.slice(i))
|
||||
process.stdout.write('\n')
|
||||
resolve()
|
||||
@@ -61,25 +62,33 @@ function printLine(content, delay, color) {
|
||||
|
||||
async function printResponses(responses, delay = 100) {
|
||||
console.log()
|
||||
console.log(colorize('-------------', 'white'))
|
||||
console.log(colorize('==============================', 'green'))
|
||||
for (const response of responses) {
|
||||
const { type, content, speaker, color } = response
|
||||
if (type === 'narrative') {
|
||||
for (const line of content) {
|
||||
await printLine(line, delay, color || 'white')
|
||||
await printLine(line, delay, color || 'cyan')
|
||||
}
|
||||
}
|
||||
if (type === 'dialogue') {
|
||||
console.log(`${speaker}:`)
|
||||
console.log(colorize(`${speaker}:`, 'green'))
|
||||
for (const line of content) {
|
||||
await printLine(` ${line}`, delay, color || 'green')
|
||||
await printLine(` ${line}`, delay, color || 'cyan')
|
||||
}
|
||||
}
|
||||
console.log(colorize('------------------------------', 'green'))
|
||||
console.log()
|
||||
}
|
||||
}
|
||||
|
||||
async function printHints(hints, delay = 100) {
|
||||
for (const hint of hints) {
|
||||
await printLine(hint, delay, 'yellow')
|
||||
}
|
||||
rl.close()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
printResponses,
|
||||
printHints,
|
||||
colorize,
|
||||
}
|
||||
@@ -3,10 +3,12 @@ import { model, Schema } from 'mongoose'
|
||||
const responseSchema = new Schema({
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['narrative', 'dialogue'],
|
||||
default: 'dialogue',
|
||||
required: true,
|
||||
},
|
||||
content: [String],
|
||||
speaker: String,
|
||||
speaker: { type: String, default: 'Ada' },
|
||||
color: String,
|
||||
})
|
||||
|
||||
@@ -35,6 +37,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 +88,10 @@ const stageSchema = new Schema({
|
||||
type: [String],
|
||||
required: true,
|
||||
},
|
||||
exceptions: {
|
||||
type: [exceptionSchema],
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const Quest = model(
|
||||
|
||||
@@ -35,10 +35,12 @@ const Session = model(
|
||||
user: {
|
||||
type: Schema.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
quest: {
|
||||
type: String,
|
||||
ref: 'Quest',
|
||||
required: true,
|
||||
},
|
||||
containerId: String,
|
||||
status: {
|
||||
@@ -54,7 +56,7 @@ const Session = model(
|
||||
required: true,
|
||||
},
|
||||
tasks: [taskSchema],
|
||||
hints: [String],
|
||||
hints: [[String]],
|
||||
graph: {
|
||||
type: nodeSchema,
|
||||
default: {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
title: 輸出文字
|
||||
instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 如何使用 `echo` 指令
|
||||
2. `echo` 指令在遊戲中的用途
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- get-started
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
condition:
|
||||
output:
|
||||
- ^start$
|
||||
responses:
|
||||
- content:
|
||||
- 你剛才使用的指令叫做 `echo`。
|
||||
- '`echo` 指令可以將後方接續的文字輸出到終端機上。'
|
||||
- 例如你輸入 `echo Hello`,終端機就會顯示 `Hello`。
|
||||
- 你可以試著輸入 `echo` 並在後面加上一些文字,然後按下 Enter 鍵,就可以看到終端機上顯示了你剛才輸入的文字。
|
||||
hints:
|
||||
- ➤ 輸入 `echo` 並在後面加上一些文字
|
||||
- ➤ 例如:`echo example`
|
||||
|
||||
- id: echo
|
||||
task: 在終端機上顯示顯示任意文字
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
command:
|
||||
- ^echo .*
|
||||
responses:
|
||||
- content:
|
||||
- 你應該可以看到終端機上顯示了你在 `echo` 指令後面的文字。
|
||||
- 接下來,請你試著在終端機上顯示 `How are you?`。
|
||||
hints:
|
||||
- ➤ 利用 `echo` 指令來在終端機上顯示 `How are you?`
|
||||
|
||||
- id: how-are-you
|
||||
task: 在終端機上顯示 `How are you?`
|
||||
requirements:
|
||||
- echo
|
||||
condition:
|
||||
output:
|
||||
- ^[Hh]ow are you\??$
|
||||
exceptions:
|
||||
- condition:
|
||||
command:
|
||||
- ^[Hh]ow are you\??$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 在 `How are you?` 前面必須加上 `echo`
|
||||
- condition:
|
||||
command:
|
||||
- ^echo$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 在 `echo` 後面還需要加上一些文字
|
||||
responses:
|
||||
- content:
|
||||
- 你成功的在終端機上顯示了 `How are you?`。
|
||||
- 相信你已經了解 `echo` 指令的用法了。
|
||||
- 接下來,要像你介紹 `echo` 在遊戲中的特殊功能,那就是詠唱咒語。
|
||||
- 你可以通過輸入 `echo [咒語]` 來詠唱咒語,並產生對應的特殊效果。
|
||||
- 但是要注意,詠唱咒語只有在 Linux Odyssey 中才會有特殊效果。
|
||||
- 在真實的 Linux 環境中,詠唱咒語並不會有任何特殊效果。
|
||||
- 現在,有一段咒語叫做 `HELP`,請你試著使用 `echo` 詠唱這段咒語。
|
||||
hints:
|
||||
- ➤ 試著使用 `echo` 來詠唱 `HELP` 咒語
|
||||
- ➤ 注意:咒語要使用大寫
|
||||
|
||||
- id: help
|
||||
task: 詠唱 `HELP` 咒語
|
||||
requirements:
|
||||
- how-are-you
|
||||
condition:
|
||||
output:
|
||||
- ^HELP$
|
||||
responses:
|
||||
- content:
|
||||
- 你詠唱了 `HELP` 咒語,一位精靈出現了。
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 你好,我是 Unix 精靈。
|
||||
- 既然你使用了 `HELP` 咒語呼喚我,就讓我來教你一個咒語吧。
|
||||
- 讓我想想... 好了,我想到了一個咒語,叫做 `WATER`。
|
||||
- 這是施展水魔法的咒語。詠唱這個咒語可以施展水魔法,產生少量的水。
|
||||
- 試著使用 `echo` 詠唱這個咒語吧。
|
||||
hints:
|
||||
- ➤ 詠唱 `WATER` 咒語
|
||||
|
||||
- id: END
|
||||
task: 詠唱 `WATER` 咒語
|
||||
requirements:
|
||||
- help
|
||||
condition:
|
||||
output:
|
||||
- ^WATER$
|
||||
responses:
|
||||
- content:
|
||||
- 你施展了水魔法,一個水球出現在你的手中。
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 很好,你已經學會了水魔法。
|
||||
- content:
|
||||
- 真厲害,你已經了解如何使用 `echo` 指令,並且學會了詠唱咒語的方法。
|
||||
- 最後複習一下,你可以通過輸入 `echo [文字]` 來在終端機上顯示文字。
|
||||
- 你也可以通過輸入 `echo [咒語]` 來詠唱咒語,並產生對應的特殊效果。
|
||||
- 不要忘了,詠唱咒語只有在 Linux Odyssey 中才會有特殊效果。
|
||||
- 在真實的 Linux 環境中,咒語只會被視為一串文字顯示出來而已。
|
||||
- 以上就是 `echo` 指令的用法,請繼續努力,完成接下來的關卡吧!
|
||||
hints: |
|
||||
複習:
|
||||
通過輸入 `echo [文字]` 來在終端機上顯示文字
|
||||
通過輸入 `echo [咒語]` 來詠唱咒語
|
||||
咒語只有在 Linux Odyssey 中才會有特殊效果
|
||||
@@ -1,3 +1,3 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY home ${HOME}
|
||||
COPY home ${HOME}
|
||||
@@ -0,0 +1,270 @@
|
||||
title: 複製檔案
|
||||
instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 如何用 `cp` 指令複製檔案
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- create
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- 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)代表將資料夾中的所有檔案都一起複製。
|
||||
- 在使用指令時,如果想要調整指令的行為,可以在指令後面加上選項。
|
||||
- 選項的格式通常是 `-` 加上一個字母,例如 `-r`。
|
||||
- 選項擺放的位置沒有特別的限制,通常會跟在指令的後面,置於檔案名稱的前面。
|
||||
- 請你使用 `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
|
||||
files:
|
||||
- path: notes2
|
||||
type: directory
|
||||
- path: notes2/note.txt
|
||||
type: file
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你成功複製 `notes` 資料夾到 `notes2` 了!
|
||||
- 我們打開來確認一下 `notes2` 資料夾的內容吧!
|
||||
hints:
|
||||
- ➤ 使用 `ls notes2` 來查看 `notes2` 資料夾的內容
|
||||
exceptions:
|
||||
- condition:
|
||||
error:
|
||||
- 'cp: omitting directory'
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你忘記加上 `-r` 的參數了嗎?
|
||||
- 請輸入 `cp -r notes notes2` 來複製 `notes` 資料夾到 `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` 了!'
|
||||
- 這是因為,當我們複製檔案時,如果目的檔案已經存在,那麼目的檔案的內容就會被覆蓋掉。
|
||||
- 所以在複製檔案的時候,要特別注意目的檔案是否已經存在,否則可能會不小心把資料弄丟。
|
||||
- 以上就是複製檔案的基本操作!
|
||||
@@ -0,0 +1 @@
|
||||
Hello
|
||||
@@ -0,0 +1 @@
|
||||
Note
|
||||
@@ -0,0 +1 @@
|
||||
World
|
||||
@@ -0,0 +1,199 @@
|
||||
title: 建立檔案與資料夾
|
||||
instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 如何使用 `touch` 建立檔案
|
||||
2. 如何使用 `mkdir` 建立資料夾
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- read
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- 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` 這個檔案。
|
||||
- 我們來看看 `hello.txt` 檔案的內容吧!
|
||||
- 我們可以使用之前學過的 `cat` 指令來查看檔案內容。
|
||||
hints:
|
||||
- ➤ 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
|
||||
|
||||
- id: cat
|
||||
condition:
|
||||
command:
|
||||
- ^cat hello.txt$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 咦?竟然什麼都沒有!
|
||||
- 沒錯,使用 `touch` 指令建立的檔案是空的。
|
||||
- 我們在未來會學習如何編輯檔案的內容。
|
||||
- 再來試試建立其他的檔案吧!
|
||||
- 請你試著在終端機上建立另一個檔案,檔名為 `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` 這個檔案。
|
||||
- 恭喜你完成了這個關卡!
|
||||
- 在下一個關卡中,你將學習如何複製檔案與資料夾。
|
||||
@@ -1,6 +0,0 @@
|
||||
title: Discover the World!
|
||||
instruction: |
|
||||
試著探索這個世界吧!
|
||||
|
||||
requirements:
|
||||
- spell
|
||||
@@ -0,0 +1,3 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY home ${HOME}
|
||||
@@ -0,0 +1,186 @@
|
||||
title: 探索檔案
|
||||
instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 如何使用 `ls` 查看資料夾的內容
|
||||
2. 如何使用 `cd` 在資料夾之間移動
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- get-started
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
condition:
|
||||
output:
|
||||
- ^start$
|
||||
responses:
|
||||
- content:
|
||||
- 在終端機中,並沒有圖形介面可以讓你查看資料夾的內容。
|
||||
- 但是你可以透過 `ls` 指令來查看資料夾的內容。
|
||||
- '`ls` 指令是 list 的縮寫,也就是列出檔案的意思。'
|
||||
- 在輸入 `ls` 指令後,終端機會輸出資料夾中包含的檔案和資料夾,右邊的檔案樹也會隨之改變。
|
||||
- 現在,請你輸入 `ls` 指令,查看資料夾中有什麼東西。
|
||||
hints:
|
||||
- ➤ 輸入 `ls` 並查看資料夾的內容
|
||||
|
||||
- id: ls
|
||||
task: 查看資料夾中的檔案
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
command:
|
||||
- ^ls$
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- /home/commander
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 請先回到 `/home/commander/` 資料夾
|
||||
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
||||
- condition:
|
||||
command:
|
||||
- ^[Ii]s$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! `ls` 的首字母是小寫的 `L`,不是 `I` !!'
|
||||
responses:
|
||||
- content:
|
||||
- 你應該能看到資料夾中的內容了。
|
||||
- 在畫面右邊的檔案樹也有了改變。
|
||||
- 可以看出,在這個資料夾中包含了兩個資料夾,分別是 `folder1` 和 `folder2`。
|
||||
- content:
|
||||
- 接下來,我要教你如何進入資料夾。
|
||||
- '`cd` 指令是 change directory 的縮寫,也就是改變路徑的意思。'
|
||||
- 你可以通過輸入 `cd [資料夾名稱]` 來進入資料夾。
|
||||
- 例如你要進入 `folder1` 資料夾,你可以輸入
|
||||
- '`cd folder1`。'
|
||||
- 現在,請你進入 `folder1` 資料夾。
|
||||
hints:
|
||||
- ➤ 輸入 `cd folder1` 來進入 `folder1` 資料夾
|
||||
|
||||
- id: cd-folder1
|
||||
task: 進入 `folder1` 資料夾
|
||||
requirements:
|
||||
- ls
|
||||
condition:
|
||||
pwd:
|
||||
- /home/commander/folder1
|
||||
exceptions:
|
||||
- condition:
|
||||
pwd:
|
||||
- /home/commander
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 輸入 `cd folder1` 來進入 `folder1` 資料夾
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- /home/commander
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 請先回到 `/home/commander/` 資料夾
|
||||
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
||||
responses:
|
||||
- content:
|
||||
- 你成功的進入 `folder1` 資料夾了。
|
||||
- 你可以發現指令提示符從原本的
|
||||
- '`commander:~ $` 變成了'
|
||||
- '`commander:~/folder1 $` 。'
|
||||
- 這表示你現在正位在 `folder1` 資料夾中。
|
||||
- 現在,請查看 `folder1` 資料夾的內容。
|
||||
hints:
|
||||
- ➤ 輸入 `ls` 查看資料夾的內容
|
||||
|
||||
- id: ls-folder1
|
||||
task: 查看 `folder1` 資料夾的內容
|
||||
requirements:
|
||||
- cd-folder1
|
||||
condition:
|
||||
pwd:
|
||||
- /home/commander/folder1
|
||||
command:
|
||||
- ls
|
||||
responses:
|
||||
- content:
|
||||
- 你現在可以看到 `folder1` 資料夾中有兩的檔案,分別是 `file1.txt` 跟 `file2.txt`。
|
||||
- 現在我們可以離開 `folder1` 回到原本的資料夾了。
|
||||
- 你可以使用 `cd ..` 回到上一層資料夾。
|
||||
- 這裡的 `..` 代表的是上一層的資料夾。
|
||||
hints:
|
||||
- ➤ 輸入 `cd ..` 回到上一層資料夾
|
||||
|
||||
- id: cd-..
|
||||
task: 回到上一層資料夾
|
||||
requirements:
|
||||
- ls-folder1
|
||||
condition:
|
||||
pwd:
|
||||
- /home/commander
|
||||
responses:
|
||||
- content:
|
||||
- 你成功的回到上一層資料夾了。
|
||||
- 現在,請你剛才所學的指令,進入 `folder2` 資料夾,檢查 `folder2` 資料夾的內容,最後回到這個資料夾。
|
||||
hints:
|
||||
- ➤ 使用 `cd` 指令進出資料夾
|
||||
- ➤ 使用 `ls` 指令查看資料夾的內容
|
||||
|
||||
- id: cd-folder2
|
||||
task: 進入 `folder2` 資料夾
|
||||
requirements:
|
||||
- cd-..
|
||||
condition:
|
||||
pwd:
|
||||
- /home/commander/folder2
|
||||
responses:
|
||||
- content:
|
||||
- 你成功的進入 `folder2` 資料夾了。
|
||||
- 現在,請你查看 `folder2` 資料夾的內容。
|
||||
hints:
|
||||
- ➤ 使用 `ls` 查看資料夾的內容
|
||||
|
||||
- id: ls-folder2
|
||||
task: 查看 `folder2` 資料夾的內容
|
||||
requirements:
|
||||
- cd-folder2
|
||||
condition:
|
||||
pwd:
|
||||
- /home/commander/folder2
|
||||
command:
|
||||
- ls
|
||||
responses:
|
||||
- content:
|
||||
- 你看到了 `folder2` 資料夾中的檔案,分別是 `file3.txt` 跟 `file4.txt`。
|
||||
- 現在你可以離開 `folder2` 回到原本的資料夾了。
|
||||
hints:
|
||||
- ➤ 使用 `cd ..` 回到上一層資料夾
|
||||
|
||||
- id: END
|
||||
task: 回到原本的資料夾
|
||||
requirements:
|
||||
- ls-folder2
|
||||
condition:
|
||||
pwd:
|
||||
- /home/commander
|
||||
responses:
|
||||
- content:
|
||||
- 你成功的回到原本的資料夾了。
|
||||
- 恭喜你學會了 `ls` 和 `cd` 指令的用法。
|
||||
hints:
|
||||
- ➤ 使用 `cd` 指令進出資料夾
|
||||
- ➤ 使用 `ls` 指令查看資料夾的內容
|
||||
File renamed without changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,83 @@
|
||||
title: 終端機的基本知識
|
||||
instruction: |
|
||||
歡迎來到 Linux Odyssey!
|
||||
在這個關卡中,你將會學到:
|
||||
1. 如何使用這個網站
|
||||
2. 如何使用終端機
|
||||
|
||||
在畫面的中間可以看到終端機和提示兩個區塊。
|
||||
請你在終端機中的 `commander:~ $` 後面輸入 `echo start` 並按下 Enter 來開始教程。
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
condition:
|
||||
output:
|
||||
- ^start$
|
||||
responses:
|
||||
- content:
|
||||
- 你好,我是你的 Unix 助手,我叫做 Ada。我將會在之後協助你了解如何使用終端機。
|
||||
- 現在所在的這個區塊就是終端機,你可以在這裡輸入指令來操作電腦。
|
||||
- 在終端機中,你所看到的
|
||||
- '`commander:~ $` 是指令提示符號,它能讓你知道現在可以輸入指令了。'
|
||||
- 你必須將指令輸入在它的後方。
|
||||
- 當你輸入完指令並按下 Enter 之後,終端機會執行你輸入的指令。
|
||||
- 並且會將執行結果輸出在終端機的下一行。
|
||||
- 這個終端機的功能和你在 Linux 電腦上看到的終端機是一樣的。
|
||||
- 唯一的差別是,在這裡會有劇情和提示來協助你學習如何使用終端機。
|
||||
- 在輸入指令之後,如果你看到用綠色 `=====` 組成的分隔線,代表在分隔線下方的內容是添加的內容,在真實的終端機中不會出現。
|
||||
- 而終端機的輸出,是在綠色 `=====` 分隔線上方的白色內容。
|
||||
- 現在請你輸入 `echo hello` 來看看會發生什麼事。
|
||||
hints:
|
||||
- ➤ 在 `commander:~ $` 後面輸入 `echo hello` 並按下 Enter
|
||||
|
||||
- id: echo-hello
|
||||
task: 在終端機輸入 `echo hello`
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
command:
|
||||
- ^echo hello$
|
||||
responses:
|
||||
- content:
|
||||
- 你應該可以看到終端機上顯示了:
|
||||
- commander:~ $ echo hello
|
||||
- hello
|
||||
- 第一行的 `commander:~ $` 是指令提示符, `echo hello` 是你輸入的指令。
|
||||
- 第二行的 `hello` 則是終端機的輸出。
|
||||
- content:
|
||||
- 現在你知道如何在終端機上輸入指令,以及查看終端機輸出的位置。
|
||||
- 指令必須輸入在指令提示符號的後面,然而當指令提示符消失時,你就無法輸入指令了。
|
||||
- 這時候你可以按下 `Ctrl + C` 來嘗試解決這個問題。
|
||||
- 在終端機中, `Ctrl + C` 並不能用來複製,而是用來中斷目前正在執行的指令。
|
||||
- 因此,如果你前一次的指令執行時卡住了,或是執行很久沒有反應,你可以按下 `Ctrl + C` 來中斷指令,讓你可以繼續輸入指令。
|
||||
- 如果 `Ctrl + C` 也沒有辦法解決問題,可以按按看 `esc` 或 `q` 鍵。如果都沒用的話,按下畫面右下角的 `重來` 按鈕重新開始關卡。
|
||||
- content:
|
||||
- 在終端機下方會顯示提示,你可以用提示右上角的箭頭來查看之前的提示。
|
||||
- 在畫面左邊的區域是關卡說明,你可以在這裡看到關卡的介紹,以及現在需要完成的任務。
|
||||
- 在畫面右邊的區域是檔案樹,它會根據你所探索的資料夾發生變化,讓你更清楚檔案之間的位置關係。
|
||||
- 畫面右上角的圖示分別是 `查看地圖`、`回報錯誤` 以及 `登出` 按鈕。
|
||||
- content:
|
||||
- 第一關的文字內容比較多,可以慢慢讀沒關係。
|
||||
- 如果忘記了也可以隨時回來複習。
|
||||
- 需要的話可以按下 `Enter` 來快速跳過劇情說明。
|
||||
- 如果你已經讀完了上述內容,請輸入 `echo finish` 來結束這一關。
|
||||
hints:
|
||||
- ➤ 按下 `Ctrl + C` 能夠中斷指令
|
||||
- ➤ 劇情進行時可以按下 `Enter` 來快速跳過
|
||||
|
||||
- id: END
|
||||
task: 結束關卡
|
||||
requirements:
|
||||
- echo-hello
|
||||
condition:
|
||||
command:
|
||||
- ^echo finish$
|
||||
@@ -4,10 +4,12 @@ instruction: |
|
||||
你感到腦中有某個聲音跟你說:
|
||||
「使用 `echo [ 咒語 ]` 指令來朗誦咒語 "help" 」
|
||||
|
||||
requirements:
|
||||
- remove
|
||||
|
||||
stages:
|
||||
- id: help
|
||||
task: 輸入 `echo help` 來朗誦咒語
|
||||
repeatable: false
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home/commander$'
|
||||
@@ -33,7 +35,6 @@ stages:
|
||||
|
||||
- id: find-scroll
|
||||
task: 搜索卷軸
|
||||
repeatable: false
|
||||
requirements:
|
||||
- help
|
||||
condition:
|
||||
@@ -42,7 +43,7 @@ stages:
|
||||
command:
|
||||
- ls
|
||||
responses:
|
||||
- types: narrative
|
||||
- type: narrative
|
||||
content:
|
||||
- 一瞬間,視野變得清晰起來,你發現自己身處在一個石頭建築的房間裡。
|
||||
- 你發現了一個檔案,檔名是 `forgotten-scroll.txt`。
|
||||
@@ -60,7 +61,6 @@ stages:
|
||||
|
||||
- id: read-forgotten-scroll
|
||||
task: 查看卷軸
|
||||
repeatable: false
|
||||
requirements:
|
||||
- find-scroll
|
||||
condition:
|
||||
@@ -79,7 +79,6 @@ stages:
|
||||
|
||||
- id: END
|
||||
task: 解除封印
|
||||
repeatable: false
|
||||
requirements:
|
||||
- read-forgotten-scroll
|
||||
condition:
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
quests:
|
||||
- helloworld
|
||||
- spell
|
||||
- discover
|
||||
- read
|
||||
- create
|
||||
- transport
|
||||
- copy
|
||||
- move
|
||||
- remove
|
||||
@@ -0,0 +1,3 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY --chown=${USERNAME} home ${HOME}
|
||||
@@ -0,0 +1,228 @@
|
||||
title: 移動檔案
|
||||
instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 如何用 `mv` 移動檔案與資料夾
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- create
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始
|
||||
condition:
|
||||
command:
|
||||
- ^echo start$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 今天我們要學習如何移動檔案與資料夾。
|
||||
- 現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,以及一個 `notes` 資料夾。
|
||||
- 請先用 `ls` 與 `cat` 來查看它們的內容。
|
||||
- 看完之後,使用 `echo continue` 來繼續吧!
|
||||
hints:
|
||||
- ➤ 用 `ls`、`ls notes` 來查看資料夾內的檔案
|
||||
- ➤ 用 `cat hello.txt` 以及 `cat world.txt` 來查看檔案內容
|
||||
- ➤ 完成後輸入 `echo continue` 來繼續
|
||||
|
||||
- id: continue
|
||||
requirements:
|
||||
- start
|
||||
task: 查看檔案內容,完成後輸入 `echo continue` 來繼續
|
||||
condition:
|
||||
command:
|
||||
- ^echo continue$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 現在我們要來學習如何移動檔案與資料夾。
|
||||
- '`mv` 指令是移動 move 的縮寫,可以用來移動檔案與資料夾。'
|
||||
- 使用方式為 `mv [來源] [目的地]`。
|
||||
- 我們先來將 `hello.txt` 移動到 `notes` 資料夾中吧!
|
||||
hints:
|
||||
- ➤ 使用 `mv hello.txt notes` 來將 `hello.txt` 移動到 `notes` 資料夾中
|
||||
|
||||
- id: mv-hello
|
||||
requirements:
|
||||
- continue
|
||||
task: 將 `hello.txt` 移動到 `notes` 資料夾中
|
||||
condition:
|
||||
command:
|
||||
- ^mv hello.txt notes$
|
||||
files:
|
||||
- path: notes/hello.txt
|
||||
type: file
|
||||
- path: hello.txt
|
||||
exists: false
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你做得很好!
|
||||
- 我們用 `ls` 跟 `ls notes` 來查看一下資料夾內的檔案吧!
|
||||
hints:
|
||||
- ➤ 使用 `ls`、`ls notes` 來查看資料夾內的檔案
|
||||
|
||||
- id: ls-no-hello
|
||||
requirements:
|
||||
- mv-hello
|
||||
task: 檢查 `hello.txt` 是否還在資料夾中
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`hello.txt` 已經不在資料夾中了!'
|
||||
|
||||
- id: ls-notes-hello
|
||||
requirements:
|
||||
- mv-hello
|
||||
task: 檢查 `hello.txt` 是否在 `notes` 資料夾中
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls notes$
|
||||
output:
|
||||
- hello.txt
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`hello.txt` 出現在 `notes` 資料夾中了!'
|
||||
|
||||
- id: continue-mv
|
||||
requirements:
|
||||
- ls-no-hello
|
||||
- ls-notes-hello
|
||||
task: 輸入 `echo continue` 來繼續
|
||||
condition:
|
||||
command:
|
||||
- ^echo continue$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`mv` 指令也可以用來更改檔案名稱。'
|
||||
- '只要將目的地改成新的檔案名稱就可以了。'
|
||||
- 現在我們要將 `world.txt` 更名為 `wow.txt`。
|
||||
hints:
|
||||
- ➤ 使用 `mv world.txt wow.txt` 來將 `world.txt` 更名為 `wow.txt`
|
||||
|
||||
- id: mv-world
|
||||
requirements:
|
||||
- continue-mv
|
||||
task: 將 `world.txt` 更名為 `wow.txt`
|
||||
condition:
|
||||
command:
|
||||
- ^mv
|
||||
files:
|
||||
- path: wow.txt
|
||||
type: file
|
||||
- path: world.txt
|
||||
exists: false
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你做得很好!
|
||||
- 我們用 `ls` 來確認一下資料夾內的檔案吧!
|
||||
hints:
|
||||
- ➤ 使用 `ls` 來查看資料夾內的檔案
|
||||
|
||||
- id: ls-wow
|
||||
requirements:
|
||||
- mv-world
|
||||
task: 檢查 `wow.txt` 是否存在
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls$
|
||||
output:
|
||||
- wow.txt
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`world.txt` 現在已經變成 `wow.txt` 了。'
|
||||
- 接下來我們來學習如何移動資料夾吧!
|
||||
- 移動資料夾非常簡單,方法跟移動檔案一樣。
|
||||
- '`mv` 指令會自動將資料夾內的所有檔案一起移動'
|
||||
- 我們先來將 `notes` 資料夾改名叫作 `docs` 吧!
|
||||
hints:
|
||||
- ➤ 使用 `mv notes docs` 來將 `notes` 資料夾改名叫作 `docs`
|
||||
|
||||
- id: mv-notes
|
||||
requirements:
|
||||
- ls-wow
|
||||
task: 將 `notes` 資料夾改名叫作 `docs`
|
||||
condition:
|
||||
command:
|
||||
- ^mv
|
||||
files:
|
||||
- path: docs
|
||||
type: directory
|
||||
- path: notes
|
||||
exists: false
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你做得很好!
|
||||
- 我們用 `ls` 來確認一下資料夾內的檔案吧!
|
||||
hints:
|
||||
- ➤ 使用 `ls` 來查看資料夾內的檔案
|
||||
|
||||
- id: ls-docs
|
||||
requirements:
|
||||
- mv-notes
|
||||
task: 檢查 `docs` 資料夾是否存在
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls$
|
||||
output:
|
||||
- docs
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`notes` 資料夾已經變成 `docs` 了。'
|
||||
- 那麼最後來考考你:
|
||||
- 試著將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`。
|
||||
- 如果你不知道怎麼做,可以輸入 `echo hint` 來取得提示。
|
||||
|
||||
- id: END
|
||||
requirements:
|
||||
- ls-docs
|
||||
task: 將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`
|
||||
condition:
|
||||
command:
|
||||
- ^mv
|
||||
files:
|
||||
- path: hi.txt
|
||||
type: file
|
||||
- path: docs/hello.txt
|
||||
exists: false
|
||||
exceptions:
|
||||
- condition:
|
||||
command:
|
||||
- ^echo hint$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你可以使用 `mv docs/hello.txt hi.txt` 來完成這個任務。
|
||||
hints:
|
||||
- ➤ 使用 `mv docs/hello.txt hi.txt` 來將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你做得很好!
|
||||
- 你已經學會如何移動檔案與資料夾了!
|
||||
@@ -0,0 +1 @@
|
||||
Hello
|
||||
@@ -0,0 +1 @@
|
||||
Note
|
||||
@@ -0,0 +1 @@
|
||||
World
|
||||
@@ -0,0 +1,3 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY home ${HOME}
|
||||
@@ -0,0 +1,183 @@
|
||||
title: 檔案路徑
|
||||
instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 絕對路徑和相對路徑
|
||||
2. 如何使用 `pwd` 顯示目前的路徑
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- cast
|
||||
- explore
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
condition:
|
||||
output:
|
||||
- ^start$
|
||||
responses:
|
||||
- content:
|
||||
- 在 Linux 系統中,每個資料夾和檔案都有獨一無二的路徑。
|
||||
- 而路徑又分為相對路徑和絕對路徑。
|
||||
- 相對路徑是從目前所在的資料夾開始的路徑,而絕對路徑則是從根目錄開始的路徑。
|
||||
- 請你先用 `ls` 指令查看這個資料夾中的檔案。
|
||||
|
||||
- id: ls
|
||||
task: 查看資料夾中的檔案
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- 現在你使用 `cd` 指令進入 `folder1` 資料夾。
|
||||
|
||||
- id: cd-folder1
|
||||
task: 進入 `folder1` 資料夾
|
||||
requirements:
|
||||
- ls
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander/folder1$
|
||||
responses:
|
||||
- content:
|
||||
- 你進入了 `folder1` 資料夾。
|
||||
- 在進入 `folder1` 資料夾時,使用的 `cd folder1` 中的 `folder1` 就是相對路徑。
|
||||
- 相同的道理 `cd ..` 中的 `..` 也是相對路徑。
|
||||
- 相對路徑可以不只有一層資料夾。
|
||||
- 例如:現在想要進入 `folder2` 資料夾的話,可以使用 `cd ..` 回到上一層資料夾,再使用 `cd folder2` 進入 `folder2` 資料夾。
|
||||
- 然而,你也可以用 `/` 將 `..` 和 `folder2` 依序連接起來,使用 `cd ../folder2` 來一次進入 `folder2` 資料夾。
|
||||
- 現在試著用這個方式進入 `folder2` 資料夾看看吧。
|
||||
hints:
|
||||
- ➤ 請輸入 `cd ../folder2` 來進入 `folder2` 資料夾。
|
||||
|
||||
- id: cd-folder2
|
||||
task: 用指定方式進入 `folder2` 資料夾
|
||||
requirements:
|
||||
- cd-folder1
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander/folder2$
|
||||
command:
|
||||
- ^cd \.\./folder2$
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander/folder1$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 請先回到 `folder1` 資料夾,並用指定的方式進入 `folder2` 資料夾。
|
||||
responses:
|
||||
- content:
|
||||
- 你成功的用相對路徑進入了 `folder2` 資料夾。
|
||||
- 而你也能夠在 `ls` 後加上相對路徑來查看其他資料夾的內容。
|
||||
- 例如:現在你想要查看 `folder1` 資料夾中有哪些檔案,你可以使用 `ls ../folder1` 來查看。
|
||||
- 試著用這個方式查看 `folder1` 資料夾中的檔案吧。
|
||||
hints:
|
||||
- ➤ 請輸入 `ls ../folder1` 來查看 `folder1` 資料夾中的檔案。
|
||||
|
||||
- id: ls-folder1
|
||||
task: 查看 `folder1` 資料夾中的檔案
|
||||
requirements:
|
||||
- cd-folder2
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander/folder2$
|
||||
command:
|
||||
- ^ls \.\.\/folder1$
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander/folder2$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 請先回到 `folder2` 資料夾,並用指定的方式查看 `folder1` 資料夾的內容。
|
||||
responses:
|
||||
- content:
|
||||
- 很好,你現在知道如何運用相對路徑了。
|
||||
- 接下來要介紹的是絕對路徑。
|
||||
- 絕對路徑是從根目錄開始的路徑,根目錄是位於檔案樹的最上層的資料夾。
|
||||
- 你可以使用 `pwd` 來查看目前所在的絕對路徑。
|
||||
- '`pwd` 的意思是 `print working directory`,也就是顯示目前所在的路徑。'
|
||||
- 現在請你使用 `pwd` 來查看目前所在的絕對路徑。
|
||||
hints:
|
||||
- ➤ 輸入 `pwd` 查看目前所在的絕對路徑
|
||||
|
||||
- id: pwd
|
||||
task: 查看目前所在的絕對路徑
|
||||
requirements:
|
||||
- ls-folder1
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander/folder2$
|
||||
command:
|
||||
- ^pwd$
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander/folder2$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 請先回到 `folder2` 資料夾,並用 `pwd` 來查看目前所在的絕對路徑
|
||||
responses:
|
||||
- content:
|
||||
- 你可以看到你現在的絕對路徑是 `/home/commander/folder2`。
|
||||
- 而絕對路徑其實也顯示在你的命令提示符中。
|
||||
- '`commander:~/folder2 $` 中的 `~/folder2` 就是你的絕對路徑。'
|
||||
- 而 `~` 則是 `/home/commander` 這個絕對路徑的縮寫,又稱為家目錄,你每次遊戲都會從這裡開始。
|
||||
- '`ls` 和 `cd` 指令也可以使用絕對路徑。'
|
||||
- 例如:你可以使用 `ls ~` 來查看家目錄的內容,或使用 `cd ~` 來回到家目錄。
|
||||
- 現在請你使用 `ls ~` 來查看家目錄的內容。
|
||||
hints:
|
||||
- ➤ 輸入 `ls ~` 來查看家目錄的內容
|
||||
|
||||
- id: ls-~
|
||||
task: 查看家目錄的內容
|
||||
requirements:
|
||||
- pwd
|
||||
condition:
|
||||
command:
|
||||
- ^ls ~$
|
||||
responses:
|
||||
- content:
|
||||
- 現在請使用 `cd ~` 回到家目錄。
|
||||
hints:
|
||||
- ➤ 輸入 `cd ~` 來回到家目錄
|
||||
|
||||
- id: END
|
||||
task: 回到家目錄
|
||||
requirements:
|
||||
- ls-~
|
||||
condition:
|
||||
command:
|
||||
- ^cd ~$
|
||||
exceptions:
|
||||
- condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 請使用 `cd ~` 回到家目錄
|
||||
responses:
|
||||
- content:
|
||||
- 你回到家目錄了。
|
||||
- 恭喜!現在你已經知道如何運用絕對路徑和相對路徑,以及查看目前所在的絕對路徑了。
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,3 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY home ${HOME}
|
||||
+109
-3
@@ -1,6 +1,112 @@
|
||||
title: Read the File
|
||||
title: 查看檔案內容
|
||||
instruction: |
|
||||
閱讀檔案
|
||||
在這個關卡中,你將會學到:
|
||||
1. 如何使用 `cat` 查看檔案的內容
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- spell
|
||||
- path
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
condition:
|
||||
output:
|
||||
- ^start$
|
||||
responses:
|
||||
- content:
|
||||
- 在終端機中,並沒有圖形介面可以讓你查看檔案的內容。
|
||||
- 但是你可以透過 `cat` 指令來查看檔案的內容。
|
||||
- '`cat` 指令是 concatenate 的縮寫,也就是串接的意思。'
|
||||
- 在輸入 `cat [檔案名稱]` 指令後,終端機會輸出檔案的內容。
|
||||
- 請你先查看這個資料夾中哪些檔案。
|
||||
hints:
|
||||
- ➤ 請輸入 `ls` 來查看這個資料夾中的檔案。
|
||||
|
||||
- id: ls-~
|
||||
task: 查看資料夾中的檔案
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- 這個資料夾中有一個檔案和一個資料夾。
|
||||
- 現在你可以使用 `cat` 指令來查看 `file.txt` 的內容。
|
||||
hints:
|
||||
- ➤ 請輸入 `cat file.txt` 來查看檔案的內容。
|
||||
|
||||
- id: cat-file
|
||||
task: 查看 `file.txt` 的內容
|
||||
requirements:
|
||||
- ls-~
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^cat file.txt$
|
||||
responses:
|
||||
- content:
|
||||
- 你應該能看到檔案的內容了。
|
||||
- '`cat` 指令也可以使用路徑來查看檔案內容'
|
||||
- 請你先用 `ls folder` 查看資料夾內的檔案。
|
||||
hints:
|
||||
- ➤ 輸入 `ls folder` 查看資料夾內的檔案
|
||||
|
||||
- id: ls-folder
|
||||
task: 查看 `folder` 資料夾
|
||||
requirements:
|
||||
- cat-file
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls folder$
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
content:
|
||||
- 請留在 `/home/commander` 資料夾中。
|
||||
- 請輸入 `cd ~` 回到 `/home/commander` 資料夾。
|
||||
responses:
|
||||
- content:
|
||||
- 你現在知道 `folder` 資料夾的內容了。
|
||||
- 請用 `cat` 指令來查看 `folder` 資料夾中的 `file.txt` 檔案。
|
||||
hints:
|
||||
- ➤ 輸入 `cat folder/file.txt` 查看 `file.txt` 檔案的內容。
|
||||
|
||||
- id: END
|
||||
task: 查看 `file.txt` 的內容
|
||||
requirements:
|
||||
- ls-folder
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^cat folder/file.txt$
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
content:
|
||||
- 請留在 `/home/commander` 資料夾中。
|
||||
- 請輸入 `cd ~` 回到 `/home/commander` 資料夾。
|
||||
responses:
|
||||
- content:
|
||||
- 你成功地查看到 `file.txt` 的內容了。
|
||||
- 你已經會使用 `cat` 指令查看檔案的內容了。
|
||||
@@ -0,0 +1 @@
|
||||
This is file1.txt
|
||||
@@ -0,0 +1 @@
|
||||
This is file.txt
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY --chown=${USERNAME} home ${HOME}
|
||||
RUN mkdir empty
|
||||
@@ -0,0 +1,265 @@
|
||||
title: 刪除檔案與資料夾
|
||||
instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 用 `rm` 指令刪除檔案
|
||||
2. 用 `rmdir` 與資料夾
|
||||
3. 用 `rm -r` 刪除資料夾與它的所有檔案
|
||||
現在資料夾裡有一些檔案與資料夾,請你使用 `ls` 跟 `cat` 來查看它們的內容。
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- copy
|
||||
- move
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始
|
||||
condition:
|
||||
command:
|
||||
- ^echo
|
||||
output:
|
||||
- start
|
||||
responses:
|
||||
- content:
|
||||
- 今天我們要學習如何刪除檔案與資料夾。
|
||||
- '首先要來學習 `rm` 指令。`rm` 指令可以用來刪除檔案。使用方式是 `rm [檔案名稱]`。'
|
||||
- '現在請你使用 `rm` 指令刪除 `hello.txt`。'
|
||||
hints:
|
||||
- ➤ 使用 `rm hello.txt` 來刪除 `hello.txt`。
|
||||
|
||||
- id: rm-hello
|
||||
requirements:
|
||||
- start
|
||||
task: 使用 `rm` 刪除 `hello.txt`
|
||||
condition:
|
||||
command:
|
||||
- ^rm
|
||||
files:
|
||||
- path: hello.txt
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!現在請你使用 `ls` 指令來查看資料夾內容。'
|
||||
|
||||
- id: ls-no-hello
|
||||
requirements:
|
||||
- rm-hello
|
||||
task: 使用 `ls` 查看資料夾內容
|
||||
condition:
|
||||
command:
|
||||
- ^ls
|
||||
files:
|
||||
- path: hello.txt
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '你會發現 `hello.txt` 已經不見了。'
|
||||
- '`rm` 也可以同時刪除多個檔案,只要把檔案名稱用空白隔開就可以了,也就是 `rm [檔案1] [檔案2] ...`。'
|
||||
- 現在請你使用 `rm` 刪除 `world.txt` 與 `wow.txt`。
|
||||
hints:
|
||||
- ➤ 使用 `rm world.txt wow.txt` 來刪除 `world.txt` 與 `wow.txt`。
|
||||
- ➤ 檔案名稱之間要用空白隔開。
|
||||
- ➤ 名稱的順序不重要。
|
||||
|
||||
- id: rm-world-wow
|
||||
requirements:
|
||||
- ls-no-hello
|
||||
task: 使用 `rm` 刪除 `world.txt` 與 `wow.txt`
|
||||
condition:
|
||||
command:
|
||||
- ^rm
|
||||
files:
|
||||
- path: world.txt
|
||||
exists: false
|
||||
- path: wow.txt
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!我們一樣用 `ls` 指令確認一下資料夾內容。'
|
||||
hints:
|
||||
- ➤ 使用 `ls` 來查看資料夾內容。
|
||||
|
||||
- id: ls-no-world-wow
|
||||
requirements:
|
||||
- rm-world-wow
|
||||
task: 使用 `ls` 查看資料夾內容
|
||||
condition:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- '你會發現 `world.txt` 與 `wow.txt` 已經不見了。'
|
||||
- '現在我們來學習 `rmdir` 指令。`rmdir` 指令可以用來刪除資料夾。使用方式是 `rmdir [資料夾名稱]`。'
|
||||
- '現在請你使用 `rmdir` 指令刪除 `empty` 資料夾。'
|
||||
hints:
|
||||
- ➤ 使用 `rmdir empty` 來刪除 `empty` 資料夾。
|
||||
|
||||
- id: rm-empty
|
||||
requirements:
|
||||
- ls-no-world-wow
|
||||
task: 使用 `rmdir` 刪除 `empty` 資料夾
|
||||
condition:
|
||||
command:
|
||||
- ^rmdir
|
||||
files:
|
||||
- path: empty
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!現在請你使用 `ls` 指令來查看資料夾內容。'
|
||||
|
||||
- id: ls-no-empty
|
||||
requirements:
|
||||
- rm-empty
|
||||
task: 使用 `ls` 查看資料夾內容
|
||||
condition:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- '你會發現 `empty` 資料夾已經不見了。'
|
||||
- '現在請你試試看 `rmdir` 指令刪除 `notes` 資料夾。'
|
||||
hints:
|
||||
- ➤ 使用 `rmdir notes` 來刪除 `notes` 資料夾
|
||||
|
||||
- id: rm-notes-failed
|
||||
requirements:
|
||||
- ls-no-empty
|
||||
task: 使用 `rmdir` 刪除 `notes` 資料夾
|
||||
condition:
|
||||
command:
|
||||
- ^rmdir notes$
|
||||
files:
|
||||
- path: notes
|
||||
type: folder
|
||||
exists: true
|
||||
responses:
|
||||
- content:
|
||||
- 指令執行失敗了!
|
||||
- '你會發現 `notes` 資料夾還在。'
|
||||
- '這是因為 `rmdir` 指令只能刪除空的資料夾。'
|
||||
- 我們先來將 `notes` 資料夾的檔案刪除再來試一次。
|
||||
- '現在請你使用 `rm` 指令刪除 `notes` 資料夾內的檔案。'
|
||||
hints:
|
||||
- ➤ 使用 `rm notes/[檔案名稱]` 來刪除 `notes` 資料夾內的檔案。
|
||||
- ➤ 你可能需要使用 `ls notes` 來查看 `notes` 資料夾內有哪些檔案。
|
||||
|
||||
- id: rm-notes-content
|
||||
requirements:
|
||||
- rm-notes-failed
|
||||
task: 使用 `rm` 刪除 `notes` 資料夾內的檔案
|
||||
condition:
|
||||
command:
|
||||
- ^rm
|
||||
files:
|
||||
- path: notes
|
||||
type: folder
|
||||
exists: true
|
||||
- path: notes/note.txt
|
||||
exists: false
|
||||
exceptions:
|
||||
- condition:
|
||||
command:
|
||||
- ^rm notes/\[.*\]
|
||||
responses:
|
||||
- content:
|
||||
- '你好像理解錯了。'
|
||||
- 這邊中括號包起來 `[檔案名稱]` 是參數的意思,需要你自己填入檔案的名稱。
|
||||
- 實際打指令的時候,不要打中括號 `[]` 喔!
|
||||
- '例如,如果要刪除 `notes` 資料夾內的 `note.txt` 檔案,指令應該要是 `rm notes/note.txt`。'
|
||||
- 請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!
|
||||
- condition:
|
||||
command:
|
||||
- ^rm
|
||||
error:
|
||||
- 'No such file or directory'
|
||||
responses:
|
||||
- content:
|
||||
- 這個錯誤的意思是找不到檔案
|
||||
- '請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!'
|
||||
- condition:
|
||||
command:
|
||||
- ^ls notes$
|
||||
responses:
|
||||
- content:
|
||||
- '`notes` 資料夾中有著 `note.txt` 檔案。'
|
||||
- 那我們就使用 `rm notes/note.txt` 來刪除 `note.txt` 檔案吧!
|
||||
responses:
|
||||
- content:
|
||||
- '很好!現在我們再使用 `rmdir` 指令刪除 `notes` 資料夾。'
|
||||
hints:
|
||||
- ➤ 使用 `rmdir notes` 來刪除 `notes` 資料夾。
|
||||
|
||||
- id: rm-notes
|
||||
requirements:
|
||||
- rm-notes-content
|
||||
task: 使用 `rmdir` 刪除 `notes` 資料夾
|
||||
condition:
|
||||
command:
|
||||
- ^rmdir
|
||||
files:
|
||||
- path: notes
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!`notes` 資料夾已經被刪除了。'
|
||||
- 接下來我們來刪除 `trash` 資料夾。
|
||||
- 先使用 `ls` 指令查看一下 `trash` 資料夾內容。
|
||||
hints:
|
||||
- ➤ 使用 `ls trash` 來查看 `trash` 中的檔案。
|
||||
|
||||
- id: ls-trash
|
||||
requirements:
|
||||
- rm-notes
|
||||
task: 使用 `ls` 查看 `trash` 資料夾內容
|
||||
condition:
|
||||
command:
|
||||
- ^ls
|
||||
output:
|
||||
- garbage.txt
|
||||
responses:
|
||||
- content:
|
||||
- 天啊!`trash` 資料夾裡有好多檔案,甚至還有其他的資料夾!
|
||||
- 如果今天資料夾中有很多檔案與資料夾,我們不想一個一個刪除,該怎麼辦呢?
|
||||
- 跟 `cp` 的時候一樣,這時候我們可以加上 `-r` 的選項來刪除資料夾與它的所有檔案。
|
||||
- '`-r` 的意思是遞迴(recursive),也就是會把資料夾中的所有檔案都刪除。'
|
||||
- 我們來試試看吧!
|
||||
hints:
|
||||
- ➤ 使用 `rm -r trash` 來刪除 `trash` 資料夾。
|
||||
|
||||
- id: rm-trash
|
||||
requirements:
|
||||
- ls-trash
|
||||
task: 使用 `rm -r` 刪除 `trash` 資料夾
|
||||
condition:
|
||||
command:
|
||||
- ^rm -r
|
||||
files:
|
||||
- path: trash
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!現在請你使用 `ls` 指令查看資料夾內容。'
|
||||
hints:
|
||||
- ➤ 使用 `ls` 來查看資料夾內容。
|
||||
|
||||
- id: END
|
||||
requirements:
|
||||
- rm-trash
|
||||
task: 使用 `ls` 查看資料夾內容
|
||||
condition:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- '`trash` 資料夾已經不見了。'
|
||||
- 恭喜你!你已經學會如何刪除檔案與資料夾了!
|
||||
@@ -0,0 +1 @@
|
||||
Hello
|
||||
@@ -0,0 +1 @@
|
||||
Note
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
World
|
||||
@@ -0,0 +1 @@
|
||||
Wow
|
||||
@@ -1,292 +0,0 @@
|
||||
title: Learn to spell
|
||||
instruction: |
|
||||
你身在一個漆黑的房間裡,沒有任何的光線或聲音。你想不起來為什麼自己會來到這裡。但你的腦中隱約感到某個聲音跟你說:
|
||||
|
||||
「輸入 echo Hello World!」
|
||||
|
||||
requirements:
|
||||
- helloworld
|
||||
|
||||
conditions:
|
||||
pwd_home: &pwd_home
|
||||
pwd:
|
||||
- '^/home/commander$'
|
||||
|
||||
stages:
|
||||
- id: helloworld
|
||||
task: 使用 `echo Hello World!` 召喚精靈
|
||||
repeatable: false
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home/commander$'
|
||||
command:
|
||||
- echo Hello World
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你聽到了一個聲音:
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 你好,指令官,我是你的 Unix 精靈。我已在此恭候多時。
|
||||
- 這個世界是在 3560000 時脈以前,由初代王建立了 Unix 律法,所形成的世界。然而在經過了好幾世代的王後,終於誕生了永恆之王——薩加(Saga),她比歷代的君王都更為強大,且從來不會衰老,使得這個世界變得前所未有的強大。
|
||||
- 然而,外在世界一直不斷在改變,同時在面臨外敵的不斷攻擊,而薩加卻始終用同一套治理的手段。最終,在 160000 時脈前的一次入侵中,根部遭到攻陷,薩加遭到封印,而這個國家也陷入了黑暗。
|
||||
- 由於長久以來依賴於薩加,世人已失去了「指令」的力量,即將走向衰敗和滅亡的命運。
|
||||
- 你的任務,就是找回被世人遺忘的指令,奪回 Linux 之根,成為 Linux 之王。
|
||||
- 首先,你必須要學會如何使用指令。
|
||||
- 你要學習的第一個指令,是「發現」的指令:`ls`。ls 可以用來查看一個目錄中有哪些檔案。
|
||||
- 現在,請你輸入 `ls` 來查看目前的目錄下有哪些檔案。
|
||||
hints:
|
||||
- 使用 `ls` 指令來查看目前的目錄下有哪些檔案。
|
||||
|
||||
- id: find-scroll
|
||||
task: 搜索卷軸
|
||||
repeatable: false
|
||||
requirements:
|
||||
- helloworld
|
||||
condition:
|
||||
<<: *pwd_home
|
||||
command:
|
||||
- ls
|
||||
responses:
|
||||
- types: narrative
|
||||
content:
|
||||
- 一瞬間,視野變得清晰起來,你發現自己身處在一個石頭建築的房間裡,房間裡有一個門,門上有一個鎖。
|
||||
- 你發現了一個檔案,檔名是 `forgotten-scroll.txt`。
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 你找到了一個被遺忘的卷軸,上面寫著解除封印的咒語。
|
||||
- 使用指令 `cat [檔案名稱]` 來查看檔案內容。
|
||||
- 我可以為你提供一些指令的提示,只要輸入指令的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
|
||||
- 例如,你可以輸入 `ca` 然後按下 `Tab` 鍵,就可以看到 `cat` 這個指令的提示了。
|
||||
- 檔案名稱也一樣,只要輸入檔案名稱的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
|
||||
hints:
|
||||
- 使用 `cat [檔案名稱]` 指令來查看檔案內容。
|
||||
- 使用 `Tab` 鍵來取得指令提示。
|
||||
|
||||
- id: jail
|
||||
task: 移動到下一個房間
|
||||
repeatable: false
|
||||
requirements:
|
||||
- find-scroll
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home/commander/jail$'
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你進入了一個牢房,牢房裡有一個門,門上有一個鎖。
|
||||
- 突然,你聽到門上鎖的聲音。
|
||||
- 轉身發現,自己被關在了牢房裡。
|
||||
|
||||
- id: read-forgotten-scroll
|
||||
task: 查看卷軸
|
||||
repeatable: false
|
||||
requirements:
|
||||
- find-scroll
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home/commander$'
|
||||
command:
|
||||
- cat forgotten-scroll.txt
|
||||
responses:
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 你找到的是一個被遺忘的卷軸,上面寫著解除封印的咒語。
|
||||
- 使用你已經知道的 `echo` 指令來詠唱咒語。
|
||||
|
||||
- id: unseal-room
|
||||
task: 解除封印
|
||||
repeatable: false
|
||||
requirements:
|
||||
- read-forgotten-scroll
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home/commander$'
|
||||
command:
|
||||
- echo Unix, rise and guide me!
|
||||
execution:
|
||||
- mv /usr/local/lib/warehouse /home/commander
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 突然之間,地板開始晃動。
|
||||
- 晃動持續了幾秒後停止了。
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 我的指令官,剛剛房間內似乎發生了某些變化。
|
||||
- 再次使用 `ls` 來查看現在房間的情況吧。
|
||||
|
||||
- id: find-items
|
||||
task: 搜索房間
|
||||
repeatable: false
|
||||
requirements:
|
||||
- unseal-room
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home/commander$'
|
||||
command:
|
||||
- ls
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你看到了房間的地上散落著許多物品。
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 看來這裡隱藏著許多有用的東西。
|
||||
- 我們可以把這裡作為一個根據地。
|
||||
- 但這裡有點太亂了,必須先把這裡整理乾淨。
|
||||
- 首先我們需要一個書櫃來存放這些文件。
|
||||
- 請使用 `mkdir document` 來創造一個書櫃。
|
||||
|
||||
- id: create-bookcase
|
||||
task: 創造書櫃
|
||||
repeatable: false
|
||||
requirements:
|
||||
- find-items
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home/commander$'
|
||||
command:
|
||||
- mkdir document
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你創造了一個書櫃。
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 接著把文件都放到書櫃中吧。
|
||||
- 請使用 `mv [檔案名稱] document` 來將物品放入書櫃。
|
||||
- 可以輸入多個檔案名稱,並以空白隔開,來將多個檔案放入書櫃。
|
||||
- 例如:`mv [檔案名稱1] [檔案名稱2] document`
|
||||
- 注意:文件是副檔名為 `.txt` 的檔案,副檔名為 `.trash` 的檔案是垃圾,我們待會再處理。
|
||||
|
||||
- id: store-books
|
||||
task: 收拾文件
|
||||
repeatable: false
|
||||
requirements:
|
||||
- create-bookcase
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home/commander$'
|
||||
files:
|
||||
- path: '*.txt'
|
||||
exists: false
|
||||
responses:
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 整理好文件了,接下來把垃圾清理掉吧。
|
||||
- 請使用 `rm [檔案名稱]` 來銷毀垃圾。
|
||||
- 這個 `rm` 指令也可以一次處理多的檔案喔,用法跟 `mv` 一樣。
|
||||
- 如果你忘記的話,看看這個範例:`rm [檔案名稱1] [檔案名稱2]`
|
||||
|
||||
- id: remove-trash
|
||||
task: 銷毀垃圾
|
||||
repeatable: false
|
||||
requirements:
|
||||
- store-books
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home/commander$'
|
||||
files:
|
||||
- path: '*.trash'
|
||||
exists: false
|
||||
responses:
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 很好,讓我們看看現在房間的樣子吧。
|
||||
|
||||
- id: look-around-room
|
||||
task: 查看房間
|
||||
repeatable: false
|
||||
requirements:
|
||||
- removetrash
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home/commander$'
|
||||
command:
|
||||
- ls
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 地上的垃圾都消失了。
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 這樣乾淨多了,讓我們進到書櫃看看。
|
||||
- 使用 `cd [目錄名稱]` 來移動到指定的目錄。
|
||||
|
||||
- id: enter-bookcase
|
||||
task: 進入書櫃
|
||||
repeatable: false
|
||||
requirements:
|
||||
- look-around-room
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home/commander$'
|
||||
command:
|
||||
- cd document
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你進入了書櫃。
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 用 `ls` 看看書櫃裡有什麼吧。
|
||||
|
||||
- id: look-around-bookcase
|
||||
task: 查看書櫃
|
||||
repeatable: false
|
||||
requirements:
|
||||
- enter-bookcase
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home/commander/document$'
|
||||
command:
|
||||
- ls
|
||||
responses:
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 現在,你可以使用 `cd ..` 來回到上一層目錄。
|
||||
|
||||
- id: exit-bookcase
|
||||
task: 離開書櫃
|
||||
repeatable: false
|
||||
requirements:
|
||||
- look-around-bookcase
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home/commander/document$'
|
||||
command:
|
||||
- cd ..
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你離開了書櫃。
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 這些就是我現在能教你的所有指令了。這個世界還有許多其他的指令,讓你在冒險的過程中去蒐集。
|
||||
- 如果你準備好迎接接下來的冒險,就可以離開這個房間,開始你的旅程了。
|
||||
|
||||
- id: END
|
||||
task: 離開房間
|
||||
repeatable: false
|
||||
requirements:
|
||||
- look-around
|
||||
condition:
|
||||
pwd:
|
||||
- '^/home$'
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你踏出了房門,真正的冒險現在才正要開始。
|
||||
@@ -72,7 +72,7 @@ export const completedCommand = asyncHandler(async (req, res) => {
|
||||
|
||||
const callback = commandCompleteCallbacks.get(session.id)
|
||||
if (!callback) {
|
||||
res.status(400).json({ message: 'no callback found' })
|
||||
res.status(200).json({ message: 'no callback found' })
|
||||
return
|
||||
}
|
||||
callback()
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
isQuestUnlocked,
|
||||
} from '../../models/sessionManager.js'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
import config from '../../config.js'
|
||||
|
||||
function sessionSummary(session) {
|
||||
return {
|
||||
@@ -65,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)) {
|
||||
if ((await isQuestUnlocked(req.user, questId)) || config.testing.enabled) {
|
||||
const session = await getOrCreateActiveSession(req.user, questId)
|
||||
res.json(sessionDetail(session))
|
||||
} else {
|
||||
|
||||
@@ -2,6 +2,6 @@ import { check } from 'express-validator'
|
||||
import { noError } from '../../middleware/validator.js'
|
||||
|
||||
export const checkQuestId = () =>
|
||||
check('questId').notEmpty().isString().isAlphanumeric().isLength({ max: 64 })
|
||||
check('questId').notEmpty().isString().isLowercase().isLength({ max: 64 })
|
||||
|
||||
export const questIdValidator = [checkQuestId(), noError]
|
||||
@@ -25,7 +25,6 @@ const config = {
|
||||
db: get('MONGO_URL', 'mongodb://localhost:27017/odyssey-test'),
|
||||
secret: get('SECRET_KEY', ''),
|
||||
isProduction: process.env.NODE_ENV === 'production',
|
||||
hostPwd: get('HOST_PWD', ''),
|
||||
containerExpiry: get('EXPIRY', 1000 * 60 * 60),
|
||||
sessionMaxAge: get('SESSION_MAX_AGE', 1000 * 60 * 60 * 24 * 7),
|
||||
google: {
|
||||
@@ -45,6 +44,7 @@ const config = {
|
||||
defaultImage: get('QUEST_IMAGE', 'linuxodyssey/quest-base'),
|
||||
imagePrefix: get('DOCKER_PREFIX', 'linuxodyssey/quest-'),
|
||||
mountQuest: get('MOUNT_QUEST', ''),
|
||||
hostPwd: get('HOST_PWD', ''),
|
||||
},
|
||||
|
||||
log: {
|
||||
|
||||
@@ -24,10 +24,12 @@ export function createContainer(name, questId) {
|
||||
name,
|
||||
Image: getQuestImage(questId),
|
||||
}
|
||||
if (!config.isProduction && config.hostPwd && config.mountQuest) {
|
||||
option.HostConfig.Bind = [
|
||||
`${config.hostPwd}/quests/${config.mountQuest}/home:/home/commander`,
|
||||
`${config.hostPwd}/packages/container:/usr/local/lib/container`,
|
||||
const { hostPwd, mountQuest } = config.docker
|
||||
if (!config.isProduction && hostPwd && mountQuest) {
|
||||
logger.debug('Mounting quest folder', mountQuest)
|
||||
option.HostConfig.Binds = [
|
||||
`${hostPwd}/quests/${mountQuest}/home:/home/commander`,
|
||||
`${hostPwd}/packages/container:/usr/local/lib/container`,
|
||||
]
|
||||
}
|
||||
return engine.createContainer(option)
|
||||
@@ -75,7 +77,7 @@ export async function deleteContainer(id) {
|
||||
try {
|
||||
await container.stop()
|
||||
} catch (error) {
|
||||
logger.warn(error)
|
||||
logger.warn('Failed to stop container', id)
|
||||
}
|
||||
await container.remove()
|
||||
}
|
||||
@@ -83,7 +85,8 @@ export async function deleteContainer(id) {
|
||||
function parseJSONOutput(data) {
|
||||
const { stream, error } = JSON.parse(data.toString())
|
||||
if (stream) {
|
||||
logger.info(stream)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(stream)
|
||||
}
|
||||
if (error) {
|
||||
throw error
|
||||
|
||||
@@ -4,6 +4,28 @@ import config from '../config.js'
|
||||
import { deleteContainer } from './docker.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
async function disableSession(session) {
|
||||
try {
|
||||
await deleteContainer(session.containerId)
|
||||
} catch (err) {
|
||||
// If the container doesn't exist, we can ignore the error
|
||||
if (err.statusCode !== 404) {
|
||||
throw new Error(
|
||||
`Failed to delete container ${session.containerId} for session ${session._id}`
|
||||
)
|
||||
}
|
||||
}
|
||||
if (session.status === 'active') {
|
||||
session.status = 'inactive'
|
||||
}
|
||||
session.containerId = null
|
||||
try {
|
||||
await session.save()
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to save session ${session._id}\n${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeExpired() {
|
||||
// Find all sessions that are containerId is not null and lastActivityAt is
|
||||
// less than the current time minus the expiry time
|
||||
@@ -13,24 +35,14 @@ export async function removeExpired() {
|
||||
lastActivityAt: { $lt: new Date(Date.now() - config.containerExpiry) },
|
||||
})
|
||||
await Promise.all(
|
||||
sessions.map(async (session) => {
|
||||
logger.info('Expiring session: ', session._id, session.containerId)
|
||||
try {
|
||||
await deleteContainer(session.containerId)
|
||||
} catch (err) {
|
||||
if (err.statusCode !== 404) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
if (session.status === 'active') {
|
||||
session.status = 'inactive'
|
||||
}
|
||||
session.containerId = null
|
||||
await session.save()
|
||||
})
|
||||
sessions.map((session) =>
|
||||
disableSession(session).catch((err) => {
|
||||
throw new Error(`Failed to expire session: ${session._id}\n${err}`)
|
||||
})
|
||||
)
|
||||
)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
logger.error('Remove expiring failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,18 @@ export default class CommandHandler extends SessionHandler {
|
||||
return matches.some((m) => m === true)
|
||||
}
|
||||
|
||||
async checkException(stage) {
|
||||
const exceptions = stage.exceptions || []
|
||||
for (const exception of exceptions) {
|
||||
// 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()
|
||||
@@ -115,6 +127,13 @@ 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) {
|
||||
return this.executeException(exception)
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
const response = this.execute(stage)
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ async function deactivateSessions(user, quest) {
|
||||
return Promise.all(
|
||||
sessions.map((session) => {
|
||||
return deleteContainer(session.containerId)
|
||||
.catch(logger.error)
|
||||
.catch((err) => logger.error('Failed to delete container', err))
|
||||
.finally(() => {
|
||||
session.status = 'inactive'
|
||||
session.containerId = null
|
||||
@@ -111,6 +111,9 @@ export async function isQuestUnlocked(user, questId) {
|
||||
if (!quest) {
|
||||
throw new Error(`Quest ${questId} not found`)
|
||||
}
|
||||
if (!quest.requirements) {
|
||||
return true
|
||||
}
|
||||
return quest.requirements.every((requiredQuestId) => {
|
||||
const progress = userProfile.progress.get(requiredQuestId)
|
||||
return progress && progress.completed
|
||||
|
||||
@@ -23,7 +23,8 @@ async function mapQuests(callback) {
|
||||
)
|
||||
}
|
||||
|
||||
export function loadAndUpdateQuests() {
|
||||
export async function loadAndUpdateQuests() {
|
||||
await Quest.deleteMany({})
|
||||
return mapQuests(async (id, questPath) => {
|
||||
const files = await fs.readdir(questPath)
|
||||
if (!files.includes('game.yml')) {
|
||||
@@ -33,23 +34,17 @@ export function loadAndUpdateQuests() {
|
||||
try {
|
||||
const body = await fs.readFile(path.join(questPath, 'game.yml'), 'utf-8')
|
||||
|
||||
const quest = yaml.parse(body, {
|
||||
const questData = yaml.parse(body, {
|
||||
merge: true,
|
||||
})
|
||||
|
||||
const image = files.includes('Dockerfile') ? id : 'base'
|
||||
|
||||
return Quest.findByIdAndUpdate(
|
||||
id,
|
||||
{
|
||||
_id: id,
|
||||
image,
|
||||
...quest,
|
||||
},
|
||||
{
|
||||
upsert: true,
|
||||
}
|
||||
)
|
||||
const quest = new Quest({
|
||||
_id: id,
|
||||
image,
|
||||
...questData,
|
||||
})
|
||||
await quest.save()
|
||||
} catch (error) {
|
||||
logger.error(`Error parsing quest ${id}:`, error)
|
||||
throw error
|
||||
|
||||
@@ -74,6 +74,7 @@ describe('example helloworld app', () => {
|
||||
})
|
||||
})
|
||||
describe('Game Start Page UI', () => {
|
||||
const title = '終端機的基本知識'
|
||||
beforeEach(() => {
|
||||
cy.PrepareForGame()
|
||||
})
|
||||
@@ -83,23 +84,11 @@ describe('example helloworld app', () => {
|
||||
cy.findByRole('button', { name: 'Sign Out' }).should('be.visible')
|
||||
})
|
||||
it('Check QuestInfo', () => {
|
||||
cy.get('#topic').should('contain', 'Hello, Linux World!')
|
||||
cy.get('#quest').find('p.text-text').should('have.length', 3)
|
||||
cy.get('#topic').should('contain', title)
|
||||
cy.get('#quest').find('p.text-text').should('have.length', 4)
|
||||
cy.CheckTextElement('#tasks', '任務', 'Tasks:')
|
||||
cy.getQuestInfo('輸入 `echo help` 來朗誦咒語').should('be.visible')
|
||||
})
|
||||
it('Check Command Cheat Sheet', () => {
|
||||
cy.get('#cmdlist').within(($cmdlist) => {
|
||||
cy.get($cmdlist).should('be.visible')
|
||||
cy.get('svg[data-icon="list"]').should('be.visible')
|
||||
cy.CheckTextElement('#CommandBook', '指令之書', 'Command Cheatsheets')
|
||||
cy.CheckTextElement(
|
||||
'#cheatsheets',
|
||||
'-- --指令列表-- --',
|
||||
'--Command List--'
|
||||
)
|
||||
})
|
||||
})
|
||||
it('Check Terminal', () => {
|
||||
cy.get('#terminal', { timeout: 20000 }).within(($terminal) => {
|
||||
cy.get($terminal).should('be.visible')
|
||||
@@ -130,7 +119,6 @@ describe('example helloworld app', () => {
|
||||
describe('Game Play', () => {
|
||||
beforeEach(() => {
|
||||
cy.PrepareForGame()
|
||||
cy.readFile('../quests/helloworld/answer.sh', 'utf-8').as('answers')
|
||||
})
|
||||
it('Typing in Terminal', () => {
|
||||
cy.typeInCommand('12345{enter}')
|
||||
@@ -139,54 +127,41 @@ describe('example helloworld app', () => {
|
||||
.and('contain', 'zsh: command not found: 12345', { timeout: 50000 })
|
||||
})
|
||||
it('Complete the Game(relating UI)', () => {
|
||||
// get answersheet
|
||||
cy.get('@answers').then((answers) => {
|
||||
const answerarr = answers.split('\n')
|
||||
// Stage1
|
||||
cy.log('Stage1')
|
||||
cy.typeInCommand(answerarr[0])
|
||||
cy.checkPending()
|
||||
cy.waitUntilActive()
|
||||
cy.get('#Lbutton').should('be.visible').and('be.disabled')
|
||||
cy.get('#Rbutton').should('be.visible').and('be.disabled')
|
||||
cy.checkHint(1, 1)
|
||||
cy.getQuestInfo('✓ 輸入 `echo help` 來朗誦咒語').should('be.visible')
|
||||
cy.getQuestInfo('搜索卷軸').should('be.visible')
|
||||
// Stage2
|
||||
cy.log('Stage2')
|
||||
cy.typeInCommand(answerarr[1])
|
||||
cy.checkPending()
|
||||
cy.waitUntilActive()
|
||||
cy.get('#tree')
|
||||
.get('a')
|
||||
.should('contain', 'forgotten_scroll.txt')
|
||||
.and('be.visible')
|
||||
cy.get('#Lbutton').should('be.visible').and('be.enabled')
|
||||
cy.get('#Rbutton').should('be.visible').and('be.disabled')
|
||||
cy.checkHint(2, 2)
|
||||
cy.get('#Lbutton').click()
|
||||
cy.get('#Rbutton').should('be.enabled')
|
||||
cy.getQuestInfo('✓ 搜索卷軸').should('be.visible')
|
||||
cy.getQuestInfo('查看卷軸').should('be.visible')
|
||||
// Stage3
|
||||
cy.log('Stage3')
|
||||
cy.typeInCommand(answerarr[2])
|
||||
cy.checkPending()
|
||||
cy.waitUntilActive()
|
||||
cy.getQuestInfo('✓ 查看卷軸').should('be.visible')
|
||||
cy.getQuestInfo('解除封印').should('be.visible')
|
||||
cy.checkHint(3, 3)
|
||||
// Stage4
|
||||
cy.log('Stage4')
|
||||
cy.typeInCommand(answerarr[3])
|
||||
cy.checkPending()
|
||||
cy.waitUntilActive()
|
||||
cy.getQuestInfo('✓ 解除封印').should('be.visible')
|
||||
// Check survey dialog pop up
|
||||
cy.CheckTextElement('#QuestCompleted', '關卡完成!', 'Quest Completed!')
|
||||
cy.get('div[class="modal"]').find('p').should('be.visible')
|
||||
cy.get('#BacktoMap').should('contain', '回到地圖').and('be.visible')
|
||||
})
|
||||
// Stage1
|
||||
cy.log('Stage1')
|
||||
cy.typeInCommand('echo start{enter}')
|
||||
cy.checkPending()
|
||||
cy.waitUntilActive()
|
||||
cy.get('#Lbutton').should('be.visible').and('be.disabled')
|
||||
cy.get('#Rbutton').should('be.visible').and('be.disabled')
|
||||
cy.checkHint(1, 1)
|
||||
cy.getQuestInfo('✓ 輸入 `echo start` 來開始教程').should('be.visible')
|
||||
cy.getQuestInfo('➤ 在終端機輸入 `echo hello`').should('be.visible')
|
||||
// Stage2
|
||||
cy.log('Stage2')
|
||||
cy.typeInCommand('echo hello{enter}')
|
||||
cy.checkPending()
|
||||
cy.waitUntilActive()
|
||||
// cy.get('#tree')
|
||||
// .get('a')
|
||||
// .should('contain', 'forgotten_scroll.txt')
|
||||
// .and('be.visible')
|
||||
cy.get('#Lbutton').should('be.visible').and('be.enabled')
|
||||
cy.get('#Rbutton').should('be.visible').and('be.disabled')
|
||||
cy.checkHint(2, 2)
|
||||
cy.get('#Lbutton').click()
|
||||
cy.get('#Rbutton').should('be.enabled')
|
||||
cy.getQuestInfo('✓ 在終端機輸入 `echo hello`').should('be.visible')
|
||||
cy.getQuestInfo('➤ 結束關卡').should('be.visible')
|
||||
// Stage3
|
||||
cy.log('Stage3')
|
||||
cy.typeInCommand('echo finish{enter}')
|
||||
cy.checkPending()
|
||||
cy.waitUntilActive()
|
||||
cy.getQuestInfo('✓ 結束關卡').should('be.visible')
|
||||
cy.CheckTextElement('#QuestCompleted', '關卡完成!', 'Quest Completed!')
|
||||
cy.get('div[class="modal"]').find('p').should('be.visible')
|
||||
cy.findByRole('link', { name: '填寫問卷' }).should('be.visible')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -20,10 +20,9 @@ Cypress.Commands.add('PrepareForGame', () => {
|
||||
)
|
||||
// make sure the map page is loaded
|
||||
cy.url().should('include', '/map')
|
||||
cy.visit('/game/helloworld')
|
||||
// make sure the game page is loaded
|
||||
cy.url().should('include', '/game/helloworld')
|
||||
cy.get('.xterm-screen', { timeout: 50000 })
|
||||
cy.visit('/game/get-started')
|
||||
cy.url().should('include', '/game/get-started')
|
||||
cy.get('.xterm-screen', { timeout: 10000 })
|
||||
.as('Terminaltextbox')
|
||||
.should('be.visible')
|
||||
cy.CheckTextElement('#reset', '重來', 'Reset').click()
|
||||
|
||||
Reference in new issue
Block a user