Compare commits

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

other improvements:
- improve error handling
2023-11-26 22:33:11 +08:00
Lin, Hung-Shin fb6411e923 feat: logger display error stack 2023-11-26 19:21:58 +08:00
Lin, Hung-Shin a7e646c28b feat: check global exceptions 2023-11-26 19:20:30 +08:00
Lin, Hung-Shin 98a226f3ff feat: load global exceptions 2023-11-26 18:40:39 +08:00
Lin, Hung-Shin 49fb5a6a61 fix: Remove exceptions from stages 2023-11-26 18:32:06 +08:00
Lin, Hung-Shin fe69841622 feat: improve parsing quest error message 2023-11-26 18:31:45 +08:00
Lin, Hung-Shin bb968706ad feat: refactor quest models
+ separate stage and exception
+ reuse commandResponder
2023-11-26 18:29:52 +08:00
Annie Huang 849e669147 Map init position, pwd display, instruct speed 2023-11-26 18:27:34 +08:00
Lin, Hung-Shin b41e67ee0b fix: error in quest read (#81) 2023-11-24 12:59:46 +08:00
Chen, Tai-Lin 50a0bbbab1 add warning 2023-11-24 12:59:09 +08:00
Chen, Tai-Lin d712925d0f modify path 2023-11-24 12:56:57 +08:00
Lin, Hung-Shin b78398680d fix: add error handling (#82) 2023-11-24 12:53:00 +08:00
Lin, Hung-Shin c7f52b1003 fix: add error handling 2023-11-24 12:52:01 +08:00
Chen, Tai-Lin f008fb9f60 modify quest map 2023-11-24 12:49:17 +08:00
Chen, Tai-Lin 76a39add29 fix: error in quest read 2023-11-24 11:54:30 +08:00
Lin, Hung-Shin eb9118d5b1 Quest update (#80) 2023-11-24 09:58:09 +08:00
Chen, Tai-Lin cfdf8730e8 fix error 2023-11-24 09:55:17 +08:00
Chen, Tai-Lin 6651071580 fix: error in quest read 2023-11-24 09:54:18 +08:00
Lin, Hung-Shin f30f6472f5 fix: error in quests (#79) 2023-11-24 09:51:33 +08:00
Chen, Tai-Lin 5ac3b3088d fix: error in create quest 2023-11-24 09:50:15 +08:00
Chen, Tai-Lin acc4d5acba fix: error in create quest 2023-11-24 09:48:41 +08:00
Chen, Tai-Lin 1fb43b78e3 fix: erro in quest explore 2023-11-24 09:46:06 +08:00
Lin, Hung-Shin b27adc1f69 Add survey and leaderboard button (#78)
Add survey and leaderboard button
2023-11-24 00:59:34 +08:00
Annie Huang 6f68107405 Add survey and 2023-11-24 00:58:03 +08:00
Lin, Hung-Shin de406135a3 modify quest cast (#77) 2023-11-24 00:37:21 +08:00
CHEN, TAI-LIN 1822188c97 modify quest cast 2023-11-24 00:33:56 +08:00
Lin, Hung-Shin 38725c588b SKIP CI checks on deployment (#76)
SO BAAAAAAAAAAAD
2023-11-23 23:51:38 +08:00
Lin, Hung-Shin d95b763b45 SKIP CI checks on deployment 2023-11-23 23:50:51 +08:00
Lin, Hung-Shin f4de2a182e feat: add series of quests (#75) 2023-11-23 23:44:27 +08:00
Lin, Hung-Shin 93e2485dee Merge branch 'main' into quest 2023-11-23 23:40:03 +08:00
Lin, Hung-Shin 5f7e2a3d5a Feat complete animation (#73)
Add complete animation in complete modal
Add 3-state-colors on map: 
- green: completed 
- gray: unlocked
- dark: locked
* haven’t add back v-if because the pc is out of power
2023-11-23 23:37:47 +08:00
Annie Huang fe604f3781 change verify order 2023-11-23 23:36:30 +08:00
Lin, Hung-Shin 143b84afb4 Feat leaderboard (#74) 2023-11-23 23:34:18 +08:00
Lin, Hung-Shin c54bc68001 fix: remove debug log 2023-11-23 23:32:35 +08:00
Annie Huang 1944a69b53 extend timeout 2023-11-23 23:30:38 +08:00
Lin, Hung-Shin f03a544108 fix; cypress use 'get-started' as test quest 2023-11-23 23:29:19 +08:00
Annie Huang 277d619518 fix cypress fail 2023-11-23 23:27:58 +08:00
Chen, Tai-Lin 96c0785a7e change e2e test 2023-11-23 23:26:11 +08:00
Lin, Hung-Shin 2cc8a98734 Merge remote-tracking branch 'origin/quest' into quest 2023-11-23 23:00:47 +08:00
Lin, Hung-Shin d07b38c7d6 fix: no callback found error 2023-11-23 23:00:16 +08:00
Chen, Tai-Lin 00e59e37ee remove cheatsheet e2e test in cypress 2023-11-23 22:57:39 +08:00
Chen, Tai-Lin 580a24402b update gamepage 2023-11-23 22:51:29 +08:00
Annie Huang 66adfec365 fix method 2023-11-23 22:48:26 +08:00
Chen, Tai-Lin c6f45e10e0 analytics update 2023-11-23 22:40:35 +08:00
Chen, Tai-Lin 140a1249a6 quest update 2023-11-23 22:39:47 +08:00
Chen, Tai-Lin 5413cf4118 quest update 2023-11-23 22:29:08 +08:00
Lin, Hung-Shin 9e7065fbd7 fix: remove fake leaderboard responses 2023-11-23 22:28:36 +08:00
Lin, Hung-Shin 52fa8d6f3a fix: put leadeboard align top 2023-11-23 22:28:23 +08:00
Lin, Hung-Shin 88d19e7f87 fix: navbar overlap leaderboard 2023-11-23 22:22:48 +08:00
Lin, Hung-Shin a9a63ea164 feat: print hints one by one, fix unexpected breakline 2023-11-23 21:56:01 +08:00
Chen, Tai-Lin fc13e9371c quest update 2023-11-23 21:51:31 +08:00
Chen, Tai-Lin e664573215 hide cheatsheet 2023-11-23 21:38:47 +08:00
Chen, Tai-Lin 63ed4c098c all quests update 2023-11-23 21:21:38 +08:00
Chen, Tai-Lin 4d53f56397 update quest 2023-11-23 21:13:54 +08:00
Chen, Tai-Lin 057635eed6 all quest modify 2023-11-23 21:03:32 +08:00
Annie Huang 84cf74cdfc extend timeout 2023-11-23 17:00:30 +08:00
Annie Huang a7eeb8470a edit link text 2023-11-23 16:55:34 +08:00
Annie Huang c9500c7575 set back if completed 2023-11-23 16:51:18 +08:00
Chen, Tai-Lin dd3e6b4aac scroll leader board 2023-11-23 16:50:07 +08:00
Chen, Tai-Lin 4f55dcd929 update quest path 2023-11-23 16:38:11 +08:00
wancat 790e09b315 wip: design leaderboard 2023-11-23 16:28:47 +08:00
Annie Huang ce6d5aad66 Add complete animation 2023-11-23 16:16:20 +08:00
wancat a90b0a3c83 feat: load leadboard data in frontend 2023-11-23 15:49:20 +08:00
wancat 1a5655d7ce feat: rename numberOfCompletedQuests to score 2023-11-23 15:49:06 +08:00
wancat 196187d5c9 feat: add leaderboard api 2023-11-23 15:42:31 +08:00
Chen, Tai-Lin accf2bd0e2 update quest path 2023-11-23 15:06:34 +08:00
Lin, Hung-Shin 0d8ab8413a feat: add leaderboard declarations 2023-11-23 15:04:39 +08:00
Chen, Tai-Lin c42c558614 quest get-started 2023-11-23 14:37:08 +08:00
Chen, Tai-Lin c6e49a10be quest map modify 2023-11-23 14:02:02 +08:00
AnnieHuang 63c2443802 Add animejs for further usage 2023-11-23 13:57:41 +08:00
Annie f9e6228226 Add 3 stage color: locked, unlocked, completed 2023-11-23 13:57:41 +08:00
Annie Huang 937c510aa0 Add username to navbar (#72)
* Add username to navbar
* Set navbar title by props
2023-11-23 13:46:01 +08:00
Lin, Hung-Shin 779733c7ba Add status badges (#71) 2023-11-23 13:40:46 +08:00
Chen, Tai-Lin 639a7d06d9 change quest title 2023-11-22 23:11:10 +08:00
Chen, Tai-Lin 814ca72040 quests requirement update 2023-11-22 23:07:33 +08:00
Chen, Tai-Lin 821386a0e4 quest map modify 2023-11-22 23:06:52 +08:00
Chen, Tai-Lin 956af624c6 Merge branch 'quest-file' into quest 2023-11-22 19:28:34 +08:00
Chen, Tai-Lin 2e6a29cb92 quest update 2023-11-22 19:27:40 +08:00
Lin, Hung-Shin e360ebcf53 fix: reset user profile on logout 2023-11-22 14:53:41 +08:00
Lin, Hung-Shin 89f86fad30 feat: add username to navbar, pass title by props 2023-11-22 14:46:41 +08:00
Lin, Hung-Shin 6fddf5ee0c quest: add study commands to instruction 2023-11-22 13:57:09 +08:00
Lin, Hung-Shin 1ab3e577c1 quest: "remove" finish, a bug in last stage 2023-11-22 12:50:17 +08:00
Lin, Hung-Shin 28cd2a7ac7 fix: change build:quests output to console 2023-11-22 12:49:47 +08:00
Lin, Hung-Shin 2e1fe369a4 feat: default response to dialogue and spoken by Ada 2023-11-22 12:14:04 +08:00
Lin, Hung-Shin 21ca083628 Merge branch 'main' into quest-file 2023-11-22 11:39:24 +08:00
Lin, Hung-Shin 3cebcddc91 fix: expiryCheck error handling 2023-11-22 11:26:03 +08:00
Chen, Tai-Lin 7ba9e6b14b quest update 2023-11-22 11:05:11 +08:00
Lin, Hung-Shin cd03277fd4 quest: "move" change optional hint to exception 2023-11-22 10:44:52 +08:00
Lin, Hung-Shin 2c4bd67996 fix: mount quest function 2023-11-22 10:40:15 +08:00
Lin, Hung-Shin 06c36565d0 fix: container cli error handling 2023-11-22 10:39:57 +08:00
Lin, Hung-Shin ba641ca222 fix: container panic when discover not exists 2023-11-22 10:23:47 +08:00
Lin, Hung-Shin ff2241daa8 Add status badges 2023-11-22 00:10:11 +08:00
wancat d19b32c6d1 quest: add "move" 2023-11-21 23:58:44 +08:00
Lin, Hung-Shin d15a197db9 Feat Translation Chinese (Mandarin) (#68)
- Translate Component to Mandarin
- Relative size of map node and font
- Relative size of header title
- Fix Hint Text changing direction
- Modifying cypress tests for translates
- Add a parameter to check the language version
- Add commands for checking different version elements and placeholder
- Fix docker parse JSON
2023-11-21 23:31:33 +08:00
wancat 5b71944588 fix: redirect to map on social login 2023-11-21 23:23:20 +08:00
wancat 627ff1476b quest: "copy" add cp -r exception 2023-11-21 22:54:39 +08:00
wancat 06c694ee68 quest: add check empty file stage 2023-11-21 22:54:12 +08:00
Annie Huang 7320688827 fix: display format, docker parsejson, hint title 2023-11-21 22:53:43 +08:00
Lin, Hung-Shin a4a261291d Merge branch 'main' into feat-translate-ch 2023-11-21 22:28:57 +08:00
Annie Huang 4c85516bcb fix cypress /map problem
update packages
2023-11-21 22:24:34 +08:00
Lin, Hung-Shin 9f9a31719e Add logger in backend (#69)
* Replace `console.log` `console.error` with Winston logger
* Add ESLint warning of `console`
2023-11-21 22:16:56 +08:00
Chen, Tai-Lin b6b8010a60 Merge branch 'quest-file' into quest 2023-11-21 21:25:59 +08:00
Chen, Tai-Lin 3e441b1104 quest update 2023-11-21 21:24:21 +08:00
Annie 174941a37e Add map(header),continue to map,login direct - map 2023-11-21 21:13:33 +08:00
wancat bdd37ae747 feat: pass oauth secret to actions 2023-11-21 20:46:42 +08:00
Lin, Hung-Shin dd7cded09e feat: finsih copy 2023-11-21 15:11:05 +08:00
Annie f1d5374137 Change order to maksure Social Login is enabled 2023-11-21 15:05:03 +08:00
Annie 364cda39ad fix eslint problems 2023-11-21 14:42:36 +08:00
Lin, Hung-Shin 66ce015f0e feat: add exceptions to stage 2023-11-21 14:27:43 +08:00
Annie cc03e57c70 fix cypress command format 2023-11-21 14:20:55 +08:00
Annie f7907a5999 fix format 2023-11-21 14:01:17 +08:00
Annie 5d26029166 fix format 2023-11-21 13:58:09 +08:00
Annie 869f01a62b Add check chtext element and ids,fix quest problem 2023-11-21 12:56:15 +08:00
Lin, Hung-Shin d221465a73 fix: prevent log flooding g from socket message 2023-11-21 12:23:24 +08:00
Lin, Hung-Shin 56d96cf7a0 fix: only send hints when it's not empty 2023-11-21 12:16:01 +08:00
Lin, Hung-Shin a8ba2a640b feat: finish quest create 2023-11-21 12:15:44 +08:00
Lin, Hung-Shin 36f53da08a feat: make hints a group in one stage 2023-11-21 11:55:32 +08:00
Chen, Tai-Lin 0343a7ccee quest update 2023-11-21 11:27:47 +08:00
Lin, Hung-Shin 42ac27126c Merge remote-tracking branch 'origin/quest' into quest-file 2023-11-21 11:20:21 +08:00
Lin, Hung-Shin 4297f96913 Merge branch 'quest' into quest-file 2023-11-21 11:19:05 +08:00
Lin, Hung-Shin 691f12f7e7 feat: add title to quests 2023-11-21 11:17:49 +08:00
Lin, Hung-Shin c89abfbac5 fix: disable quest lock when testing 2023-11-21 11:14:02 +08:00
Chen, Tai-Lin 764c6812bd quest update 2023-11-21 11:11:40 +08:00
CHEN, TAI-LIN 882ce5f75d Quest file (#70) 2023-11-21 11:10:27 +08:00
CHEN, TAI-LIN b514fa8bea Merge branch 'quest' into quest-file 2023-11-21 11:09:23 +08:00
Lin, Hung-Shin 3fed851b7b feat: reload all quest on starting 2023-11-21 11:04:52 +08:00
Lin, Hung-Shin 3f8bcb5446 feat: disable quest locks when testing 2023-11-21 10:48:35 +08:00
Chen, Tai-Lin c7fb0997b7 quest update 2023-11-21 10:48:35 +08:00
Chen, Tai-Lin d4da065a95 fix: load quest erorr handling 2023-11-21 10:48:35 +08:00
Chen, Tai-Lin 65894f831a quest update 2023-11-21 10:48:35 +08:00
Chen, Tai-Lin 5fbdcd5182 fix: ui revise 2023-11-21 10:48:35 +08:00
Lin, Hung-Shin ee5e8afe7b fix: logger import 2023-11-21 10:38:36 +08:00
Lin, Hung-Shin a6b51a08e5 fix: mount log folder in test and production 2023-11-21 10:19:33 +08:00
Lin, Hung-Shin 0d886a686e fix: remove console log in backend 2023-11-21 10:15:06 +08:00
Annie Huang 9da23b5dc1 translation and little format fixture 2023-11-20 22:49:46 +08:00
Lin, Hung-Shin f6c3b33da9 Feat quests page (#67)
Feat quests page
2023-11-20 16:24:25 +08:00
Annie Huang 83994106a0 Add error handling 2023-11-20 15:41:08 +08:00
Annie Huang 2837354456 render-->renderer 2023-11-20 15:36:06 +08:00
Chen, Tai-Lin 6ff21053cf quest update 2023-11-20 15:26:32 +08:00
Annie Huang 4c1be84942 Swap code position of template and script 2023-11-20 15:08:14 +08:00
Annie Huang 659161a76e modify header title 2023-11-20 14:59:30 +08:00
Lin, Hung-Shin 82d1004ecf wip: removing console log 2023-11-20 14:59:14 +08:00
Annie Huang 12c6e493a5 Change lineStyle by unlocked,delete docker swagger 2023-11-20 14:41:03 +08:00
Lin, Hung-Shin 515aefcadc feat: add winston 2023-11-20 14:36:38 +08:00
Annie Huang 5d353b71e2 Change background to svg 2023-11-20 13:49:09 +08:00
Chen, Tai-Lin c9a24b92ad fix: load quest erorr handling 2023-11-20 12:58:18 +08:00
Chen, Tai-Lin 92b12a90d3 quest update 2023-11-20 12:51:48 +08:00
Chen, Tai-Lin f84a163b77 fix: ui revise 2023-11-20 12:51:25 +08:00
Annie Huang c272fe61a8 modify background, change color when completed 2023-11-20 01:38:06 +08:00
Annie Huang 46efb15273 Change node shape, fix broke (y-axis), add header 2023-11-19 00:40:16 +08:00
Annie Huang b5be0608ae Add background, modify classes 2023-11-18 22:10:17 +08:00
Lin, Hung-Shin 08e28f7725 feat: load user progress and check before click
* not display completed and unlocked status yet
2023-11-16 13:22:24 +08:00
Lin, Hung-Shin 34e9b20f2e feat: fetch quests from api 2023-11-16 13:09:35 +08:00
Lin, Hung-Shin b19d6bf6e2 feat: center the same layers 2023-11-16 12:53:44 +08:00
Lin, Hung-Shin 7a8df716da feat: add x coordinates and links, use SVG 2023-11-16 12:46:53 +08:00
Lin, Hung-Shin e018e8dfe4 refactor: encapsulate DAG 2023-11-16 12:46:30 +08:00
Lin, Hung-Shin ca8215486c feat: add dag into frontend (work in progress) 2023-11-15 14:58:00 +08:00
Lin, Hung-Shin 14d6ab961b fix: move format into constructor 2023-11-15 14:46:41 +08:00
Lin, Hung-Shin e18f0db5ad feat: Add edges 2023-11-15 14:43:34 +08:00
Lin, Hung-Shin fae2d8199d feat: calculate layer 2023-11-15 14:30:37 +08:00
Lin, Hung-Shin c92a2e1d70 feat: install echarts and initialize graph chart 2023-11-15 14:02:33 +08:00
132 changed files with 5002 additions and 851 deletions

No files matched your search

+3 -2
View File
@@ -13,11 +13,11 @@ module.exports = {
],
parserOptions: {
// parser: 'babel-eslint',
ecmaVersion: 2020,
ecmaVersion: 13,
sourceType: 'module',
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-console': 'warn',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'import/extensions': 'off',
camelcase: ['error', { properties: 'never', ignoreDestructuring: true }],
@@ -28,5 +28,6 @@ module.exports = {
'no-plusplus': 'off',
'prettier/prettier': ['error', { endOfLine: 'auto', semi: false }],
'import/prefer-default-export': 'off',
'no-restricted-syntax': ['off', 'ForOfStatement'],
},
}
+11 -2
View File
@@ -2,6 +2,15 @@ name: CI Checks
on:
workflow_call:
secrets:
GOOGLE_CLIENT_ID:
required: true
GOOGLE_CLIENT_SECRET:
required: true
GH_CLIENT_ID:
required: true
GH_CLIENT_SECRET:
required: true
jobs:
ci:
@@ -40,6 +49,6 @@ jobs:
env:
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
GITHUB_CLIENT_ID: ${{ secrets.GITHUB_CLIENT_ID }}
GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_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
+2 -1
View File
@@ -12,9 +12,10 @@ concurrency:
jobs:
ci-checks:
uses: ./.github/workflows/ci-checks.yml
secrets: inherit
build:
needs: ci-checks
# needs: ci-checks
uses: ./.github/workflows/build.yml
secrets: inherit
+1
View File
@@ -10,6 +10,7 @@ concurrency:
jobs:
ci-checks:
uses: ./.github/workflows/ci-checks.yml
secrets: inherit
code_review:
permissions:
+2 -1
View File
@@ -12,9 +12,10 @@ concurrency:
jobs:
ci-checks:
uses: ./.github/workflows/ci-checks.yml
secrets: inherit
build:
needs: ci-checks
# needs: ci-checks
uses: ./.github/workflows/build.yml
secrets: inherit
+3
View File
@@ -1,5 +1,8 @@
# Linux Odyssey
[![Production](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/production.yml/badge.svg)](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/production.yml)
[![Staging](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/staging.yml/badge.svg)](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/staging.yml)
## Requirements
- NodeJS
+1 -1
View File
@@ -2,10 +2,10 @@
FROM node:20-alpine as base
WORKDIR /app
COPY package.json yarn.lock ./
COPY app/package.json ./app/package.json
COPY packages/file-graph/package.json ./packages/file-graph/package.json
COPY packages/utils/package.json ./packages/utils/package.json
COPY packages/constants/package.json ./packages/constants/package.json
COPY app/package.json ./app/package.json
RUN yarn install --frozen-lockfile
# Dev stage
+4
View File
@@ -20,11 +20,15 @@
"@linux-odyssey/file-graph": "*",
"@linux-odyssey/utils": "*",
"axios": "^1.6.0",
"dompurify": "^3.0.6",
"echarts": "^5.4.3",
"marked": "^10.0.0",
"socket.io-client": "^4.7.1",
"validator": "^13.11.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vue-toastification": "^2.0.0-rc.5",
"vue3-lottie": "^3.2.0",
"xterm": "^5.2.1",
"xterm-addon-attach": "^0.8.0",
"xterm-addon-fit": "^0.7.0"
+88 -20
View File
@@ -41,7 +41,7 @@ const hasSocialLogins = computed(() => {
})
const success = () => {
router.push({ name: 'game', params: { questId: 'helloworld' } })
router.push({ name: 'map' })
}
const error = (err) => {
@@ -90,7 +90,7 @@ const handleChange = () => {
class="bg-bg border-8 border-bg-primary flex flex-1 flex-col items-center justify-center rounded-3xl p-10"
>
<h1 class="text-text-primary text-3xl font-black mb-2">{{ title }}</h1>
<div class="text-error inline-flex justify-center items-center xl:hidden">
<div class="text-error inline-flex justify-center items-center md:hidden">
<font-awesome-icon
:icon="['fas', 'circle-exclamation']"
class="px-1 text-sm"
@@ -100,6 +100,7 @@ const handleChange = () => {
<form @submit.prevent="handleSubmit()" class="w-full">
<div v-if="socialLogin && hasSocialLogins">
<a
id="GoogleLogin"
v-if="availableMethods.google"
class="inline-flex justify-center items-center rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
href="/api/v1/auth/google"
@@ -108,9 +109,13 @@ const handleChange = () => {
:icon="['fab', 'google']"
class="text-text-secondary px-3 text-lg"
/>
<span class="font-medium">Continue with Google</span>
<span class="font-medium">
<!-- Continue with Google -->
以 Google 繼續
</span>
</a>
<a
id="GitHubLogin"
v-if="availableMethods.github"
class="inline-flex justify-center items-center rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
href="/api/v1/auth/github"
@@ -119,17 +124,22 @@ const handleChange = () => {
:icon="['fab', 'github']"
class="text-text-secondary px-3 text-lg"
/>
<span class="font-medium">Continue with GitHub</span>
<span class="font-medium">
<!-- Continue with GitHub -->
以 GitHub 繼續
</span>
</a>
<p
id="or"
class="text-text-secondary flex justify-center mt-3"
v-if="type === 'login' || type === 'register'"
>
or
<!-- or -->
或
</p>
</div>
<div class="mb-6">
<input
<!-- <input
type="text"
id="username"
ref="usernameInput"
@@ -139,6 +149,17 @@ const handleChange = () => {
required
@input="handleChange()"
autocomplete="username"
/> -->
<input
type="text"
id="username"
ref="usernameInput"
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
:placeholder="type === 'login' ? '電子郵件 / 帳號名稱' : '帳號名稱'"
v-model="username"
required
@input="handleChange()"
autocomplete="username"
/>
<label
class="text-sm font-normal text-text-secondary"
@@ -149,7 +170,7 @@ const handleChange = () => {
</label>
</div>
<div class="mb-6" v-if="type === 'register'">
<input
<!-- <input
type="email"
id="email"
ref="emailInput"
@@ -159,10 +180,21 @@ const handleChange = () => {
required
@input="handleChange()"
autocomplete="email"
/> -->
<input
type="email"
id="email"
ref="emailInput"
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
placeholder="電子郵件"
v-model="email"
required
@input="handleChange()"
autocomplete="email"
/>
</div>
<div class="mb-6" v-if="type === 'login' || type === 'register'">
<input
<!-- <input
type="password"
id="password"
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
@@ -170,30 +202,66 @@ const handleChange = () => {
v-model="password"
required
@input="handleChange()"
/> -->
<input
type="password"
id="password"
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
placeholder="密碼"
v-model="password"
required
@input="handleChange()"
/>
</div>
<p class="text-error flex justify-center" v-if="errorMessage">
<p
id="ErrorDisplay"
class="text-error flex justify-center"
v-if="errorMessage"
>
{{ errorMessage }}
</p>
<button
id="LogInOrSignUp"
class="inline-flex justify-center rounded-lg font-black py-2 bg-text-primary text-bg w-full"
type="submit"
>
<span v-if="type === 'login'">Log In</span>
<span v-else>Sign Up</span>
<span v-if="type === 'login'">
<!-- Log In -->
登入
</span>
<span v-else>
<!-- Sign Up -->
註冊
</span>
</button>
<p class="text-text flex justify-center mt-3">
<span v-if="type === 'register'"
>Already have an account?
<RouterLink class="text-text-primary font-bold" to="/login">
<u>Log in</u>
<p id="SwitchLoginRegister" class="text-text flex justify-center mt-3">
<span id="CheckRegistered" v-if="type === 'register'">
<!-- Already have an account? -->
已經有帳號了嗎?
<RouterLink
id="SwitchtoLogin"
class="text-text-primary font-bold"
to="/login"
>
<u>
<!-- Log in -->
登入
</u>
</RouterLink></span
>
<span v-else-if="type === 'login'"
>Don't have an account?
<RouterLink class="text-text-primary font-bold" to="/register">
<u>Sign up</u>
<span id="CheckRegistered" v-else-if="type === 'login'">
<!-- Don't have an account? -->
還沒有帳號嗎?
<RouterLink
id="SwitchtoRegister"
class="text-text-primary font-bold"
to="/register"
>
<u>
<!-- Sign up -->
註冊
</u>
</RouterLink></span
>
</p>
+8 -4
View File
@@ -1,9 +1,13 @@
<script setup></script>
<template>
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
<h1 class="inline text-text-primary font-xl">Command Cheatsheets</h1>
<h1 id="CommandBook" class="inline text-text-primary font-xl">
<!-- Command Cheatsheets -->
指令之書
</h1>
<div id="cheatsheets">
<p class="text-text-secondary">--Command List--</p>
<p class="text-text-secondary">
<!-- --Command List-- -->
-- --指令列表-- --
</p>
</div>
</template>
+55 -7
View File
@@ -1,12 +1,36 @@
<script setup>
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { ref } from 'vue'
import { surveyUrl } from '../config'
import { Vue3Lottie } from 'vue3-lottie'
// import { surveyUrl } from '../config'
import CompleteAnimation from '../img/complete-animation.json'
const showModal = ref(true) // Controls whether the modal is visible or not
const router = useRouter()
const toast = useToast()
const closeModal = () => {
showModal.value = false
}
const defineComponent = () => ({
components: {
Vue3Lottie,
},
setup() {
return {
CompleteAnimation,
}
},
})
const backtoMap = async () => {
try {
router.push({ name: 'map' })
} catch (err) {
// useToast().error('Failed to load map')
toast.error('無法讀取地圖')
console.error(err)
}
}
// Simulate completing a quest to show the modal
setTimeout(() => {
@@ -18,18 +42,42 @@ setTimeout(() => {
<div v-if="showModal" class="modal">
<div class="modal-content bg-bg-secondary rounded-lg">
<span class="close text-text" @click="closeModal">&times;</span>
<h2 class="text-2xl text-text mb-5 w-full inline-flex justify-center">
Quest Completed!
<h2
id="QuestCompleted"
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
>
<!-- Quest Completed! -->
關卡完成!
</h2>
<p class="text-base text-text">
<div class="z-10 right-1/2 top-1/2" @animationend="defineComponent">
<Vue3Lottie
:animationData="CompleteAnimation"
:height="200"
:width="200"
/>
</div>
<p class="text-base text-text text-center">恭喜你完成這一關!</p>
<p class="text-base text-text text-center">
接下來回到地圖,迎接新的挑戰吧!
</p>
<!-- <p class="text-base text-text">
您已成功完成挑戰,真的非常感謝您的參與!<br />
如果您能撥冗填寫這份問卷,我們將不勝感激,這對我們非常重要!
</p>
<a
</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
id="BacktoMap"
target="_blank"
@click="backtoMap"
class="inline-flex justify-center rounded-lg text-base font-black py-2 mt-3 bg-text-primary w-full"
>回到地圖</a
>
</div>
</div>
+45 -7
View File
@@ -1,34 +1,72 @@
<template>
<section class="h-[10%] p-3 grid grid-cols-3 gap-4 place-content-between">
<button id="solution" class="bg-bg-disabled rounded-lg p-2">
<section class="h-[10%] p-3 grid grid-cols-4 gap-4 place-content-between">
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">
<!-- Survey -->
問卷
</p>
</a>
</button>
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
<font-awesome-icon
:icon="['far', 'circle-question']"
class="text-text-disabled"
/>
<p class="text-text-disabled inline ml-1">Solution</p>
<p class="text-text-disabled inline ml-1">
<!-- Solution -->
解答
</p>
</button>
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
<font-awesome-icon
:icon="['fas', 'arrow-rotate-left']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">Reset</p>
<p class="text-text-primary inline ml-1">
<!-- Reset -->
重來
</p>
</button>
<button id="continue" class="bg-bg-disabled rounded-lg p-2">
<button
id="continue"
class="bg-bg-secondary rounded-lg p-2"
@click="continuePlay"
>
<font-awesome-icon
:icon="['far', 'circle-right']"
class="text-text-disabled"
class="text-text-primary"
/>
<p class="text-text-disabled inline ml-1">Continue</p>
<p class="text-text-primary inline ml-1">
<!-- Continue -->
繼續
</p>
</button>
</section>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { createSession } from '../store/session'
import { surveyUrl } from '../config'
const router = useRouter()
const reset = async () => {
console.log('resetting...')
await createSession()
}
const continuePlay = async () => {
try {
router.push({ name: 'map' })
} catch (err) {
// useToast().error('Failed to load map')
useToast().error('無法讀取地圖')
console.error(err)
}
}
</script>
+6 -6
View File
@@ -1,9 +1,3 @@
<template>
<div id="box">
<canvas ref="canvas"></canvas>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
@@ -79,6 +73,12 @@ onMounted(() => {
})
</script>
<template>
<div id="box">
<canvas ref="canvas"></canvas>
</div>
</template>
<style scoped>
#box {
position: relative;
+9 -9
View File
@@ -1,12 +1,3 @@
<template>
<a href="#" :class="nodeClass()">{{ node.discovered ? node.name : '???' }}</a>
<ul :class="nodeClass()" v-if="node.children && node.children.length > 0">
<li :class="nodeClass()" v-for="child in node.children" :key="child.path">
<FileNode :node="child" :pwd="pwd" />
</li>
</ul>
</template>
<script setup>
const props = defineProps({
node: {
@@ -27,3 +18,12 @@ function nodeClass() {
return classes
}
</script>
<template>
<a href="#" :class="nodeClass()">{{ node.discovered ? node.name : '???' }}</a>
<ul :class="nodeClass()" v-if="node.children && node.children.length > 0">
<li :class="nodeClass()" v-for="child in node.children" :key="child.path">
<FileNode :node="child" :pwd="pwd" />
</li>
</ul>
</template>
+81 -7
View File
@@ -1,9 +1,14 @@
<script setup>
import { defineProps, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { logout } from '../utils/auth'
import { bugReportUrl } from '../config'
import sessionStore, { reset } from '../store/session'
import { bugReportUrl, surveyUrl } from '../config'
import { reset } from '../store/session'
import userProfileStore, {
loadUserProfile,
resetUserProfile,
} from '../store/userProfile'
const router = useRouter()
@@ -12,13 +17,49 @@ const toast = useToast()
const handleLogout = async () => {
try {
await logout()
resetUserProfile()
reset()
router.push({ name: 'login' })
} catch (err) {
toast.error('Failed to logout')
// toast.error('Failed to logout')
toast.error('登出失敗')
console.error(err)
}
}
const handleMap = async () => {
try {
router.push({ name: 'map' })
} catch (err) {
// toast.error('Failed to load map')
toast.error('無法讀取地圖')
console.error(err)
}
}
const handleLeaderBoard = async () => {
try {
router.push({ name: 'leaderboard' })
} catch (err) {
// toast.error('Failed to load map')
toast.error('無法讀取排行榜')
console.error(err)
}
}
defineProps({
title: {
type: String,
required: true,
},
})
onMounted(async () => {
try {
await loadUserProfile()
} catch (err) {
// toast.error('Failed to load user profile')
toast.error('無法讀取使用者資料')
}
})
</script>
<template>
@@ -34,14 +75,22 @@ const handleLogout = async () => {
<button id="backward" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'arrow-left']"
class="text-text-tertiary h-full w-full"
class="text-border h-full w-full"
/>
</button> -->
<p
<button
id="HeaderText"
class="text-text-primary inline-block font-bold whitespace-nowrap"
v-if="sessionStore.quest"
style="font-size: 2vh"
>
{{ sessionStore.quest.title }}
Linux Odyssey
</button>
<p
id="HeaderText"
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">
@@ -63,6 +112,31 @@ const handleLogout = async () => {
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>
<button title="LeaderBoard" @click="handleLeaderBoard" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'trophy']"
class="text-text-primary h-full w-full"
/>
</button>
<button title="Map" @click="handleMap" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'map']"
class="text-text-primary h-full w-full"
/>
</button>
<a
title="Bug Report"
:href="bugReportUrl"
+15 -5
View File
@@ -1,6 +1,7 @@
<script setup>
import { ref, watch } from 'vue'
import sessionStore from '../store/session'
import MarkdownText from './MarkdownText.vue'
const current = ref(-1)
watch(
@@ -30,8 +31,11 @@ const right = () => {
:icon="['far', 'lightbulb']"
class="text-yellow-200 p-2 content-center"
/>
<h1 class="inline text-text font-xl p-2 m-1">Hint</h1>
<div v-if="current !== -1" class="w-full flex justify-end">
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">
<!-- Hint -->
提示
</h1>
<div v-if="current !== -1" class="flex w-full justify-end items-end">
<button
class="p-2 m-1 w-1/8"
id="Lbutton"
@@ -60,9 +64,15 @@ const right = () => {
</div>
</div>
<div id="hint" class="bg-bg flex flex-wrap p-8">
<p class="text-text font-xl whitespace-pre-wrap">
{{ sessionStore.session.hints[current] }}
</p>
<ul>
<li
v-for="hint in sessionStore.session.hints[current]"
:key="hint"
class="text-text font-xl whitespace-pre-wrap"
>
<MarkdownText :content="hint" />
</li>
</ul>
<br />
</div>
</template>
+38
View File
@@ -0,0 +1,38 @@
<script setup>
import { computed, defineProps } from 'vue'
import markdown2HTML from '../utils/markdown'
const props = defineProps({
content: {
type: String,
required: true,
},
})
const html = computed(() => {
return markdown2HTML(props.content)
})
</script>
<template>
<div class="markdown-content" v-html="html"></div>
</template>
<style scoped>
.markdown-content {
margin: 0;
padding: 0;
line-height: 1;
}
.markdown-content :deep(ol) {
list-style: decimal;
padding-left: 1em;
}
.markdown-content :deep(code) {
background-color: #282a33;
border-radius: 0.25rem;
padding: 0.25rem;
font-size: 0.875rem;
}
</style>
+191
View File
@@ -0,0 +1,191 @@
<!-- eslint-disable no-console -->
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { use, init } from 'echarts/core'
import { GraphChart } from 'echarts/charts'
import { SVGRenderer } from 'echarts/renderers'
import {
TitleComponent,
TooltipComponent,
DataZoomComponent,
} from 'echarts/components'
import { DAG } from '@linux-odyssey/utils'
import api from '../utils/api'
import { NodeImage } from '../img/svg.js'
const marginX = 500
const marginY = 300
const chartContainer = ref(null)
let chartInstance = null
const fullwidth = window.screen.width
use([
GraphChart,
SVGRenderer,
TitleComponent,
TooltipComponent,
DataZoomComponent,
])
async function getQuests() {
try {
const res = await api.get('/quests')
return res.data
} catch (err) {
console.error(err)
throw err
}
}
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, edges) => ({
renderer: 'svg',
tooltip: {},
animationDurationUpdate: 1500,
animationEasingUpdate: 'quinticInOut',
// dataZoom: [
// {
// type: 'slider', // This type means "slider data zoom" which provides a slider bar for zooming
// orient: 'vertical', // This makes the data zoom component vertical
// start: 0, // The starting percentage of the window out of the data extent
// end: 100, // The ending percentage of the window out of the data extent
// show: true,
// },
// ],
series: [
{
type: 'graph',
layout: 'none',
symbol: () => {
return NodeImage
},
symbolSize: [fullwidth / 13, fullwidth / 30],
roam: 'move',
zoom: 1.5,
center: ['120%', '30%'],
label: {
show: true,
fontSize: fullwidth / 90,
},
edgeSymbol: ['pin', 'arrow'],
edgeSymbolSize: [2, 15],
edgeLabel: {
fontSize: 25,
},
data: nodes,
// links: [],
links: edges,
lineStyle: {
width: 3,
color: 'gray',
opacity: 4,
curveness: 0.08,
},
itemStyle: {
color: (params) => {
const {
data: { completed, unlocked },
} = params
if (completed) {
return '#00ff00'
}
if (unlocked) {
return '#ADADB5'
}
return '#505050'
},
},
},
],
})
function getOption(quests, progress) {
const dag = new DAG(quests)
const nodes = dag.getNodes().map((node) => ({
id: node._id,
name: node.title,
y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
x: marginY * node.layer * 2,
completed: progress[node._id]?.completed || false,
unlocked: node.requirements.every((req) => progress[req]?.completed),
}))
const lineAppearence = (node) => {
try {
const { unlocked } = node
return {
type: unlocked ? 'line' : 'dashed',
color: unlocked ? '#ADADB5' : '#454552',
}
} catch (err) {
console.error(err)
throw err
}
}
const edges = dag.getEdgesArray().map((edge) => ({
source: edge[0],
target: edge[1],
lineStyle: lineAppearence(nodes.find((node) => node.id === edge[1])),
}))
return genOption(nodes, edges)
}
const router = useRouter()
const toast = useToast()
function initChart(option) {
chartInstance = init(chartContainer.value)
chartInstance.setOption(option)
chartInstance.on('click', (params) => {
const {
data: { id, unlocked },
} = params
if (unlocked) {
router.push({ name: 'game', params: { questId: id } })
} else {
// toast.warning('You have not completed the previous quest yet!')
toast.warning('你還沒完成前一個關卡!')
}
})
}
onMounted(async () => {
const quests = await getQuests()
const progress = await getProgress()
const option = getOption(quests, progress)
initChart(option)
})
</script>
<template>
<div class="relative bg-black w-full h-full flex">
<img
src="../img/catelogbg.svg"
alt="bg"
class="p-2 stroke-2 scale-y-120 justify-center items-center"
/>
<h1
class="p-4 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
>
<!-- Get through your linux journey! -->
踏上你的Linux冒險之旅吧!
</h1>
<div class="flex flex-wrap absolute w-full h-full z-1">
<div ref="chartContainer" class="w-full h-full flex flex-wrap"></div>
</div>
</div>
</template>
+27 -14
View File
@@ -1,28 +1,41 @@
<script setup>
import { computed } from 'vue'
import MarkdownText from './MarkdownText.vue'
import sessionStore from '../store/session'
const tasks = computed(() => {
return sessionStore.session.tasks.map((task) => {
const prefix = task.completed ? '✓' : '➤'
const color = task.completed ? 'text-text-primary' : 'text-text'
return {
...task,
class: `${color} markdown-content`,
content: `${prefix} ${task.name}`,
}
})
})
</script>
<template>
<div class="h-full w-full max-h-full">
<h1 id="topic" class="text-text" v-if="sessionStore.quest">
<p id="topic" class="text-text font-black" v-if="sessionStore.quest">
{{ sessionStore.quest.title }}
</h1>
<hr class="border-text-tertiary border-2 my-2" />
</p>
<br />
<div>
<div v-if="sessionStore.quest" class="whitespace-pre-wrap">
<p class="text-text">{{ sessionStore.quest.instruction }}</p>
<MarkdownText
class="text-text markdown-content"
:content="sessionStore.quest.instruction"
/>
<br />
<p class="text-text">Tasks:</p>
<p id="tasks" class="text-text">
<!-- Tasks: -->
任務:
</p>
<ul v-if="sessionStore.session">
<li v-for="task in sessionStore.session.tasks" :key="task.id">
<p v-if="task.completed" class="text-text-primary">
<span class="">✓</span>
{{ task.name }}
</p>
<p v-else class="text-text">
<span class="">➤</span>
{{ task.name }}
</p>
<li v-for="task in tasks" :key="task.id">
<MarkdownText :class="task.class" :content="task.content" />
</li>
</ul>
</div>
+4 -1
View File
@@ -1,6 +1,9 @@
<template>
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text p-1" />
<button class="text-text h-[5%]">Terminal</button>
<button id="Terminal" class="text-text h-[5%]">
<!-- Terminal -->
終端機
</button>
<div
id="terminal"
ref="terminal"
+1 -1
View File
@@ -121,6 +121,6 @@ li.discovered {
}
a.pwd {
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
box-shadow: 0 0 40px rgba(255, 255, 0, 20);
}
</style>
File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 1.3 MiB

+1
View File
@@ -0,0 +1 @@
{"v":"5.5.9","fr":60,"ip":0,"op":90,"w":640,"h":640,"nm":"Lottie_petard","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"Pre-comp 1","parent":2,"refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[320,320,0],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[358.25,238.75],[313.75,278],[456.813,424.5]],"c":true},"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":3},{"ddd":0,"ind":2,"ty":0,"nm":"Pre-comp 1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.47,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[318,312,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.47,"y":1},"o":{"x":0.167,"y":0.987},"t":6.5,"s":[328,316,0],"to":[0,0,0],"ti":[0,0,0]},{"t":45,"s":[318,312,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"confetti","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-61.57,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[345.257,314.37,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[272.458,139.571,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[-54,54,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"confetti","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-61.57,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[345.257,314.37,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[207.458,186.571,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[-54,54,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"confetti","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-61.57,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[345.257,314.37,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[274.458,236.571,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[-54,54,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"confetti","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-61.57,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[345.257,314.37,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[224.458,267.571,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[54,54,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"confetti","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-61.57,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[345.257,314.37,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[326.458,202.571,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[54,54,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"confetti","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-61.57,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[345.257,314.37,0],"to":[0,0,0],"ti":[0,0,0]},{"t":15,"s":[314.458,272.571,0]}],"ix":2},"a":{"a":0,"k":[320,320,0],"ix":1},"s":{"a":0,"k":[54,54,100],"ix":6}},"ao":0,"w":640,"h":640,"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 4","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[319.25,320.25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,104.369,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-27.614,0],[0,-27.614],[27.568,-1.605],[0,27.614]],"o":[[27.614,0],[0,27.614],[-26.718,1.555],[0,-27.614]],"v":[[-9,-26.75],[41,23.25],[-11.439,70.948],[-59,23.25]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.949020026712,0.886274988511,0.019608000213,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9,0.840928201114,0.008999999364,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[8,-36.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[89.096,20.801],"ix":3},"r":{"a":0,"k":-45,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60Line truncated
+3
View File
@@ -0,0 +1,3 @@
<svg width="272" height="139" viewBox="0 0 272 139" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.640015 25.0339C0.640015 11.2268 11.8329 0.0339355 25.64 0.0339355H246.36C260.167 0.0339355 271.36 11.2268 271.36 25.0339V113.14C271.36 126.948 260.167 138.14 246.36 138.14H25.64C11.8329 138.14 0.640015 126.948 0.640015 113.14V25.0339Z" fill="#454552"/>
</svg>

After

Width:  |  Height:  |  Size: 372 B

+2
View File
@@ -0,0 +1,2 @@
export const NodeImage =
'path://M0.640015 25.0339C0.640015 11.2268 11.8329 0.0339355 25.64 0.0339355H246.36C260.167 0.0339355 271.36 11.2268 271.36 25.0339V113.14C271.36 126.948 260.167 138.14 246.36 138.14H25.64C11.8329 138.14 0.640015 126.948 0.640015 113.14V25.0339Z'
+7 -1
View File
@@ -15,6 +15,9 @@ import {
faArrowRight,
faBug,
faCircleExclamation,
faMap,
faTrophy,
faFileInvoice,
} from '@fortawesome/free-solid-svg-icons'
import {
faLightbulb,
@@ -36,10 +39,13 @@ library.add(
faArrowRightFromBracket,
faArrowRight,
faBug,
faMap,
faTrophy,
faCircleExclamation,
faLightbulb,
faCircleQuestion,
faCircleRight,
faGithub,
faGoogle
faGoogle,
faFileInvoice
)
+4
View File
@@ -5,6 +5,8 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import './library'
/* import Vue Toastification */
import Toast, { POSITION } from 'vue-toastification'
/* import Vue Lottie */
import Vue3Lottie from 'vue3-lottie'
import 'vue-toastification/dist/index.css'
import App from './App.vue'
@@ -22,4 +24,6 @@ const options = {
app.use(Toast, options)
app.use(Vue3Lottie, { name: 'Vue3Lottie' })
app.mount('#app')
+17 -9
View File
@@ -3,17 +3,17 @@ import GamePage from './views/GamePage.vue'
import LoginPage from './views/LoginPage.vue'
import RegisterPage from './views/RegisterPage.vue'
import ChooseUsernamePage from './views/ChooseUsernamePage.vue'
import QuestMapPage from './views/QuestMapPage.vue'
import LeaderboardPage from './views/LeaderboardPage.vue'
import { isLoggedIn } from './utils/auth'
const routes = [
{
path: '/',
redirect: {
name: 'game',
params: {
questId: 'helloworld',
},
name: 'register',
},
meta: { requiresGuest: true },
},
{
path: '/login',
@@ -40,6 +40,17 @@ const routes = [
component: ChooseUsernamePage,
meta: { requiresGuest: true },
},
{
path: '/map',
name: 'map',
component: QuestMapPage,
meta: { requiresAuth: true },
},
{
path: '/leaderboard',
name: 'leaderboard',
component: LeaderboardPage,
},
]
const router = createRouter({
@@ -59,13 +70,10 @@ router.beforeEach(async (to, from, next) => {
})
} else if (to.meta.requiresGuest && loggedIn) {
next({
name: 'game', // Or any default authenticated user route
params: {
questId: 'helloworld',
},
name: 'map',
})
} else {
next() // Always call next() at the end
next()
}
})
+1 -1
View File
@@ -86,7 +86,7 @@ function updateGraph(event) {
}
function updateHints(hints) {
store.session.hints.push(...hints)
store.session.hints.push(hints)
}
function updateStatus(status) {
+30
View File
@@ -0,0 +1,30 @@
import { reactive } from 'vue'
import api from '../utils/api'
const store = reactive({
username: '',
email: '',
progress: {},
})
export async function loadUserProfile() {
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) {
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
+7
View File
@@ -0,0 +1,7 @@
import { marked } from 'marked'
import DOMPurify from 'dompurify'
export default function markdown2HTML(markdown) {
const html = marked.parse(markdown)
return DOMPurify.sanitize(html)
}
+5 -6
View File
@@ -13,12 +13,11 @@ class SocketTerminal {
disableStdin: false, // 是否应禁用输入
// cursorStyle: "underline", //光标样式
cursorBlink: true, // 光标闪烁
theme: {
foreground: '#ECECEC', // 字体
background: '#000000', // 背景色
cursor: 'help', // 设置光标
lineHeight: 20,
},
foreground: '#ECECEC', // 字体
background: '#000000', // 背景色
cursor: 'help', // 设置光标
fontSize: 18,
fontWeight: 'normal',
})
this.fitAddon = new FitAddon()
this.term.loadAddon(this.fitAddon)
+9 -4
View File
@@ -8,21 +8,24 @@ import { checkUsername, chooseUsername } from '../utils/auth'
async function check({ username, error }) {
if (username) {
if (!isValidUsername(username)) {
error('Invalid username.')
// error('Invalid username.')
error('無效的帳號名稱')
return
}
try {
await checkUsername(username)
} catch (err) {
if (err instanceof TooManyRequestsError) {
error('Too many requests. Try again in 2 minutes.')
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。')
return
}
if (err instanceof ValidationError) {
error(err.message)
return
}
error('Something went wrong. Please try again later.')
// error('Something went wrong. Please try again later.')
error('出了點問題,請再試一次。')
}
}
}
@@ -33,7 +36,8 @@ async function handleSubmit({ username, success, error }) {
success()
} catch (err) {
if (err instanceof TooManyRequestsError) {
error('Too many requests. Try again in 2 minutes.')
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。')
return
}
if (err instanceof ValidationError) {
@@ -44,6 +48,7 @@ async function handleSubmit({ username, success, error }) {
}
}
</script>
<template>
<div class="w-screen h-screen">
<Background class="w-full h-full" />
+13 -11
View File
@@ -3,7 +3,7 @@ import { computed, onMounted } from 'vue'
import { useToast } from 'vue-toastification'
import sessionStore, { init } from '../store/session'
import GameHeaderPart from '../components/GameHeaderPart.vue'
import CommandlistPart from '../components/CommandlistPart.vue'
// import CommandlistPart from '../components/CommandlistPart.vue'
import HintPart from '../components/HintPart.vue'
import QuestPart from '../components/QuestPart.vue'
import TerminalPart from '../components/TerminalPart.vue'
@@ -30,13 +30,15 @@ onMounted(async () => {
await init(props.questId)
} catch (err) {
if (err instanceof LoadQuestError) {
toast.error(`Failed to load quest: ${err.questId}. Please check the URL.`)
// toast.error(`Failed to load quest: ${err.questId}. Please check the URL.`)
toast.error(`無法讀取關卡: ${err.questId},請確認網頁連結。`)
return
}
if (err instanceof LoadSessionError) {
toast.error(
`Failed to create session for quest: ${err.questId}. Please re-login and try again.`
)
// toast.error(
// `Failed to create session for quest: ${err.questId}. Please re-login and try again.`
// )
toast.error(`無法建立工作階段: ${err.questId},請重新登入再試一次。`)
return
}
console.error(err)
@@ -48,7 +50,7 @@ onMounted(async () => {
<template>
<!-- game header -->
<div class="h-[6vh] w-full">
<GameHeaderPart />
<GameHeaderPart :title="sessionStore.quest?.title" />
</div>
<!-- current status indicator -->
<input
@@ -60,19 +62,20 @@ onMounted(async () => {
<div id="main" class="h-[94vh] w-full flex p-3 space-x-3">
<!-- Topic and Command List -->
<div class="bg-bg-secondary h-full w-1/3 rounded-lg">
<section id="quest" class="h-2/3 p-3">
<section id="quest" class="h-full p-3 overflow-auto">
<QuestPart />
</section>
<hr class="border-text-tertiary border" />
<!-- <hr class="border-border border" />
<section id="cmdlist" class="h-1/3">
<CommandlistPart />
</section>
</section> -->
</div>
<!-- Terminal and Hint -->
<div class="bg-bg h-full w-1/3 rounded-lg">
<section id="terminal" class="h-3/5">
<section id="terminal" class="h-3/5 overflow-hidden">
<TerminalPart />
</section>
<hr class="border-border border" />
<section id="hint" class="h-2/5">
<HintPart />
</section>
@@ -91,4 +94,3 @@ onMounted(async () => {
<CompleteModal v-if="completed" />
</div>
</template>
<script></script>
+81
View File
@@ -0,0 +1,81 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useToast } from 'vue-toastification'
import DynamicBackground from '../components/DynamicBackground.vue'
import GameHeaderPart from '../components/GameHeaderPart.vue'
import api from '../utils/api'
const leaderboard = ref([])
const toast = useToast()
async function getLeaderboard() {
try {
const res = await api.get('/leaderboard')
leaderboard.value = res.data
} catch (err) {
console.error(err)
toast.error('讀取排行榜失敗')
}
}
onMounted(async () => {
await getLeaderboard()
})
</script>
<template>
<div class="w-screen h-screen bg-bg">
<DynamicBackground class="w-full h-full" />
<div
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<div class="h-[6vh] w-full">
<GameHeaderPart title="排行榜" />
</div>
<div class="w-fit h-[94vh] m-3">
<div
class="h-full bg-bg border-8 border-bg-primary flex flex-col items-center rounded-3xl p-10"
>
<h1 class="text-text-primary text-3xl mb-5">排行榜</h1>
<div class="max-h-300 overflow-auto">
<table class="w-full text-sm text-left text-text">
<thead
class="text-xs text-text-secondary uppercase bg-bg-secondary"
>
<tr>
<th scope="col" class="px-6 py-3">使用者名稱</th>
<th scope="col" class="px-6 py-3">分數</th>
<th scope="col" class="px-6 py-3">完成關卡</th>
</tr>
</thead>
<tbody>
<tr
v-for="user in leaderboard"
:key="user.username"
class="border-b bg-bg"
>
<th
scope="row"
class="px-6 py-4 font-medium text-text-primary whitespace-nowrap"
>
{{ user.username }}
</th>
<td class="px-6 py-4">{{ user.score }}</td>
<td class="px-6 py-4 text-text-secondary">
<span
class="mr-2"
v-for="quest in user.completedQuests"
:key="quest"
>
{{ quest }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</template>
+9 -4
View File
@@ -15,22 +15,27 @@ const handleLogin = async ({ username, password, success, error }) => {
if (isSuccess) success()
} catch (err) {
if (err instanceof TooManyRequestsError) {
error('Too many requests. Try again in 2 minutes.')
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。')
return
}
if (err instanceof UnauthorizedError) {
error('Wrong username or password.')
// error('Wrong username or password.')
error('錯誤的帳號名稱或密碼。')
return
}
if (err instanceof ValidationError) {
error('Invalid username or password.')
// error('Invalid username or password.')
error('無效的帳號名稱或密碼。')
return
}
console.error(err)
error('Something went wrong.')
// error('Something went wrong.')
error('出了點問題。')
}
}
</script>
<template>
<div class="w-screen h-screen">
<Background class="w-full h-full" />
+20
View File
@@ -0,0 +1,20 @@
<script setup>
import QuestMap from '../components/QuestMap.vue'
import GameHeaderPart from '../components/GameHeaderPart.vue'
</script>
<template>
<div class="h-[6vh] w-full">
<GameHeaderPart title="關卡地圖" />
<!-- <p
class="text-text-primary absolute z-1 font-bold inline-block tracking-widest"
style="left: 1.2%; top: 1.5%; height: 100%; font-size: 2vh"
>
Quest Map
Unix編年史
</p> -->
</div>
<div class="flex flex-col justify-center items-center w-full h-[94vh]">
<QuestMap />
</div>
</template>
+19 -10
View File
@@ -17,11 +17,13 @@ function handleRegister({ username, email, password, success, error }) {
.then(success)
.catch((err) => {
if (err instanceof TooManyRequestsError) {
error('Too many requests. Try again in 2 minutes.')
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。')
return
}
if (err instanceof UnauthorizedError) {
error('Invalid username or password.')
// error('Invalid username or password.')
error('無效的帳號名稱或密碼。')
return
}
if (err instanceof ValidationError) {
@@ -29,23 +31,27 @@ function handleRegister({ username, email, password, success, error }) {
return
}
console.error(err)
error('Something went wrong.')
// error('Something went wrong.')
error('出了點問題。')
})
}
async function check({ username, email, password, error }) {
if (username && !isValidUsername(username)) {
error('Invalid username.')
// error('Invalid username.')
error('無效的帳號名稱')
return
}
if (email && !validator.isEmail(email)) {
error('Invalid email.')
// error('Invalid email.')
error('無效的電子郵件')
return
}
if (password && !validator.isStrongPassword(password, passwordPolicy)) {
error(
'Your password must be 8+ characters with at least one number, one upper and one lower case letter.'
)
// error(
// 'Your password must be 8+ characters with at least one number, one upper and one lower case letter.'
// )
error('密碼必須超過8個字元,至少包含一個數字、一個大寫及一個小寫。')
return
}
try {
@@ -56,14 +62,17 @@ async function check({ username, email, password, error }) {
return
}
if (err instanceof TooManyRequestsError) {
error('Too many requests. Try again in 2 minutes.')
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。')
return
}
console.error(err)
error('Something went wrong.')
// error('Something went wrong.')
error('出了點問題。')
}
}
</script>
<template>
<div class="w-screen h-screen">
<Background class="w-full h-full" />
+1 -1
View File
@@ -18,7 +18,7 @@ export default {
DEFAULT: '#FFFFFF',
},
border: {
DEFAULT: '#526B4E',
DEFAULT: '#4A4B63',
},
error: {
DEFAULT: '#FF0000',
+1
View File
@@ -28,6 +28,7 @@ services:
- players
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./logs:/app/logs
env_file:
- .env
environment:
+1
View File
@@ -41,6 +41,7 @@ services:
- players
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./logs:/app/logs
environment:
- BASE_URL=http://localhost:5173
- MONGO_URL=mongodb://user:user-password@db-test:27017/odyssey-test
+5 -12
View File
@@ -33,6 +33,7 @@ services:
- ./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
@@ -41,7 +42,6 @@ services:
- TRUSTED_PROXIES=${TRUSTED_PROXIES}
- SURVEY_URL=${SURVEY_URL}
- BUG_REPORT_URL=${BUG_REPORT_URL}
- HOST_PWD=${PWD}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
@@ -49,6 +49,8 @@ services:
- TESTING=true
- TESTING_USERNAME=alex
- TESTING_PASSWORD=Alex1234
- HOST_PWD=${PWD}
- MOUNT_QUEST=${MOUNT_QUEST}
analytics:
build:
@@ -63,8 +65,8 @@ services:
- '3001:3001'
environment:
- MONGO_URL=mongodb://db:27017/odyssey-test
- USERNAME=user
- PASSWORD=user
- ADMIN_USERNAME=alex
- ADMIN_PASSWORD=Alex1234
volumes:
- ./packages/analytics:/app/packages/analytics
- ./packages/models:/app/packages/models
@@ -78,15 +80,6 @@ services:
volumes:
- ./data:/data/db
swagger:
image: swaggerapi/swagger-editor:latest
ports:
- 8080:8080
volumes:
- ./server/swagger.yaml:/tmp/swagger.yaml
environment:
- SWAGGER_FILE=/tmp/swagger.yaml
base:
image: linuxodyssey/quest-base
build:
+43 -33
View File
@@ -3,9 +3,26 @@ const fs = require('fs').promises
const { exit } = require('process')
const minimist = require('minimist')
const axios = require('axios')
const { colorize, printResponses } = require('./print.js')
const { colorize, printResponses, printHints } = require('./print.js')
const discoverFiles = require('./discover.js')
const {
API_ENDPOINT,
TOKEN,
PWD,
CMD_NAME,
CMD_OUTPUT_FILE,
CMD_ERROR_FILE,
CMD_EXIT_CODE,
} = process.env
const api = axios.create({
baseURL: `${API_ENDPOINT}/api/v1`,
headers: {
Authorization: `Bearer ${TOKEN}`,
},
})
async function readOrNone(file) {
try {
return await fs.readFile(file, 'utf8')
@@ -29,24 +46,21 @@ async function handleCommand(command) {
}, {})
}
async function main() {
const {
API_ENDPOINT,
TOKEN,
PWD,
CMD_NAME,
CMD_OUTPUT_FILE,
CMD_ERROR_FILE,
CMD_EXIT_CODE,
} = process.env
if (!CMD_NAME) exit(0)
async function handleResponse({ responses, hints, end }) {
if (responses) {
await printResponses(responses, 90)
}
if (hints) {
await printHints(hints, 90)
}
if (end) {
console.log(colorize('Quest completed!', 'blue'))
}
await api.post('/commands/completed')
}
const api = axios.create({
baseURL: `${API_ENDPOINT}/api/v1`,
headers: {
Authorization: `Bearer ${TOKEN}`,
},
})
async function main() {
if (!CMD_NAME) exit(0)
const params = await handleCommand(CMD_NAME)
@@ -62,20 +76,8 @@ async function main() {
}
try {
const res = await api.post('/commands', payload)
if (!res.data) return
const { responses, hints, end } = res.data
if (responses) {
await printResponses(responses, 60)
await api.post('/commands/completed')
}
if (hints) {
for (const hint of hints) {
console.log(colorize(hint, 'yellow'))
}
}
if (end) {
console.log(colorize('Quest completed!', 'blue'))
}
if (!res.data || !res.data.responses) return
await handleResponse(res.data)
} catch (err) {
if (err.response) {
console.error(err.response.data)
@@ -85,4 +87,12 @@ async function main() {
}
}
main().then(exit)
main()
.then(exit)
.catch((err) => {
console.log(
'發生預期外的錯誤,請嘗試重新啟動 Quest,如果問題持續發生,請幫我們填寫右上角的錯誤回報表單。'
)
console.error(err)
exit(1)
})
+10 -6
View File
@@ -34,12 +34,16 @@ async function collectFilesInfo(inputPath, level = 0, hiddenFiles = false) {
async function discoverFiles(argv) {
const targetPath = argv._.length < 2 ? ['.'] : argv._.slice(1)
const result = await Promise.all(
targetPath
.map((p) => path.resolve(process.cwd(), p))
.map((p) => collectFilesInfo(p, 0, argv.a || argv.all))
)
return { discover: result.flat() }
try {
const result = await Promise.all(
targetPath
.map((p) => path.resolve(process.cwd(), p))
.map((p) => collectFilesInfo(p, 0, argv.a || argv.all))
)
return { discover: result.flat() }
} catch (err) {
return {}
}
}
module.exports = discoverFiles
+39 -9
View File
@@ -6,6 +6,7 @@ const RETURN_SYMBOL = '\u21B5'
// ANSI color map
const ANSI_COLORS = {
default: 0,
black: 30,
red: 31,
green: 32,
@@ -16,10 +17,13 @@ const ANSI_COLORS = {
white: 37,
}
function colorCode(color) {
return `\x1b[${ANSI_COLORS[color]}m`
}
function colorize(content, color) {
const colorCode = ANSI_COLORS[color]
if (!colorCode) return content
return `\x1b[${colorCode}m${content}\x1b[0m`
if (!color) return content
return `${colorCode(color)}${content}${colorCode('default')}`
}
process.stdin.setRawMode(true)
@@ -36,8 +40,25 @@ function waitForEnter() {
})
}
function formatLine(content, color) {
let result = colorCode(color)
let codeBlock = false
for (const c of content) {
switch (c) {
case '`':
result += colorCode(codeBlock ? color : 'green')
codeBlock = !codeBlock
break
default:
result += c
}
}
return result + colorCode('default') + RETURN_SYMBOL
}
function printLine(content, delay, color) {
const printContent = `${colorize(content, color)}${RETURN_SYMBOL}`
const printContent = formatLine(content, color)
return new Promise((resolve) => {
let i = 0
const timer = setInterval(() => {
@@ -52,6 +73,7 @@ function printLine(content, delay, color) {
waitForEnter().then(() => {
clearInterval(timer)
if (i >= printContent.length) return
process.stdout.write(printContent.slice(i))
process.stdout.write('\n')
resolve()
@@ -61,25 +83,33 @@ function printLine(content, delay, color) {
async function printResponses(responses, delay = 100) {
console.log()
console.log(colorize('-------------', 'white'))
console.log(colorize('==============================', 'green'))
for (const response of responses) {
const { type, content, speaker, color } = response
if (type === 'narrative') {
for (const line of content) {
await printLine(line, delay, color || 'white')
await printLine(line, delay, color || 'cyan')
}
}
if (type === 'dialogue') {
console.log(`${speaker}:`)
console.log(colorize(`${speaker}:`, 'green'))
for (const line of content) {
await printLine(` ${line}`, delay, color || 'green')
await printLine(` ${line}`, delay, color || 'cyan')
}
}
console.log(colorize('------------------------------', 'green'))
console.log()
}
}
async function printHints(hints, delay = 100) {
for (const hint of hints) {
await printLine(hint, delay, 'yellow')
}
rl.close()
}
module.exports = {
printResponses,
printHints,
colorize,
}
+19
View File
@@ -0,0 +1,19 @@
import { Schema } from 'mongoose'
import conditionSchema from './condition.js'
import responseSchema from './response.js'
import hintSchema from './hint.js'
const commandResponderSchema = new Schema({
condition: {
type: conditionSchema,
required: false,
},
responses: {
type: [responseSchema],
required: true,
},
})
commandResponderSchema.add(hintSchema)
export default commandResponderSchema
+28
View File
@@ -0,0 +1,28 @@
import { Schema } from 'mongoose'
const fileConditionSchema = new Schema({
path: {
type: String,
required: true,
},
type: String,
exists: {
type: Boolean,
default: true,
},
})
const conditionSchema = new Schema({
command: [String],
output: [String],
error: [String],
pwd: [String],
files: [fileConditionSchema],
})
conditionSchema.add({
or: [conditionSchema],
not: conditionSchema,
})
export default conditionSchema
+8
View File
@@ -0,0 +1,8 @@
import { Schema } from 'mongoose'
import commandResponderSchema from './commandResponder.js'
const exceptionSchema = new Schema({})
exceptionSchema.add(commandResponderSchema)
export default exceptionSchema
+10
View File
@@ -0,0 +1,10 @@
import { Schema } from 'mongoose'
import exceptionSchema from './exception.js'
import requirementSchema from './requirement.js'
const globalExceptionSchema = new Schema({})
globalExceptionSchema.add(exceptionSchema)
globalExceptionSchema.add(requirementSchema)
export default globalExceptionSchema
+10
View File
@@ -0,0 +1,10 @@
import { Schema } from 'mongoose'
const hintSchema = new Schema({
hints: {
type: [String],
required: true,
},
})
export default hintSchema
+6 -68
View File
@@ -1,72 +1,6 @@
import { model, Schema } from 'mongoose'
const responseSchema = new Schema({
type: {
type: String,
required: true,
},
content: [String],
speaker: String,
color: String,
})
const fileConditionSchema = new Schema({
path: {
type: String,
required: true,
},
type: String,
exists: {
type: Boolean,
default: true,
},
})
const conditionSchema = new Schema({
command: [String],
output: [String],
error: [String],
pwd: [String],
files: [fileConditionSchema],
})
conditionSchema.add({
or: [conditionSchema],
not: conditionSchema,
})
const stageSchema = new Schema({
id: {
type: String,
required: true,
},
task: {
type: String,
},
requirements: [
{
type: String,
required: true,
},
],
repeatable: {
type: Boolean,
required: true,
default: false,
},
condition: {
type: conditionSchema,
required: true,
},
responses: {
type: [responseSchema],
required: true,
},
hints: {
type: [String],
required: true,
},
})
import stageSchema from './stage.js'
import globalExceptionSchema from './globalException.js'
const Quest = model(
'Quest',
@@ -100,6 +34,10 @@ const Quest = model(
type: [stageSchema],
required: true,
},
exceptions: {
type: [globalExceptionSchema],
required: true,
},
})
)
+12
View File
@@ -0,0 +1,12 @@
import { Schema } from 'mongoose'
const requirementSchema = new Schema({
requirements: [
{
type: String,
required: true,
},
],
})
export default requirementSchema
+18
View File
@@ -0,0 +1,18 @@
import { Schema } from 'mongoose'
const responseSchema = new Schema({
type: {
type: String,
enum: ['narrative', 'dialogue'],
default: 'dialogue',
required: true,
},
content: {
type: [String],
required: true,
},
speaker: { type: String, default: 'Ada' },
color: String,
})
export default responseSchema
+3 -1
View File
@@ -35,10 +35,12 @@ const Session = model(
user: {
type: Schema.ObjectId,
ref: 'User',
required: true,
},
quest: {
type: String,
ref: 'Quest',
required: true,
},
containerId: String,
status: {
@@ -54,7 +56,7 @@ const Session = model(
required: true,
},
tasks: [taskSchema],
hints: [String],
hints: [[String]],
graph: {
type: nodeSchema,
default: {
+24
View File
@@ -0,0 +1,24 @@
import { Schema } from 'mongoose'
import commandResponderSchema from './commandResponder.js'
import stageExceptionSchema from './stageException.js'
import requirementSchema from './requirement.js'
const stageSchema = new Schema({
id: {
type: String,
required: true,
},
task: {
type: String,
required: true,
},
exceptions: {
type: [stageExceptionSchema],
required: true,
},
})
stageSchema.add(commandResponderSchema)
stageSchema.add(requirementSchema)
export default stageSchema
+14
View File
@@ -0,0 +1,14 @@
import { Schema } from 'mongoose'
import exceptionSchema from './exception.js'
const stageExceptionSchema = new Schema({
catchAll: {
type: Boolean,
required: true,
default: false,
},
})
stageExceptionSchema.add(exceptionSchema)
export default stageExceptionSchema
+73
View File
@@ -0,0 +1,73 @@
export default class DAG {
#nodes = new Map()
#edges = new Set()
#layers = []
constructor(nodes) {
this.#nodes = new Map(nodes.map((node) => [node._id, node]))
this.#edges = new Set()
this.#nodes.forEach((_, id) => {
this.#setLayer(id)
this.#addEdges(id)
})
}
getNode(id) {
return this.#nodes.get(id)
}
getNodes() {
return Array.from(this.#nodes.values())
}
getEdges() {
return this.#edges
}
getEdgesArray() {
return Array.from(this.#edges.values())
}
getLayers() {
return this.#layers
}
getLayer(id) {
const node = this.getNode(id)
if (!node) {
throw new Error(`Node ${id} not found`)
}
return this.#layers[node.layer - 1]
}
#setLayer(id) {
const node = this.getNode(id)
if (!node) {
throw new Error(`Node ${id} not found`)
}
if (node.layer !== undefined) {
return node.layer
}
const previousLayer = node.requirements.map((rId) => this.#setLayer(rId))
node.layer = Math.max(...previousLayer, 0) + 1
if (this.#layers.length < node.layer) {
this.#layers.push(1)
} else {
this.#layers[node.layer - 1] += 1
}
node.index = this.#layers[node.layer - 1]
return node.layer
}
#addEdges(id) {
const node = this.getNode(id)
if (!node) {
throw new Error(`Node ${id} not found`)
}
node.requirements.forEach((rId) => {
this.#edges.add([rId, id])
})
}
}
+165
View File
@@ -0,0 +1,165 @@
import DAG from './dag.js'
describe('dag layers', () => {
it('two layer', () => {
const data = [
{
_id: 'helloworld',
requirements: [],
},
{
_id: 'spell',
requirements: ['helloworld'],
},
]
const dag = new DAG(data)
expect(dag.getNode('helloworld').layer).toBe(1)
expect(dag.getNode('spell').layer).toBe(2)
})
it('branch', () => {
const data = [
{
_id: 'helloworld',
requirements: [],
},
{
_id: 'spell',
requirements: ['helloworld'],
},
{
_id: 'discover',
requirements: ['helloworld'],
},
]
const dag = new DAG(data)
expect(dag.getNode('helloworld').layer).toBe(1)
expect(dag.getNode('spell').layer).toBe(2)
expect(dag.getNode('discover').layer).toBe(2)
})
it('merge', () => {
const data = [
{
_id: 'A',
requirements: [],
},
{
_id: 'B',
requirements: [],
},
{
_id: 'C',
requirements: ['A', 'B'],
},
]
const dag = new DAG(data)
expect(dag.getNode('A').layer).toBe(1)
expect(dag.getNode('B').layer).toBe(1)
expect(dag.getNode('C').layer).toBe(2)
})
it('skip', () => {
const data = [
{
_id: 'A',
requirements: [],
},
{
_id: 'B',
requirements: [],
},
{
_id: 'C',
requirements: ['A'],
},
{
_id: 'D',
requirements: ['B', 'C'],
},
]
const dag = new DAG(data)
expect(dag.getNode('A').layer).toBe(1)
expect(dag.getNode('B').layer).toBe(1)
expect(dag.getNode('C').layer).toBe(2)
expect(dag.getNode('D').layer).toBe(3)
})
})
describe('dag edges', () => {
it('one edge', () => {
const data = [
{
_id: 'helloworld',
requirements: [],
},
{
_id: 'spell',
requirements: ['helloworld'],
},
]
const dag = new DAG(data)
expect(dag.getEdges()).toEqual(new Set([['helloworld', 'spell']]))
})
it('branch edges', () => {
const data = [
{
_id: 'helloworld',
requirements: [],
},
{
_id: 'spell',
requirements: ['helloworld'],
},
{
_id: 'discover',
requirements: ['helloworld'],
},
]
const dag = new DAG(data)
expect(dag.getEdges()).toEqual(
new Set([
['helloworld', 'discover'],
['helloworld', 'spell'],
])
)
})
})
describe('dag layers', () => {
it('two layer', () => {
const data = [
{
_id: 'helloworld',
requirements: [],
},
{
_id: 'spell',
requirements: ['helloworld'],
},
]
const dag = new DAG(data)
expect(dag.getLayers()).toEqual([1, 1])
})
it('branch layers', () => {
const data = [
{
_id: 'helloworld',
requirements: [],
},
{
_id: 'spell',
requirements: ['helloworld'],
},
{
_id: 'discover',
requirements: ['helloworld'],
},
]
const dag = new DAG(data)
expect(dag.getLayers()).toEqual([1, 2])
})
})
+1
View File
@@ -1,2 +1,3 @@
export { isValidEmail, isValidUsername } from './auth.js'
export { buildFileCheckCmd } from './commands.js'
export { default as DAG } from './dag.js'
+75
View File
@@ -0,0 +1,75 @@
title: 輸出文字
instruction: |
在這個關卡中,你將會學到:
1. 如何使用 `echo` 指令
準備好就輸入 `echo start` 開始吧!
requirements:
- get-started
stages:
- 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: END
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` 指令。
exceptions:
- repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
+131
View File
@@ -0,0 +1,131 @@
title: 輸出文字
instruction: |
在這個關卡中,你將會學到:
1. 如何使用 `echo` 指令
2. `echo` 指令在遊戲中的用途
準備好就輸入 `echo start` 開始吧!
requirements:
- get-started
stages:
- id: warning-01
repeatable: true
condition:
command:
- ' '
hints:
- '!! 請勿使用全形空格 !!'
- id: start
task: 輸入 `echo start` 來開始教程
condition:
output:
- ^start$
responses:
- content:
- 你剛才使用的指令叫做 `echo`。
- '`echo` 指令可以將後方接續的文字輸出到終端機上。'
- 例如你輸入 `echo Hello`,終端機就會顯示 `Hello`。
- 你可以試著輸入 `echo` 並在後面加上一些文字,然後按下 Enter 鍵,就可以看到終端機上顯示了你剛才輸入的文字。
hints:
- ➤ 輸入 `echo` 並在後面加上一些文字
- ➤ 例如:`echo example`
- id: echo
task: 在終端機上顯示顯示任意文字
requirements:
- start
condition:
command:
- ^echo .*
responses:
- content:
- 你應該可以看到終端機上顯示了你在 `echo` 指令後面的文字。
- 接下來,請你試著在終端機上顯示 `How are you?`。
hints:
- ➤ 利用 `echo` 指令來在終端機上顯示 `How are you?`
- id: how-are-you
task: 在終端機上顯示 `How are you?`
requirements:
- echo
condition:
output:
- ^[Hh]ow are you\??$
exceptions:
- condition:
command:
- ^[Hh]ow are you\??$
responses:
- type: narrative
content:
- 在 `How are you?` 前面必須加上 `echo`
- condition:
command:
- ^echo$
responses:
- type: narrative
content:
- 在 `echo` 後面還需要加上一些文字
responses:
- content:
- 你成功的在終端機上顯示了 `How are you?`。
- 相信你已經了解 `echo` 指令的用法了。
- 接下來,要像你介紹 `echo` 在遊戲中的特殊功能,那就是詠唱咒語。
- 你可以通過輸入 `echo [咒語]` 來詠唱咒語,並產生對應的特殊效果。
- 但是要注意,詠唱咒語只有在 Linux Odyssey 中才會有特殊效果。
- 在真實的 Linux 環境中,詠唱咒語並不會有任何特殊效果。
- 現在,有一段咒語叫做 `HELP`,請你試著使用 `echo` 詠唱這段咒語。
hints:
- ➤ 試著使用 `echo` 來詠唱 `HELP` 咒語
- ➤ 注意:咒語要使用大寫
- id: help
task: 詠唱 `HELP` 咒語
requirements:
- how-are-you
condition:
output:
- ^HELP$
responses:
- content:
- 你詠唱了 `HELP` 咒語,一位精靈出現了。
- type: dialogue
speaker: Unix 精靈
content:
- 你好,我是 Unix 精靈。
- 既然你使用了 `HELP` 咒語呼喚我,就讓我來教你一個咒語吧。
- 讓我想想... 好了,我想到了一個咒語,叫做 `WATER`。
- 這是施展水魔法的咒語。詠唱這個咒語可以施展水魔法,產生少量的水。
- 試著使用 `echo` 詠唱這個咒語吧。
hints:
- ➤ 詠唱 `WATER` 咒語
- id: END
task: 詠唱 `WATER` 咒語
requirements:
- help
condition:
output:
- ^WATER$
responses:
- content:
- 你施展了水魔法,一個水球出現在你的手中。
- type: dialogue
speaker: Unix 精靈
content:
- 很好,你已經學會了水魔法。
- content:
- 真厲害,你已經了解如何使用 `echo` 指令,並且學會了詠唱咒語的方法。
- 最後複習一下,你可以通過輸入 `echo [文字]` 來在終端機上顯示文字。
- 你也可以通過輸入 `echo [咒語]` 來詠唱咒語,並產生對應的特殊效果。
- 不要忘了,詠唱咒語只有在 Linux Odyssey 中才會有特殊效果。
- 在真實的 Linux 環境中,咒語只會被視為一串文字顯示出來而已。
- 以上就是 `echo` 指令的用法,請繼續努力,完成接下來的關卡吧!
hints: |
複習:
通過輸入 `echo [文字]` 來在終端機上顯示文字
通過輸入 `echo [咒語]` 來詠唱咒語
咒語只有在 Linux Odyssey 中才會有特殊效果
@@ -1,3 +1,3 @@
FROM linuxodyssey/quest-base
COPY home ${HOME}
COPY home ${HOME}
+262
View File
@@ -0,0 +1,262 @@
title: 複製檔案
instruction: |
在這個關卡中,你將會學到:
1. 如何用 `cp` 指令複製檔案
準備好就輸入 `echo start` 開始吧!
requirements:
- create
stages:
- id: start
task: 輸入 `echo start` 來開始
condition:
output:
- start
exceptions:
- condition:
command:
- ^echo$
responses:
- type: narrative
content:
- 你忘記輸入 `start` 了嗎?
- 請輸入 `echo start` 來開始
- catchAll: true
responses:
- type: narrative
content:
- 請輸入 `echo start` 來開始
responses:
- type: narrative
content:
- 今天我們要學習如何複製檔案。
- 現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,請使用 `ls` 與 `cat` 來查看它們的內容。
- 看完之後,使用 `echo continue` 來繼續吧!
hints:
- ➤ 使用 `ls` 來查看資料夾內的檔案
- ➤ 使用 `cat hello.txt` 以及 `cat world.txt` 來查看檔案內容
- ➤ 如果終端機卡在某個指令無法輸入,請按下 `Ctrl + C` 來取消該指令
- id: cat-hello
task: 查看 `hello.txt` 的內容
requirements:
- start
condition:
command:
- ^cat hello.txt$
output:
- Hello
exceptions:
- condition:
command:
- ^cat$
responses:
- type: narrative
content:
- 你忘記輸入 `cat` 了嗎?
- 請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
- condition:
command:
- ^cat hello$
responses:
- type: narrative
content:
- 你忘記輸入 `.txt` 了嗎?
- 請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
responses:
- type: narrative
content:
- 你成功查看到 `hello.txt` 的內容了!
- id: cat-world
task: 查看 `world.txt` 的內容
requirements:
- start
condition:
command:
- ^cat world.txt$
output:
- World
responses:
- type: narrative
content:
- 你成功查看到 `world.txt` 的內容了!
- id: cat-continue
task: 使用 `echo continue` 來繼續
requirements:
- cat-hello
- cat-world
condition:
command:
- echo
responses:
- type: narrative
content:
- 接下來我們要來學習複製檔案。
- '`cp` 這個指令,是 copy 的縮寫,就是複製的意思。'
- 我們可以用 `cp [來源檔案] [目的檔案]` 的方式來複製檔案。
- 請你使用 `cp` 來複製 `hello.txt` 到 `hello2.txt`。
hints:
- ➤ 使用 `cp hello.txt hello2.txt` 來複製 `hello.txt` 到 `hello2.txt`
- id: cp-hello
task: 複製 `hello.txt` 到 `hello2.txt`
requirements:
- cat-continue
condition:
command:
- ^cp hello.txt hello2.txt$
files:
- path: hello2.txt
type: file
responses:
- type: narrative
content:
- 你成功複製 `hello.txt` 到 `hello2.txt` 了!
- 我們打開來確認一下 `hello2.txt` 的內容吧!
hints:
- ➤ 使用 `cat hello2.txt` 來查看 `hello2.txt` 的內容
- id: cat-hello2
requirements:
- cp-hello
task: 查看 `hello2.txt` 的內容
condition:
command:
- ^cat hello2.txt$
output:
- Hello
responses:
- type: narrative
content:
- 你成功查看到 `hello2.txt` 的內容了!
- 正如我們所見,`hello2.txt` 的內容跟 `hello.txt` 一模一樣。
- 那我們再來試試看複製 `world.txt` 到 `world2.txt` 吧!
hints:
- ➤ 使用 `cp world.txt world2.txt` 來複製 `world.txt` 到 `world2.txt`
- id: cp-world
requirements:
- cat-hello2
task: 複製 `world.txt` 到 `world2.txt`
condition:
command:
- ^cp world.txt world2.txt$
files:
- path: world2.txt
type: file
responses:
- type: narrative
content:
- 你成功複製 `world.txt` 到 `world2.txt` 了!
- 我們打開來確認一下 `world2.txt` 的內容吧!
hints:
- ➤ 使用 `cat world2.txt` 來查看 `world2.txt` 的內容
- id: cat-world2
requirements:
- cp-world
task: 查看 `world2.txt` 的內容
condition:
command:
- ^cat world2.txt$
output:
- World
responses:
- type: narrative
content:
- 你成功查看到 `world2.txt` 的內容了!
- '`world2.txt` 的內容跟 `world.txt` 一模一樣。'
- 接下來我們要來學習如何複製資料夾。
- 現在目錄下有一個 `notes` 資料夾,裡面有一個 `note.txt` 的檔案。
- 我們要能整個資料夾包含裡面的檔案一起複製到 `notes2`。
- 跟複製單一檔案不一樣的是,複製資料夾時,我們要加上 `-r` 的選項(recursive)代表將資料夾中的所有檔案都一起複製。
- 在使用指令時,如果想要調整指令的行為,可以在指令後面加上選項。
- 選項的格式通常是 `-` 加上一個字母,例如 `-r`。
- 選項擺放的位置沒有特別的限制,通常會跟在指令的後面,置於檔案名稱的前面。
- 請你使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`。
hints:
- ➤ 使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
- ➤ 記得要加上 `-r` 的選項來複製所有檔案
- id: cp-notes
requirements:
- cat-world2
task: 複製 `notes` 資料夾到 `notes2`
condition:
command:
- ^cp
files:
- path: notes2
type: directory
- path: notes2/note.txt
type: file
responses:
- type: narrative
content:
- 你成功複製 `notes` 資料夾到 `notes2` 了!
- 我們打開來確認一下 `notes2` 資料夾的內容吧!
hints:
- ➤ 使用 `ls notes2` 來查看 `notes2` 資料夾的內容
exceptions:
- condition:
error:
- 'cp: omitting directory'
responses:
- type: narrative
content:
- 你忘記加上 `-r` 的參數了嗎?
- 請輸入 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
- id: ls-notes2
requirements:
- cp-notes
task: 查看 `notes2` 資料夾的內容
condition:
command:
- ^ls notes2$
output:
- note.txt
responses:
- type: narrative
content:
- 你成功查看到 `notes2` 資料夾的內容了!
- 我們可以看到,整個 `notes` 資料夾包含裡面的檔案,都被複製到 `notes2` 資料夾了。
- 那麼最後我們要來試試看,如果複製一個檔案到已經存在的檔案會發生什麼事。
hints:
- ➤ 使用 `cp hello.txt world.txt` 來複製 `hello.txt` 到 `world.txt`
- id: cp-overwrite
requirements:
- ls-notes2
task: 複製 `hello.txt` 到 `world.txt`
condition:
command:
- ^cp hello.txt world.txt$
responses:
- type: narrative
content:
- 你成功複製 `hello.txt` 到 `world.txt` 了!
- 我們打開來確認一下 `world.txt` 的內容吧!
hints:
- ➤ 使用 `cat world.txt`
- id: END
requirements:
- cp-overwrite
task: 檢查 `world.txt` 的內容
condition:
command:
- ^cat world.txt$
output:
- Hello
responses:
- type: narrative
content:
- '`world.txt` 的內容變成 `Hello` 了!'
- 這是因為,當我們複製檔案時,如果目的檔案已經存在,那麼目的檔案的內容就會被覆蓋掉。
- 所以在複製檔案的時候,要特別注意目的檔案是否已經存在,否則可能會不小心把資料弄丟。
- 以上就是複製檔案的基本操作!
+1
View File
@@ -0,0 +1 @@
Hello
+1
View File
@@ -0,0 +1 @@
Note
+1
View File
@@ -0,0 +1 @@
World
+192
View File
@@ -0,0 +1,192 @@
title: 建立檔案與資料夾
instruction: |
在這個關卡中,你將會學到:
1. 如何使用 `touch` 建立檔案
2. 如何使用 `mkdir` 建立資料夾
準備好就輸入 `echo start` 開始吧!
requirements:
- read
stages:
- id: start
task: 輸入 `echo start` 來開始
condition:
command:
- ^echo start
responses:
- type: narrative
content:
- 這次要學習的指令是 `touch` 指令。
- '`touch` 指令可以建立一個檔案。'
- 接下來,請你試著在終端機上建立一個檔案,檔名為 `hello.txt`。
hints:
- ➤ 輸入 `touch hello.txt` 來建立一個檔案
- id: touch
task: 使用 `touch` 指令來建立一個檔案
requirements:
- start
condition:
files:
- path: hello.txt
type: file
responses:
- type: narrative
content:
- 你應該可以看到終端機上沒有任何回應。
- 這表示 `touch` 指令已經成功建立了一個檔案。
- 接下來,請你用之前學過的 `ls` 指令來查看目前所在資料夾中有哪些檔案。
hints:
- ➤ 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
- id: ls-touch
task: 使用 `ls` 指令來查看新建立的檔案
requirements:
- touch
condition:
command:
- ^ls$
output:
- hello.txt
responses:
- type: narrative
content:
- 你應該可以看到終端機上顯示了 `hello.txt` 這個檔案。
- 我們來看看 `hello.txt` 檔案的內容吧!
- 我們可以使用之前學過的 `cat` 指令來查看檔案內容。
hints:
- ➤ 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
- id: cat
task: 使用 `cat` 指令來查看 `hello.txt` 檔案的內容
condition:
command:
- ^cat hello.txt$
responses:
- type: narrative
content:
- 咦?竟然什麼都沒有!
- 沒錯,使用 `touch` 指令建立的檔案是空的。
- 我們在未來會學習如何編輯檔案的內容。
- 再來試試建立其他的檔案吧!
- 請你試著在終端機上建立另一個檔案,檔名為 `world.txt`。
hints:
- ➤ 輸入 `touch world.txt` 來建立一個檔案
- id: touch-2
task: 使用 `touch` 指令來建立另一個檔案 `world.txt`
requirements:
- ls-touch
condition:
files:
- path: world.txt
type: file
responses:
- type: narrative
content:
- 非常好,你已經成功建立了第二個檔案。
- 接下來,請你用之前學過的 `ls` 指令來查看剛剛建立的兩個檔案。
hints:
- ➤ 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
- id: ls-2
task: 使用 `ls` 指令來查看剛剛建立的兩個檔案
requirements:
- touch-2
condition:
command:
- ^ls$
output:
- hello.txt
- world.txt
responses:
- type: narrative
content:
- 你應該可以看到終端機上顯示了 `hello.txt` 和 `world.txt` 這兩個檔案。
- 接下來,我們來試試看建立資料夾吧!
- '`mkdir` 指令可以建立一個空的資料夾。'
- 使用方式是 `mkdir [資料夾名稱]`。
- 請你試著在終端機上建立一個資料夾,名稱為 `notes`。
hints:
- ➤ 輸入 `mkdir notes` 來建立一個資料夾
- id: mkdir
task: 使用 `mkdir` 指令建立 `notes` 資料夾
requirements:
- ls-2
condition:
files:
- path: notes
type: folder
responses:
- type: narrative
content:
- 非常好,你已經成功建立了 `notes` 資料夾。
- 接下來,請你用 `ls` 指令來查看剛剛建立的資料夾。
- id: ls-mkdir
task: 使用 `ls` 查看剛建立的資料夾
requirements:
- mkdir
condition:
command:
- ^ls$
output:
- notes
responses:
- type: narrative
content:
- 你應該可以看到終端機上顯示了 `notes` 這個資料夾。
- 接下來,我們要在 `notes` 資料夾中建立一個 `note.txt` 的檔案。
- 首先,我們要先進入 `notes` 資料夾。
- 還記得之前的 `cd` 指令嗎?請你試著使用 `cd` 指令來進入 `notes` 資料夾。
hints:
- ➤ 輸入 `cd notes` 來進入 `notes` 資料夾
- ➤ 可以在輸入到 `cd n` 時按下 `Tab` 鍵來自動補齊資料夾名稱
- id: cd-notes
task: 移動到 `notes` 資料夾
requirements:
- ls-mkdir
condition:
pwd:
- ^/home/commander/notes$
responses:
- type: narrative
content:
- 你已經成功進入了 `notes` 資料夾。
- 接下來,請你試著在 `notes` 資料夾裡建立一個檔案,檔名為 `note.txt`。
hints:
- ➤ 輸入 `touch note.txt` 來建立一個檔案
- id: touch-note
task: 在 `notes` 中建立 `note.txt` 檔案
requirements:
- cd-notes
condition:
files:
- path: notes/note.txt
type: file
responses:
- type: narrative
content:
- 非常好,你已經成功建立了 `note.txt` 檔案。
- 跟之前一樣,你可以使用 `ls` 指令來查看目前所在資料夾中有哪些檔案。
- id: END
task: 使用 `ls` 指令來查看目前所在資料夾中有哪些檔案
requirements:
- touch-note
condition:
command:
- ^ls
output:
- note.txt
responses:
- type: narrative
content:
- 你應該可以看到終端機上顯示了 `note.txt` 這個檔案。
- 恭喜你完成了這個關卡!
- 在下一個關卡中,你將學習如何複製檔案與資料夾。
-6
View File
@@ -1,6 +0,0 @@
title: Discover the World!
instruction: |
試著探索這個世界吧!
requirements:
- spell
+142
View File
@@ -0,0 +1,142 @@
exceptions:
- condition:
command:
- '[\uff21-\uff3a\uff41-\uff5a]'
responses:
- type: narrative
content:
- '!! 請使用半形英文字母 !!'
- condition:
command:
- ' '
responses:
- type: narrative
content:
- '!! 請使用半形空格 !!'
- condition:
or:
- command:
- '`'
- command:
- \'
responses:
- type: narrative
content:
- '!! 不需要輸入上下引號 !!'
- condition:
command:
- ^echo\S$
responses:
- type: narrative
content:
- '!! `echo` 後面要加空格 !!'
- condition:
command:
- ^cd\S$
responses:
- type: narrative
content:
- '!! `cd` 後面要加空格 !!'
- condition:
command:
- ^ls\S$
responses:
- type: narrative
content:
- '!! `ls` 後面要加空格 !!'
- condition:
command:
- ^cat\S$
responses:
- type: narrative
content:
- '!! `cat` 後面要加空格 !!'
- condition:
command:
- ^touch\S$
responses:
- type: narrative
content:
- '!! `touch` 後面要加空格 !!'
- condition:
not:
command:
- ^rmdir$
command:
- ^rm\S$
responses:
- type: narrative
content:
- '!! `rm` 後面要加空格 !!'
- condition:
command:
- ^mv\S$
responses:
- type: narrative
content:
- '!! `mv` 後面要加空格 !!'
- condition:
command:
- ^mkdir\S$
responses:
- type: narrative
content:
- '!! `mkdir` 後面要加空格 !!'
- condition:
command:
- ^rmdir\S$
responses:
- type: narrative
content:
- '!! `rmdir` 後面要加空格 !!'
- condition:
command:
- ^cp\S$
responses:
- type: narrative
content:
- '!! `cp` 後面要加空格 !!'
- condition:
command:
- ^[Ii1]s$
responses:
- type: narrative
content:
- '!! `ls` 的首字母是小寫的 `L` !!'
- condition:
command:
- 嗨
responses:
- type: narrative
content:
- '嗨~'
- condition:
command:
- folderl
responses:
- type: narrative
content:
- '`folder1` 的 `1` 是數字 `1`,不是小寫的 `L` !!'
- condition:
command:
- filel
responses:
- type: narrative
content:
- '`file1` 的 `1` 是數字 `1`,不是小寫的 `L` !!'
+3
View File
@@ -0,0 +1,3 @@
FROM linuxodyssey/quest-base
COPY home ${HOME}
+171
View File
@@ -0,0 +1,171 @@
title: 探索檔案
instruction: |
在這個關卡中,你將會學到:
1. 如何使用 `ls` 查看資料夾的內容
2. 如何使用 `cd` 在資料夾之間移動
準備好就輸入 `echo start` 開始吧!
requirements:
- get-started
stages:
- id: start
task: 輸入 `echo start` 來開始教程
condition:
output:
- ^start$
responses:
- content:
- 在終端機中,並沒有圖形介面可以讓你查看資料夾的內容。
- 但是你可以透過 `ls` 指令來查看資料夾的內容。
- '`ls` 指令是 list 的縮寫,也就是列出檔案的意思。'
- 在輸入 `ls` 指令後,終端機會輸出資料夾中包含的檔案和資料夾,右邊的檔案樹也會隨之改變。
- 現在,請你輸入 `ls` 指令,查看資料夾中有什麼東西。
hints:
- ➤ 輸入 `ls` 並查看資料夾的內容
- id: ls
task: 查看資料夾中的檔案
requirements:
- start
condition:
command:
- ^ls$
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
responses:
- type: narrative
content:
- 請先回到 `/home/commander/` 資料夾
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
responses:
- content:
- 你應該能看到資料夾中的內容了。
- 在畫面右邊的檔案樹也有了改變。
- 可以看出,在這個資料夾中包含了兩個資料夾,分別是 `folder1` 和 `folder2`。
- content:
- 接下來,我要教你如何進入資料夾。
- '`cd` 指令是 change directory 的縮寫,也就是改變路徑的意思。'
- 你可以通過輸入 `cd [資料夾名稱]` 來進入資料夾。
- 例如你要進入 `folder1` 資料夾,你可以輸入
- '`cd folder1`。'
- 現在,請你進入 `folder1` 資料夾。
hints:
- ➤ 輸入 `cd folder1` 來進入 `folder1` 資料夾
- id: cd-folder1
task: 進入 `folder1` 資料夾
requirements:
- ls
condition:
pwd:
- ^/home/commander/folder1$
exceptions:
- condition:
pwd:
- ^/home/commander$
responses:
- type: narrative
content:
- 輸入 `cd folder1` 來進入 `folder1` 資料夾
- condition:
not:
pwd:
- ^/home/commander$
responses:
- type: narrative
content:
- 請先回到 `/home/commander/` 資料夾
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
responses:
- content:
- 你成功的進入 `folder1` 資料夾了。
- 你可以發現指令提示符從原本的
- '`commander:~ $` 變成了'
- '`commander:~/folder1 $` 。'
- 這表示你現在正位在 `folder1` 資料夾中。
- 現在,請查看 `folder1` 資料夾的內容。
hints:
- ➤ 輸入 `ls` 查看資料夾的內容
- id: ls-folder1
task: 查看 `folder1` 資料夾的內容
requirements:
- cd-folder1
condition:
pwd:
- ^/home/commander/folder1$
command:
- ^ls$
responses:
- content:
- 你現在可以看到 `folder1` 資料夾中有兩的檔案,分別是 `file1.txt` 跟 `file2.txt`。
- 現在我們可以離開 `folder1` 回到原本的資料夾了。
- 你可以使用 `cd ..` 回到上一層資料夾。
- 這裡的 `..` 代表的是上一層的資料夾。
hints:
- ➤ 輸入 `cd ..` 回到上一層資料夾
- id: cd-..
task: 回到上一層資料夾
requirements:
- ls-folder1
condition:
pwd:
- ^/home/commander$
responses:
- content:
- 你成功的回到上一層資料夾了。
- 現在,請你剛才所學的指令,進入 `folder2` 資料夾,檢查 `folder2` 資料夾的內容,最後回到這個資料夾。
hints:
- ➤ 使用 `cd` 指令進出資料夾
- ➤ 使用 `ls` 指令查看資料夾的內容
- id: cd-folder2
task: 進入 `folder2` 資料夾
requirements:
- cd-..
condition:
pwd:
- ^/home/commander/folder2$
responses:
- content:
- 你成功的進入 `folder2` 資料夾了。
- 現在,請你查看 `folder2` 資料夾的內容。
hints:
- ➤ 使用 `ls` 查看資料夾的內容
- id: ls-folder2
task: 查看 `folder2` 資料夾的內容
requirements:
- cd-folder2
condition:
pwd:
- ^/home/commander/folder2$
command:
- ls
responses:
- content:
- 你看到了 `folder2` 資料夾中的檔案,分別是 `file3.txt` 跟 `file4.txt`。
- 現在你可以離開 `folder2` 回到原本的資料夾了。
hints:
- ➤ 使用 `cd ..` 回到上一層資料夾
- id: END
task: 回到原本的資料夾
requirements:
- ls-folder2
condition:
pwd:
- ^/home/commander$
responses:
- content:
- 你成功的回到原本的資料夾了。
- 恭喜你學會了 `ls` 和 `cd` 指令的用法。
hints:
- ➤ 使用 `cd` 指令進出資料夾
- ➤ 使用 `ls` 指令查看資料夾的內容
File renamed without changes.
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
+77
View File
@@ -0,0 +1,77 @@
title: 終端機的基本知識
instruction: |
歡迎來到 Linux Odyssey!
在這個關卡中,你將會學到:
1. 如何使用這個網站
2. 如何使用終端機
在畫面的中間可以看到終端機和提示兩個區塊。
請你在終端機中的 `commander:~ $` 後面輸入 `echo start` 並按下 Enter 來開始教程。
!! 注意:不需要輸入上下引號 !!
stages:
- id: start
task: 輸入 `echo start` 來開始教程
condition:
output:
- ^start$
responses:
- content:
- 你好,我是你的 Unix 助手,我叫做 Ada。我將會在之後協助你了解如何使用終端機。
- 現在所在的這個區塊就是終端機,你可以在這裡輸入指令來操作電腦。
- 在終端機中,你所看到的
- '`commander:~ $` 是指令提示符號,它能讓你知道現在可以輸入指令了。'
- 你必須將指令輸入在它的後方。
- 當你輸入完指令並按下 Enter 之後,終端機會執行你輸入的指令。
- 並且會將執行結果輸出在終端機的下一行。
- 這個終端機的功能和你在 Linux 電腦上看到的終端機是一樣的。
- 唯一的差別是,在這裡會有劇情和提示來協助你學習如何使用終端機。
- 在輸入指令之後,如果你看到用綠色 `=====` 組成的分隔線,代表在分隔線下方的內容是添加的內容,在真實的終端機中不會出現。
- 而終端機的輸出,是在綠色 `=====` 分隔線上方的白色內容。
- 現在請你輸入 `echo hello` 來看看會發生什麼事。
hints:
- ➤ 在 `commander:~ $` 後面輸入 `echo hello` 並按下 Enter
- id: echo-hello
task: 在終端機輸入 `echo hello`
requirements:
- start
condition:
command:
- ^echo hello$
responses:
- content:
- 你應該可以看到終端機上顯示了:
- commander:~ $ echo hello
- hello
- 第一行的 `commander:~ $` 是指令提示符, `echo hello` 是你輸入的指令。
- 第二行的 `hello` 則是終端機的輸出。
- content:
- 現在你知道如何在終端機上輸入指令,以及查看終端機輸出的位置。
- 指令必須輸入在指令提示符號的後面,然而當指令提示符消失時,你就無法輸入指令了。
- 這時候你可以按下 `Ctrl + C` 來嘗試解決這個問題。
- 在終端機中, `Ctrl + C` 並不能用來複製,而是用來中斷目前正在執行的指令。
- 因此,如果你前一次的指令執行時卡住了,或是執行很久沒有反應,你可以按下 `Ctrl + C` 來中斷指令,讓你可以繼續輸入指令。
- 如果 `Ctrl + C` 也沒有辦法解決問題,可以按按看 `esc` 或 `q` 鍵。如果都沒用的話,按下畫面右下角的 `重來` 按鈕重新開始關卡。
- content:
- 在終端機下方會顯示提示,你可以用提示右上角的箭頭來查看之前的提示。
- 在畫面左邊的區域是關卡說明,你可以在這裡看到關卡的介紹,以及現在需要完成的任務。
- 在畫面右邊的區域是檔案樹,它會根據你所探索的資料夾發生變化,讓你更清楚檔案之間的位置關係。
- 畫面右上角的圖示分別是 `查看地圖`、`回報錯誤` 以及 `登出` 按鈕。
- content:
- 第一關的文字內容比較多,可以慢慢讀沒關係。
- 如果忘記了也可以隨時回來複習。
- 需要的話可以按下 `Enter` 來快速跳過劇情說明。
- 如果你已經讀完了上述內容,請輸入 `echo finish` 來結束這一關。
hints:
- ➤ 按下 `Ctrl + C` 能夠中斷指令
- ➤ 劇情進行時可以按下 `Enter` 來快速跳過
- ➤ 輸入 `echo finish` 來結束這一關
- id: END
task: 結束關卡
requirements:
- echo-hello
condition:
command:
- ^echo finish$
+12 -13
View File
@@ -4,15 +4,17 @@ instruction: |
你感到腦中有某個聲音跟你說:
「使用 `echo [ 咒語 ]` 指令來朗誦咒語 "help" 」
requirements:
- remove
stages:
- id: help
task: 輸入 `echo help` 來朗誦咒語
repeatable: false
condition:
pwd:
- '^/home/commander$'
- ^/home/commander$
command:
- echo [Hh]elp
- ^echo [Hh]elp$
responses:
- type: narrative
content:
@@ -33,16 +35,15 @@ stages:
- id: find-scroll
task: 搜索卷軸
repeatable: false
requirements:
- help
condition:
pwd:
- '^/home/commander$'
- ^/home/commander$
command:
- ls
- ^ls$
responses:
- types: narrative
- type: narrative
content:
- 一瞬間,視野變得清晰起來,你發現自己身處在一個石頭建築的房間裡。
- 你發現了一個檔案,檔名是 `forgotten-scroll.txt`。
@@ -60,14 +61,13 @@ stages:
- id: read-forgotten-scroll
task: 查看卷軸
repeatable: false
requirements:
- find-scroll
condition:
pwd:
- '^/home/commander$'
- ^/home/commander$
command:
- cat forgotten_scroll.txt
- ^cat forgotten_scroll.txt$
responses:
- type: dialogue
speaker: Unix 精靈
@@ -79,14 +79,13 @@ stages:
- id: END
task: 解除封印
repeatable: false
requirements:
- read-forgotten-scroll
condition:
pwd:
- '^/home/commander$'
- ^/home/commander$
command:
- echo [Hh]ello\s*[Ww]orld
- ^echo [Hh]ello\s*[Ww]orld$
responses:
- type: narrative
content:
-10
View File
@@ -1,10 +0,0 @@
quests:
- helloworld
- spell
- discover
- read
- create
- transport
- copy
- move
- remove
+3
View File
@@ -0,0 +1,3 @@
FROM linuxodyssey/quest-base
COPY --chown=${USERNAME} home ${HOME}
+220
View File
@@ -0,0 +1,220 @@
title: 移動檔案
instruction: |
在這個關卡中,你將會學到:
1. 如何用 `mv` 移動檔案與資料夾
準備好就輸入 `echo start` 開始吧!
requirements:
- create
stages:
- id: start
task: 輸入 `echo start` 來開始
condition:
command:
- ^echo start$
responses:
- type: narrative
content:
- 今天我們要學習如何移動檔案與資料夾。
- 現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,以及一個 `notes` 資料夾。
- 請先用 `ls` 與 `cat` 來查看它們的內容。
- 看完之後,使用 `echo continue` 來繼續吧!
hints:
- ➤ 用 `ls`、`ls notes` 來查看資料夾內的檔案
- ➤ 用 `cat hello.txt` 以及 `cat world.txt` 來查看檔案內容
- ➤ 完成後輸入 `echo continue` 來繼續
- id: continue
requirements:
- start
task: 查看檔案內容,完成後輸入 `echo continue` 來繼續
condition:
command:
- ^echo continue$
responses:
- type: narrative
content:
- 現在我們要來學習如何移動檔案與資料夾。
- '`mv` 指令是移動 move 的縮寫,可以用來移動檔案與資料夾。'
- 使用方式為 `mv [來源] [目的地]`。
- 我們先來將 `hello.txt` 移動到 `notes` 資料夾中吧!
hints:
- ➤ 使用 `mv hello.txt notes` 來將 `hello.txt` 移動到 `notes` 資料夾中
- id: mv-hello
requirements:
- continue
task: 將 `hello.txt` 移動到 `notes` 資料夾中
condition:
command:
- ^mv hello.txt notes$
files:
- path: notes/hello.txt
type: file
- path: hello.txt
exists: false
responses:
- type: narrative
content:
- 你做得很好!
- 我們用 `ls` 跟 `ls notes` 來查看一下資料夾內的檔案吧!
hints:
- ➤ 使用 `ls`、`ls notes` 來查看資料夾內的檔案
- id: ls-no-hello
requirements:
- mv-hello
task: 檢查 `hello.txt` 是否還在資料夾中
condition:
pwd:
- ^/home/commander$
command:
- ^ls$
responses:
- type: narrative
content:
- '`hello.txt` 已經不在資料夾中了!'
- id: ls-notes-hello
requirements:
- mv-hello
task: 檢查 `hello.txt` 是否在 `notes` 資料夾中
condition:
pwd:
- ^/home/commander$
command:
- ^ls notes$
output:
- hello.txt
responses:
- type: narrative
content:
- '`hello.txt` 出現在 `notes` 資料夾中了!'
- id: continue-mv
requirements:
- ls-no-hello
- ls-notes-hello
task: 輸入 `echo continue` 來繼續
condition:
command:
- ^echo continue$
responses:
- type: narrative
content:
- '`mv` 指令也可以用來更改檔案名稱。'
- '只要將目的地改成新的檔案名稱就可以了。'
- 現在我們要將 `world.txt` 更名為 `wow.txt`。
hints:
- ➤ 使用 `mv world.txt wow.txt` 來將 `world.txt` 更名為 `wow.txt`
- id: mv-world
requirements:
- continue-mv
task: 將 `world.txt` 更名為 `wow.txt`
condition:
command:
- ^mv
files:
- path: wow.txt
type: file
- path: world.txt
exists: false
responses:
- type: narrative
content:
- 你做得很好!
- 我們用 `ls` 來確認一下資料夾內的檔案吧!
hints:
- ➤ 使用 `ls` 來查看資料夾內的檔案
- id: ls-wow
requirements:
- mv-world
task: 檢查 `wow.txt` 是否存在
condition:
pwd:
- ^/home/commander$
command:
- ^ls$
output:
- wow.txt
responses:
- type: narrative
content:
- '`world.txt` 現在已經變成 `wow.txt` 了。'
- 接下來我們來學習如何移動資料夾吧!
- 移動資料夾非常簡單,方法跟移動檔案一樣。
- '`mv` 指令會自動將資料夾內的所有檔案一起移動'
- 我們先來將 `notes` 資料夾改名叫作 `docs` 吧!
hints:
- ➤ 使用 `mv notes docs` 來將 `notes` 資料夾改名叫作 `docs`
- id: mv-notes
requirements:
- ls-wow
task: 將 `notes` 資料夾改名叫作 `docs`
condition:
command:
- ^mv
files:
- path: docs
type: directory
- path: notes
exists: false
responses:
- type: narrative
content:
- 你做得很好!
- 我們用 `ls` 來確認一下資料夾內的檔案吧!
hints:
- ➤ 使用 `ls` 來查看資料夾內的檔案
- id: ls-docs
requirements:
- mv-notes
task: 檢查 `docs` 資料夾是否存在
condition:
pwd:
- ^/home/commander$
command:
- ^ls$
output:
- docs
responses:
- type: narrative
content:
- '`notes` 資料夾已經變成 `docs` 了。'
- 那麼最後來考考你:
- 試著將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`。
- 如果你不知道怎麼做,可以輸入 `echo hint` 來取得提示。
- id: END
requirements:
- ls-docs
task: 將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`
condition:
command:
- ^mv
files:
- path: hi.txt
type: file
- path: docs/hello.txt
exists: false
exceptions:
- condition:
command:
- ^echo hint$
responses:
- type: narrative
content:
- 你可以使用 `mv docs/hello.txt hi.txt` 來完成這個任務。
hints:
- ➤ 使用 `mv docs/hello.txt hi.txt` 來將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`
responses:
- type: narrative
content:
- 你做得很好!
- 你已經學會如何移動檔案與資料夾了!
+1
View File
@@ -0,0 +1 @@
Hello
+1
View File
@@ -0,0 +1 @@
Note
+1
View File
@@ -0,0 +1 @@
World
+3
View File
@@ -0,0 +1,3 @@
FROM linuxodyssey/quest-base
COPY home ${HOME}
+170
View File
@@ -0,0 +1,170 @@
title: 檔案路徑
instruction: |
在這個關卡中,你將會學到:
1. 絕對路徑和相對路徑
2. 如何使用 `pwd` 顯示目前的路徑
準備好就輸入 `echo start` 開始吧!
requirements:
- cast
- explore
stages:
- id: start
task: 輸入 `echo start` 來開始教程
condition:
output:
- ^start$
responses:
- content:
- 在 Linux 系統中,每個資料夾和檔案都有獨一無二的路徑。
- 而路徑又分為相對路徑和絕對路徑。
- 相對路徑是從目前所在的資料夾開始的路徑,而絕對路徑則是從根目錄開始的路徑。
- 請你先用 `ls` 指令查看這個資料夾中的檔案。
- id: ls
task: 查看資料夾中的檔案
requirements:
- start
condition:
pwd:
- ^/home/commander$
command:
- ^ls$
responses:
- content:
- 現在你使用 `cd` 指令進入 `folder1` 資料夾。
- id: cd-folder1
task: 進入 `folder1` 資料夾
requirements:
- ls
condition:
pwd:
- ^/home/commander/folder1$
responses:
- content:
- 你進入了 `folder1` 資料夾。
- 在進入 `folder1` 資料夾時,使用的 `cd folder1` 中的 `folder1` 就是相對路徑。
- 相同的道理 `cd ..` 中的 `..` 也是相對路徑。
- 相對路徑可以不只有一層資料夾。
- 例如:現在想要進入 `folder2` 資料夾的話,可以使用 `cd ..` 回到上一層資料夾,再使用 `cd folder2` 進入 `folder2` 資料夾。
- 然而,你也可以用 `/` 將 `..` 和 `folder2` 依序連接起來,使用 `cd ../folder2` 來一次進入 `folder2` 資料夾。
- 現在試著用這個方式進入 `folder2` 資料夾看看吧。
hints:
- ➤ 請輸入 `cd ../folder2` 來進入 `folder2` 資料夾。
- id: cd-folder2
task: 進入 `folder2` 資料夾
requirements:
- cd-folder1
condition:
pwd:
- ^/home/commander/folder2$
exceptions:
- condition:
pwd:
- ^/home/commander$
responses:
- type: narrative
content:
- 輸入 `cd folder2` 來進入 `folder2` 資料夾
responses:
- content:
- 你成功的用相對路徑進入了 `folder2` 資料夾。
- 而你也能夠在 `ls` 後加上相對路徑來查看其他資料夾的內容。
- 例如:現在你想要查看 `folder1` 資料夾中有哪些檔案,你可以使用 `ls ../folder1` 來查看。
- 試著用這個方式查看 `folder1` 資料夾中的檔案吧。
hints:
- ➤ 請輸入 `ls ../folder1` 來查看 `folder1` 資料夾中的檔案。
- id: ls-folder1
task: 查看 `folder1` 資料夾中的檔案
requirements:
- cd-folder2
condition:
output:
- file1.txt
exceptions:
- condition:
not:
pwd:
- ^/home/commander/folder2$
responses:
- type: narrative
content:
- 進到 `folder1` 資料夾並用 `ls` 查看內容。
responses:
- content:
- 很好,你現在知道如何運用相對路徑了。
- 接下來要介紹的是絕對路徑。
- 絕對路徑是從根目錄開始的路徑,根目錄是位於檔案樹的最上層的資料夾。
- 你可以使用 `pwd` 來查看目前所在的絕對路徑。
- '`pwd` 的意思是 `print working directory`,也就是顯示目前所在的路徑。'
- 現在請你使用 `pwd` 來查看目前所在的絕對路徑。
hints:
- ➤ 輸入 `pwd` 查看目前所在的絕對路徑
- id: pwd
task: 查看目前所在的絕對路徑
requirements:
- ls-folder1
condition:
pwd:
- ^/home/commander/folder2$
command:
- ^pwd$
exceptions:
- condition:
not:
pwd:
- ^/home/commander/folder2$
responses:
- type: narrative
content:
- 請先回到 `folder2` 資料夾,並用 `pwd` 來查看目前所在的絕對路徑
responses:
- content:
- 你可以看到你現在的絕對路徑是 `/home/commander/folder2`。
- 而絕對路徑其實也顯示在你的命令提示符中。
- '`commander:~/folder2 $` 中的 `~/folder2` 就是你的絕對路徑。'
- 而 `~` 則是 `/home/commander` 這個絕對路徑的縮寫,又稱為家目錄,你每次遊戲都會從這裡開始。
- '`ls` 和 `cd` 指令也可以使用絕對路徑。'
- 例如:你可以使用 `ls ~` 來查看家目錄的內容,或使用 `cd ~` 來回到家目錄。
- 現在請你使用 `ls ~` 來查看家目錄的內容。
hints:
- ➤ 輸入 `ls ~` 來查看家目錄的內容
- id: ls-~
task: 查看家目錄的內容
requirements:
- pwd
condition:
command:
- ^ls ~$
responses:
- content:
- 現在請使用 `cd ~` 回到家目錄。
hints:
- ➤ 輸入 `cd ~` 來回到家目錄
- id: END
task: 回到家目錄
requirements:
- ls-~
condition:
command:
- ^cd ~$
exceptions:
- condition:
pwd:
- ^/home/commander$
responses:
- type: narrative
content:
- 請使用 `cd ~` 回到家目錄
responses:
- content:
- 你回到家目錄了。
- 恭喜!現在你已經知道如何運用絕對路徑和相對路徑,以及查看目前所在的絕對路徑了。
View File
Whitespace-only changes.
View File
Whitespace-only changes.
View File
Whitespace-only changes.
View File
Whitespace-only changes.
+3
View File
@@ -0,0 +1,3 @@
FROM linuxodyssey/quest-base
COPY home ${HOME}
+101 -3
View File
@@ -1,6 +1,104 @@
title: Read the File
title: 查看檔案內容
instruction: |
閱讀檔案
在這個關卡中,你將會學到:
1. 如何使用 `cat` 查看檔案的內容
準備好就輸入 `echo start` 開始吧!
requirements:
- spell
- path
stages:
- id: start
task: 輸入 `echo start` 來開始教程
condition:
output:
- ^start$
responses:
- content:
- 在終端機中,並沒有圖形介面可以讓你查看檔案的內容。
- 但是你可以透過 `cat` 指令來查看檔案的內容。
- '`cat` 指令是 concatenate 的縮寫,也就是串接的意思。'
- 在輸入 `cat [檔案名稱]` 指令後,終端機會輸出檔案的內容。
- 請你先查看這個資料夾中哪些檔案。
hints:
- ➤ 請輸入 `ls` 來查看這個資料夾中的檔案。
- id: ls-~
task: 查看資料夾中的檔案
requirements:
- start
condition:
pwd:
- ^/home/commander$
command:
- ^ls$
responses:
- content:
- 這個資料夾中有一個檔案和一個資料夾。
- 現在你可以使用 `cat` 指令來查看 `file1.txt` 的內容。
hints:
- ➤ 請輸入 `cat file1.txt` 來查看檔案的內容。
- id: cat-file1
task: 查看 `file1.txt` 的內容
requirements:
- ls-~
condition:
pwd:
- ^/home/commander$
command:
- ^cat file1.txt$
responses:
- content:
- 你應該能看到檔案的內容了。
- '`cat` 指令也可以使用路徑來查看檔案內容'
- 請你先用 `ls folder` 查看資料夾內的檔案。
hints:
- ➤ 輸入 `ls folder` 查看資料夾內的檔案
- id: ls-folder
task: 查看 `folder` 資料夾
requirements:
- cat-file1
condition:
pwd:
- ^/home/commander$
command:
- ^ls folder$
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
content:
- 請留在 `/home/commander` 資料夾中。
- 請輸入 `cd ~` 回到 `/home/commander` 資料夾。
responses:
- content:
- 你現在知道 `folder` 資料夾的內容了。
- 請用 `cat` 指令來查看 `folder` 資料夾中的 `file.txt` 檔案。
hints:
- ➤ 輸入 `cat folder/file.txt` 查看 `file.txt` 檔案的內容。
- id: END
task: 查看 `file.txt` 的內容
requirements:
- ls-folder
condition:
pwd:
- ^/home/commander$
command:
- ^cat folder/file.txt$
exceptions:
- condition:
not:
pwd:
- ^/home/commander$
content:
- 請留在 `/home/commander` 資料夾中。
- 請輸入 `cd ~` 回到 `/home/commander` 資料夾。
responses:
- content:
- 你成功地查看到 `file.txt` 的內容了。
- 你已經會使用 `cat` 指令查看檔案的內容了。
+1
View File
@@ -0,0 +1 @@
This is file1.txt
+1
View File
@@ -0,0 +1 @@
This is file.txt
+4
View File
@@ -0,0 +1,4 @@
FROM linuxodyssey/quest-base
COPY --chown=${USERNAME} home ${HOME}
RUN mkdir empty
+257
View File
@@ -0,0 +1,257 @@
title: 刪除檔案與資料夾
instruction: |
在這個關卡中,你將會學到:
1. 用 `rm` 指令刪除檔案
2. 用 `rmdir` 與資料夾
3. 用 `rm -r` 刪除資料夾與它的所有檔案
現在資料夾裡有一些檔案與資料夾,請你使用 `ls` 跟 `cat` 來查看它們的內容。
準備好就輸入 `echo start` 開始吧!
requirements:
- copy
- move
stages:
- id: start
task: 輸入 `echo start` 來開始
condition:
command:
- ^echo
output:
- start
responses:
- content:
- 今天我們要學習如何刪除檔案與資料夾。
- '首先要來學習 `rm` 指令。`rm` 指令可以用來刪除檔案。使用方式是 `rm [檔案名稱]`。'
- '現在請你使用 `rm` 指令刪除 `hello.txt`。'
hints:
- ➤ 使用 `rm hello.txt` 來刪除 `hello.txt`。
- id: rm-hello
requirements:
- start
task: 使用 `rm` 刪除 `hello.txt`
condition:
command:
- ^rm
files:
- path: hello.txt
exists: false
responses:
- content:
- '很好!現在請你使用 `ls` 指令來查看資料夾內容。'
- id: ls-no-hello
requirements:
- rm-hello
task: 使用 `ls` 查看資料夾內容
condition:
command:
- ^ls
files:
- path: hello.txt
exists: false
responses:
- content:
- '你會發現 `hello.txt` 已經不見了。'
- '`rm` 也可以同時刪除多個檔案,只要把檔案名稱用空白隔開就可以了,也就是 `rm [檔案1] [檔案2] ...`。'
- 現在請你使用 `rm` 刪除 `world.txt` 與 `wow.txt`。
hints:
- ➤ 使用 `rm world.txt wow.txt` 來刪除 `world.txt` 與 `wow.txt`。
- ➤ 檔案名稱之間要用空白隔開。
- ➤ 名稱的順序不重要。
- id: rm-world-wow
requirements:
- ls-no-hello
task: 使用 `rm` 刪除 `world.txt` 與 `wow.txt`
condition:
command:
- ^rm
files:
- path: world.txt
exists: false
- path: wow.txt
exists: false
responses:
- content:
- '很好!我們一樣用 `ls` 指令確認一下資料夾內容。'
hints:
- ➤ 使用 `ls` 來查看資料夾內容。
- id: ls-no-world-wow
requirements:
- rm-world-wow
task: 使用 `ls` 查看資料夾內容
condition:
command:
- ^ls$
responses:
- content:
- '你會發現 `world.txt` 與 `wow.txt` 已經不見了。'
- '現在我們來學習 `rmdir` 指令。`rmdir` 指令可以用來刪除資料夾。使用方式是 `rmdir [資料夾名稱]`。'
- '現在請你使用 `rmdir` 指令刪除 `empty` 資料夾。'
hints:
- ➤ 使用 `rmdir empty` 來刪除 `empty` 資料夾。
- id: rm-empty
requirements:
- ls-no-world-wow
task: 使用 `rmdir` 刪除 `empty` 資料夾
condition:
command:
- ^rmdir
files:
- path: empty
exists: false
responses:
- content:
- '很好!現在請你使用 `ls` 指令來查看資料夾內容。'
- id: ls-no-empty
requirements:
- rm-empty
task: 使用 `ls` 查看資料夾內容
condition:
command:
- ^ls$
responses:
- content:
- '你會發現 `empty` 資料夾已經不見了。'
- '現在請你試試看 `rmdir` 指令刪除 `notes` 資料夾。'
hints:
- ➤ 使用 `rmdir notes` 來刪除 `notes` 資料夾
- id: rm-notes-failed
requirements:
- ls-no-empty
task: 使用 `rmdir` 刪除 `notes` 資料夾
condition:
command:
- ^rmdir notes$
files:
- path: notes
type: folder
exists: true
responses:
- content:
- 指令執行失敗了!
- '你會發現 `notes` 資料夾還在。'
- '這是因為 `rmdir` 指令只能刪除空的資料夾。'
- 我們先來將 `notes` 資料夾的檔案刪除再來試一次。
- '現在請你使用 `rm` 指令刪除 `notes` 資料夾內的檔案。'
hints:
- ➤ 使用 `rm notes/[檔案名稱]` 來刪除 `notes` 資料夾內的檔案。
- ➤ 你可能需要使用 `ls notes` 來查看 `notes` 資料夾內有哪些檔案。
- id: rm-notes-content
requirements:
- rm-notes-failed
task: 使用 `rm` 刪除 `notes` 資料夾內的檔案
condition:
command:
- ^rm
files:
- path: notes
type: folder
exists: true
- path: notes/note.txt
exists: false
exceptions:
- condition:
command:
- ^rm notes/\[.*\]
responses:
- content:
- '你好像理解錯了。'
- 這邊中括號包起來 `[檔案名稱]` 是參數的意思,需要你自己填入檔案的名稱。
- 實際打指令的時候,不要打中括號 `[]` 喔!
- '例如,如果要刪除 `notes` 資料夾內的 `note.txt` 檔案,指令應該要是 `rm notes/note.txt`。'
- 請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!
- condition:
command:
- ^rm
error:
- 'No such file or directory'
responses:
- content:
- 這個錯誤的意思是找不到檔案
- '請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!'
- condition:
command:
- ^ls notes$
responses:
- content:
- '`notes` 資料夾中有著 `note.txt` 檔案。'
- 那我們就使用 `rm notes/note.txt` 來刪除 `note.txt` 檔案吧!
responses:
- content:
- '很好!現在我們再使用 `rmdir` 指令刪除 `notes` 資料夾。'
hints:
- ➤ 使用 `rmdir notes` 來刪除 `notes` 資料夾。
- id: rm-notes
requirements:
- rm-notes-content
task: 使用 `rmdir` 刪除 `notes` 資料夾
condition:
command:
- ^rmdir
files:
- path: notes
exists: false
responses:
- content:
- '很好!`notes` 資料夾已經被刪除了。'
- 接下來我們來刪除 `trash` 資料夾。
- 先使用 `ls` 指令查看一下 `trash` 資料夾內容。
hints:
- ➤ 使用 `ls trash` 來查看 `trash` 中的檔案。
- id: ls-trash
requirements:
- rm-notes
task: 使用 `ls` 查看 `trash` 資料夾內容
condition:
command:
- ^ls
output:
- garbage.txt
responses:
- content:
- 天啊!`trash` 資料夾裡有好多檔案,甚至還有其他的資料夾!
- 如果今天資料夾中有很多檔案與資料夾,我們不想一個一個刪除,該怎麼辦呢?
- 跟 `cp` 的時候一樣,這時候我們可以加上 `-r` 的選項來刪除資料夾與它的所有檔案。
- '`-r` 的意思是遞迴(recursive),也就是會把資料夾中的所有檔案都刪除。'
- 我們來試試看吧!
hints:
- ➤ 使用 `rm -r trash` 來刪除 `trash` 資料夾。
- id: rm-trash
requirements:
- ls-trash
task: 使用 `rm -r` 刪除 `trash` 資料夾
condition:
command:
- ^rm -r
files:
- path: trash
exists: false
responses:
- content:
- '很好!現在請你使用 `ls` 指令查看資料夾內容。'
hints:
- ➤ 使用 `ls` 來查看資料夾內容。
- id: END
requirements:
- rm-trash
task: 使用 `ls` 查看資料夾內容
condition:
command:
- ^ls$
responses:
- content:
- '`trash` 資料夾已經不見了。'
- 恭喜你!你已經學會如何刪除檔案與資料夾了!
+1
View File
@@ -0,0 +1 @@
Hello
+1
View File
@@ -0,0 +1 @@
Note
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
Loaded 100 of 132 files, more files were not shown because too many files have changed in this diff. Show more