mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-06 03:20:13 +08:00
Compare commits
141
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d0a210f0e | ||
|
|
c38104d043 | ||
|
|
83e563ab56 | ||
|
|
fa2059814c | ||
|
|
f9fa5a2102 | ||
|
|
8ed6acbcc1 | ||
|
|
d2452c91a6 | ||
|
|
5bac9ce099 | ||
|
|
2aaff0f7bc | ||
|
|
96ab39437b | ||
|
|
d6b606e807 | ||
|
|
b792992391 | ||
|
|
922100d8da | ||
|
|
52597e8c5e | ||
|
|
a6c160ac0e | ||
|
|
3859403b56 | ||
|
|
2a29b0b8ac | ||
|
|
aaa97e0dca | ||
|
|
3a28af0a67 | ||
|
|
551d502845 | ||
|
|
3f0191112d | ||
|
|
d565709a59 | ||
|
|
435a426f5b | ||
|
|
377afb3078 | ||
|
|
9d26951978 | ||
|
|
db3b00f8ee | ||
|
|
c09c4ff9e0 | ||
|
|
b3b374b3cd | ||
|
|
5ca187651d | ||
|
|
f2ca4a26ac | ||
|
|
462f17a779 | ||
|
|
3deabb25a9 | ||
|
|
3dad7ddccb | ||
|
|
25888a1465 | ||
|
|
3559814f55 | ||
|
|
8142ff816c | ||
|
|
9c160253c9 | ||
|
|
80e1a40b6e | ||
|
|
2b70f3c7cd | ||
|
|
2e29919ad9 | ||
|
|
ca79042a3b | ||
|
|
c8d2320bf5 | ||
|
|
441909efd5 | ||
|
|
90e3fc2bd0 | ||
|
|
598141d735 | ||
|
|
e11432be45 | ||
|
|
2c1fe7bb96 | ||
|
|
8c54bfe192 | ||
|
|
d292195302 | ||
|
|
6bbf70a258 | ||
|
|
526b856de7 | ||
|
|
1a20ffb295 | ||
|
|
4efa9b8831 | ||
|
|
3944e91d0c | ||
|
|
c30b4a7c6e | ||
|
|
14cd16b611 | ||
|
|
b07b60f6bd | ||
|
|
9d1bed7543 | ||
|
|
715352f77c | ||
|
|
c4550a395a | ||
|
|
5795cb16b8 | ||
|
|
691a6dcb25 | ||
|
|
979ea72f3a | ||
|
|
f9e40edb8a | ||
|
|
276bfee5ed | ||
|
|
f03c359f89 | ||
|
|
7db08a5cda | ||
|
|
b19f0da678 | ||
|
|
c87eef233c | ||
|
|
dabc40eb1e | ||
|
|
0efe2235f8 | ||
|
|
9d45993d0d | ||
|
|
99871fa299 | ||
|
|
248271355f | ||
|
|
79f9cb3e95 | ||
|
|
47c7ff04a9 | ||
|
|
f831f4d4e5 | ||
|
|
29799faa79 | ||
|
|
2727c9f4bb | ||
|
|
988cb65b51 | ||
|
|
4324932fc9 | ||
|
|
e9b49502ab | ||
|
|
d5519e7157 | ||
|
|
ed596c245d | ||
|
|
57c080d88b | ||
|
|
d795dbd788 | ||
|
|
218d433c14 | ||
|
|
48835f8830 | ||
|
|
9ed726d597 | ||
|
|
fcb52b9d9e | ||
|
|
dcd8f441f8 | ||
|
|
94ef3749f5 | ||
|
|
3398d18055 | ||
|
|
c64043316e | ||
|
|
7cdc542c19 | ||
|
|
78b9529699 | ||
|
|
8a31a58c56 | ||
|
|
103ce4774c | ||
|
|
4e6602d82f | ||
|
|
2eaf3c8f24 | ||
|
|
15465feda9 | ||
|
|
ea2ba2c7a0 | ||
|
|
0b136a830a | ||
|
|
3848ca6941 | ||
|
|
443e810471 | ||
|
|
198dcae3f8 | ||
|
|
7347c37a9e | ||
|
|
1d725d82ae | ||
|
|
b5c601f4a9 | ||
|
|
5069ac12da | ||
|
|
cec4d86058 | ||
|
|
93b0bae204 | ||
|
|
4713b11c95 | ||
|
|
36e6bc10d3 | ||
|
|
580d5fc3f8 | ||
|
|
d8392ec3f6 | ||
|
|
5f99f42f5f | ||
|
|
bdf757c2d4 | ||
|
|
453bfb7f1e | ||
|
|
3b470ed952 | ||
|
|
b4af2d46d3 | ||
|
|
0e96e30b96 | ||
|
|
3e3ac58c6b | ||
|
|
9f690548cf | ||
|
|
cd2ec6df75 | ||
|
|
8e7b5f2552 | ||
|
|
385c6bc110 | ||
|
|
092cf284dd | ||
|
|
7806852cab | ||
|
|
1e31cf3be5 | ||
|
|
aa903fbacf | ||
|
|
df980a7d56 | ||
|
|
980d0497f4 | ||
|
|
e37a23c34f | ||
|
|
f4fc6de4cc | ||
|
|
9bfc6614ca | ||
|
|
8c4a9ea871 | ||
|
|
ad29137b86 | ||
|
|
b5e49547ea | ||
|
|
9a4f9b958a | ||
|
|
29602785cd |
No files matched your search
@@ -35,23 +35,3 @@ jobs:
|
|||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: yarn test
|
run: yarn test
|
||||||
|
|
||||||
e2e:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Build Test Environment, Stage 1
|
|
||||||
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
|
|
||||||
|
|
||||||
- name: Build Test Quests
|
|
||||||
run: docker-compose -f docker-compose.testing.yml run --rm backend yarn build:quests
|
|
||||||
|
|
||||||
- name: Run Cypress Tests
|
|
||||||
env:
|
|
||||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
|
||||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
|
||||||
GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
|
|
||||||
GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
|
|
||||||
run: docker-compose -f docker-compose.testing.yml run cypress
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Build Test Environment, Stage 1
|
||||||
|
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
|
||||||
|
|
||||||
|
- name: Build Test Quests
|
||||||
|
run: docker-compose -f docker-compose.testing.yml run --rm backend yarn build:quests
|
||||||
|
|
||||||
|
- name: Run Cypress Tests
|
||||||
|
env:
|
||||||
|
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||||
|
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||||
|
GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
|
||||||
|
GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
|
||||||
|
run: docker-compose -f docker-compose.testing.yml run cypress
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
Run mongoDB first:
|
Run mongoDB first:
|
||||||
|
|
||||||
docker compose -f docker-compose.dev.yml up -d db
|
docker compose up -d
|
||||||
|
|
||||||
The db will automatically restarts by Docker.
|
The db will automatically restarts by Docker.
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -6,6 +6,7 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"dev:docker": "API_TARGET=http://backend:3000 vite",
|
"dev:docker": "API_TARGET=http://backend:3000 vite",
|
||||||
"build": "vue-tsc -b && vite build",
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"type-check": "vue-tsc -b",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -16,13 +17,14 @@
|
|||||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||||
"@linux-odyssey/constants": "*",
|
"@linux-odyssey/constants": "*",
|
||||||
"@linux-odyssey/file-graph": "*",
|
"@linux-odyssey/file-graph": "*",
|
||||||
"@linux-odyssey/utils": "*",
|
|
||||||
"@linux-odyssey/models": "*",
|
"@linux-odyssey/models": "*",
|
||||||
|
"@linux-odyssey/utils": "*",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"dompurify": "^3.0.6",
|
"dompurify": "^3.0.6",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
"marked": "^10.0.0",
|
"marked": "^10.0.0",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
"socket.io-client": "^4.7.1",
|
"socket.io-client": "^4.7.1",
|
||||||
"validator": "^13.11.0",
|
"validator": "^13.11.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
|
|||||||
@@ -83,6 +83,16 @@ const handleChange = () => {
|
|||||||
errorMessage.value = 'Something went wrong'
|
errorMessage.value = 'Something went wrong'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const registerGuest = async () => {
|
||||||
|
try {
|
||||||
|
await api.post('/auth/register-guest')
|
||||||
|
router.push({ name: 'map' })
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toast.error('Something went wrong.')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<!-- login page -->
|
<!-- login page -->
|
||||||
@@ -219,6 +229,13 @@ const handleChange = () => {
|
|||||||
</RouterLink></span
|
</RouterLink></span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
<button
|
||||||
|
id="register-guest"
|
||||||
|
class="rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
|
||||||
|
@click="registerGuest()"
|
||||||
|
>
|
||||||
|
以訪客身分繼續
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { defineProps, onMounted } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { useToast } from 'vue-toastification'
|
|
||||||
import { logout } from '../utils/auth'
|
|
||||||
import { bugReportUrl, surveyUrl } from '../config'
|
|
||||||
import { reset } from '../store/session'
|
|
||||||
import userProfileStore, {
|
|
||||||
loadUserProfile,
|
|
||||||
resetUserProfile,
|
|
||||||
} from '../store/userProfile'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
try {
|
|
||||||
await logout()
|
|
||||||
resetUserProfile()
|
|
||||||
reset()
|
|
||||||
router.push({ name: 'login' })
|
|
||||||
} catch (err) {
|
|
||||||
toast.error('登出失敗')
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps({
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
await loadUserProfile()
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
toast.error('無法讀取使用者資料')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div id="header" class="bg-bg-secondary content-center h-full w-full p-1">
|
|
||||||
<div class="h-full flex items-center justify-center mx-3">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<!-- <button id="stream" class="flex items-center h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'bars']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button id="backward" class="flex items-center h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'arrow-left']"
|
|
||||||
class="text-border h-full w-full"
|
|
||||||
/>
|
|
||||||
</button> -->
|
|
||||||
<img src="../img/icon_totem.svg" class="h-5" />
|
|
||||||
<button
|
|
||||||
id="GameTitle"
|
|
||||||
class="text-text-primary inline-block font-bold whitespace-nowrap"
|
|
||||||
style="font-size: 2vh"
|
|
||||||
>
|
|
||||||
Linux Odyssey
|
|
||||||
</button>
|
|
||||||
<p
|
|
||||||
id="QuestTitle"
|
|
||||||
class="text-text inline-block font-bold whitespace-nowrap"
|
|
||||||
style="font-size: 2vh"
|
|
||||||
>
|
|
||||||
{{ title }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="h-full w-full flex gap-3 items-center justify-end">
|
|
||||||
<!-- <button id="warning" class="flex items-center h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'triangle-exclamation']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button id="setting" class="flex items-center h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'gear']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button id="fullscreen" class="flex items-center h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'expand']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</button> -->
|
|
||||||
<p
|
|
||||||
id="UsernameText"
|
|
||||||
class="text-text inline-block whitespace-nowrap"
|
|
||||||
style="font-size: 2vh"
|
|
||||||
>
|
|
||||||
{{ userProfileStore.username }}
|
|
||||||
</p>
|
|
||||||
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'file-invoice']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<RouterLink title="LeaderBoard" to="/leaderboard" class="h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'trophy']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
<RouterLink title="Map" to="/map" class="h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'map']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
|
||||||
<a
|
|
||||||
title="Bug Report"
|
|
||||||
:href="bugReportUrl"
|
|
||||||
target="_blank"
|
|
||||||
class="h-5 w-5"
|
|
||||||
>
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'bug']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<button title="Sign Out" @click="handleLogout" class="h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'arrow-right-from-bracket']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -25,6 +25,9 @@ const html = computed(() => {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(p) {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
.markdown-content :deep(ol) {
|
.markdown-content :deep(ol) {
|
||||||
list-style: decimal;
|
list-style: decimal;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import {
|
|||||||
import { DAG } from '@linux-odyssey/utils'
|
import { DAG } from '@linux-odyssey/utils'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import { NodeImage } from '../img/svg.js'
|
import { NodeImage } from '../img/svg.js'
|
||||||
|
import useUserProfile from '../store/userProfile'
|
||||||
|
|
||||||
|
const store = useUserProfile()
|
||||||
const marginX = 500
|
const marginX = 500
|
||||||
const marginY = 300
|
const marginY = 300
|
||||||
const chartContainer = ref(null)
|
const chartContainer = ref(null)
|
||||||
@@ -40,16 +42,6 @@ async function getQuests() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getProgress() {
|
|
||||||
try {
|
|
||||||
const res = await api.get('/users/me')
|
|
||||||
return res.data.progress
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const genOption = (nodes: any, edges: any) => ({
|
const genOption = (nodes: any, edges: any) => ({
|
||||||
renderer: 'svg',
|
renderer: 'svg',
|
||||||
tooltip: {},
|
tooltip: {},
|
||||||
@@ -162,9 +154,9 @@ function initChart(option: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
await store.loadUserProfile()
|
||||||
const quests = await getQuests()
|
const quests = await getQuests()
|
||||||
const progress = await getProgress()
|
const option = getOption(quests, store.progress)
|
||||||
const option = getOption(quests, progress)
|
|
||||||
initChart(option)
|
initChart(option)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -177,7 +169,7 @@ onMounted(async () => {
|
|||||||
class="p-2 stroke-2 scale-y-120 justify-center items-center"
|
class="p-2 stroke-2 scale-y-120 justify-center items-center"
|
||||||
/>
|
/>
|
||||||
<h1
|
<h1
|
||||||
class="p-4 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
|
class="p-10 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
|
||||||
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
|
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
|
||||||
>
|
>
|
||||||
踏上你的Linux冒險之旅吧!
|
踏上你的Linux冒險之旅吧!
|
||||||
|
|||||||
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
|
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
|
||||||
<h1 id="CommandBook" class="inline text-text-primary font-xl">指令之書</h1>
|
<h1 id="CommandBook" class="inline text-text-primary font-xl">指令之書</h1>
|
||||||
<div id="cheatsheets">
|
<div id="cheatsheets">
|
||||||
<p class="text-text-secondary">--指令列表--</p>
|
<p class="text-text-secondary">-- --指令列表-- --</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -4,7 +4,7 @@ import { useToast } from 'vue-toastification'
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { Vue3Lottie } from 'vue3-lottie'
|
import { Vue3Lottie } from 'vue3-lottie'
|
||||||
// import { surveyUrl } from '../config'
|
// import { surveyUrl } from '../config'
|
||||||
import CompleteAnimation from '../img/complete-animation.json'
|
import CompleteAnimation from '../../img/complete-animation.json'
|
||||||
|
|
||||||
const showModal = ref(true) // Controls whether the modal is visible or not
|
const showModal = ref(true) // Controls whether the modal is visible or not
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -26,7 +26,6 @@ const backtoMap = async () => {
|
|||||||
try {
|
try {
|
||||||
router.push({ name: 'map' })
|
router.push({ name: 'map' })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// useToast().error('Failed to load map')
|
|
||||||
toast.error('無法讀取地圖')
|
toast.error('無法讀取地圖')
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@@ -60,17 +59,6 @@ setTimeout(() => {
|
|||||||
<p class="text-base text-text text-center">
|
<p class="text-base text-text text-center">
|
||||||
接下來回到地圖,迎接新的挑戰吧!
|
接下來回到地圖,迎接新的挑戰吧!
|
||||||
</p>
|
</p>
|
||||||
<!-- <p class="text-base text-text">
|
|
||||||
您已成功完成挑戰,真的非常感謝您的參與!<br />
|
|
||||||
如果您能撥冗填寫這份問卷,我們將不勝感激,這對我們非常重要!
|
|
||||||
</p> -->
|
|
||||||
<!-- <a
|
|
||||||
id="SurveyButton"
|
|
||||||
:href="surveyUrl"
|
|
||||||
target="_blank"
|
|
||||||
class="inline-flex justify-center rounded-lg text-base font-black py-2 mt-3 bg-text-primary w-full"
|
|
||||||
>填寫問卷</a
|
|
||||||
> -->
|
|
||||||
<a
|
<a
|
||||||
id="BacktoMap"
|
id="BacktoMap"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
+15
-12
@@ -1,13 +1,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { createSession } from '../store/session'
|
import useSession from '../../store/session'
|
||||||
import { surveyUrl } from '../config'
|
import { surveyUrl } from '../../config'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const sessionStore = useSession()
|
||||||
const reset = async () => {
|
const reset = async () => {
|
||||||
console.log('resetting...')
|
console.log('resetting...')
|
||||||
await createSession()
|
await sessionStore.createSession()
|
||||||
}
|
}
|
||||||
const continuePlay = async () => {
|
const continuePlay = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -20,29 +21,31 @@ const continuePlay = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="h-[10%] p-3 grid grid-cols-4 gap-4 place-content-between">
|
<section
|
||||||
|
class="h-fit w-fit px-1 py-1 grid grid-cols-4 gap-2 place-content-between"
|
||||||
|
>
|
||||||
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
|
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
|
||||||
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
|
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
:icon="['fas', 'file-invoice']"
|
:icon="['fas', 'file-invoice']"
|
||||||
class="text-text-primary"
|
class="text-text-primary px-1"
|
||||||
/>
|
/>
|
||||||
<p class="text-text-primary inline ml-1">問卷</p>
|
<p class="text-text-primary inline ml-1 md:flex">問卷</p>
|
||||||
</a>
|
</a>
|
||||||
</button>
|
</button>
|
||||||
<button disabled 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
|
<font-awesome-icon
|
||||||
:icon="['far', 'circle-question']"
|
:icon="['far', 'circle-question']"
|
||||||
class="text-text-disabled"
|
class="text-text-disabled px-1"
|
||||||
/>
|
/>
|
||||||
<p class="text-text-disabled inline ml-1">解答</p>
|
<p class="text-text-disabled inline ml-1 md:flex">解答</p>
|
||||||
</button>
|
</button>
|
||||||
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
:icon="['fas', 'arrow-rotate-left']"
|
:icon="['fas', 'arrow-rotate-left']"
|
||||||
class="text-text-primary"
|
class="text-text-primary px-1"
|
||||||
/>
|
/>
|
||||||
<p class="text-text-primary inline ml-1">重來</p>
|
<p class="text-text-primary inline ml-1 hidden md:flex">重來</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
id="continue"
|
id="continue"
|
||||||
@@ -51,9 +54,9 @@ const continuePlay = async () => {
|
|||||||
>
|
>
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
:icon="['far', 'circle-right']"
|
:icon="['far', 'circle-right']"
|
||||||
class="text-text-primary"
|
class="text-text-primary px-1"
|
||||||
/>
|
/>
|
||||||
<p class="text-text-primary inline ml-1">繼續</p>
|
<p class="text-text-primary inline ml-1 hidden md:flex">繼續</p>
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import sessionStore from '../store/session'
|
import useSession from '../../store/session'
|
||||||
import MarkdownText from './MarkdownText.vue'
|
import MarkdownText from '../MarkdownText.vue'
|
||||||
|
|
||||||
|
const sessionStore = useSession()
|
||||||
const current = ref(-1)
|
const current = ref(-1)
|
||||||
watch(
|
const length = computed(() => {
|
||||||
() => sessionStore.session.hints,
|
return sessionStore.session?.responses.length ?? 0
|
||||||
(hints) => {
|
})
|
||||||
current.value = hints.length - 1
|
|
||||||
},
|
const disabled = computed(() => {
|
||||||
{ deep: true }
|
return current.value === length.value - 1
|
||||||
)
|
})
|
||||||
|
|
||||||
|
watch(length, () => {
|
||||||
|
current.value = length.value - 1
|
||||||
|
})
|
||||||
|
|
||||||
const left = () => {
|
const left = () => {
|
||||||
if (current.value > 0) {
|
if (current.value > 0) {
|
||||||
@@ -19,7 +24,7 @@ const left = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const right = () => {
|
const right = () => {
|
||||||
if (current.value < sessionStore.session.hints.length - 1) {
|
if (current.value < length.value - 1) {
|
||||||
current.value += 1
|
current.value += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,16 +32,12 @@ const right = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full flex flex-col">
|
<div class="h-full flex flex-col">
|
||||||
<!-- Hint Header -->
|
|
||||||
<div class="flex items-center p-2 h-10">
|
<div class="flex items-center p-2 h-10">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
:icon="['far', 'lightbulb']"
|
:icon="['far', 'lightbulb']"
|
||||||
class="text-yellow-200 p-2 content-center"
|
class="text-yellow-200 p-2 content-center"
|
||||||
/>
|
/>
|
||||||
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">
|
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">說明</h1>
|
||||||
<!-- Hint -->
|
|
||||||
提示
|
|
||||||
</h1>
|
|
||||||
<div v-if="current !== -1" class="flex w-full justify-end items-end">
|
<div v-if="current !== -1" class="flex w-full justify-end items-end">
|
||||||
<button
|
<button
|
||||||
class="p-2 m-1 w-1/8"
|
class="p-2 m-1 w-1/8"
|
||||||
@@ -53,13 +54,13 @@ const right = () => {
|
|||||||
<p class="inline text-text font-xl w-1/14 p-2 m-1">{{ current + 1 }}</p>
|
<p class="inline text-text font-xl w-1/14 p-2 m-1">{{ current + 1 }}</p>
|
||||||
<p class="inline text-text font-xl w-1/14 p-2 m-1">/</p>
|
<p class="inline text-text font-xl w-1/14 p-2 m-1">/</p>
|
||||||
<p class="inline text-text font-xl w-1/14 p-2 m-1">
|
<p class="inline text-text font-xl w-1/14 p-2 m-1">
|
||||||
{{ sessionStore.session.hints.length }}
|
{{ length }}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
class="p-2 m-1 w-1/8"
|
class="p-2 m-1 w-1/8"
|
||||||
id="Rbutton"
|
id="Rbutton"
|
||||||
@click="right"
|
@click="right"
|
||||||
:disabled="current === sessionStore.session.hints.length - 1"
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
<font-awesome-icon :icon="['fas', 'arrow-right']" class="text-text" />
|
<font-awesome-icon :icon="['fas', 'arrow-right']" class="text-text" />
|
||||||
</button>
|
</button>
|
||||||
@@ -68,7 +69,7 @@ const right = () => {
|
|||||||
<div id="hint" class="bg-bg p-8 overflow-y-auto">
|
<div id="hint" class="bg-bg p-8 overflow-y-auto">
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="response in sessionStore.session.responses[current]"
|
v-for="response in sessionStore.session?.responses[current]"
|
||||||
:key="response.type"
|
:key="response.type"
|
||||||
class="text-text font-xl whitespace-pre-wrap"
|
class="text-text font-xl whitespace-pre-wrap"
|
||||||
>
|
>
|
||||||
@@ -77,7 +78,7 @@ const right = () => {
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-for="hint in sessionStore.session.hints[current]"
|
v-for="hint in sessionStore.session?.hints[current]"
|
||||||
:key="hint"
|
:key="hint"
|
||||||
class="text-text-primary font-xl whitespace-pre-wrap"
|
class="text-text-primary font-xl whitespace-pre-wrap"
|
||||||
>
|
>
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import MarkdownText from './MarkdownText.vue'
|
import MarkdownText from '../MarkdownText.vue'
|
||||||
import sessionStore from '../store/session'
|
import useSession from '../../store/session'
|
||||||
import { Task } from '../types'
|
import { Task } from '../../types'
|
||||||
|
|
||||||
|
const sessionStore = useSession()
|
||||||
const tasks = computed(() => {
|
const tasks = computed(() => {
|
||||||
return sessionStore.session.tasks.map((task: Task) => {
|
return sessionStore.session?.tasks.map((task: Task) => {
|
||||||
const prefix = task.completed ? '✓' : '➤'
|
const prefix = task.completed ? '✓' : '➤'
|
||||||
const color = task.completed ? 'text-text-primary' : 'text-text'
|
const color = task.completed ? 'text-text-primary' : 'text-text'
|
||||||
return {
|
return {
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useToast } from 'vue-toastification'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import useSession from '../../store/session'
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const sessionStore = useSession()
|
||||||
|
|
||||||
|
const showCover = computed(() => sessionStore.session === null)
|
||||||
|
|
||||||
|
const startSession = async () => {
|
||||||
|
try {
|
||||||
|
await sessionStore.createSession()
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err)
|
||||||
|
toast.error(err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="showCover"
|
||||||
|
class="bg-bg-secondary h-full w-1/2 fixed bg-opacity-50 flex justify-center items-center"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
id="start"
|
||||||
|
class="bg-bg-secondary rounded-lg p-3"
|
||||||
|
@click="startSession"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'play']"
|
||||||
|
class="text-text-primary h-full px-3"
|
||||||
|
/>
|
||||||
|
<p class="text-text-primary inline ml-1 md:flex">開始</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { useTerminal } from '../store/session'
|
import { useTerminal } from '../../store/session'
|
||||||
|
|
||||||
const terminal = ref<HTMLElement | null>(null)
|
const terminal = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import FileNode from './FileNode.vue'
|
import FileNode from '../FileNode.vue'
|
||||||
import sessionStore from '../store/session'
|
import useSession from '../../store/session'
|
||||||
|
|
||||||
|
const sessionStore = useSession()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -8,6 +10,7 @@ import sessionStore from '../store/session'
|
|||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<FileNode
|
<FileNode
|
||||||
|
v-if="sessionStore.session"
|
||||||
:node="sessionStore.session.graph"
|
:node="sessionStore.session.graph"
|
||||||
:pwd="sessionStore.session.pwd"
|
:pwd="sessionStore.session.pwd"
|
||||||
/>
|
/>
|
||||||
File renamed without changes.
@@ -0,0 +1,107 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useToast } from 'vue-toastification'
|
||||||
|
import { logout } from '../../utils/auth'
|
||||||
|
import { bugReportUrl, surveyUrl } from '../../config'
|
||||||
|
import useSession from '../../store/session'
|
||||||
|
import useUserProfile from '../../store/userProfile'
|
||||||
|
|
||||||
|
const userStore = useUserProfile()
|
||||||
|
const sessionStore = useSession()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
await logout()
|
||||||
|
userStore.resetUserProfile()
|
||||||
|
sessionStore.reset()
|
||||||
|
router.push({ name: 'login' })
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('登出失敗')
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await userStore.loadUserProfile()
|
||||||
|
} catch (err) {
|
||||||
|
toast.error('無法讀取使用者資料')
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<!-- <button id="warning" class="flex items-center h-5 w-5">
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'triangle-exclamation']"
|
||||||
|
class="text-text-primary h-full w-full"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button id="setting" class="flex items-center h-5 w-5">
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'gear']"
|
||||||
|
class="text-text-primary h-full w-full"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button id="fullscreen" class="flex items-center h-5 w-5">
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'expand']"
|
||||||
|
class="text-text-primary h-full w-full"
|
||||||
|
/>
|
||||||
|
</button> -->
|
||||||
|
<p
|
||||||
|
id="UsernameText"
|
||||||
|
class="text-text inline-block whitespace-nowrap px-1.5"
|
||||||
|
style="font-size: 2vh"
|
||||||
|
>
|
||||||
|
{{ userStore.username }}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
title="Survey"
|
||||||
|
:href="surveyUrl"
|
||||||
|
target="_blank"
|
||||||
|
class="md:h-5 md:w-5 px-1.5 w-auto"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'file-invoice']"
|
||||||
|
class="text-text-primary inline"
|
||||||
|
/>
|
||||||
|
<span class="text-text sm:hidden px-2">Survey</span>
|
||||||
|
</a>
|
||||||
|
<RouterLink
|
||||||
|
title="LeaderBoard"
|
||||||
|
to="/leaderboard"
|
||||||
|
class="md:h-5 md:w-5 px-1.5 w-auto"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="['fas', 'trophy']" class="text-text-primary" />
|
||||||
|
<span class="text-text sm:hidden px-2">Leader Board</span>
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink title="Map" to="/map" class="md:h-5 md:w-5 px-1.5 w-auto">
|
||||||
|
<font-awesome-icon :icon="['fas', 'map']" class="text-text-primary" />
|
||||||
|
<span class="text-text sm:hidden px-2">Map</span>
|
||||||
|
</RouterLink>
|
||||||
|
<a
|
||||||
|
title="Bug Report"
|
||||||
|
:href="bugReportUrl"
|
||||||
|
target="_blank"
|
||||||
|
class="md:h-5 md:w-5 px-1.5 w-auto"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
|
||||||
|
<span class="text-text sm:hidden px-2">Bug Report</span>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
title="Sign Out"
|
||||||
|
@click="handleLogout"
|
||||||
|
class="md:h-5 md:w-5 px-1.5 w-auto"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'arrow-right-from-bracket']"
|
||||||
|
class="text-text-primary"
|
||||||
|
/>
|
||||||
|
<span class="text-text sm:hidden px-2">Sign Out</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, ref } from 'vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
headerComponent: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const menuOpen = ref(false)
|
||||||
|
const windowWidth = window.innerWidth < 768
|
||||||
|
const toggleMenu = () => {
|
||||||
|
menuOpen.value = !menuOpen.value
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="w-full top-0 px-5 fixed flex flex-row flex-wrap bg-bg-secondary justify-between items-center sm:justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row items-center gap-3" :class="{ hidden: menuOpen }">
|
||||||
|
<img src="../../img/icon_totem.svg" class="h-8 justify-self-start" />
|
||||||
|
<RouterLink
|
||||||
|
to="/"
|
||||||
|
id="GameTitle"
|
||||||
|
class="text-text-primary inline-block font-bold whitespace-nowrap pt-1 transition ease-in-out hover:scale-110 duration-300"
|
||||||
|
style="font-size: 2vh"
|
||||||
|
>
|
||||||
|
Linux Odyssey
|
||||||
|
</RouterLink>
|
||||||
|
<p
|
||||||
|
id="QuestTitle"
|
||||||
|
class="text-text inline-block font-bold whitespace-nowrap"
|
||||||
|
style="font-size: 2vh"
|
||||||
|
>
|
||||||
|
{{ title }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-full flex flex-col items-center justify-end sm:w-auto sm:block sm:ml-6"
|
||||||
|
:class="{ hidden: !menuOpen }"
|
||||||
|
>
|
||||||
|
<component :is="headerComponent" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="windowWidth"
|
||||||
|
:class="{ 'w-full flex justify-end pb-3': menuOpen }"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center h-5 w-5 rounded-md sm:hidden"
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
aria-expanded="false"
|
||||||
|
@click="toggleMenu"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'bars']"
|
||||||
|
class="text-text-primary h-full w-full"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { bugReportUrl } from '../../config'
|
||||||
|
|
||||||
|
const scrollToSection = (sectionId: string) => {
|
||||||
|
const section = document.getElementById(sectionId)
|
||||||
|
if (section) {
|
||||||
|
const container = document.querySelector('.overflow-y-auto') as HTMLElement // Replace '.overflow-y-auto' with the appropriate class or selector for your container
|
||||||
|
if (container) {
|
||||||
|
container.scrollTo({
|
||||||
|
top: section.offsetTop - container.offsetTop,
|
||||||
|
behavior: 'smooth', // Add smooth scrolling behavior if desired
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
id="Intro"
|
||||||
|
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||||
|
@click="scrollToSection('intro')"
|
||||||
|
style="font-size: 2vh"
|
||||||
|
>
|
||||||
|
前言
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="Feature"
|
||||||
|
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||||
|
@click="scrollToSection('feature')"
|
||||||
|
style="font-size: 2vh"
|
||||||
|
>
|
||||||
|
特色</button
|
||||||
|
><button
|
||||||
|
id="Info"
|
||||||
|
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||||
|
@click="scrollToSection('about')"
|
||||||
|
style="font-size: 2vh"
|
||||||
|
>
|
||||||
|
關於
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="Contact"
|
||||||
|
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||||
|
@click="scrollToSection('contact')"
|
||||||
|
style="font-size: 2vh"
|
||||||
|
>
|
||||||
|
聯絡我們
|
||||||
|
</button>
|
||||||
|
<RouterLink
|
||||||
|
title="Sign In"
|
||||||
|
id="SignIn"
|
||||||
|
to="/register"
|
||||||
|
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||||
|
style="font-size: 2vh"
|
||||||
|
>
|
||||||
|
登入
|
||||||
|
</RouterLink>
|
||||||
|
<a title="Bug Report" :href="bugReportUrl" target="_blank" class="h-5 w-5">
|
||||||
|
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
+3
-1
@@ -19,6 +19,7 @@ import {
|
|||||||
faTrophy,
|
faTrophy,
|
||||||
faFileInvoice,
|
faFileInvoice,
|
||||||
faEnvelope,
|
faEnvelope,
|
||||||
|
faPlay,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import {
|
import {
|
||||||
faLightbulb,
|
faLightbulb,
|
||||||
@@ -54,5 +55,6 @@ library.add(
|
|||||||
faGoogle,
|
faGoogle,
|
||||||
faFileInvoice,
|
faFileInvoice,
|
||||||
faFacebook,
|
faFacebook,
|
||||||
faEnvelope
|
faEnvelope,
|
||||||
|
faPlay
|
||||||
)
|
)
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
import './style.css'
|
import './style.css'
|
||||||
/* import font awesome icon component */
|
/* import font awesome icon component */
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
@@ -13,9 +15,11 @@ import App from './App.vue'
|
|||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
app.component('font-awesome-icon', FontAwesomeIcon)
|
app.component('font-awesome-icon', FontAwesomeIcon)
|
||||||
|
|
||||||
|
app.use(pinia)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
|
|||||||
+80
-138
@@ -1,156 +1,98 @@
|
|||||||
import { reactive } from 'vue'
|
import { defineStore } from 'pinia'
|
||||||
import { useToast } from 'vue-toastification'
|
|
||||||
import { FileGraph, FileObject } from '@linux-odyssey/file-graph'
|
import { FileGraph, FileObject } from '@linux-odyssey/file-graph'
|
||||||
import { IQuest } from '@linux-odyssey/models'
|
import { IQuest } from '@linux-odyssey/models'
|
||||||
import api from '../utils/api'
|
import { createSession, getActiveSession, getQuest } from '../utils/api'
|
||||||
import Socket from '../utils/socket'
|
import Socket from '../utils/socket'
|
||||||
import SocketTerminal from '../utils/terminal'
|
import SocketTerminal from '../utils/terminal'
|
||||||
import { LoadQuestError, LoadSessionError } from '../utils/errors'
|
|
||||||
import { Session, StageResponse } from '../types'
|
import { Session, StageResponse } from '../types'
|
||||||
|
|
||||||
const toast = useToast()
|
const socket = new Socket()
|
||||||
|
const term = new SocketTerminal(40, 80)
|
||||||
function newSession() {
|
let hasSetup = false
|
||||||
const graph = new FileGraph({
|
|
||||||
path: '/',
|
|
||||||
type: 'folder',
|
|
||||||
discovered: false,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
_id: '',
|
|
||||||
status: 'inactive',
|
|
||||||
graph,
|
|
||||||
pwd: '',
|
|
||||||
hints: [],
|
|
||||||
tasks: [],
|
|
||||||
responses: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Store {
|
interface Store {
|
||||||
session: Session
|
session: Session | null
|
||||||
questId: string
|
questId: string
|
||||||
quest: IQuest | null
|
quest: IQuest | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = reactive<Store>({
|
const useSession = defineStore('session', {
|
||||||
session: newSession(),
|
state: (): Store => ({
|
||||||
questId: '',
|
session: null,
|
||||||
quest: null,
|
questId: '',
|
||||||
|
quest: null,
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async setQuest(questId: string) {
|
||||||
|
this.quest = await getQuest(questId)
|
||||||
|
this.questId = questId
|
||||||
|
},
|
||||||
|
async setSession(session: Session) {
|
||||||
|
this.session = session
|
||||||
|
this.session.graph = new FileGraph(session.graph)
|
||||||
|
term.reset()
|
||||||
|
await socket.connect(session)
|
||||||
|
term.focus()
|
||||||
|
},
|
||||||
|
async createSession() {
|
||||||
|
const session = await createSession(this.questId)
|
||||||
|
await this.setSession(session)
|
||||||
|
socket.emit('terminal', 'echo start\n')
|
||||||
|
},
|
||||||
|
async getActiveSession() {
|
||||||
|
const session = await getActiveSession(this.questId)
|
||||||
|
if (session) {
|
||||||
|
this.setSession(session)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateGraph(event: { discover?: FileObject[]; pwd?: string }) {
|
||||||
|
if (!this.session) return
|
||||||
|
if (event.discover) {
|
||||||
|
this.session.graph.discover(event.discover)
|
||||||
|
}
|
||||||
|
if (event.pwd) {
|
||||||
|
this.session.pwd = event.pwd
|
||||||
|
}
|
||||||
|
},
|
||||||
|
newResponse(response: StageResponse) {
|
||||||
|
if (!this.session) return
|
||||||
|
this.session.responses.push(response.responses)
|
||||||
|
this.session.hints.push(response.hints)
|
||||||
|
if (response.tasks) {
|
||||||
|
this.session.tasks = response.tasks
|
||||||
|
}
|
||||||
|
this.session.status = response.status
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
socket.reset()
|
||||||
|
term.reset()
|
||||||
|
this.$reset()
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
if (hasSetup) return
|
||||||
|
socket.on('terminal', (data: string) => {
|
||||||
|
term.write(data)
|
||||||
|
})
|
||||||
|
term.onData((data: string) => {
|
||||||
|
socket.emit('terminal', data)
|
||||||
|
})
|
||||||
|
socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
|
||||||
|
this.updateGraph(event)
|
||||||
|
})
|
||||||
|
socket.on('response', (response: StageResponse) => {
|
||||||
|
this.newResponse(response)
|
||||||
|
})
|
||||||
|
hasSetup = true
|
||||||
|
},
|
||||||
|
setStatus(status: string) {
|
||||||
|
if (!this.session) return
|
||||||
|
this.session.status = status
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const socket = new Socket()
|
|
||||||
const term = new SocketTerminal(40, 80)
|
|
||||||
|
|
||||||
function setQuest(questId: string) {
|
|
||||||
return api.get(`/quests/${questId}`).then((res) => {
|
|
||||||
store.questId = res.data._id
|
|
||||||
store.quest = res.data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setSession(session: Session) {
|
|
||||||
try {
|
|
||||||
console.log('Setting session...', session)
|
|
||||||
store.session = session
|
|
||||||
store.session.graph = new FileGraph(session.graph)
|
|
||||||
term.reset()
|
|
||||||
await socket.connect(session)
|
|
||||||
term.focus()
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(err)
|
|
||||||
if (err instanceof Error) {
|
|
||||||
toast.error(err.message)
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to connect to the session. Please try again later.')
|
|
||||||
}
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createSession() {
|
|
||||||
try {
|
|
||||||
console.log('Creating a new session...')
|
|
||||||
const res = await api.post('/sessions', { questId: store.questId })
|
|
||||||
const { data } = res
|
|
||||||
await setSession(data)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
toast.error('Failed to create a new session. Please try again later.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getActiveSession(questId: string) {
|
|
||||||
try {
|
|
||||||
const res = await api.post('/sessions/active', { questId })
|
|
||||||
await setSession(res.data)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
toast.warning('Failed to connect previous session. Creating a new one...')
|
|
||||||
await createSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateGraph(event: { discover: FileObject[]; pwd: string }) {
|
|
||||||
if (event.discover) {
|
|
||||||
store.session.graph.discover(event.discover)
|
|
||||||
}
|
|
||||||
if (event.pwd) {
|
|
||||||
store.session.pwd = event.pwd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function newResponse(response: StageResponse) {
|
|
||||||
console.log('newResponse', response, 'session', store.session)
|
|
||||||
store.session.responses.push(response.responses)
|
|
||||||
store.session.hints.push(response.hints)
|
|
||||||
store.session.tasks = response.tasks
|
|
||||||
store.session.status = response.status
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTerminal() {
|
export function useTerminal() {
|
||||||
return term
|
return term
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reset() {
|
export default useSession
|
||||||
socket.reset()
|
|
||||||
term.reset()
|
|
||||||
store.session = newSession()
|
|
||||||
store.quest = null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function init(questId: string) {
|
|
||||||
if (!questId) throw new Error('No quest ID provided')
|
|
||||||
reset()
|
|
||||||
try {
|
|
||||||
await setQuest(questId)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
throw new LoadQuestError('Failed to load quest', questId)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await getActiveSession(questId)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
throw new LoadSessionError('Failed to load session', questId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setup() {
|
|
||||||
socket.on('terminal', (data: string) => {
|
|
||||||
term.write(data)
|
|
||||||
})
|
|
||||||
term.onData((data: string) => {
|
|
||||||
socket.emit('terminal', data)
|
|
||||||
})
|
|
||||||
socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
|
|
||||||
updateGraph(event)
|
|
||||||
})
|
|
||||||
socket.on('response', (response: StageResponse) => {
|
|
||||||
newResponse(response)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setup()
|
|
||||||
|
|
||||||
export default store
|
|
||||||
@@ -1,30 +1,35 @@
|
|||||||
import { reactive } from 'vue'
|
import { defineStore } from 'pinia'
|
||||||
import api from '../utils/api'
|
import { getUserProfile } from '../utils/api'
|
||||||
|
|
||||||
const store = reactive({
|
interface IProgress {
|
||||||
username: '',
|
quest: string
|
||||||
email: '',
|
sessions: string[]
|
||||||
progress: {},
|
completed: boolean
|
||||||
|
startedAt: Date
|
||||||
|
finishedAt?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserProfile {
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
progress: Map<string, IProgress>
|
||||||
|
}
|
||||||
|
|
||||||
|
const useUserProfile = defineStore('userProfile', {
|
||||||
|
state: (): UserProfile => ({
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
progress: new Map<string, IProgress>(),
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async loadUserProfile() {
|
||||||
|
const res = await getUserProfile()
|
||||||
|
this.$patch(res)
|
||||||
|
},
|
||||||
|
async resetUserProfile() {
|
||||||
|
this.$reset()
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function loadUserProfile() {
|
export default useUserProfile
|
||||||
try {
|
|
||||||
const res = await api.get('/users/me')
|
|
||||||
const { username, email, progress } = res.data
|
|
||||||
store.username = username
|
|
||||||
store.email = email
|
|
||||||
store.progress = progress
|
|
||||||
return store
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error(err)
|
|
||||||
throw new Error(`Failed to load user profile: ${err.message}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetUserProfile() {
|
|
||||||
store.username = ''
|
|
||||||
store.email = ''
|
|
||||||
store.progress = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default store
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { IQuest } from '@linux-odyssey/models'
|
||||||
|
import { GetSessionsRequest } from '@linux-odyssey/constants'
|
||||||
|
import { Session } from '../types'
|
||||||
|
import { UserProfile } from '../store/userProfile'
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: '/api/v1',
|
baseURL: '/api/v1',
|
||||||
@@ -8,3 +12,34 @@ const api = axios.create({
|
|||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
export default api
|
export default api
|
||||||
|
|
||||||
|
export async function getUserProfile(): Promise<UserProfile> {
|
||||||
|
const res = await api.get<UserProfile>('/users/me')
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getQuest(id: string): Promise<IQuest> {
|
||||||
|
const res = await api.get<IQuest>(`/quests/${id}`)
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSession(questId: string): Promise<Session> {
|
||||||
|
const res = await api.post<Session>('/sessions', { questId })
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSessions(
|
||||||
|
request: GetSessionsRequest
|
||||||
|
): Promise<Session[]> {
|
||||||
|
const res = await api.get<Session[]>('/sessions', { params: request })
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getActiveSession(
|
||||||
|
questId: string
|
||||||
|
): Promise<Session | null> {
|
||||||
|
const res = await api.get<Session | null>('/sessions/active', {
|
||||||
|
params: { questId },
|
||||||
|
})
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
@@ -46,7 +46,6 @@ class SocketTerminal {
|
|||||||
|
|
||||||
mount(reference: HTMLElement): void {
|
mount(reference: HTMLElement): void {
|
||||||
this.term.open(reference)
|
this.term.open(reference)
|
||||||
this.term.focus()
|
|
||||||
this.resizeScreen()
|
this.resizeScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
-34
@@ -1,19 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useToast } from 'vue-toastification'
|
import useSession from '../store/session'
|
||||||
import sessionStore, { init } from '../store/session'
|
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||||
import GameHeaderPart from '../components/GameHeaderPart.vue'
|
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
|
||||||
// import CommandlistPart from '../components/CommandlistPart.vue'
|
import DescriptionPart from '../components/game/DescriptionPart.vue'
|
||||||
import HintPart from '../components/HintPart.vue'
|
import QuestPart from '../components/game/QuestPart.vue'
|
||||||
import QuestPart from '../components/QuestPart.vue'
|
import TerminalPart from '../components/game/TerminalPart.vue'
|
||||||
import TerminalPart from '../components/TerminalPart.vue'
|
import VisualizationPart from '../components/game/VisualizationPart.vue'
|
||||||
import VisualizationPart from '../components/VisualizationPart.vue'
|
import ControlPalette from '../components/game/ControlPalette.vue'
|
||||||
import ControlPalette from '../components/ControlPalette.vue'
|
import CompleteModal from '../components/game/CompleteModal.vue'
|
||||||
import CompleteModal from '../components/CompleteModal.vue'
|
import StartButton from '../components/game/StartButton.vue'
|
||||||
import { LoadQuestError, LoadSessionError } from '../utils/errors'
|
|
||||||
|
const sessionStore = useSession()
|
||||||
|
|
||||||
const completed = computed(() => {
|
const completed = computed(() => {
|
||||||
return sessionStore.session.status === 'finished'
|
return sessionStore.session?.status === 'finished'
|
||||||
})
|
})
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -23,39 +24,30 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
sessionStore.reset()
|
||||||
await init(props.questId)
|
sessionStore.setup()
|
||||||
} catch (err: any) {
|
await sessionStore.setQuest(props.questId)
|
||||||
if (err instanceof LoadQuestError) {
|
await sessionStore.getActiveSession()
|
||||||
toast.error(`無法讀取關卡: ${err.questId},請確認網頁連結。`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (err instanceof LoadSessionError) {
|
|
||||||
toast.error(`無法建立工作階段: ${err.questId},請重新登入再試一次。`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.error(err)
|
|
||||||
toast.error(err.message)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- game header -->
|
<!-- game header -->
|
||||||
<div class="h-[6vh] w-full">
|
<div class="w-full">
|
||||||
<GameHeaderPart :title="sessionStore.quest?.title" />
|
<HeaderPart
|
||||||
|
:title="sessionStore.quest?.title"
|
||||||
|
:headerComponent="GameHeaderComponents"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- current status indicator -->
|
<!-- current status indicator -->
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
id="currentStatus"
|
id="currentStatus"
|
||||||
:value="sessionStore.session.status"
|
:value="sessionStore.session?.status"
|
||||||
/>
|
/>
|
||||||
<!-- main -->
|
<!-- main -->
|
||||||
<div id="main" class="h-[94vh] w-full flex p-3 space-x-3">
|
<div id="main" class="h-full pt-10 w-full flex p-3 space-x-3">
|
||||||
<!-- Topic and Command List -->
|
<!-- Topic and Command List -->
|
||||||
<div class="bg-bg-secondary h-full w-1/4 rounded-lg">
|
<div class="bg-bg-secondary h-full w-1/4 rounded-lg">
|
||||||
<section id="quest" class="h-full p-3 overflow-auto">
|
<section id="quest" class="h-full p-3 overflow-auto">
|
||||||
@@ -67,9 +59,10 @@ onMounted(async () => {
|
|||||||
</section> -->
|
</section> -->
|
||||||
</div>
|
</div>
|
||||||
<!-- Terminal and Hint -->
|
<!-- Terminal and Hint -->
|
||||||
<div class="bg-bg h-full w-1/2 rounded-lg">
|
<div class="bg-bg h-full rounded-lg w-1/2">
|
||||||
|
<StartButton :questId="$props.questId" />
|
||||||
<section id="hint" class="h-3/5">
|
<section id="hint" class="h-3/5">
|
||||||
<HintPart />
|
<DescriptionPart />
|
||||||
</section>
|
</section>
|
||||||
<hr class="border-border border" />
|
<hr class="border-border border" />
|
||||||
<section id="terminal" class="h-2/5 overflow-hidden">
|
<section id="terminal" class="h-2/5 overflow-hidden">
|
||||||
|
|||||||
+45
-119
@@ -1,97 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ContactButton from '../components/ContactButton.vue'
|
import ContactButton from '../components/ContactButton.vue'
|
||||||
import DynamicBackground from '../components/DynamicBackground.vue'
|
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||||
import { bugReportUrl, facebookUrl, githubUrl } from '../config'
|
import LandingHeaderComponents from '../components/header/LandingHeaderComponents.vue'
|
||||||
|
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||||
|
|
||||||
const scrollToSection = (sectionId: string) => {
|
import { facebookUrl, githubUrl } from '../config'
|
||||||
const section = document.getElementById(sectionId)
|
|
||||||
if (section) {
|
|
||||||
const container = document.querySelector('.overflow-y-auto') // Replace '.overflow-y-auto' with the appropriate class or selector for your container
|
|
||||||
if (container) {
|
|
||||||
container.scrollTo({
|
|
||||||
top: section.offsetTop,
|
|
||||||
behavior: 'smooth', // Add smooth scrolling behavior if desired
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="w-screen h-screen bg-scroll bg-bg">
|
<div class="w-screen h-screen bg-scroll bg-bg">
|
||||||
<DynamicBackground class="w-full h-full" />
|
<DynamicBackground class="w-full h-full" />
|
||||||
<div class="h-screen w-screen absolute top-0 left-0">
|
<div class="h-screen w-screen absolute top-0 left-0">
|
||||||
<!-- game header -->
|
<!-- game header -->
|
||||||
<div
|
<HeaderPart :headerComponent="LandingHeaderComponents" class="z-50" />
|
||||||
class="w-full h-[6vh] px-5 flex flex-row items-center justify-between bg-bg-secondary"
|
|
||||||
>
|
|
||||||
<div class="flex flex-row items-center gap-3">
|
|
||||||
<img src="../img/icon_totem.svg" class="h-8 justify-self-start" />
|
|
||||||
<button
|
|
||||||
id="GameTitle"
|
|
||||||
class="text-text-primary inline-block font-bold whitespace-nowrap pt-1"
|
|
||||||
style="font-size: 2vh"
|
|
||||||
>
|
|
||||||
Linux Odyssey
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-row items-center gap-3">
|
|
||||||
<button
|
|
||||||
id="Intro"
|
|
||||||
class="text-text inline-block font-bold whitespace-nowrap pt-1"
|
|
||||||
@click="scrollToSection('intro')"
|
|
||||||
style="font-size: 2vh"
|
|
||||||
>
|
|
||||||
Intro
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
id="Feature"
|
|
||||||
class="text-text inline-block font-bold whitespace-nowrap pt-1"
|
|
||||||
@click="scrollToSection('feature')"
|
|
||||||
style="font-size: 2vh"
|
|
||||||
>
|
|
||||||
Feature</button
|
|
||||||
><button
|
|
||||||
id="Info"
|
|
||||||
class="text-text inline-block font-bold whitespace-nowrap pt-1"
|
|
||||||
@click="scrollToSection('about')"
|
|
||||||
style="font-size: 2vh"
|
|
||||||
>
|
|
||||||
About
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
id="Contact"
|
|
||||||
class="text-text inline-block font-bold whitespace-nowrap pt-1"
|
|
||||||
@click="scrollToSection('contact')"
|
|
||||||
style="font-size: 2vh"
|
|
||||||
>
|
|
||||||
Contact Us
|
|
||||||
</button>
|
|
||||||
<RouterLink
|
|
||||||
title="Sign In"
|
|
||||||
id="SignIn"
|
|
||||||
to="/register"
|
|
||||||
class="text-text inline-block font-bold whitespace-nowrap pt-1"
|
|
||||||
style="font-size: 2vh"
|
|
||||||
>
|
|
||||||
Sign In
|
|
||||||
</RouterLink>
|
|
||||||
<a
|
|
||||||
title="Bug Report"
|
|
||||||
:href="bugReportUrl"
|
|
||||||
target="_blank"
|
|
||||||
class="h-5 w-5"
|
|
||||||
>
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'bug']"
|
|
||||||
class="text-text-primary h-full w-full"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- main content -->
|
<!-- main content -->
|
||||||
<main class="w-full h-[94vh] overflow-y-auto">
|
<main class="w-full h-full overflow-y-auto">
|
||||||
<div class="w-full flex flex-col items-center">
|
<div class="w-full flex flex-col items-center">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col md:flex-row mt-10 justify-around gap-5 items-center my-10"
|
class="flex flex-col md:flex-row mt-10 justify-around gap-5 items-center my-10"
|
||||||
@@ -104,7 +26,6 @@ const scrollToSection = (sectionId: string) => {
|
|||||||
Odyssey
|
Odyssey
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- intro -->
|
<!-- intro -->
|
||||||
<div class="w-full md:w-2/3 px-5">
|
<div class="w-full md:w-2/3 px-5">
|
||||||
<section
|
<section
|
||||||
@@ -228,40 +149,43 @@ const scrollToSection = (sectionId: string) => {
|
|||||||
鈦坦敏捷開發特別獎 第二名
|
鈦坦敏捷開發特別獎 第二名
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container mx-auto">
|
||||||
<!-- <h1
|
<div
|
||||||
class="text-text-primary p-5 font-extrabold font-mono col-span-3 gap-3"
|
class="justify-items-center grid grid-cols-1 sm:grid-cols-3 gap-5"
|
||||||
style="font-size: 3.5vh"
|
>
|
||||||
>
|
<h1
|
||||||
玩家感想
|
class="text-text-primary text-3xl font-extrabold font-mono sm:col-span-3 gap-6"
|
||||||
</h1>
|
>
|
||||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
玩家感想
|
||||||
"很好玩很像益智遊戲"
|
<!--可以做成翻頁卡牌一次一個-->
|
||||||
</h1>
|
</h1>
|
||||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
<h1 class="text-xl text-center">"很好玩很像益智遊戲"</h1>
|
||||||
"我從來不知道自由軟體的存在,謝謝你們。"
|
<h1 class="text-xl text-center">
|
||||||
</h1>
|
"我從來不知道自由軟體的存在,謝謝你們。"
|
||||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
</h1>
|
||||||
"還蠻好玩的,讓程式變好玩"
|
<h1 class="text-xl text-center">
|
||||||
</h1>
|
"還蠻好玩的,讓程式變好玩"
|
||||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
</h1>
|
||||||
"以前不喜歡電腦課現在覺得喜歡一點點"
|
<h1 class="text-xl text-center">
|
||||||
</h1>
|
"以前不喜歡電腦課現在覺得喜歡一點點"
|
||||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
</h1>
|
||||||
"程式真的對新手很友善,讓我學到了很多東西"
|
<h1 class="text-xl text-center">
|
||||||
</h1>
|
"程式真的對新手很友善,讓我學到了很多東西"
|
||||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
</h1>
|
||||||
"感覺做得很好很簡明易懂關卡設計很用心"
|
<h1 class="text-xl text-center">
|
||||||
</h1>
|
"感覺做得很好很簡明易懂關卡設計很用心"
|
||||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
</h1>
|
||||||
"這個體驗我覺得非常有趣,可以體驗到沒辦法體驗的程式,學長也都熱心指導,非常感謝學長的用心的講解,希望以後還可以再體驗這種活動"
|
<h1 class="text-xl text-center">
|
||||||
</h1>
|
"這個體驗我覺得非常有趣,可以體驗到沒辦法體驗的程式,學長也都熱心指導,非常感謝學長的用心的講解,希望以後還可以再體驗這種活動"
|
||||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
</h1>
|
||||||
"以前我覺得寫程式很難不過現在覺得很有趣"
|
<h1 class="text-xl text-center">
|
||||||
</h1>
|
"以前我覺得寫程式很難不過現在覺得很有趣"
|
||||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
</h1>
|
||||||
"以前覺得寫程式是我生活中碰不到而且很難的項目。但通過這堂課之後,覺得寫程式不再那麼難以親近"
|
<h1 class="text-xl text-center">
|
||||||
</h1> -->
|
"以前覺得寫程式是我生活中碰不到而且很難的項目。但通過這堂課之後,覺得寫程式不再那麼難以親近"
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<section
|
<section
|
||||||
@@ -271,7 +195,9 @@ const scrollToSection = (sectionId: string) => {
|
|||||||
<h1 class="text-text-primary font-extrabold font-mono text-3xl">
|
<h1 class="text-text-primary font-extrabold font-mono text-3xl">
|
||||||
聯絡我們
|
聯絡我們
|
||||||
</h1>
|
</h1>
|
||||||
<div class="flex flex-row items-center justify-around mt-5 gap-5">
|
<div
|
||||||
|
class="flex flex-row items-center justify-around mt-5 gap-5 mb-5"
|
||||||
|
>
|
||||||
<ContactButton
|
<ContactButton
|
||||||
:url="facebookUrl"
|
:url="facebookUrl"
|
||||||
:icons="['fab', 'facebook']"
|
:icons="['fab', 'facebook']"
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import DynamicBackground from '../components/DynamicBackground.vue'
|
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||||
import GameHeaderPart from '../components/GameHeaderPart.vue'
|
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||||
|
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
|
|
||||||
const leaderboard = ref<
|
const leaderboard = ref<
|
||||||
@@ -35,8 +36,8 @@ onMounted(async () => {
|
|||||||
<div
|
<div
|
||||||
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
|
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
|
||||||
>
|
>
|
||||||
<div class="h-[6vh] w-full">
|
<div class="w-full">
|
||||||
<GameHeaderPart title="排行榜" />
|
<HeaderPart title="排行榜" :headerComponent="GameHeaderComponents" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-fit h-[94vh] m-3">
|
<div class="w-fit h-[94vh] m-3">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AuthForm from '../components/AuthForm.vue'
|
import AuthForm from '../components/AuthForm.vue'
|
||||||
import Background from '../components/DynamicBackground.vue'
|
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||||
|
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TooManyRequestsError,
|
TooManyRequestsError,
|
||||||
@@ -45,10 +46,11 @@ const handleLogin = async ({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-screen h-screen">
|
<div class="w-screen h-screen">
|
||||||
<Background class="w-full h-full" />
|
<DynamicBackground class="w-full h-full" />
|
||||||
<div
|
<div
|
||||||
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
|
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
|
||||||
>
|
>
|
||||||
|
<HeaderPart />
|
||||||
<div class="w-fit">
|
<div class="w-fit">
|
||||||
<AuthForm @onSubmit="handleLogin" type="login" />
|
<AuthForm @onSubmit="handleLogin" type="login" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import QuestMap from '../components/QuestMap.vue'
|
import QuestMap from '../components/QuestMap.vue'
|
||||||
import GameHeaderPart from '../components/GameHeaderPart.vue'
|
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||||
|
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-[6vh] w-full">
|
<div class="w-fit">
|
||||||
<GameHeaderPart title="關卡地圖" />
|
<HeaderPart
|
||||||
|
title="關卡地圖"
|
||||||
|
:headerComponent="GameHeaderComponents"
|
||||||
|
class="z-50"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-center items-center w-full h-[94vh]">
|
<div class="flex flex-col justify-center items-center w-full h-full pt-5">
|
||||||
<QuestMap />
|
<QuestMap />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -3,8 +3,8 @@ import validator from 'validator'
|
|||||||
import { isValidUsername } from '@linux-odyssey/utils'
|
import { isValidUsername } from '@linux-odyssey/utils'
|
||||||
import { passwordPolicy } from '@linux-odyssey/constants'
|
import { passwordPolicy } from '@linux-odyssey/constants'
|
||||||
import AuthForm from '../components/AuthForm.vue'
|
import AuthForm from '../components/AuthForm.vue'
|
||||||
import Background from '../components/DynamicBackground.vue'
|
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||||
|
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||||
import { checkUsername, register } from '../utils/auth'
|
import { checkUsername, register } from '../utils/auth'
|
||||||
import {
|
import {
|
||||||
UnauthorizedError,
|
UnauthorizedError,
|
||||||
@@ -89,11 +89,12 @@ async function check({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="w-screen h-screen">
|
<div class="w-screen h-screen">
|
||||||
<Background class="w-full h-full" />
|
<DynamicBackground class="w-full h-full" />
|
||||||
<div
|
<div
|
||||||
class="h-screen w-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
|
class="h-screen w-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
|
||||||
>
|
>
|
||||||
<div class="w-fit">
|
<HeaderPart />
|
||||||
|
<div class="w-fit mt-8">
|
||||||
<AuthForm
|
<AuthForm
|
||||||
@onSubmit="handleRegister"
|
@onSubmit="handleRegister"
|
||||||
@onChange="check"
|
@onChange="check"
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
services:
|
|
||||||
app:
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: app/Dockerfile
|
|
||||||
target: dev
|
|
||||||
ports:
|
|
||||||
- 5173:5173
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
volumes:
|
|
||||||
- ./app:/app/app
|
|
||||||
- ./packages:/app/packages
|
|
||||||
|
|
||||||
backend:
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: server/Dockerfile
|
|
||||||
target: dev
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
- players
|
|
||||||
ports:
|
|
||||||
- '3000:3000'
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- ./server/src:/app/server/src
|
|
||||||
- ./server/dist:/app/server/dist
|
|
||||||
- ./server/swagger.yaml:/app/server/swagger.yaml
|
|
||||||
- ./packages:/app/packages
|
|
||||||
- ./quests:/app/quests
|
|
||||||
- ./logs:/app/logs
|
|
||||||
environment:
|
|
||||||
- BASE_URL=http://localhost:5173
|
|
||||||
- MONGO_URL=mongodb://db:27017/odyssey-test
|
|
||||||
- DOCKER_NETWORK=linux-odyssey-players
|
|
||||||
- SECRET_KEY=b1d2e917-17b9-4fce-9055-082dee6ec5d6 # Only for testing
|
|
||||||
- TRUSTED_PROXIES=${TRUSTED_PROXIES}
|
|
||||||
- SURVEY_URL=${SURVEY_URL}
|
|
||||||
- BUG_REPORT_URL=${BUG_REPORT_URL}
|
|
||||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
|
||||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
|
||||||
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
|
|
||||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
|
|
||||||
- TESTING=true
|
|
||||||
- TESTING_USERNAME=alex
|
|
||||||
- TESTING_PASSWORD=Alex1234
|
|
||||||
- HOST_PWD=${PWD}
|
|
||||||
- MOUNT_QUEST=${MOUNT_QUEST}
|
|
||||||
|
|
||||||
analytics:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: packages/analytics/Dockerfile
|
|
||||||
target: dev
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
ports:
|
|
||||||
- '3001:3001'
|
|
||||||
environment:
|
|
||||||
- MONGO_URL=mongodb://db:27017/odyssey-test
|
|
||||||
- ADMIN_USERNAME=alex
|
|
||||||
- ADMIN_PASSWORD=Alex1234
|
|
||||||
volumes:
|
|
||||||
- ./packages/analytics:/app/packages/analytics
|
|
||||||
- ./packages/models:/app/packages/models
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: mongo
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
ports:
|
|
||||||
- '27017:27017'
|
|
||||||
volumes:
|
|
||||||
- ./data:/data/db
|
|
||||||
|
|
||||||
base:
|
|
||||||
image: linuxodyssey/quest-base
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./quests/Dockerfile
|
|
||||||
|
|
||||||
networks:
|
|
||||||
internal:
|
|
||||||
players:
|
|
||||||
name: linux-odyssey-players
|
|
||||||
internal: true
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mongo
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
ports:
|
||||||
|
- '27017:27017'
|
||||||
|
volumes:
|
||||||
|
- ./data:/data/db
|
||||||
|
|
||||||
|
base:
|
||||||
|
image: linuxodyssey/quest-base
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ./quests/Dockerfile
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
players:
|
||||||
|
name: linux-odyssey-players
|
||||||
|
internal: true
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
export const passwordPolicy = {
|
export const passwordPolicy = {
|
||||||
minLength: 8,
|
minLength: 8,
|
||||||
minNumbers: 1,
|
minNumbers: 1,
|
||||||
@@ -6,6 +8,11 @@ export const passwordPolicy = {
|
|||||||
minSymbols: 0,
|
minSymbols: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export const getSessionsRequestSchema = z.object({
|
||||||
passwordPolicy,
|
questId: z.string().optional(),
|
||||||
}
|
status: z.enum(['active', 'inactive', 'finished']).optional(),
|
||||||
|
limit: z.coerce.number().optional(),
|
||||||
|
order: z.enum(['asc', 'desc']).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type GetSessionsRequest = z.infer<typeof getSessionsRequestSchema>
|
||||||
@@ -10,5 +10,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "tsc -w"
|
"dev": "tsc -w"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.23.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,7 @@ export interface IUser {
|
|||||||
socialLogins: Map<string, ISocialLogin>
|
socialLogins: Map<string, ISocialLogin>
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
|
isGuest: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const User = model<IUser>(
|
export const User = model<IUser>(
|
||||||
@@ -51,6 +52,11 @@ export const User = model<IUser>(
|
|||||||
default: {},
|
default: {},
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
isGuest: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
|||||||
+12
-20
@@ -3,23 +3,23 @@ instruction: |
|
|||||||
在這個關卡中,你將會學到:
|
在這個關卡中,你將會學到:
|
||||||
1. 如何使用 `echo` 指令
|
1. 如何使用 `echo` 指令
|
||||||
|
|
||||||
準備好就輸入 `echo start` 開始吧!
|
按下中間的 `start` 按鈕來開始這個關卡。
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- get-started
|
- get-started
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 按下 `start` 按鈕開始
|
||||||
condition:
|
condition:
|
||||||
output:
|
command:
|
||||||
- ^start$
|
- ^echo start$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你剛才使用的指令叫做 `echo`。
|
還記得第一關使用的指令 `echo` 嗎?
|
||||||
- '`echo` 指令可以將後方接續的文字輸出到終端機上。'
|
`echo` 指令可以將後方接續的文字輸出到終端機上。
|
||||||
- 例如你輸入 `echo Hello`,終端機就會顯示 `Hello`。
|
例如你輸入 `echo Hello`,終端機就會顯示 `Hello`。
|
||||||
- 你可以試著輸入 `echo` 並在後面加上一些文字,然後按下 Enter 鍵,就可以看到終端機上顯示了你剛才輸入的文字。
|
你可以試著輸入 `echo ` 並在後面加上一些文字,然後按下 Enter 鍵,就可以看到終端機上顯示了你剛才輸入的文字。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `echo` 並在後面加上一些文字
|
- ➤ 輸入 `echo` 並在後面加上一些文字
|
||||||
- ➤ 例如:`echo example`
|
- ➤ 例如:`echo example`
|
||||||
@@ -43,8 +43,8 @@ stages:
|
|||||||
requirements:
|
requirements:
|
||||||
- echo
|
- echo
|
||||||
condition:
|
condition:
|
||||||
output:
|
command:
|
||||||
- ^[Hh]ow are you\??$
|
- ^echo [Hh]ow are you\??$
|
||||||
exceptions:
|
exceptions:
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
@@ -59,17 +59,9 @@ stages:
|
|||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
- 在 `echo` 後面還需要加上一些文字
|
- 在 `echo ` 後面還需要加上 `How are you?`
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 你成功的在終端機上顯示了 `How are you?`。
|
- 你成功的在終端機上顯示了 `How are you?`。
|
||||||
- 相信你已經了解如何使用 `echo` 指令來顯示文字了。
|
- 相信你已經了解如何使用 `echo` 指令來顯示文字了。
|
||||||
- 之後如果要在終端機顯示文字,記得可以使用 `echo` 指令。
|
- 之後如果要在終端機顯示文字,記得可以使用 `echo` 指令。
|
||||||
|
|
||||||
exceptions:
|
|
||||||
- repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
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,4 +1,5 @@
|
|||||||
echo start
|
echo start
|
||||||
|
ls
|
||||||
cat hello.txt
|
cat hello.txt
|
||||||
cat world.txt
|
cat world.txt
|
||||||
echo continue
|
echo continue
|
||||||
|
|||||||
+235
-87
@@ -3,120 +3,185 @@ instruction: |
|
|||||||
在這個關卡中,你將會學到:
|
在這個關卡中,你將會學到:
|
||||||
1. 如何用 `cp` 指令複製檔案
|
1. 如何用 `cp` 指令複製檔案
|
||||||
|
|
||||||
準備好就輸入 `echo start` 開始吧!
|
按下中間的 `start` 按鈕來開始這個關卡。
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- create
|
- create
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始
|
task: 按下 `start` 按鈕開始
|
||||||
condition:
|
condition:
|
||||||
output:
|
command:
|
||||||
- start
|
- ^echo start$
|
||||||
exceptions:
|
|
||||||
- condition:
|
|
||||||
command:
|
|
||||||
- ^echo$
|
|
||||||
responses:
|
|
||||||
- type: narrative
|
|
||||||
content:
|
|
||||||
- 你忘記輸入 `start` 了嗎?
|
|
||||||
- 請輸入 `echo start` 來開始
|
|
||||||
- catchAll: true
|
|
||||||
responses:
|
|
||||||
- type: narrative
|
|
||||||
content:
|
|
||||||
- 請輸入 `echo start` 來開始
|
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 今天我們要學習如何複製檔案。
|
今天我們要學習如何複製檔案。
|
||||||
- 現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,請使用 `ls` 與 `cat` 來查看它們的內容。
|
先用 `ls` 指令查看目前資料夾裡有哪些檔案。
|
||||||
- 看完之後,使用 `echo continue` 來繼續吧!
|
hints:
|
||||||
|
- ➤ 使用 `ls` 來查看目前資料夾裡有哪些檔案
|
||||||
|
|
||||||
|
- id: ls
|
||||||
|
task: 查看目前資料夾裡的檔案
|
||||||
|
requirements:
|
||||||
|
- start
|
||||||
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
command:
|
||||||
|
- ^ls$
|
||||||
|
output:
|
||||||
|
- (^|\s)hello.txt($|\s)
|
||||||
|
- (^|\s)world.txt($|\s)
|
||||||
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
not:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你不在 `/home/commander` 資料夾裡。
|
||||||
|
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: 可以看到現在資料夾裡有 `hello.txt` 跟 `world.txt`。
|
||||||
|
請使用 `cat` 來查看它們的內容。
|
||||||
|
看完之後,使用 `echo next` 來繼續吧!
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `ls` 來查看資料夾內的檔案
|
|
||||||
- ➤ 使用 `cat hello.txt` 以及 `cat world.txt` 來查看檔案內容
|
- ➤ 使用 `cat hello.txt` 以及 `cat world.txt` 來查看檔案內容
|
||||||
- ➤ 如果終端機卡在某個指令無法輸入,請按下 `Ctrl + C` 來取消該指令
|
- ➤ 如果終端機卡在某個指令無法輸入,請按下 `Ctrl + C` 來取消該指令
|
||||||
|
|
||||||
- id: cat-hello
|
- id: cat-hello
|
||||||
task: 查看 `hello.txt` 的內容
|
task: 查看 `hello.txt` 的內容
|
||||||
requirements:
|
requirements:
|
||||||
- start
|
- ls
|
||||||
condition:
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ^cat hello.txt$
|
- ^cat hello.txt$
|
||||||
output:
|
output:
|
||||||
- Hello
|
- Hello
|
||||||
exceptions:
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
not:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你不在 `/home/commander` 資料夾裡。
|
||||||
|
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ^cat$
|
- ^cat$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你忘記輸入 `cat` 了嗎?
|
你忘記輸入 `cat` 了嗎?
|
||||||
- 請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
|
請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ^cat hello$
|
- ^cat hello$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你忘記輸入 `.txt` 了嗎?
|
你忘記輸入 `.txt` 了嗎?
|
||||||
- 請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
|
請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你成功查看到 `hello.txt` 的內容了!
|
你成功查看到 `hello.txt` 的內容了!
|
||||||
|
|
||||||
- id: cat-world
|
- id: cat-world
|
||||||
task: 查看 `world.txt` 的內容
|
task: 查看 `world.txt` 的內容
|
||||||
requirements:
|
requirements:
|
||||||
- start
|
- ls
|
||||||
condition:
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ^cat world.txt$
|
- ^cat world.txt$
|
||||||
output:
|
output:
|
||||||
- World
|
- World
|
||||||
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
not:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你不在 `/home/commander` 資料夾裡。
|
||||||
|
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^cat$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你忘記輸入 `cat` 了嗎?
|
||||||
|
請輸入 `cat world.txt` 來查看 `world.txt` 的內容
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^cat world$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你忘記輸入 `.txt` 了嗎?
|
||||||
|
請輸入 `cat world.txt` 來查看 `world.txt` 的內容
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你成功查看到 `world.txt` 的內容了!
|
你成功查看到 `world.txt` 的內容了!
|
||||||
|
|
||||||
- id: cat-continue
|
- id: cat-next
|
||||||
task: 使用 `echo continue` 來繼續
|
task: 輸入 `echo next` 來繼續
|
||||||
requirements:
|
requirements:
|
||||||
- cat-hello
|
- cat-hello
|
||||||
- cat-world
|
- cat-world
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- echo
|
- ^echo next$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 接下來我們要來學習複製檔案。
|
接下來我們要來學習如何複製檔案。
|
||||||
- '`cp` 這個指令,是 copy 的縮寫,就是複製的意思。'
|
`cp` 這個指令,是 copy 的縮寫,就是複製的意思。
|
||||||
- 我們可以用 `cp [來源檔案] [目的檔案]` 的方式來複製檔案。
|
我們可以用 `cp [來源檔案] [目的檔案]` 的方式來複製檔案。
|
||||||
- 請你使用 `cp` 來複製 `hello.txt` 到 `hello2.txt`。
|
請你使用 `cp` 來複製 `hello.txt` 的內容到 `hello2.txt`。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `cp hello.txt hello2.txt` 來複製 `hello.txt` 到 `hello2.txt`
|
- ➤ 使用 `cp hello.txt hello2.txt` 來複製 `hello.txt` 到 `hello2.txt`
|
||||||
|
|
||||||
- id: cp-hello
|
- id: cp-hello
|
||||||
task: 複製 `hello.txt` 到 `hello2.txt`
|
task: 複製 `hello.txt` 到 `hello2.txt`
|
||||||
requirements:
|
requirements:
|
||||||
- cat-continue
|
- cat-next
|
||||||
condition:
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ^cp hello.txt hello2.txt$
|
- ^cp hello.txt hello2.txt$
|
||||||
files:
|
files:
|
||||||
- path: hello2.txt
|
- path: hello2.txt
|
||||||
type: file
|
type: file
|
||||||
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
not:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你不在 `/home/commander` 資料夾裡。
|
||||||
|
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你成功複製 `hello.txt` 到 `hello2.txt` 了!
|
你成功複製 `hello.txt` 到 `hello2.txt` 了!
|
||||||
- 我們打開來確認一下 `hello2.txt` 的內容吧!
|
我們打開來確認一下 `hello2.txt` 的內容吧!
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `cat hello2.txt` 來查看 `hello2.txt` 的內容
|
- ➤ 使用 `cat hello2.txt` 來查看 `hello2.txt` 的內容
|
||||||
|
|
||||||
@@ -125,16 +190,28 @@ stages:
|
|||||||
- cp-hello
|
- cp-hello
|
||||||
task: 查看 `hello2.txt` 的內容
|
task: 查看 `hello2.txt` 的內容
|
||||||
condition:
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ^cat hello2.txt$
|
- ^cat hello2.txt$
|
||||||
output:
|
output:
|
||||||
- Hello
|
- Hello
|
||||||
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
not:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你不在 `/home/commander` 資料夾裡。
|
||||||
|
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你成功查看到 `hello2.txt` 的內容了!
|
你成功查看到 `hello2.txt` 的內容了!
|
||||||
- 正如我們所見,`hello2.txt` 的內容跟 `hello.txt` 一模一樣。
|
正如我們所見,`hello2.txt` 的內容跟 `hello.txt` 一模一樣。
|
||||||
- 那我們再來試試看複製 `world.txt` 到 `world2.txt` 吧!
|
那我們再來試試看複製 `world.txt` 到 `world2.txt` 吧!
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `cp world.txt world2.txt` 來複製 `world.txt` 到 `world2.txt`
|
- ➤ 使用 `cp world.txt world2.txt` 來複製 `world.txt` 到 `world2.txt`
|
||||||
|
|
||||||
@@ -143,16 +220,28 @@ stages:
|
|||||||
- cat-hello2
|
- cat-hello2
|
||||||
task: 複製 `world.txt` 到 `world2.txt`
|
task: 複製 `world.txt` 到 `world2.txt`
|
||||||
condition:
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ^cp world.txt world2.txt$
|
- ^cp world.txt world2.txt$
|
||||||
files:
|
files:
|
||||||
- path: world2.txt
|
- path: world2.txt
|
||||||
type: file
|
type: file
|
||||||
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
not:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你不在 `/home/commander` 資料夾裡。
|
||||||
|
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你成功複製 `world.txt` 到 `world2.txt` 了!
|
你成功複製 `world.txt` 到 `world2.txt` 了!
|
||||||
- 我們打開來確認一下 `world2.txt` 的內容吧!
|
我們打開來確認一下 `world2.txt` 的內容吧!
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `cat world2.txt` 來查看 `world2.txt` 的內容
|
- ➤ 使用 `cat world2.txt` 來查看 `world2.txt` 的內容
|
||||||
|
|
||||||
@@ -161,23 +250,35 @@ stages:
|
|||||||
- cp-world
|
- cp-world
|
||||||
task: 查看 `world2.txt` 的內容
|
task: 查看 `world2.txt` 的內容
|
||||||
condition:
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ^cat world2.txt$
|
- ^cat world2.txt$
|
||||||
output:
|
output:
|
||||||
- World
|
- World
|
||||||
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
not:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你不在 `/home/commander` 資料夾裡。
|
||||||
|
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你成功查看到 `world2.txt` 的內容了!
|
你成功查看到 `world2.txt` 的內容了!
|
||||||
- '`world2.txt` 的內容跟 `world.txt` 一模一樣。'
|
`world2.txt` 的內容跟 `world.txt` 一模一樣。
|
||||||
- 接下來我們要來學習如何複製資料夾。
|
接下來我們要來學習如何複製資料夾。
|
||||||
- 現在目錄下有一個 `notes` 資料夾,裡面有一個 `note.txt` 的檔案。
|
現在目錄下有一個 `notes` 資料夾,裡面有一個 `note.txt` 的檔案。
|
||||||
- 我們要能整個資料夾包含裡面的檔案一起複製到 `notes2`。
|
我們要能整個資料夾包含裡面的檔案一起複製到 `notes2`。
|
||||||
- 跟複製單一檔案不一樣的是,複製資料夾時,我們要加上 `-r` 的選項(recursive)代表將資料夾中的所有檔案都一起複製。
|
跟複製單一檔案不一樣的是,複製資料夾時,我們要加上 `-r` 的選項(recursive)代表將資料夾中的所有檔案都一起複製。
|
||||||
- 在使用指令時,如果想要調整指令的行為,可以在指令後面加上選項。
|
在使用指令時,如果想要調整指令的行為,可以在指令後面加上選項。
|
||||||
- 選項的格式通常是 `-` 加上一個字母,例如 `-r`。
|
選項的格式通常是 `-` 加上一個字母,例如 `-r`。
|
||||||
- 選項擺放的位置沒有特別的限制,通常會跟在指令的後面,置於檔案名稱的前面。
|
選項擺放的位置沒有特別的限制,通常會跟在指令的後面,置於檔案名稱的前面。
|
||||||
- 請你使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`。
|
請你使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
|
- ➤ 使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
|
||||||
- ➤ 記得要加上 `-r` 的選項來複製所有檔案
|
- ➤ 記得要加上 `-r` 的選項來複製所有檔案
|
||||||
@@ -187,6 +288,8 @@ stages:
|
|||||||
- cat-world2
|
- cat-world2
|
||||||
task: 複製 `notes` 資料夾到 `notes2`
|
task: 複製 `notes` 資料夾到 `notes2`
|
||||||
condition:
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ^cp
|
- ^cp
|
||||||
files:
|
files:
|
||||||
@@ -194,38 +297,59 @@ stages:
|
|||||||
type: directory
|
type: directory
|
||||||
- path: notes2/note.txt
|
- path: notes2/note.txt
|
||||||
type: file
|
type: file
|
||||||
responses:
|
|
||||||
- type: narrative
|
|
||||||
content:
|
|
||||||
- 你成功複製 `notes` 資料夾到 `notes2` 了!
|
|
||||||
- 我們打開來確認一下 `notes2` 資料夾的內容吧!
|
|
||||||
hints:
|
|
||||||
- ➤ 使用 `ls notes2` 來查看 `notes2` 資料夾的內容
|
|
||||||
exceptions:
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
not:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你不在 `/home/commander` 資料夾裡。
|
||||||
|
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||||
- condition:
|
- condition:
|
||||||
error:
|
error:
|
||||||
- 'cp: omitting directory'
|
- 'cp: omitting directory'
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你忘記加上 `-r` 的參數了嗎?
|
你忘記加上 `-r` 的參數了嗎?
|
||||||
- 請輸入 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
|
請輸入 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你成功複製 `notes` 資料夾到 `notes2` 了!
|
||||||
|
我們打開來確認一下 `notes2` 資料夾的內容吧!
|
||||||
|
hints:
|
||||||
|
- ➤ 使用 `ls notes2` 來查看 `notes2` 資料夾的內容
|
||||||
|
|
||||||
- id: ls-notes2
|
- id: ls-notes2
|
||||||
requirements:
|
requirements:
|
||||||
- cp-notes
|
- cp-notes
|
||||||
task: 查看 `notes2` 資料夾的內容
|
task: 查看 `notes2` 資料夾的內容
|
||||||
condition:
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ^ls notes2$
|
- ^ls notes2$
|
||||||
output:
|
output:
|
||||||
- note.txt
|
- note.txt
|
||||||
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
not:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你不在 `/home/commander` 資料夾裡。
|
||||||
|
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你成功查看到 `notes2` 資料夾的內容了!
|
你成功查看到 `notes2` 資料夾的內容了!
|
||||||
- 我們可以看到,整個 `notes` 資料夾包含裡面的檔案,都被複製到 `notes2` 資料夾了。
|
我們可以看到,整個 `notes` 資料夾包含裡面的檔案,都被複製到 `notes2` 資料夾了。
|
||||||
- 那麼最後我們要來試試看,如果複製一個檔案到已經存在的檔案會發生什麼事。
|
那麼最後我們要來試試看,如果複製一個檔案到已經存在的檔案會發生什麼事。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `cp hello.txt world.txt` 來複製 `hello.txt` 到 `world.txt`
|
- ➤ 使用 `cp hello.txt world.txt` 來複製 `hello.txt` 到 `world.txt`
|
||||||
|
|
||||||
@@ -234,13 +358,25 @@ stages:
|
|||||||
- ls-notes2
|
- ls-notes2
|
||||||
task: 複製 `hello.txt` 到 `world.txt`
|
task: 複製 `hello.txt` 到 `world.txt`
|
||||||
condition:
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ^cp hello.txt world.txt$
|
- ^cp hello.txt world.txt$
|
||||||
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
not:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你不在 `/home/commander` 資料夾裡。
|
||||||
|
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你成功複製 `hello.txt` 到 `world.txt` 了!
|
你成功複製 `hello.txt` 到 `world.txt` 了!
|
||||||
- 我們打開來確認一下 `world.txt` 的內容吧!
|
我們打開來確認一下 `world.txt` 的內容吧!
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `cat world.txt`
|
- ➤ 使用 `cat world.txt`
|
||||||
|
|
||||||
@@ -249,14 +385,26 @@ stages:
|
|||||||
- cp-overwrite
|
- cp-overwrite
|
||||||
task: 檢查 `world.txt` 的內容
|
task: 檢查 `world.txt` 的內容
|
||||||
condition:
|
condition:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ^cat world.txt$
|
- ^cat world.txt$
|
||||||
output:
|
output:
|
||||||
- Hello
|
- Hello
|
||||||
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
not:
|
||||||
|
pwd:
|
||||||
|
- ^/home/commander$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你不在 `/home/commander` 資料夾裡。
|
||||||
|
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '`world.txt` 的內容變成 `Hello` 了!'
|
`world.txt` 的內容變成 `Hello` 了!
|
||||||
- 這是因為,當我們複製檔案時,如果目的檔案已經存在,那麼目的檔案的內容就會被覆蓋掉。
|
這是因為,當我們複製檔案時,如果目的檔案已經存在,那麼目的檔案的內容就會被覆蓋掉。
|
||||||
- 所以在複製檔案的時候,要特別注意目的檔案是否已經存在,否則可能會不小心把資料弄丟。
|
所以在複製檔案的時候,要特別注意目的檔案是否已經存在,否則可能會不小心把資料弄丟。
|
||||||
- 以上就是複製檔案的基本操作!
|
以上就是複製檔案的基本操作!
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
echo start
|
echo start
|
||||||
|
ls
|
||||||
touch hello.txt
|
touch hello.txt
|
||||||
ls
|
ls
|
||||||
cat hello.txt
|
cat hello.txt
|
||||||
|
|||||||
+89
-51
@@ -4,40 +4,66 @@ instruction: |
|
|||||||
1. 如何使用 `touch` 建立檔案
|
1. 如何使用 `touch` 建立檔案
|
||||||
2. 如何使用 `mkdir` 建立資料夾
|
2. 如何使用 `mkdir` 建立資料夾
|
||||||
|
|
||||||
準備好就輸入 `echo start` 開始吧!
|
按下中間的 `start` 按鈕來開始這個關卡。
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- read
|
- read
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始
|
task: 按下 `start` 按鈕開始
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo start
|
- ^echo start$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 這次要學習的指令是 `touch` 指令。
|
這次要學習的指令是 `touch` 指令。
|
||||||
- '`touch` 指令可以建立一個檔案。'
|
請你先用 `ls` 指令查看目前所在資料夾中有哪些檔案。
|
||||||
- 接下來,請你試著在終端機上建立一個檔案,檔名為 `hello.txt`。
|
你應該會發現目前所在資料夾中沒有任何檔案。
|
||||||
|
hints:
|
||||||
|
- ➤ 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
|
||||||
|
|
||||||
|
- id: ls
|
||||||
|
task: 查看資料夾中的檔案
|
||||||
|
requirements:
|
||||||
|
- start
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^ls$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你應該可以看到終端機上沒有任何回應,這表示目前所在資料夾中沒有任何檔案。
|
||||||
|
而我們可以使用 `touch [檔案名稱]` 指令來建立一個檔案。
|
||||||
|
接下來,請你試著在終端機上建立一個檔案,檔名為 `hello.txt`。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `touch hello.txt` 來建立一個檔案
|
- ➤ 輸入 `touch hello.txt` 來建立一個檔案
|
||||||
|
|
||||||
- id: touch
|
- id: touch
|
||||||
task: 使用 `touch` 指令來建立一個檔案
|
task: 使用 `touch` 指令來建立一個檔案
|
||||||
requirements:
|
requirements:
|
||||||
- start
|
- ls
|
||||||
condition:
|
condition:
|
||||||
files:
|
files:
|
||||||
- path: hello.txt
|
- path: hello.txt
|
||||||
type: file
|
type: file
|
||||||
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
files:
|
||||||
|
- path: hello$
|
||||||
|
type: file
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你似乎忘記輸入副檔名 `txt` 了。
|
||||||
|
請再試一次,並確保檔名為 `hello.txt`。
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你應該可以看到終端機上沒有任何回應。
|
你應該可以看到終端機上沒有任何回應。
|
||||||
- 這表示 `touch` 指令已經成功建立了一個檔案。
|
然而實際上 `touch` 指令已經成功建立了一個檔案。
|
||||||
- 接下來,請你用之前學過的 `ls` 指令來查看目前所在資料夾中有哪些檔案。
|
請你用之前學過的 `ls` 指令來查看目前所在資料夾中有哪些檔案。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
|
- ➤ 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
|
||||||
|
|
||||||
@@ -52,26 +78,28 @@ stages:
|
|||||||
- hello.txt
|
- hello.txt
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你應該可以看到終端機上顯示了 `hello.txt` 這個檔案。
|
你應該可以看到終端機上顯示了 `hello.txt` 這個檔案。
|
||||||
- 我們來看看 `hello.txt` 檔案的內容吧!
|
我們來看看 `hello.txt` 檔案的內容吧!
|
||||||
- 我們可以使用之前學過的 `cat` 指令來查看檔案內容。
|
我們可以使用之前學過的 `cat` 指令來查看檔案內容。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
|
- ➤ 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
|
||||||
|
|
||||||
- id: cat
|
- id: cat
|
||||||
task: 使用 `cat` 指令來查看 `hello.txt` 檔案的內容
|
task: 使用 `cat` 指令來查看 `hello.txt` 檔案的內容
|
||||||
|
requirements:
|
||||||
|
- ls-touch
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^cat hello.txt$
|
- ^cat hello.txt$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 咦?竟然什麼都沒有!
|
咦?竟然什麼都沒有!
|
||||||
- 沒錯,使用 `touch` 指令建立的檔案是空的。
|
沒錯,使用 `touch` 指令建立的檔案是空的。
|
||||||
- 我們在未來會學習如何編輯檔案的內容。
|
我們在未來會學習如何編輯檔案的內容。
|
||||||
- 再來試試建立其他的檔案吧!
|
再來試試建立其他的檔案吧!
|
||||||
- 請你試著在終端機上建立另一個檔案,檔名為 `world.txt`。
|
請你試著在終端機上建立另一個檔案,檔名為 `world.txt`。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `touch world.txt` 來建立一個檔案
|
- ➤ 輸入 `touch world.txt` 來建立一個檔案
|
||||||
|
|
||||||
@@ -83,11 +111,21 @@ stages:
|
|||||||
files:
|
files:
|
||||||
- path: world.txt
|
- path: world.txt
|
||||||
type: file
|
type: file
|
||||||
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
files:
|
||||||
|
- path: world$
|
||||||
|
type: file
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
你似乎忘記輸入副檔名 `txt` 了。
|
||||||
|
請再試一次,並確保檔名為 `world.txt`。
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 非常好,你已經成功建立了第二個檔案。
|
非常好,你已經成功建立了第二個檔案。
|
||||||
- 接下來,請你用之前學過的 `ls` 指令來查看剛剛建立的兩個檔案。
|
接下來,請你用之前學過的 `ls` 指令來查看剛剛建立的兩個檔案。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
|
- ➤ 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
|
||||||
|
|
||||||
@@ -99,16 +137,16 @@ stages:
|
|||||||
command:
|
command:
|
||||||
- ^ls$
|
- ^ls$
|
||||||
output:
|
output:
|
||||||
- hello.txt
|
- (^|\s)hello.txt($|\s)
|
||||||
- world.txt
|
- (^|\s)world.txt($|\s)
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你應該可以看到終端機上顯示了 `hello.txt` 和 `world.txt` 這兩個檔案。
|
你應該可以看到終端機上顯示了 `hello.txt` 和 `world.txt` 這兩個檔案。
|
||||||
- 接下來,我們來試試看建立資料夾吧!
|
接下來,我們來試試看建立資料夾吧!
|
||||||
- '`mkdir` 指令可以建立一個空的資料夾。'
|
`mkdir` 指令可以建立一個空的資料夾。
|
||||||
- 使用方式是 `mkdir [資料夾名稱]`。
|
使用方式是 `mkdir [資料夾名稱]`。
|
||||||
- 請你試著在終端機上建立一個資料夾,名稱為 `notes`。
|
請你試著在終端機上建立一個資料夾,名稱為 `notes`。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `mkdir notes` 來建立一個資料夾
|
- ➤ 輸入 `mkdir notes` 來建立一個資料夾
|
||||||
|
|
||||||
@@ -122,9 +160,9 @@ stages:
|
|||||||
type: folder
|
type: folder
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 非常好,你已經成功建立了 `notes` 資料夾。
|
非常好,你已經成功建立了 `notes` 資料夾。
|
||||||
- 接下來,請你用 `ls` 指令來查看剛剛建立的資料夾。
|
接下來,請你用 `ls` 指令來查看剛剛建立的資料夾。
|
||||||
|
|
||||||
- id: ls-mkdir
|
- id: ls-mkdir
|
||||||
task: 使用 `ls` 查看剛建立的資料夾
|
task: 使用 `ls` 查看剛建立的資料夾
|
||||||
@@ -137,11 +175,11 @@ stages:
|
|||||||
- notes
|
- notes
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你應該可以看到終端機上顯示了 `notes` 這個資料夾。
|
你應該可以看到終端機上顯示了 `notes` 這個資料夾。
|
||||||
- 接下來,我們要在 `notes` 資料夾中建立一個 `note.txt` 的檔案。
|
接下來,我們要在 `notes` 資料夾中建立一個 `note.txt` 的檔案。
|
||||||
- 首先,我們要先進入 `notes` 資料夾。
|
首先,我們要先進入 `notes` 資料夾。
|
||||||
- 還記得之前的 `cd` 指令嗎?請你試著使用 `cd` 指令來進入 `notes` 資料夾。
|
還記得之前的 `cd` 指令嗎?請你試著使用 `cd` 指令來進入 `notes` 資料夾。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `cd notes` 來進入 `notes` 資料夾
|
- ➤ 輸入 `cd notes` 來進入 `notes` 資料夾
|
||||||
- ➤ 可以在輸入到 `cd n` 時按下 `Tab` 鍵來自動補齊資料夾名稱
|
- ➤ 可以在輸入到 `cd n` 時按下 `Tab` 鍵來自動補齊資料夾名稱
|
||||||
@@ -155,9 +193,9 @@ stages:
|
|||||||
- ^/home/commander/notes$
|
- ^/home/commander/notes$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你已經成功進入了 `notes` 資料夾。
|
你已經成功進入了 `notes` 資料夾。
|
||||||
- 接下來,請你試著在 `notes` 資料夾裡建立一個檔案,檔名為 `note.txt`。
|
接下來,請你試著在 `notes` 資料夾裡建立一個檔案,檔名為 `note.txt`。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `touch note.txt` 來建立一個檔案
|
- ➤ 輸入 `touch note.txt` 來建立一個檔案
|
||||||
|
|
||||||
@@ -171,9 +209,9 @@ stages:
|
|||||||
type: file
|
type: file
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 非常好,你已經成功建立了 `note.txt` 檔案。
|
非常好,你已經成功建立了 `note.txt` 檔案。
|
||||||
- 跟之前一樣,你可以使用 `ls` 指令來查看目前所在資料夾中有哪些檔案。
|
跟之前一樣,你可以使用 `ls` 指令來查看目前所在資料夾中有哪些檔案。
|
||||||
|
|
||||||
- id: END
|
- id: END
|
||||||
task: 使用 `ls` 指令來查看目前所在資料夾中有哪些檔案
|
task: 使用 `ls` 指令來查看目前所在資料夾中有哪些檔案
|
||||||
@@ -186,7 +224,7 @@ stages:
|
|||||||
- note.txt
|
- note.txt
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你應該可以看到終端機上顯示了 `note.txt` 這個檔案。
|
你應該可以看到終端機上顯示了 `note.txt` 這個檔案。
|
||||||
- 恭喜你完成了這個關卡!
|
恭喜你完成了這個關卡!
|
||||||
- 在下一個關卡中,你將學習如何複製檔案與資料夾。
|
在下一個關卡中,你將學習如何複製檔案與資料夾。
|
||||||
+35
-78
@@ -4,139 +4,96 @@ exceptions:
|
|||||||
- '[\uff21-\uff3a\uff41-\uff5a]'
|
- '[\uff21-\uff3a\uff41-\uff5a]'
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '!! 請使用半形英文字母 !!'
|
!! 請使用半形英文字母 !!
|
||||||
|
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ' '
|
- ' '
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '!! 請使用半形空格 !!'
|
!! 請使用半形空格 !!
|
||||||
|
|
||||||
- condition:
|
|
||||||
or:
|
|
||||||
- command:
|
|
||||||
- '`'
|
|
||||||
- command:
|
|
||||||
- \'
|
|
||||||
responses:
|
|
||||||
- type: narrative
|
|
||||||
content:
|
|
||||||
- '!! 不需要輸入上下引號 !!'
|
|
||||||
|
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ^echo\S$
|
- ^echo\S
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '!! `echo` 後面要加空格 !!'
|
!! `echo` 後面要加空格 !!
|
||||||
|
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ^cd\S$
|
- ^cd\S
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '!! `cd` 後面要加空格 !!'
|
!! `cd` 後面要加空格 !!
|
||||||
|
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ^ls\S$
|
- ^ls\S
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '!! `ls` 後面要加空格 !!'
|
!! `ls` 後面要加空格 !!
|
||||||
|
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ^cat\S$
|
- ^cat\S
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '!! `cat` 後面要加空格 !!'
|
!! `cat` 後面要加空格 !!
|
||||||
|
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ^touch\S$
|
- ^touch\S
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '!! `touch` 後面要加空格 !!'
|
!! `touch` 後面要加空格 !!
|
||||||
|
|
||||||
- condition:
|
- condition:
|
||||||
not:
|
not:
|
||||||
command:
|
command:
|
||||||
- ^rmdir$
|
- ^rmdir
|
||||||
command:
|
command:
|
||||||
- ^rm\S$
|
- ^rm\S
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '!! `rm` 後面要加空格 !!'
|
!! `rm` 後面要加空格 !!
|
||||||
|
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ^mv\S$
|
- ^mv\S
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '!! `mv` 後面要加空格 !!'
|
!! `mv` 後面要加空格 !!
|
||||||
|
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ^mkdir\S$
|
- ^mkdir\S
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '!! `mkdir` 後面要加空格 !!'
|
!! `mkdir` 後面要加空格 !!
|
||||||
|
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ^rmdir\S$
|
- ^rmdir\S
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '!! `rmdir` 後面要加空格 !!'
|
!! `rmdir` 後面要加空格 !!
|
||||||
|
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ^cp\S$
|
- ^cp\S
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '!! `cp` 後面要加空格 !!'
|
!! `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` !!'
|
|
||||||
+61
-55
@@ -4,24 +4,24 @@ instruction: |
|
|||||||
1. 如何使用 `ls` 查看資料夾的內容
|
1. 如何使用 `ls` 查看資料夾的內容
|
||||||
2. 如何使用 `cd` 在資料夾之間移動
|
2. 如何使用 `cd` 在資料夾之間移動
|
||||||
|
|
||||||
準備好就輸入 `echo start` 開始吧!
|
按下中間的 `start` 按鈕來開始這個關卡。
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- get-started
|
- get-started
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 按下 `start` 按鈕開始教程
|
||||||
condition:
|
condition:
|
||||||
output:
|
command:
|
||||||
- ^start$
|
- ^echo start$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 在終端機中,並沒有圖形介面可以讓你查看資料夾的內容。
|
在終端機中,並沒有圖形介面可以讓你查看資料夾的內容。
|
||||||
- 但是你可以透過 `ls` 指令來查看資料夾的內容。
|
但是你可以透過 `ls` 指令來查看資料夾的內容。
|
||||||
- '`ls` 指令是 list 的縮寫,也就是列出檔案的意思。'
|
`ls` 指令(第一個字是小寫L)是 list 的簡寫,也就是列出檔案的意思。
|
||||||
- 在輸入 `ls` 指令後,終端機會輸出資料夾中包含的檔案和資料夾,右邊的檔案樹也會隨之改變。
|
在輸入 `ls` 指令後,終端機會輸出資料夾中包含的檔案和資料夾。
|
||||||
- 現在,請你輸入 `ls` 指令,查看資料夾中有什麼東西。
|
現在,請你輸入 `ls` 指令,查看資料夾中有什麼東西。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `ls` 並查看資料夾的內容
|
- ➤ 輸入 `ls` 並查看資料夾的內容
|
||||||
|
|
||||||
@@ -36,24 +36,23 @@ stages:
|
|||||||
- condition:
|
- condition:
|
||||||
not:
|
not:
|
||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander$
|
- /home/commander
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 請先回到 `/home/commander/` 資料夾
|
請先回到 `/home/commander/` 資料夾
|
||||||
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你應該能看到資料夾中的內容了。
|
你應該能看到資料夾中的內容了。
|
||||||
- 在畫面右邊的檔案樹也有了改變。
|
在畫面右邊的檔案樹也有了改變。
|
||||||
- 可以看出,在這個資料夾中包含了兩個資料夾,分別是 `folder1` 和 `folder2`。
|
可以看出,在這個資料夾中包含了兩個資料夾,分別是 `folder1` 和 `folder2` 。
|
||||||
- content:
|
- content: |
|
||||||
- 接下來,我要教你如何進入資料夾。
|
接下來,我要教你如何進入資料夾。
|
||||||
- '`cd` 指令是 change directory 的縮寫,也就是改變路徑的意思。'
|
`cd` 指令是 change directory 的縮寫,也就是改變路徑的意思。
|
||||||
- 你可以通過輸入 `cd [資料夾名稱]` 來進入資料夾。
|
你可以通過輸入 `cd [資料夾名稱]` 來進入資料夾。
|
||||||
- 例如你要進入 `folder1` 資料夾,你可以輸入
|
例如你要進入 `folder1` 資料夾,你可以輸入 `cd folder1` 。
|
||||||
- '`cd folder1`。'
|
現在,請你進入 `folder1` 資料夾。
|
||||||
- 現在,請你進入 `folder1` 資料夾。
|
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `cd folder1` 來進入 `folder1` 資料夾
|
- ➤ 輸入 `cd folder1` 來進入 `folder1` 資料夾
|
||||||
|
|
||||||
@@ -65,30 +64,37 @@ stages:
|
|||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander/folder1$
|
- ^/home/commander/folder1$
|
||||||
exceptions:
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- folderl
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content: |
|
||||||
|
`folder1` 的最後是數字 `1` 不是字母 `l`。
|
||||||
- condition:
|
- condition:
|
||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander$
|
- ^/home/commander$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: 輸入 `cd folder1` 來進入 `folder1` 資料夾
|
||||||
- 輸入 `cd folder1` 來進入 `folder1` 資料夾
|
|
||||||
- condition:
|
- condition:
|
||||||
not:
|
not:
|
||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander$
|
- ^/home/commander$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 請先回到 `/home/commander/` 資料夾
|
請先回到 `/home/commander/` 資料夾
|
||||||
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
||||||
|
再輸入 `cd folder1` 來進入 `folder1` 資料夾
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你成功的進入 `folder1` 資料夾了。
|
你成功的進入 `folder1` 資料夾了。
|
||||||
- 你可以發現指令提示符從原本的
|
你可以發現指令提示符從原本的
|
||||||
- '`commander:~ $` 變成了'
|
`commander:~ $` 變成了
|
||||||
- '`commander:~/folder1 $` 。'
|
`commander:~/folder1 $` 。
|
||||||
- 這表示你現在正位在 `folder1` 資料夾中。
|
這表示你現在正位在 `folder1` 資料夾中。
|
||||||
- 現在,請查看 `folder1` 資料夾的內容。
|
現在,請查看 `folder1` 資料夾的內容。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `ls` 查看資料夾的內容
|
- ➤ 輸入 `ls` 查看資料夾的內容
|
||||||
|
|
||||||
@@ -102,11 +108,11 @@ stages:
|
|||||||
command:
|
command:
|
||||||
- ^ls$
|
- ^ls$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你現在可以看到 `folder1` 資料夾中有兩的檔案,分別是 `file1.txt` 跟 `file2.txt`。
|
你現在可以看到 `folder1` 資料夾中有兩個檔案,分別是 `file1.txt` 跟 `file2.txt`。
|
||||||
- 現在我們可以離開 `folder1` 回到原本的資料夾了。
|
現在我們可以離開 `folder1` 回到原本的資料夾了。
|
||||||
- 你可以使用 `cd ..` 回到上一層資料夾。
|
你可以使用 `cd ..` 回到上一層資料夾。
|
||||||
- 這裡的 `..` 代表的是上一層的資料夾。
|
這裡的 `..` 代表的是上一層的資料夾。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `cd ..` 回到上一層資料夾
|
- ➤ 輸入 `cd ..` 回到上一層資料夾
|
||||||
|
|
||||||
@@ -118,9 +124,9 @@ stages:
|
|||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander$
|
- ^/home/commander$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你成功的回到上一層資料夾了。
|
你成功的回到上一層資料夾了。
|
||||||
- 現在,請你剛才所學的指令,進入 `folder2` 資料夾,檢查 `folder2` 資料夾的內容,最後回到這個資料夾。
|
現在,請你剛才所學的指令,進入 `folder2` 資料夾,檢查 `folder2` 資料夾的內容,最後回到這個資料夾。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `cd` 指令進出資料夾
|
- ➤ 使用 `cd` 指令進出資料夾
|
||||||
- ➤ 使用 `ls` 指令查看資料夾的內容
|
- ➤ 使用 `ls` 指令查看資料夾的內容
|
||||||
@@ -133,9 +139,9 @@ stages:
|
|||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander/folder2$
|
- ^/home/commander/folder2$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你成功的進入 `folder2` 資料夾了。
|
你成功的進入 `folder2` 資料夾了。
|
||||||
- 現在,請你查看 `folder2` 資料夾的內容。
|
現在,請你查看 `folder2` 資料夾的內容。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `ls` 查看資料夾的內容
|
- ➤ 使用 `ls` 查看資料夾的內容
|
||||||
|
|
||||||
@@ -147,11 +153,11 @@ stages:
|
|||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander/folder2$
|
- ^/home/commander/folder2$
|
||||||
command:
|
command:
|
||||||
- ls
|
- ^ls$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你看到了 `folder2` 資料夾中的檔案,分別是 `file3.txt` 跟 `file4.txt`。
|
你看到了 `folder2` 資料夾中的檔案,分別是 `file3.txt` 跟 `file4.txt`。
|
||||||
- 現在你可以離開 `folder2` 回到原本的資料夾了。
|
現在你可以離開 `folder2` 回到原本的資料夾了。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `cd ..` 回到上一層資料夾
|
- ➤ 使用 `cd ..` 回到上一層資料夾
|
||||||
|
|
||||||
@@ -163,9 +169,9 @@ stages:
|
|||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander$
|
- ^/home/commander$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你成功的回到原本的資料夾了。
|
你成功的回到原本的資料夾了。
|
||||||
- 恭喜你學會了 `ls` 和 `cd` 指令的用法。
|
恭喜你學會了 `ls` 和 `cd` 指令的用法。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `cd` 指令進出資料夾
|
- ➤ 使用 `cd` 指令進出資料夾
|
||||||
- ➤ 使用 `ls` 指令查看資料夾的內容
|
- ➤ 使用 `ls` 指令查看資料夾的內容
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
echo start
|
echo start
|
||||||
echo hello
|
echo hello
|
||||||
|
echo next
|
||||||
echo finish
|
echo finish
|
||||||
+40
-45
@@ -5,30 +5,22 @@ instruction: |
|
|||||||
1. 如何使用這個網站
|
1. 如何使用這個網站
|
||||||
2. 如何使用終端機
|
2. 如何使用終端機
|
||||||
|
|
||||||
在畫面的中間可以看到終端機和提示兩個區塊。
|
按下中間的 `start` 按鈕來開始這個關卡。
|
||||||
請你在終端機中的 `commander:~ $` 後面輸入 `echo start` 並按下 Enter 來開始教程。
|
|
||||||
!! 注意:不需要輸入上下引號 !!
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 按下 `start` 按鈕開始
|
||||||
condition:
|
condition:
|
||||||
output:
|
command:
|
||||||
- ^start$
|
- ^echo start$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你好,我是你的 Unix 助手,我叫做 Ada。我將會在之後協助你了解如何使用終端機。
|
在畫面的中下方的區塊是終端機,你可以在終端機裡輸入指令來操作電腦。
|
||||||
- 現在所在的這個區塊就是終端機,你可以在這裡輸入指令來操作電腦。
|
在終端機中,你所看到的 `commander:~ $` 叫做命令提示字串。
|
||||||
- 在終端機中,你所看到的
|
命令提示字串最後的錢字號 `$` 叫命令提示符,你必須將指令輸入在它的後方。
|
||||||
- '`commander:~ $` 是指令提示符號,它能讓你知道現在可以輸入指令了。'
|
當你輸入完指令並按下 Enter 之後,終端機會執行你輸入的指令,並且會從終端機的下一行開始輸出指令的執行結果。
|
||||||
- 你必須將指令輸入在它的後方。
|
直到你看到新的 `commander:~ $` 出現時,代表你可以輸入下一個指令了。
|
||||||
- 當你輸入完指令並按下 Enter 之後,終端機會執行你輸入的指令。
|
現在請你輸入 `echo hello` 來看看會發生什麼事。
|
||||||
- 並且會將執行結果輸出在終端機的下一行。
|
|
||||||
- 這個終端機的功能和你在 Linux 電腦上看到的終端機是一樣的。
|
|
||||||
- 唯一的差別是,在這裡會有劇情和提示來協助你學習如何使用終端機。
|
|
||||||
- 在輸入指令之後,如果你看到用綠色 `=====` 組成的分隔線,代表在分隔線下方的內容是添加的內容,在真實的終端機中不會出現。
|
|
||||||
- 而終端機的輸出,是在綠色 `=====` 分隔線上方的白色內容。
|
|
||||||
- 現在請你輸入 `echo hello` 來看看會發生什麼事。
|
|
||||||
hints:
|
hints:
|
||||||
- ➤ 在 `commander:~ $` 後面輸入 `echo hello` 並按下 Enter
|
- ➤ 在 `commander:~ $` 後面輸入 `echo hello` 並按下 Enter
|
||||||
|
|
||||||
@@ -40,38 +32,41 @@ stages:
|
|||||||
command:
|
command:
|
||||||
- ^echo hello$
|
- ^echo hello$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你應該可以看到終端機上顯示了:
|
你應該可以看到終端機上顯示了以下內容:
|
||||||
- commander:~ $ echo hello
|
```
|
||||||
- hello
|
commander:~ $ echo hello
|
||||||
- 第一行的 `commander:~ $` 是指令提示符, `echo hello` 是你輸入的指令。
|
hello
|
||||||
- 第二行的 `hello` 則是終端機的輸出。
|
```
|
||||||
- content:
|
第一行的 `commander:~ $` 是命令提示字串,接在後方的 `echo hello` 是你輸入的指令。
|
||||||
- 現在你知道如何在終端機上輸入指令,以及查看終端機輸出的位置。
|
第二行的 `hello` 則是終端機的輸出結果。
|
||||||
- 指令必須輸入在指令提示符號的後面,然而當指令提示符消失時,你就無法輸入指令了。
|
你現在知道了如何在終端機上輸入指令,以及查看終端機輸出的位置。
|
||||||
- 這時候你可以按下 `Ctrl + C` 來嘗試解決這個問題。
|
現在請輸入 `echo next` 來繼續下一個步驟。
|
||||||
- 在終端機中, `Ctrl + C` 並不能用來複製,而是用來中斷目前正在執行的指令。
|
|
||||||
- 因此,如果你前一次的指令執行時卡住了,或是執行很久沒有反應,你可以按下 `Ctrl + C` 來中斷指令,讓你可以繼續輸入指令。
|
|
||||||
- 如果 `Ctrl + C` 也沒有辦法解決問題,可以按按看 `esc` 或 `q` 鍵。如果都沒用的話,按下畫面右下角的 `重來` 按鈕重新開始關卡。
|
|
||||||
- content:
|
|
||||||
- 在終端機下方會顯示提示,你可以用提示右上角的箭頭來查看之前的提示。
|
|
||||||
- 在畫面左邊的區域是關卡說明,你可以在這裡看到關卡的介紹,以及現在需要完成的任務。
|
|
||||||
- 在畫面右邊的區域是檔案樹,它會根據你所探索的資料夾發生變化,讓你更清楚檔案之間的位置關係。
|
|
||||||
- 畫面右上角的圖示分別是 `查看地圖`、`回報錯誤` 以及 `登出` 按鈕。
|
|
||||||
- content:
|
|
||||||
- 第一關的文字內容比較多,可以慢慢讀沒關係。
|
|
||||||
- 如果忘記了也可以隨時回來複習。
|
|
||||||
- 需要的話可以按下 `Enter` 來快速跳過劇情說明。
|
|
||||||
- 如果你已經讀完了上述內容,請輸入 `echo finish` 來結束這一關。
|
|
||||||
hints:
|
hints:
|
||||||
- ➤ 按下 `Ctrl + C` 能夠中斷指令
|
- ➤ 輸入 `echo next` 繼續下一個步驟
|
||||||
- ➤ 劇情進行時可以按下 `Enter` 來快速跳過
|
|
||||||
|
- id: echo-next
|
||||||
|
task: 在終端機輸入 `echo next`
|
||||||
|
requirements:
|
||||||
|
- echo-hello
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^echo next$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
`Ctrl + C` 在終端機中是一個很重要的快捷鍵,它可以用來中斷正在執行的指令,並產生一個新的命令提示字串,讓你可以繼續輸入指令。
|
||||||
|
如果指令執行很久沒有反應,或你因為其他原因想要中斷指令,你可以按下 `Ctrl + C` 來中斷指令。
|
||||||
|
又或者你輸入了錯誤的指令,卻懶得慢慢刪除,你也可以按下 `Ctrl + C` 來產生新的命令提示字串重新輸入指令。
|
||||||
|
如果你已經理解了剛才的說明,請輸入 `echo finish` 來結束這一關。
|
||||||
|
hints:
|
||||||
|
- ➤ 按下 `Ctrl + C` 能夠中斷正在執行的指令
|
||||||
|
- ➤ 按下 `Ctrl + C` 能夠產生新的命令提示字串
|
||||||
- ➤ 輸入 `echo finish` 來結束這一關
|
- ➤ 輸入 `echo finish` 來結束這一關
|
||||||
|
|
||||||
- id: END
|
- id: END
|
||||||
task: 結束關卡
|
task: 結束關卡
|
||||||
requirements:
|
requirements:
|
||||||
- echo-hello
|
- echo-next
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo finish$
|
- ^echo finish$
|
||||||
+31
-31
@@ -21,17 +21,17 @@ stages:
|
|||||||
- 你聽到了一個聲音:
|
- 你聽到了一個聲音:
|
||||||
- type: dialogue
|
- type: dialogue
|
||||||
speaker: Unix 精靈
|
speaker: Unix 精靈
|
||||||
content:
|
content: |
|
||||||
- 你好,指令官,我是你的 Unix 精靈。我已在此恭候多時。
|
你好,指令官,我是你的 Unix 精靈。我已在此恭候多時。
|
||||||
- 這個世界是在 3560000 時脈以前,由初代王建立了 Unix 律法,所形成的世界。然而在經過了好幾世代的王後,終於誕生了永恆之王——薩加(Saga),她比歷代的君王都更為強大,且從來不會衰老,使得這個世界變得前所未有的強大。
|
這個世界是在 3560000 時脈以前,由初代王建立了 Unix 律法,所形成的世界。然而在經過了好幾世代的王後,終於誕生了永恆之王——薩加(Saga),她比歷代的君王都更為強大,且從來不會衰老,使得這個世界變得前所未有的強大。
|
||||||
- 然而,外在世界一直不斷在改變,同時在面臨外敵的不斷攻擊,而薩加卻始終用同一套治理的手段。最終,在 160000 時脈前的一次入侵中,根部遭到攻陷,薩加遭到封印,而這個國家也陷入了黑暗。
|
然而,外在世界一直不斷在改變,同時在面臨外敵的不斷攻擊,而薩加卻始終用同一套治理的手段。最終,在 160000 時脈前的一次入侵中,根部遭到攻陷,薩加遭到封印,而這個國家也陷入了黑暗。
|
||||||
- 由於長久以來依賴於薩加,世人已失去了「指令」的力量,即將走向衰敗和滅亡的命運。
|
由於長久以來依賴於薩加,世人已失去了「指令」的力量,即將走向衰敗和滅亡的命運。
|
||||||
- 你的任務,就是找回被世人遺忘的指令,奪回 Linux 之根,成為 Linux 之王。
|
你的任務,就是找回被世人遺忘的指令,奪回 Linux 之根,成為 Linux 之王。
|
||||||
- 首先,你必須要學會如何使用指令。
|
首先,你必須要學會如何使用指令。
|
||||||
- 你要學習的第一個指令,是「搜索」的指令:`ls` (list)。 `ls` 可以用來查看一個目錄中有哪些檔案。
|
你要學習的第一個指令,是「搜索」的指令:`ls` (list)。 `ls` 可以用來查看一個目錄中有哪些檔案。
|
||||||
- 現在,請你輸入 `ls` 來查看目前的目錄下有哪些檔案。
|
現在,請你輸入 `ls` 來查看目前的目錄下有哪些檔案。
|
||||||
hints: |
|
hints:
|
||||||
➤ 使用 `ls` 指令來查看目前的目錄下有哪些檔案。
|
- ➤ 使用 `ls` 指令來查看目前的目錄下有哪些檔案。
|
||||||
|
|
||||||
- id: find-scroll
|
- id: find-scroll
|
||||||
task: 搜索卷軸
|
task: 搜索卷軸
|
||||||
@@ -44,20 +44,20 @@ stages:
|
|||||||
- ^ls$
|
- ^ls$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 一瞬間,視野變得清晰起來,你發現自己身處在一個石頭建築的房間裡。
|
一瞬間,視野變得清晰起來,你發現自己身處在一個石頭建築的房間裡。
|
||||||
- 你發現了一個檔案,檔名是 `forgotten-scroll.txt`。
|
你發現了一個檔案,檔名是 `forgotten-scroll.txt`。
|
||||||
- type: dialogue
|
- type: dialogue
|
||||||
speaker: Unix 精靈
|
speaker: Unix 精靈
|
||||||
content:
|
content: |
|
||||||
- 你找到了一個被遺忘的卷軸,上面寫著解除封印的咒語。
|
你找到了一個被遺忘的卷軸,上面寫著解除封印的咒語。
|
||||||
- 使用指令 `cat [ 檔案名稱 ]` (concatenate)來查看檔案內容。
|
使用指令 `cat [ 檔案名稱 ]` (concatenate)來查看檔案內容。
|
||||||
- 我可以為你提供一些指令的提示,只要輸入指令的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
|
我可以為你提供一些指令的提示,只要輸入指令的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
|
||||||
- 例如你只要輸入檔案名稱的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
|
例如你只要輸入檔案名稱的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
|
||||||
hints: |
|
hints:
|
||||||
➤ 使用 `cat [ 檔案名稱 ]` 指令來查看檔案內容。
|
- ➤ 使用 `cat [ 檔案名稱 ]` 指令來查看檔案內容。
|
||||||
➤ 使用 `Tab` 鍵來取得指令提示。
|
- ➤ 使用 `Tab` 鍵來取得指令提示。
|
||||||
➤ 如果卡在未知的情況,按下 `Ctrl + C` 嘗試離開。
|
- ➤ 如果卡在未知的情況,按下 `Ctrl + C` 嘗試離開。
|
||||||
|
|
||||||
- id: read-forgotten-scroll
|
- id: read-forgotten-scroll
|
||||||
task: 查看卷軸
|
task: 查看卷軸
|
||||||
@@ -71,11 +71,11 @@ stages:
|
|||||||
responses:
|
responses:
|
||||||
- type: dialogue
|
- type: dialogue
|
||||||
speaker: Unix 精靈
|
speaker: Unix 精靈
|
||||||
content:
|
content: |
|
||||||
- 你找到的是一個被遺忘的卷軸,上面寫著解除封印的咒語。
|
你找到的是一個被遺忘的卷軸,上面寫著解除封印的咒語。
|
||||||
- 使用你已經知道的 `echo [ 咒語 ]` 指令來詠唱咒語。
|
使用你已經知道的 `echo [ 咒語 ]` 指令來詠唱咒語。
|
||||||
hints: |
|
hints:
|
||||||
➤ 輸入 `echo Hello World` 來詠唱咒語。
|
- ➤ 輸入 `echo Hello World` 來詠唱咒語。
|
||||||
|
|
||||||
- id: END
|
- id: END
|
||||||
task: 解除封印
|
task: 解除封印
|
||||||
@@ -92,6 +92,6 @@ stages:
|
|||||||
- 突然之間,你感覺到一股強大的力量從你的身體中湧出,你的身體開始發光,你的手中出現了一把魔法之劍。
|
- 突然之間,你感覺到一股強大的力量從你的身體中湧出,你的身體開始發光,你的手中出現了一把魔法之劍。
|
||||||
- type: dialogue
|
- type: dialogue
|
||||||
speaker: Unix 精靈
|
speaker: Unix 精靈
|
||||||
content:
|
content: |
|
||||||
- 恭喜您,我的指令官,您已成功解除了封印。
|
恭喜您,我的指令官,您已成功解除了封印。
|
||||||
- 請跟著在接下來的關卡,一步步取回 Linux 之根吧!
|
請跟著在接下來的關卡,一步步取回 Linux 之根吧!
|
||||||
@@ -8,5 +8,4 @@ mv world.txt wow.txt
|
|||||||
ls
|
ls
|
||||||
mv notes docs
|
mv notes docs
|
||||||
ls
|
ls
|
||||||
echo hint
|
|
||||||
mv docs/hello.txt hi.txt
|
mv docs/hello.txt hi.txt
|
||||||
+51
-61
@@ -3,49 +3,49 @@ instruction: |
|
|||||||
在這個關卡中,你將會學到:
|
在這個關卡中,你將會學到:
|
||||||
1. 如何用 `mv` 移動檔案與資料夾
|
1. 如何用 `mv` 移動檔案與資料夾
|
||||||
|
|
||||||
準備好就輸入 `echo start` 開始吧!
|
按下中間的 `start` 按鈕來開始這個關卡。
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- create
|
- create
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始
|
task: 按下 `start` 按鈕開始
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo start$
|
- ^echo start$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 今天我們要學習如何移動檔案與資料夾。
|
今天我們要學習如何移動檔案與資料夾。
|
||||||
- 現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,以及一個 `notes` 資料夾。
|
現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,以及一個 `notes` 資料夾。
|
||||||
- 請先用 `ls` 與 `cat` 來查看它們的內容。
|
請先用 `ls` 與 `cat` 來查看它們的內容。
|
||||||
- 看完之後,使用 `echo continue` 來繼續吧!
|
看完之後,使用 `echo next` 來繼續吧!
|
||||||
hints:
|
hints:
|
||||||
- ➤ 用 `ls`、`ls notes` 來查看資料夾內的檔案
|
- ➤ 用 `ls`、`ls notes` 來查看資料夾內的檔案
|
||||||
- ➤ 用 `cat hello.txt` 以及 `cat world.txt` 來查看檔案內容
|
- ➤ 用 `cat hello.txt` 以及 `cat world.txt` 來查看檔案內容
|
||||||
- ➤ 完成後輸入 `echo continue` 來繼續
|
- ➤ 完成後輸入 `echo next` 來繼續
|
||||||
|
|
||||||
- id: continue
|
- id: next
|
||||||
requirements:
|
requirements:
|
||||||
- start
|
- start
|
||||||
task: 查看檔案內容,完成後輸入 `echo continue` 來繼續
|
task: 查看檔案內容,完成後輸入 `echo next` 來繼續
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo continue$
|
- ^echo next$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 現在我們要來學習如何移動檔案與資料夾。
|
現在我們要來學習如何移動檔案與資料夾。
|
||||||
- '`mv` 指令是移動 move 的縮寫,可以用來移動檔案與資料夾。'
|
`mv` 指令是移動 move 的縮寫,可以用來移動檔案與資料夾。
|
||||||
- 使用方式為 `mv [來源] [目的地]`。
|
使用方式為 `mv [來源] [目的地]`。
|
||||||
- 我們先來將 `hello.txt` 移動到 `notes` 資料夾中吧!
|
我們先來將 `hello.txt` 移動到 `notes` 資料夾中吧!
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `mv hello.txt notes` 來將 `hello.txt` 移動到 `notes` 資料夾中
|
- ➤ 使用 `mv hello.txt notes` 來將 `hello.txt` 移動到 `notes` 資料夾中
|
||||||
|
|
||||||
- id: mv-hello
|
- id: mv-hello
|
||||||
requirements:
|
requirements:
|
||||||
- continue
|
- next
|
||||||
task: 將 `hello.txt` 移動到 `notes` 資料夾中
|
task: 將 `hello.txt` 移動到 `notes` 資料夾中
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
@@ -57,9 +57,9 @@ stages:
|
|||||||
exists: false
|
exists: false
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你做得很好!
|
你做得很好!
|
||||||
- 我們用 `ls` 跟 `ls notes` 來查看一下資料夾內的檔案吧!
|
我們用 `ls` 跟 `ls notes` 來查看一下資料夾內的檔案吧!
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `ls`、`ls notes` 來查看資料夾內的檔案
|
- ➤ 使用 `ls`、`ls notes` 來查看資料夾內的檔案
|
||||||
|
|
||||||
@@ -74,8 +74,8 @@ stages:
|
|||||||
- ^ls$
|
- ^ls$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '`hello.txt` 已經不在資料夾中了!'
|
`hello.txt` 已經不在資料夾中了!
|
||||||
|
|
||||||
- id: ls-notes-hello
|
- id: ls-notes-hello
|
||||||
requirements:
|
requirements:
|
||||||
@@ -90,29 +90,19 @@ stages:
|
|||||||
- hello.txt
|
- hello.txt
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '`hello.txt` 出現在 `notes` 資料夾中了!'
|
`hello.txt` 出現在 `notes` 資料夾中了!
|
||||||
|
|
||||||
- id: continue-mv
|
|
||||||
requirements:
|
|
||||||
- ls-no-hello
|
|
||||||
- ls-notes-hello
|
|
||||||
task: 輸入 `echo continue` 來繼續
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ^echo continue$
|
|
||||||
responses:
|
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '`mv` 指令也可以用來更改檔案名稱。'
|
`mv` 指令也可以用來更改檔案名稱。
|
||||||
- '只要將目的地改成新的檔案名稱就可以了。'
|
只要將目的地改成新的檔案名稱就可以了。
|
||||||
- 現在我們要將 `world.txt` 更名為 `wow.txt`。
|
現在我們要將 `world.txt` 更名為 `wow.txt`。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `mv world.txt wow.txt` 來將 `world.txt` 更名為 `wow.txt`
|
- ➤ 使用 `mv world.txt wow.txt` 來將 `world.txt` 更名為 `wow.txt`
|
||||||
|
|
||||||
- id: mv-world
|
- id: mv-world
|
||||||
requirements:
|
requirements:
|
||||||
- continue-mv
|
- ls-notes-hello
|
||||||
task: 將 `world.txt` 更名為 `wow.txt`
|
task: 將 `world.txt` 更名為 `wow.txt`
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
@@ -124,9 +114,9 @@ stages:
|
|||||||
exists: false
|
exists: false
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你做得很好!
|
你做得很好!
|
||||||
- 我們用 `ls` 來確認一下資料夾內的檔案吧!
|
我們用 `ls` 來確認一下資料夾內的檔案吧!
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `ls` 來查看資料夾內的檔案
|
- ➤ 使用 `ls` 來查看資料夾內的檔案
|
||||||
|
|
||||||
@@ -143,12 +133,12 @@ stages:
|
|||||||
- wow.txt
|
- wow.txt
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '`world.txt` 現在已經變成 `wow.txt` 了。'
|
`world.txt` 現在已經變成 `wow.txt` 了。
|
||||||
- 接下來我們來學習如何移動資料夾吧!
|
接下來我們來學習如何移動資料夾吧!
|
||||||
- 移動資料夾非常簡單,方法跟移動檔案一樣。
|
移動資料夾非常簡單,方法跟移動檔案一樣。
|
||||||
- '`mv` 指令會自動將資料夾內的所有檔案一起移動'
|
`mv` 指令會自動將資料夾內的所有檔案一起移動
|
||||||
- 我們先來將 `notes` 資料夾改名叫作 `docs` 吧!
|
我們先來將 `notes` 資料夾改名叫作 `docs` 吧!
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `mv notes docs` 來將 `notes` 資料夾改名叫作 `docs`
|
- ➤ 使用 `mv notes docs` 來將 `notes` 資料夾改名叫作 `docs`
|
||||||
|
|
||||||
@@ -166,9 +156,9 @@ stages:
|
|||||||
exists: false
|
exists: false
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你做得很好!
|
你做得很好!
|
||||||
- 我們用 `ls` 來確認一下資料夾內的檔案吧!
|
我們用 `ls` 來確認一下資料夾內的檔案吧!
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `ls` 來查看資料夾內的檔案
|
- ➤ 使用 `ls` 來查看資料夾內的檔案
|
||||||
|
|
||||||
@@ -185,11 +175,11 @@ stages:
|
|||||||
- docs
|
- docs
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- '`notes` 資料夾已經變成 `docs` 了。'
|
`notes` 資料夾已經變成 `docs` 了。
|
||||||
- 那麼最後來考考你:
|
那麼最後來考考你:
|
||||||
- 試著將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`。
|
試著將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`。
|
||||||
- 如果你不知道怎麼做,可以輸入 `echo hint` 來取得提示。
|
如果你不知道怎麼做,可以輸入 `echo hint` 來取得提示。
|
||||||
|
|
||||||
- id: END
|
- id: END
|
||||||
requirements:
|
requirements:
|
||||||
@@ -209,12 +199,12 @@ stages:
|
|||||||
- ^echo hint$
|
- ^echo hint$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你可以使用 `mv docs/hello.txt hi.txt` 來完成這個任務。
|
你可以使用 `mv docs/hello.txt hi.txt` 來完成這個任務。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `mv docs/hello.txt hi.txt` 來將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`
|
- ➤ 使用 `mv docs/hello.txt hi.txt` 來將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 你做得很好!
|
你做得很好!
|
||||||
- 你已經學會如何移動檔案與資料夾了!
|
你已經學會如何移動檔案與資料夾了!
|
||||||
+52
-52
@@ -4,7 +4,7 @@ instruction: |
|
|||||||
1. 絕對路徑和相對路徑
|
1. 絕對路徑和相對路徑
|
||||||
2. 如何使用 `pwd` 顯示目前的路徑
|
2. 如何使用 `pwd` 顯示目前的路徑
|
||||||
|
|
||||||
準備好就輸入 `echo start` 開始吧!
|
按下中間的 `start` 按鈕來開始這個關卡。
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- cast
|
- cast
|
||||||
@@ -12,16 +12,16 @@ requirements:
|
|||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 按下 `start` 按鈕開始教程
|
||||||
condition:
|
condition:
|
||||||
output:
|
command:
|
||||||
- ^start$
|
- ^echo start$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 在 Linux 系統中,每個資料夾和檔案都有獨一無二的路徑。
|
在 Linux 系統中,每個資料夾和檔案都有獨一無二的路徑。
|
||||||
- 而路徑又分為相對路徑和絕對路徑。
|
而路徑又分為相對路徑和絕對路徑。
|
||||||
- 相對路徑是從目前所在的資料夾開始的路徑,而絕對路徑則是從根目錄開始的路徑。
|
相對路徑是從目前所在的資料夾開始的路徑,而絕對路徑則是從根目錄開始的路徑。
|
||||||
- 請你先用 `ls` 指令查看這個資料夾中的檔案。
|
請你先用 `ls` 指令查看這個資料夾中的檔案。
|
||||||
|
|
||||||
- id: ls
|
- id: ls
|
||||||
task: 查看資料夾中的檔案
|
task: 查看資料夾中的檔案
|
||||||
@@ -33,8 +33,8 @@ stages:
|
|||||||
command:
|
command:
|
||||||
- ^ls$
|
- ^ls$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 現在你使用 `cd` 指令進入 `folder1` 資料夾。
|
現在你使用 `cd` 指令進入 `folder1` 資料夾。
|
||||||
|
|
||||||
- id: cd-folder1
|
- id: cd-folder1
|
||||||
task: 進入 `folder1` 資料夾
|
task: 進入 `folder1` 資料夾
|
||||||
@@ -44,14 +44,14 @@ stages:
|
|||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander/folder1$
|
- ^/home/commander/folder1$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你進入了 `folder1` 資料夾。
|
你進入了 `folder1` 資料夾。
|
||||||
- 在進入 `folder1` 資料夾時,使用的 `cd folder1` 中的 `folder1` 就是相對路徑。
|
在進入 `folder1` 資料夾時,使用的 `cd folder1` 中的 `folder1` 就是相對路徑。
|
||||||
- 相同的道理 `cd ..` 中的 `..` 也是相對路徑。
|
相同的道理 `cd ..` 中的 `..` 也是相對路徑。
|
||||||
- 相對路徑可以不只有一層資料夾。
|
相對路徑可以不只有一層資料夾。
|
||||||
- 例如:現在想要進入 `folder2` 資料夾的話,可以使用 `cd ..` 回到上一層資料夾,再使用 `cd folder2` 進入 `folder2` 資料夾。
|
例如:現在想要進入 `folder2` 資料夾的話,可以使用 `cd ..` 回到上一層資料夾,再使用 `cd folder2` 進入 `folder2` 資料夾。
|
||||||
- 然而,你也可以用 `/` 將 `..` 和 `folder2` 依序連接起來,使用 `cd ../folder2` 來一次進入 `folder2` 資料夾。
|
然而,你也可以用 `/` 將 `..` 和 `folder2` 依序連接起來,使用 `cd ../folder2` 來一次進入 `folder2` 資料夾。
|
||||||
- 現在試著用這個方式進入 `folder2` 資料夾看看吧。
|
現在試著用這個方式進入 `folder2` 資料夾看看吧。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 請輸入 `cd ../folder2` 來進入 `folder2` 資料夾。
|
- ➤ 請輸入 `cd ../folder2` 來進入 `folder2` 資料夾。
|
||||||
|
|
||||||
@@ -68,14 +68,14 @@ stages:
|
|||||||
- ^/home/commander$
|
- ^/home/commander$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 輸入 `cd folder2` 來進入 `folder2` 資料夾
|
輸入 `cd folder2` 來進入 `folder2` 資料夾
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你成功的用相對路徑進入了 `folder2` 資料夾。
|
你成功的用相對路徑進入了 `folder2` 資料夾。
|
||||||
- 而你也能夠在 `ls` 後加上相對路徑來查看其他資料夾的內容。
|
而你也能夠在 `ls` 後加上相對路徑來查看其他資料夾的內容。
|
||||||
- 例如:現在你想要查看 `folder1` 資料夾中有哪些檔案,你可以使用 `ls ../folder1` 來查看。
|
例如:現在你想要查看 `folder1` 資料夾中有哪些檔案,你可以使用 `ls ../folder1` 來查看。
|
||||||
- 試著用這個方式查看 `folder1` 資料夾中的檔案吧。
|
試著用這個方式查看 `folder1` 資料夾中的檔案吧。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 請輸入 `ls ../folder1` 來查看 `folder1` 資料夾中的檔案。
|
- ➤ 請輸入 `ls ../folder1` 來查看 `folder1` 資料夾中的檔案。
|
||||||
|
|
||||||
@@ -93,16 +93,16 @@ stages:
|
|||||||
- ^/home/commander/folder2$
|
- ^/home/commander/folder2$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 進到 `folder1` 資料夾並用 `ls` 查看內容。
|
進到 `folder1` 資料夾並用 `ls` 查看內容。
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 很好,你現在知道如何運用相對路徑了。
|
很好,你現在知道如何運用相對路徑了。
|
||||||
- 接下來要介紹的是絕對路徑。
|
接下來要介紹的是絕對路徑。
|
||||||
- 絕對路徑是從根目錄開始的路徑,根目錄是位於檔案樹的最上層的資料夾。
|
絕對路徑是從根目錄開始的路徑,根目錄是位於檔案樹的最上層的資料夾。
|
||||||
- 你可以使用 `pwd` 來查看目前所在的絕對路徑。
|
你可以使用 `pwd` 來查看目前所在的絕對路徑。
|
||||||
- '`pwd` 的意思是 `print working directory`,也就是顯示目前所在的路徑。'
|
`pwd` 的意思是 `print working directory`,也就是顯示目前所在的路徑。
|
||||||
- 現在請你使用 `pwd` 來查看目前所在的絕對路徑。
|
現在請你使用 `pwd` 來查看目前所在的絕對路徑。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `pwd` 查看目前所在的絕對路徑
|
- ➤ 輸入 `pwd` 查看目前所在的絕對路徑
|
||||||
|
|
||||||
@@ -122,17 +122,17 @@ stages:
|
|||||||
- ^/home/commander/folder2$
|
- ^/home/commander/folder2$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 請先回到 `folder2` 資料夾,並用 `pwd` 來查看目前所在的絕對路徑
|
請先回到 `folder2` 資料夾,並用 `pwd` 來查看目前所在的絕對路徑
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你可以看到你現在的絕對路徑是 `/home/commander/folder2`。
|
你可以看到你現在的絕對路徑是 `/home/commander/folder2`。
|
||||||
- 而絕對路徑其實也顯示在你的命令提示符中。
|
而絕對路徑其實也顯示在你的命令提示符中。
|
||||||
- '`commander:~/folder2 $` 中的 `~/folder2` 就是你的絕對路徑。'
|
`commander:~/folder2 $` 中的 `~/folder2` 就是你的絕對路徑。
|
||||||
- 而 `~` 則是 `/home/commander` 這個絕對路徑的縮寫,又稱為家目錄,你每次遊戲都會從這裡開始。
|
而 `~` 則是 `/home/commander` 這個絕對路徑的縮寫,又稱為家目錄,你每次遊戲都會從這裡開始。
|
||||||
- '`ls` 和 `cd` 指令也可以使用絕對路徑。'
|
`ls` 和 `cd` 指令也可以使用絕對路徑。
|
||||||
- 例如:你可以使用 `ls ~` 來查看家目錄的內容,或使用 `cd ~` 來回到家目錄。
|
例如:你可以使用 `ls ~` 來查看家目錄的內容,或使用 `cd ~` 來回到家目錄。
|
||||||
- 現在請你使用 `ls ~` 來查看家目錄的內容。
|
現在請你使用 `ls ~` 來查看家目錄的內容。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `ls ~` 來查看家目錄的內容
|
- ➤ 輸入 `ls ~` 來查看家目錄的內容
|
||||||
|
|
||||||
@@ -144,8 +144,8 @@ stages:
|
|||||||
command:
|
command:
|
||||||
- ^ls ~$
|
- ^ls ~$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 現在請使用 `cd ~` 回到家目錄。
|
現在請使用 `cd ~` 回到家目錄。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `cd ~` 來回到家目錄
|
- ➤ 輸入 `cd ~` 來回到家目錄
|
||||||
|
|
||||||
@@ -162,9 +162,9 @@ stages:
|
|||||||
- ^/home/commander$
|
- ^/home/commander$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content: |
|
||||||
- 請使用 `cd ~` 回到家目錄
|
請使用 `cd ~` 回到家目錄
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你回到家目錄了。
|
你回到家目錄了。
|
||||||
- 恭喜!現在你已經知道如何運用絕對路徑和相對路徑,以及查看目前所在的絕對路徑了。
|
恭喜!現在你已經知道如何運用絕對路徑和相對路徑,以及查看目前所在的絕對路徑了。
|
||||||
+31
-29
@@ -3,24 +3,24 @@ instruction: |
|
|||||||
在這個關卡中,你將會學到:
|
在這個關卡中,你將會學到:
|
||||||
1. 如何使用 `cat` 查看檔案的內容
|
1. 如何使用 `cat` 查看檔案的內容
|
||||||
|
|
||||||
準備好就輸入 `echo start` 開始吧!
|
按下中間的 `start` 按鈕來開始這個關卡。
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- path
|
- path
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 按下 `start` 按鈕開始教程
|
||||||
condition:
|
condition:
|
||||||
output:
|
command:
|
||||||
- ^start$
|
- ^echo start$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 在終端機中,並沒有圖形介面可以讓你查看檔案的內容。
|
在終端機中,並沒有圖形介面可以讓你查看檔案的內容。
|
||||||
- 但是你可以透過 `cat` 指令來查看檔案的內容。
|
但是你可以透過 `cat` 指令來查看檔案的內容。
|
||||||
- '`cat` 指令是 concatenate 的縮寫,也就是串接的意思。'
|
`cat` 指令是 concatenate 的縮寫,也就是串接的意思。
|
||||||
- 在輸入 `cat [檔案名稱]` 指令後,終端機會輸出檔案的內容。
|
在輸入 `cat [檔案名稱]` 指令後,終端機會輸出檔案的內容。
|
||||||
- 請你先查看這個資料夾中哪些檔案。
|
請你先查看這個資料夾中哪些檔案。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 請輸入 `ls` 來查看這個資料夾中的檔案。
|
- ➤ 請輸入 `ls` 來查看這個資料夾中的檔案。
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ stages:
|
|||||||
command:
|
command:
|
||||||
- ^ls$
|
- ^ls$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 這個資料夾中有一個檔案和一個資料夾。
|
這個資料夾中有一個檔案和一個資料夾。
|
||||||
- 現在你可以使用 `cat` 指令來查看 `file1.txt` 的內容。
|
現在你可以使用 `cat` 指令來查看 `file1.txt` 的內容。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 請輸入 `cat file1.txt` 來查看檔案的內容。
|
- ➤ 請輸入 `cat file1.txt` 來查看檔案的內容。
|
||||||
|
|
||||||
@@ -50,10 +50,10 @@ stages:
|
|||||||
command:
|
command:
|
||||||
- ^cat file1.txt$
|
- ^cat file1.txt$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你應該能看到檔案的內容了。
|
你應該能看到檔案的內容了。
|
||||||
- '`cat` 指令也可以使用路徑來查看檔案內容'
|
`cat` 指令也可以使用路徑來查看檔案內容
|
||||||
- 請你先用 `ls folder` 查看資料夾內的檔案。
|
請你先用 `ls folder` 查看資料夾內的檔案。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `ls folder` 查看資料夾內的檔案
|
- ➤ 輸入 `ls folder` 查看資料夾內的檔案
|
||||||
|
|
||||||
@@ -71,13 +71,13 @@ stages:
|
|||||||
not:
|
not:
|
||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander$
|
- ^/home/commander$
|
||||||
content:
|
content: |
|
||||||
- 請留在 `/home/commander` 資料夾中。
|
請留在 `/home/commander` 資料夾中。
|
||||||
- 請輸入 `cd ~` 回到 `/home/commander` 資料夾。
|
請輸入 `cd ~` 回到 `/home/commander` 資料夾。
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你現在知道 `folder` 資料夾的內容了。
|
你現在知道 `folder` 資料夾的內容了。
|
||||||
- 請用 `cat` 指令來查看 `folder` 資料夾中的 `file.txt` 檔案。
|
請用 `cat` 指令來查看 `folder` 資料夾中的 `file.txt` 檔案。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 輸入 `cat folder/file.txt` 查看 `file.txt` 檔案的內容。
|
- ➤ 輸入 `cat folder/file.txt` 查看 `file.txt` 檔案的內容。
|
||||||
|
|
||||||
@@ -95,10 +95,12 @@ stages:
|
|||||||
not:
|
not:
|
||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander$
|
- ^/home/commander$
|
||||||
content:
|
responses:
|
||||||
- 請留在 `/home/commander` 資料夾中。
|
- type: narrative
|
||||||
- 請輸入 `cd ~` 回到 `/home/commander` 資料夾。
|
content: |
|
||||||
|
請留在 `/home/commander` 資料夾中。
|
||||||
|
請輸入 `cd ~` 回到 `/home/commander` 資料夾。
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 你成功地查看到 `file.txt` 的內容了。
|
你成功地查看到 `file.txt` 的內容了。
|
||||||
- 你已經會使用 `cat` 指令查看檔案的內容了。
|
你已經會使用 `cat` 指令查看檔案的內容了。
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
echo start
|
echo start
|
||||||
|
ls
|
||||||
rm hello.txt
|
rm hello.txt
|
||||||
ls
|
ls
|
||||||
rm world.txt wow.txt
|
rm world.txt wow.txt
|
||||||
|
|||||||
+79
-74
@@ -6,7 +6,7 @@ instruction: |
|
|||||||
3. 用 `rm -r` 刪除資料夾與它的所有檔案
|
3. 用 `rm -r` 刪除資料夾與它的所有檔案
|
||||||
現在資料夾裡有一些檔案與資料夾,請你使用 `ls` 跟 `cat` 來查看它們的內容。
|
現在資料夾裡有一些檔案與資料夾,請你使用 `ls` 跟 `cat` 來查看它們的內容。
|
||||||
|
|
||||||
準備好就輸入 `echo start` 開始吧!
|
按下中間的 `start` 按鈕來開始這個關卡。
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
- copy
|
- copy
|
||||||
@@ -14,33 +14,44 @@ requirements:
|
|||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始
|
task: 按下 `start` 按鈕開始
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^echo
|
- ^echo start$
|
||||||
output:
|
|
||||||
- start
|
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 今天我們要學習如何刪除檔案與資料夾。
|
今天我們要學習如何刪除檔案與資料夾。
|
||||||
- '首先要來學習 `rm` 指令。`rm` 指令可以用來刪除檔案。使用方式是 `rm [檔案名稱]`。'
|
請先使用 `ls` 指令查看資料夾內容。
|
||||||
- '現在請你使用 `rm` 指令刪除 `hello.txt`。'
|
hints:
|
||||||
|
- ➤ 使用 `ls` 來查看資料夾內容。
|
||||||
|
|
||||||
|
- id: ls
|
||||||
|
requirements:
|
||||||
|
- start
|
||||||
|
task: 使用 `ls` 查看資料夾內容
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ^ls$
|
||||||
|
responses:
|
||||||
|
- content: |
|
||||||
|
首先要來學習 `rm` 指令。`rm` 指令可以用來刪除檔案。使用方式是 `rm [檔案名稱]`。
|
||||||
|
現在請你使用 `rm` 指令刪除 `hello.txt`。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `rm hello.txt` 來刪除 `hello.txt`。
|
- ➤ 使用 `rm hello.txt` 來刪除 `hello.txt`。
|
||||||
|
|
||||||
- id: rm-hello
|
- id: rm-hello
|
||||||
requirements:
|
requirements:
|
||||||
- start
|
- ls
|
||||||
task: 使用 `rm` 刪除 `hello.txt`
|
task: 使用 `rm` 刪除 `hello.txt`
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^rm
|
- ^rm\s
|
||||||
files:
|
files:
|
||||||
- path: hello.txt
|
- path: hello.txt
|
||||||
exists: false
|
exists: false
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- '很好!現在請你使用 `ls` 指令來查看資料夾內容。'
|
很好!現在請你使用 `ls` 指令來查看資料夾內容。
|
||||||
|
|
||||||
- id: ls-no-hello
|
- id: ls-no-hello
|
||||||
requirements:
|
requirements:
|
||||||
@@ -48,15 +59,15 @@ stages:
|
|||||||
task: 使用 `ls` 查看資料夾內容
|
task: 使用 `ls` 查看資料夾內容
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^ls
|
- ^ls$
|
||||||
files:
|
files:
|
||||||
- path: hello.txt
|
- path: hello.txt
|
||||||
exists: false
|
exists: false
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- '你會發現 `hello.txt` 已經不見了。'
|
你會發現 `hello.txt` 已經不見了。
|
||||||
- '`rm` 也可以同時刪除多個檔案,只要把檔案名稱用空白隔開就可以了,也就是 `rm [檔案1] [檔案2] ...`。'
|
`rm` 也可以同時刪除多個檔案,只要把檔案名稱用空白隔開就可以了,也就是 `rm [檔案1] [檔案2] ...`。
|
||||||
- 現在請你使用 `rm` 刪除 `world.txt` 與 `wow.txt`。
|
現在請你使用 `rm` 刪除 `world.txt` 與 `wow.txt`。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `rm world.txt wow.txt` 來刪除 `world.txt` 與 `wow.txt`。
|
- ➤ 使用 `rm world.txt wow.txt` 來刪除 `world.txt` 與 `wow.txt`。
|
||||||
- ➤ 檔案名稱之間要用空白隔開。
|
- ➤ 檔案名稱之間要用空白隔開。
|
||||||
@@ -75,8 +86,8 @@ stages:
|
|||||||
- path: wow.txt
|
- path: wow.txt
|
||||||
exists: false
|
exists: false
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- '很好!我們一樣用 `ls` 指令確認一下資料夾內容。'
|
很好!我們一樣用 `ls` 指令確認一下資料夾內容。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `ls` 來查看資料夾內容。
|
- ➤ 使用 `ls` 來查看資料夾內容。
|
||||||
|
|
||||||
@@ -88,10 +99,10 @@ stages:
|
|||||||
command:
|
command:
|
||||||
- ^ls$
|
- ^ls$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- '你會發現 `world.txt` 與 `wow.txt` 已經不見了。'
|
你會發現 `world.txt` 與 `wow.txt` 已經不見了。
|
||||||
- '現在我們來學習 `rmdir` 指令。`rmdir` 指令可以用來刪除資料夾。使用方式是 `rmdir [資料夾名稱]`。'
|
現在我們來學習 `rmdir` 指令。`rmdir` 指令可以用來刪除資料夾。使用方式是 `rmdir [資料夾名稱]`。
|
||||||
- '現在請你使用 `rmdir` 指令刪除 `empty` 資料夾。'
|
現在請你使用 `rmdir` 指令刪除 `empty` 資料夾。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `rmdir empty` 來刪除 `empty` 資料夾。
|
- ➤ 使用 `rmdir empty` 來刪除 `empty` 資料夾。
|
||||||
|
|
||||||
@@ -101,13 +112,13 @@ stages:
|
|||||||
task: 使用 `rmdir` 刪除 `empty` 資料夾
|
task: 使用 `rmdir` 刪除 `empty` 資料夾
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^rmdir
|
- ^rmdir empty$
|
||||||
files:
|
files:
|
||||||
- path: empty
|
- path: empty
|
||||||
exists: false
|
exists: false
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- '很好!現在請你使用 `ls` 指令來查看資料夾內容。'
|
很好!現在請你使用 `ls` 指令來查看資料夾內容。
|
||||||
|
|
||||||
- id: ls-no-empty
|
- id: ls-no-empty
|
||||||
requirements:
|
requirements:
|
||||||
@@ -117,9 +128,9 @@ stages:
|
|||||||
command:
|
command:
|
||||||
- ^ls$
|
- ^ls$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- '你會發現 `empty` 資料夾已經不見了。'
|
你會發現 `empty` 資料夾已經不見了。
|
||||||
- '現在請你試試看 `rmdir` 指令刪除 `notes` 資料夾。'
|
現在請你試試看 `rmdir` 指令刪除 `notes` 資料夾。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `rmdir notes` 來刪除 `notes` 資料夾
|
- ➤ 使用 `rmdir notes` 來刪除 `notes` 資料夾
|
||||||
|
|
||||||
@@ -135,12 +146,12 @@ stages:
|
|||||||
type: folder
|
type: folder
|
||||||
exists: true
|
exists: true
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 指令執行失敗了!
|
指令執行失敗了!
|
||||||
- '你會發現 `notes` 資料夾還在。'
|
你會發現 `notes` 資料夾還在。
|
||||||
- '這是因為 `rmdir` 指令只能刪除空的資料夾。'
|
這是因為 `rmdir` 指令只能刪除空的資料夾。
|
||||||
- 我們先來將 `notes` 資料夾的檔案刪除再來試一次。
|
我們先來將 `notes` 資料夾的檔案刪除再來試一次。
|
||||||
- '現在請你使用 `rm` 指令刪除 `notes` 資料夾內的檔案。'
|
現在請你使用 `rm` 指令刪除 `notes` 資料夾內的檔案。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `rm notes/[檔案名稱]` 來刪除 `notes` 資料夾內的檔案。
|
- ➤ 使用 `rm notes/[檔案名稱]` 來刪除 `notes` 資料夾內的檔案。
|
||||||
- ➤ 你可能需要使用 `ls notes` 來查看 `notes` 資料夾內有哪些檔案。
|
- ➤ 你可能需要使用 `ls notes` 來查看 `notes` 資料夾內有哪些檔案。
|
||||||
@@ -163,31 +174,29 @@ stages:
|
|||||||
command:
|
command:
|
||||||
- ^rm notes/\[.*\]
|
- ^rm notes/\[.*\]
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- '你好像理解錯了。'
|
你好像理解錯了。
|
||||||
- 這邊中括號包起來 `[檔案名稱]` 是參數的意思,需要你自己填入檔案的名稱。
|
這邊中括號包起來 `[檔案名稱]` 是參數的意思,需要你自己填入檔案的名稱。
|
||||||
- 實際打指令的時候,不要打中括號 `[]` 喔!
|
實際打指令的時候,不要打中括號 `[]` 喔!
|
||||||
- '例如,如果要刪除 `notes` 資料夾內的 `note.txt` 檔案,指令應該要是 `rm notes/note.txt`。'
|
例如,如果要刪除 `notes` 資料夾內的 `note.txt` 檔案,指令應該要是 `rm notes/note.txt`。
|
||||||
- 請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!
|
請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ^rm
|
- ^rmdir notes$
|
||||||
error:
|
|
||||||
- 'No such file or directory'
|
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 這個錯誤的意思是找不到檔案
|
這個錯誤的意思是找不到檔案
|
||||||
- '請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!'
|
請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!
|
||||||
- condition:
|
- condition:
|
||||||
command:
|
command:
|
||||||
- ^ls notes$
|
- ^ls notes$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- '`notes` 資料夾中有著 `note.txt` 檔案。'
|
`notes` 資料夾中有著 `note.txt` 檔案。
|
||||||
- 那我們就使用 `rm notes/note.txt` 來刪除 `note.txt` 檔案吧!
|
那我們就使用 `rm notes/note.txt` 來刪除 `note.txt` 檔案吧!
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- '很好!現在我們再使用 `rmdir` 指令刪除 `notes` 資料夾。'
|
很好!現在我們再使用 `rmdir` 指令刪除 `notes` 資料夾。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `rmdir notes` 來刪除 `notes` 資料夾。
|
- ➤ 使用 `rmdir notes` 來刪除 `notes` 資料夾。
|
||||||
|
|
||||||
@@ -196,16 +205,14 @@ stages:
|
|||||||
- rm-notes-content
|
- rm-notes-content
|
||||||
task: 使用 `rmdir` 刪除 `notes` 資料夾
|
task: 使用 `rmdir` 刪除 `notes` 資料夾
|
||||||
condition:
|
condition:
|
||||||
command:
|
|
||||||
- ^rmdir
|
|
||||||
files:
|
files:
|
||||||
- path: notes
|
- path: notes
|
||||||
exists: false
|
exists: false
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- '很好!`notes` 資料夾已經被刪除了。'
|
很好!`notes` 資料夾已經被刪除了。
|
||||||
- 接下來我們來刪除 `trash` 資料夾。
|
接下來我們來刪除 `trash` 資料夾。
|
||||||
- 先使用 `ls` 指令查看一下 `trash` 資料夾內容。
|
先使用 `ls` 指令查看一下 `trash` 資料夾內容。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `ls trash` 來查看 `trash` 中的檔案。
|
- ➤ 使用 `ls trash` 來查看 `trash` 中的檔案。
|
||||||
|
|
||||||
@@ -215,16 +222,14 @@ stages:
|
|||||||
task: 使用 `ls` 查看 `trash` 資料夾內容
|
task: 使用 `ls` 查看 `trash` 資料夾內容
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^ls
|
- ^ls trash$
|
||||||
output:
|
|
||||||
- garbage.txt
|
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- 天啊!`trash` 資料夾裡有好多檔案,甚至還有其他的資料夾!
|
天啊!`trash` 資料夾裡有好多檔案,甚至還有其他的資料夾!
|
||||||
- 如果今天資料夾中有很多檔案與資料夾,我們不想一個一個刪除,該怎麼辦呢?
|
如果今天資料夾中有很多檔案與資料夾,我們不想一個一個刪除,該怎麼辦呢?
|
||||||
- 跟 `cp` 的時候一樣,這時候我們可以加上 `-r` 的選項來刪除資料夾與它的所有檔案。
|
跟 `cp` 的時候一樣,這時候我們可以加上 `-r` 的選項來刪除資料夾與它的所有檔案。
|
||||||
- '`-r` 的意思是遞迴(recursive),也就是會把資料夾中的所有檔案都刪除。'
|
`-r` 的意思是遞迴(recursive),也就是會把資料夾中的所有檔案都刪除。
|
||||||
- 我們來試試看吧!
|
我們來試試看吧!
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `rm -r trash` 來刪除 `trash` 資料夾。
|
- ➤ 使用 `rm -r trash` 來刪除 `trash` 資料夾。
|
||||||
|
|
||||||
@@ -234,13 +239,13 @@ stages:
|
|||||||
task: 使用 `rm -r` 刪除 `trash` 資料夾
|
task: 使用 `rm -r` 刪除 `trash` 資料夾
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^rm -r
|
- ^rm -r trash$
|
||||||
files:
|
files:
|
||||||
- path: trash
|
- path: trash
|
||||||
exists: false
|
exists: false
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- '很好!現在請你使用 `ls` 指令查看資料夾內容。'
|
很好!現在請你使用 `ls` 指令查看資料夾內容。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 使用 `ls` 來查看資料夾內容。
|
- ➤ 使用 `ls` 來查看資料夾內容。
|
||||||
|
|
||||||
@@ -252,6 +257,6 @@ stages:
|
|||||||
command:
|
command:
|
||||||
- ^ls$
|
- ^ls$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content: |
|
||||||
- '`trash` 資料夾已經不見了。'
|
`trash` 資料夾已經不見了。
|
||||||
- 恭喜你!你已經學會如何刪除檔案與資料夾了!
|
恭喜你!你已經學會如何刪除檔案與資料夾了!
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import connectDB, { User } from '@linux-odyssey/models'
|
||||||
|
|
||||||
|
async function migrate() {
|
||||||
|
await connectDB(
|
||||||
|
process.env.MONGO_URL || 'mongodb://localhost:27017/odyssey-test'
|
||||||
|
)
|
||||||
|
await User.updateMany(
|
||||||
|
{
|
||||||
|
isGuest: { $exists: false },
|
||||||
|
},
|
||||||
|
{ $set: { isGuest: false } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate()
|
||||||
|
.then(() => {
|
||||||
|
console.log('Migration completed')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
+3
-1
@@ -9,7 +9,8 @@
|
|||||||
"clean": "node dist/cli/clean.js",
|
"clean": "node dist/cli/clean.js",
|
||||||
"build": "tsc -b",
|
"build": "tsc -b",
|
||||||
"dev": "nodemon",
|
"dev": "nodemon",
|
||||||
"build:quests": "node dist/cli/build.js"
|
"build:quests": "node dist/cli/build.js",
|
||||||
|
"ts-node": "node --loader ts-node/esm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@linux-odyssey/constants": "*",
|
"@linux-odyssey/constants": "*",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"commander": "^11.0.0",
|
"commander": "^11.0.0",
|
||||||
"connect-mongo": "^5.0.0",
|
"connect-mongo": "^5.0.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
"dockerode": "^3.3.5",
|
"dockerode": "^3.3.5",
|
||||||
"dotenv": "^16.4.1",
|
"dotenv": "^16.4.1",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Request, Response, NextFunction, Express } from 'express'
|
import type { Request, Response, NextFunction, Express } from 'express'
|
||||||
import { matchedData } from 'express-validator'
|
import { matchedData } from 'express-validator'
|
||||||
import { genJWT } from '../../utils/auth.js'
|
import { genJWT } from '../../utils/auth.js'
|
||||||
import { createUser } from '../../models/userManager.js'
|
import { createUser, createGuestUser } from '../../models/userManager.js'
|
||||||
import { asyncHandler } from '../../middleware/error.js'
|
import { asyncHandler } from '../../middleware/error.js'
|
||||||
import logger from '../../utils/logger.js'
|
import logger from '../../utils/logger.js'
|
||||||
|
|
||||||
@@ -39,6 +39,30 @@ export const register = asyncHandler(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const registerGuest = asyncHandler(
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const user = await createGuestUser()
|
||||||
|
req.login(
|
||||||
|
{
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).json({
|
||||||
|
message: 'error logging in',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'user created',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export function logout(req: Request, res: Response) {
|
export function logout(req: Request, res: Response) {
|
||||||
req.logout((err) => {
|
req.logout((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ export const newCommand = asyncHandler(async (req: Request, res: Response) => {
|
|||||||
c.stage = response.stage
|
c.stage = response.stage
|
||||||
await c.save()
|
await c.save()
|
||||||
|
|
||||||
session.responses.push(response.responses)
|
|
||||||
|
|
||||||
if ((session.status as string) === 'finished') {
|
if ((session.status as string) === 'finished') {
|
||||||
await finishSession(session)
|
await finishSession(session)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { Express, Request, Response } from 'express'
|
|||||||
import { User, Session, ISession } from '@linux-odyssey/models'
|
import { User, Session, ISession } from '@linux-odyssey/models'
|
||||||
import {
|
import {
|
||||||
createNewSession,
|
createNewSession,
|
||||||
getOrCreateActiveSession,
|
|
||||||
isQuestUnlocked,
|
isQuestUnlocked,
|
||||||
} from '../../models/sessionManager.js'
|
} from '../../models/sessionManager.js'
|
||||||
import { asyncHandler } from '../../middleware/error.js'
|
import { asyncHandler } from '../../middleware/error.js'
|
||||||
@@ -56,7 +55,6 @@ export const createSession = asyncHandler(
|
|||||||
export const getSessionById = asyncHandler(
|
export const getSessionById = asyncHandler(
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
const { sessionId } = matchedData(req)
|
const { sessionId } = matchedData(req)
|
||||||
console.log(sessionId)
|
|
||||||
const session = await Session.findOne({
|
const session = await Session.findOne({
|
||||||
_id: sessionId,
|
_id: sessionId,
|
||||||
user: (req.user as Express.ExistingUser).id,
|
user: (req.user as Express.ExistingUser).id,
|
||||||
@@ -71,7 +69,7 @@ export const getSessionById = asyncHandler(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const getOrCreateSession = asyncHandler(
|
export const getActiveSessionHandler = asyncHandler(
|
||||||
async (req: Request, res: Response) => {
|
async (req: Request, res: Response) => {
|
||||||
const { questId } = matchedData(req)
|
const { questId } = matchedData(req)
|
||||||
const user = await User.findById((req.user as Express.ExistingUser).id)
|
const user = await User.findById((req.user as Express.ExistingUser).id)
|
||||||
@@ -79,8 +77,12 @@ export const getOrCreateSession = asyncHandler(
|
|||||||
user &&
|
user &&
|
||||||
((await isQuestUnlocked(user, questId)) || config.testing.enabled)
|
((await isQuestUnlocked(user, questId)) || config.testing.enabled)
|
||||||
) {
|
) {
|
||||||
const session = await getOrCreateActiveSession(user, questId)
|
const session = await Session.findOne({
|
||||||
res.json(sessionDetail(session))
|
user: user.id,
|
||||||
|
quest: questId,
|
||||||
|
status: 'active',
|
||||||
|
})
|
||||||
|
res.json(session ? sessionDetail(session) : null)
|
||||||
} else {
|
} else {
|
||||||
res.status(403).json({ message: 'Quest is locked.' })
|
res.status(403).json({ message: 'Quest is locked.' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
logout,
|
logout,
|
||||||
registerFromSession,
|
registerFromSession,
|
||||||
socialLoginHandler,
|
socialLoginHandler,
|
||||||
|
registerGuest,
|
||||||
} from '../controllers/authController.js'
|
} from '../controllers/authController.js'
|
||||||
import {
|
import {
|
||||||
checkUsernameValidators,
|
checkUsernameValidators,
|
||||||
@@ -32,6 +33,8 @@ router.post(
|
|||||||
|
|
||||||
router.post('/register', authenticateRateLimit, registerValidators, register)
|
router.post('/register', authenticateRateLimit, registerValidators, register)
|
||||||
|
|
||||||
|
router.post('/register-guest', authenticateRateLimit, registerGuest)
|
||||||
|
|
||||||
router.post('/logout', logout)
|
router.post('/logout', logout)
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ router.post(
|
|||||||
sessionRateLimit,
|
sessionRateLimit,
|
||||||
sessionController.createSession
|
sessionController.createSession
|
||||||
)
|
)
|
||||||
router.post(
|
router.get(
|
||||||
'/active',
|
'/active',
|
||||||
activeSessionValidator,
|
activeSessionValidator,
|
||||||
sessionController.getOrCreateSession
|
sessionController.getActiveSessionHandler
|
||||||
)
|
)
|
||||||
router.get(
|
router.get(
|
||||||
'/:sessionId',
|
'/:sessionId',
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import SessionMiddleware from '../middleware/session.js'
|
|||||||
import { genJWT } from '../utils/auth.js'
|
import { genJWT } from '../utils/auth.js'
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
type SocketCallback = (event: string, ...args: any[]) => void
|
type SocketCallback = (event: string, ...args: any[]) => void
|
||||||
|
|
||||||
const sessions = new Map<string, SocketCallback[]>()
|
const sessions = new Map<string, SocketCallback[]>()
|
||||||
@@ -44,7 +43,6 @@ async function connectContainer(socket: Socket, next: (err?: Error) => void) {
|
|||||||
next(new Error('User not found.'))
|
next(new Error('User not found.'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// console.log(socket.handshake)
|
|
||||||
const { sessionId } = socket.handshake.query
|
const { sessionId } = socket.handshake.query
|
||||||
if (
|
if (
|
||||||
!sessionId ||
|
!sessionId ||
|
||||||
@@ -79,7 +77,7 @@ async function connectContainer(socket: Socket, next: (err?: Error) => void) {
|
|||||||
stream,
|
stream,
|
||||||
}
|
}
|
||||||
next()
|
next()
|
||||||
} catch (err) {
|
} catch {
|
||||||
next(new Error('Failed to start container.'))
|
next(new Error('Failed to start container.'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export async function deleteContainer(id: string) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await container.stop()
|
await container.stop()
|
||||||
} catch (error) {
|
} catch {
|
||||||
logger.warn('Failed to stop container', id)
|
logger.warn('Failed to stop container', id)
|
||||||
}
|
}
|
||||||
await container.remove()
|
await container.remove()
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export default class CommandHandler extends SessionHandler {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
return checks.every((c) => c === true)
|
return checks.every((c) => c === true)
|
||||||
} catch (e) {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,21 +79,6 @@ export async function createNewSession(
|
|||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrCreateActiveSession(
|
|
||||||
user: HydratedDocument<IUser>,
|
|
||||||
questId: string
|
|
||||||
): Promise<ISession> {
|
|
||||||
const session = await Session.findOne({
|
|
||||||
user,
|
|
||||||
quest: questId,
|
|
||||||
status: 'active',
|
|
||||||
})
|
|
||||||
if (session) {
|
|
||||||
return session
|
|
||||||
}
|
|
||||||
return createNewSession(user, questId)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function finishSession(
|
export async function finishSession(
|
||||||
session: HydratedDocument<ISession>
|
session: HydratedDocument<ISession>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Express } from 'express'
|
import { Express } from 'express'
|
||||||
|
import { format } from 'date-fns'
|
||||||
import { User, UserProfile } from '@linux-odyssey/models'
|
import { User, UserProfile } from '@linux-odyssey/models'
|
||||||
import { hashPassword } from '../utils/auth.js'
|
import { hashPassword } from '../utils/auth.js'
|
||||||
|
|
||||||
@@ -31,6 +32,23 @@ export async function createUser(
|
|||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createGuestUser() {
|
||||||
|
const username = `guest_${format(new Date(), 'yyyyMMddHHmmss')}`
|
||||||
|
const user = new User({
|
||||||
|
username,
|
||||||
|
email: `${username}@example.com`,
|
||||||
|
isGuest: true,
|
||||||
|
})
|
||||||
|
await user.save()
|
||||||
|
|
||||||
|
const profile = new UserProfile({
|
||||||
|
user,
|
||||||
|
})
|
||||||
|
await profile.save()
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
export function leaderboard() {
|
export function leaderboard() {
|
||||||
return UserProfile.aggregate([
|
return UserProfile.aggregate([
|
||||||
{
|
{
|
||||||
|
|||||||
+31
-2
@@ -1,14 +1,14 @@
|
|||||||
openapi: 3.0.0
|
openapi: 3.0.0
|
||||||
servers:
|
servers:
|
||||||
- url: /api/v1
|
- url: /api/v1
|
||||||
- url: https://odyssey.wancat.cc/api/v1
|
- url: https://linuxodyssey.xyz/api/v1
|
||||||
- url: http://localhost:3000/api/v1
|
- url: http://localhost:3000/api/v1
|
||||||
info:
|
info:
|
||||||
description: Linux Odyssey API
|
description: Linux Odyssey API
|
||||||
version: '1.0.0'
|
version: '1.0.0'
|
||||||
title: Session API
|
title: Session API
|
||||||
contact:
|
contact:
|
||||||
email: wancat@wancat.cc
|
email: contact@linuxodyssey.xyz
|
||||||
license:
|
license:
|
||||||
name: Apache 2.0
|
name: Apache 2.0
|
||||||
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
|
url: 'http://www.apache.org/licenses/LICENSE-2.0.html'
|
||||||
@@ -37,6 +37,19 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
enum: [active, finished, inactive]
|
enum: [active, finished, inactive]
|
||||||
|
- in: query
|
||||||
|
name: limit
|
||||||
|
description: limit the number of sessions to return
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
example: 10
|
||||||
|
- in: query
|
||||||
|
name: order
|
||||||
|
description: order the sessions by createdAt
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum: [asc, desc]
|
||||||
|
example: desc
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: list of existing session
|
description: list of existing session
|
||||||
@@ -233,6 +246,22 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
example: connect.sid=s%3Aq1Z; Path=/; HttpOnly; SameSite=Strict; Secure
|
example: connect.sid=s%3Aq1Z; Path=/; HttpOnly; SameSite=Strict; Secure
|
||||||
|
|
||||||
|
/auth/register-guest:
|
||||||
|
post:
|
||||||
|
summary: Register a new guest user
|
||||||
|
responses:
|
||||||
|
201:
|
||||||
|
description: User created
|
||||||
|
headers:
|
||||||
|
Set-Cookie:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: connect.sid=s%3Aq1Z; Path=/; HttpOnly; SameSite=Strict; Secure
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/User'
|
||||||
|
|
||||||
/auth/login:
|
/auth/login:
|
||||||
post:
|
post:
|
||||||
summary: Log in a user
|
summary: Log in a user
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default function checkLoginUI() {
|
|||||||
describe('example helloworld app', () => {
|
describe('example helloworld app', () => {
|
||||||
describe('Login tests', () => {
|
describe('Login tests', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.visit('/')
|
cy.visit('/register')
|
||||||
})
|
})
|
||||||
it('Check Login Page Elements-Register', () => {
|
it('Check Login Page Elements-Register', () => {
|
||||||
checkLoginUI()
|
checkLoginUI()
|
||||||
@@ -83,11 +83,11 @@ describe('example helloworld app', () => {
|
|||||||
cy.get('#UsernameText')
|
cy.get('#UsernameText')
|
||||||
.should('contain', Cypress.env('defaultAccount'))
|
.should('contain', Cypress.env('defaultAccount'))
|
||||||
.and('be.visible')
|
.and('be.visible')
|
||||||
|
cy.findByRole('link', { name: 'Survey' }).should('be.visible')
|
||||||
|
cy.findByRole('link', { name: 'LeaderBoard' }).should('be.visible')
|
||||||
|
cy.findByRole('link', { name: 'Map' }).should('be.visible')
|
||||||
cy.findByRole('link', { name: 'Bug Report' }).should('be.visible')
|
cy.findByRole('link', { name: 'Bug Report' }).should('be.visible')
|
||||||
cy.findByRole('button', { name: 'Sign Out' }).should('be.visible')
|
cy.findByRole('button', { name: 'Sign Out' }).should('be.visible')
|
||||||
cy.findByRole('button', { name: 'Map' }).should('be.visible')
|
|
||||||
cy.findByRole('button', { name: 'LeaderBoard' }).should('be.visible')
|
|
||||||
cy.findByRole('link', { name: 'Survey' }).should('be.visible')
|
|
||||||
})
|
})
|
||||||
it('Check QuestInfo', () => {
|
it('Check QuestInfo', () => {
|
||||||
cy.get('#topic').should('contain', title)
|
cy.get('#topic').should('contain', title)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ declare global {
|
|||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
interface Chainable {
|
interface Chainable {
|
||||||
LoginWithPassword(username: string, password: string): Chainable<string>
|
LoginWithPassword(username: string, password: string): Chainable<string>
|
||||||
PrepareForGame(): Chainable<void>
|
PrepareForGame(questId: string): Chainable<string>
|
||||||
InitTerminal(): Chainable<void>
|
InitTerminal(): Chainable<void>
|
||||||
typeInCommand(command: string): Chainable<string>
|
typeInCommand(command: string): Chainable<string>
|
||||||
getQuestInfo(id: string): Chainable<JQuery<HTMLParagraphElement>>
|
getQuestInfo(id: string): Chainable<JQuery<HTMLParagraphElement>>
|
||||||
@@ -38,8 +38,8 @@ Cypress.Commands.add('LoginWithPassword', (username, password) => {
|
|||||||
cy.get('#password').type(password)
|
cy.get('#password').type(password)
|
||||||
cy.get('#LogInOrSignUp').click()
|
cy.get('#LogInOrSignUp').click()
|
||||||
})
|
})
|
||||||
Cypress.Commands.add('PrepareForGame', (questId) => {
|
Cypress.Commands.add('PrepareForGame', (questId: string) => {
|
||||||
cy.visit('/')
|
cy.visit('/login')
|
||||||
// make sure the login page is loaded
|
// make sure the login page is loaded
|
||||||
cy.url().should('satisfy', (elements: string) => {
|
cy.url().should('satisfy', (elements: string) => {
|
||||||
const text = elements
|
const text = elements
|
||||||
@@ -143,9 +143,9 @@ Cypress.Commands.add('CompleteStageWithCommands', (stagename) => {
|
|||||||
})
|
})
|
||||||
Cypress.Commands.add('CheckTextElement', (id, chText, enText) => {
|
Cypress.Commands.add('CheckTextElement', (id, chText, enText) => {
|
||||||
if (Cypress.env('isCHVersion')) {
|
if (Cypress.env('isCHVersion')) {
|
||||||
cy.get(id, { timeout: 50000 }).should('contain', chText).and('be.visible')
|
cy.get(id, { timeout: 7000 }).should('contain', chText).and('be.visible')
|
||||||
} else {
|
} else {
|
||||||
cy.get(id, { timeout: 50000 }).should('contain', enText).and('be.visible')
|
cy.get(id, { timeout: 7000 }).should('contain', enText).and('be.visible')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
Cypress.Commands.add('CheckPlaceholder', (id, chText, enText) => {
|
Cypress.Commands.add('CheckPlaceholder', (id, chText, enText) => {
|
||||||
|
|||||||
@@ -2479,6 +2479,11 @@
|
|||||||
"@vue/compiler-dom" "3.4.19"
|
"@vue/compiler-dom" "3.4.19"
|
||||||
"@vue/shared" "3.4.19"
|
"@vue/shared" "3.4.19"
|
||||||
|
|
||||||
|
"@vue/devtools-api@^6.5.0":
|
||||||
|
version "6.6.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.3.tgz#b23a588154cba8986bba82b6e1d0248bde3fd1a0"
|
||||||
|
integrity sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==
|
||||||
|
|
||||||
"@vue/devtools-api@^6.5.1":
|
"@vue/devtools-api@^6.5.1":
|
||||||
version "6.6.1"
|
version "6.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.1.tgz#7c14346383751d9f6ad4bea0963245b30220ef83"
|
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.1.tgz#7c14346383751d9f6ad4bea0963245b30220ef83"
|
||||||
@@ -3594,6 +3599,11 @@ date-fns@^2.30.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.21.0"
|
"@babel/runtime" "^7.21.0"
|
||||||
|
|
||||||
|
date-fns@^3.6.0:
|
||||||
|
version "3.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf"
|
||||||
|
integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
|
||||||
|
|
||||||
dayjs@^1.10.4:
|
dayjs@^1.10.4:
|
||||||
version "1.11.10"
|
version "1.11.10"
|
||||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
|
||||||
@@ -6768,6 +6778,14 @@ pify@^2.2.0, pify@^2.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||||
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
|
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
|
||||||
|
|
||||||
|
pinia@^2.1.7:
|
||||||
|
version "2.1.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.1.7.tgz#4cf5420d9324ca00b7b4984d3fbf693222115bbc"
|
||||||
|
integrity sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==
|
||||||
|
dependencies:
|
||||||
|
"@vue/devtools-api" "^6.5.0"
|
||||||
|
vue-demi ">=0.14.5"
|
||||||
|
|
||||||
pirates@^4.0.1, pirates@^4.0.4:
|
pirates@^4.0.1, pirates@^4.0.4:
|
||||||
version "4.0.6"
|
version "4.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
|
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
|
||||||
@@ -7717,9 +7735,9 @@ tar-stream@^2.0.0:
|
|||||||
readable-stream "^3.1.1"
|
readable-stream "^3.1.1"
|
||||||
|
|
||||||
tar@^6.1.11:
|
tar@^6.1.11:
|
||||||
version "6.2.0"
|
version "6.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.0.tgz#b14ce49a79cb1cd23bc9b016302dea5474493f73"
|
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
|
||||||
integrity sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==
|
integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
|
||||||
dependencies:
|
dependencies:
|
||||||
chownr "^2.0.0"
|
chownr "^2.0.0"
|
||||||
fs-minipass "^2.0.0"
|
fs-minipass "^2.0.0"
|
||||||
@@ -8155,6 +8173,11 @@ vite@^4.5.3:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
vue-demi@>=0.14.5:
|
||||||
|
version "0.14.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.8.tgz#00335e9317b45e4a68d3528aaf58e0cec3d5640a"
|
||||||
|
integrity sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==
|
||||||
|
|
||||||
vue-eslint-parser@^9.4.2:
|
vue-eslint-parser@^9.4.2:
|
||||||
version "9.4.2"
|
version "9.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz#02ffcce82042b082292f2d1672514615f0d95b6d"
|
resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz#02ffcce82042b082292f2d1672514615f0d95b6d"
|
||||||
@@ -8452,6 +8475,11 @@ yocto-queue@^0.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
|
zod@^3.23.8:
|
||||||
|
version "3.23.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
|
||||||
|
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
|
||||||
|
|
||||||
zrender@5.5.0:
|
zrender@5.5.0:
|
||||||
version "5.5.0"
|
version "5.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.5.0.tgz#54d0d6c4eda81a96d9f60a9cd74dc48ea026bc1e"
|
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.5.0.tgz#54d0d6c4eda81a96d9f60a9cd74dc48ea026bc1e"
|
||||||
|
|||||||
Reference in new issue
Block a user