Compare commits

...
Author SHA1 Message Date
Lin, Hung-Shin 8d0a210f0e Fix current quests (#144) 2024-07-03 08:15:18 +08:00
wancat c38104d043 fix: typo 2024-07-03 08:09:57 +08:00
wancat 83e563ab56 fix: requirement in get-started 2024-07-03 08:05:58 +08:00
wancat fa2059814c fix: increase line height in description part 2024-07-03 08:05:40 +08:00
Chen, Tai-Lin f9fa5a2102 fix: update quest [remove] 2024-07-03 01:57:39 +08:00
Chen, Tai-Lin 8ed6acbcc1 fix: update quest [move] 2024-07-03 01:47:59 +08:00
Chen, Tai-Lin d2452c91a6 fix: update quest [copy] 2024-07-03 01:42:37 +08:00
Chen, Tai-Lin 5bac9ce099 style: revise description part 2024-07-03 01:25:34 +08:00
Chen, Tai-Lin 2aaff0f7bc fix: quests exceptions 2024-07-03 01:10:59 +08:00
Chen, Tai-Lin 96ab39437b fix: quest error [remove] 2024-07-03 01:02:15 +08:00
Chen, Tai-Lin d6b606e807 fix: update quest [get-started] 2024-07-03 01:01:30 +08:00
Chen, Tai-Lin b792992391 fix: update quest [explore] 2024-07-03 01:01:30 +08:00
Chen, Tai-Lin 922100d8da fix: update quest [cast] 2024-07-03 01:01:30 +08:00
Chen, Tai-Lin 52597e8c5e fix: update quest [explore] 2024-07-03 01:01:30 +08:00
Chen, Tai-Lin a6c160ac0e fix: update quest [cast] 2024-07-03 01:00:33 +08:00
Chen, Tai-Lin 3859403b56 fix: revise quest content [get-started] (1 to be fixed) 2024-07-03 01:00:17 +08:00
Chen, Tai-Lin 2a29b0b8ac fix: update quest [read] 2024-07-03 00:57:33 +08:00
Chen, Tai-Lin aaa97e0dca fix: update quest [path] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 3a28af0a67 fix: update quest [move] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 551d502845 fix: update quest [helloworld] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 3f0191112d fix: update quest [get-started] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin d565709a59 fix: update quest [explore] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 435a426f5b fix: update quest [remove] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 377afb3078 fix: update quest [create] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 9d26951978 fix: update quest [cast] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin db3b00f8ee fix: update quest [copy] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin c09c4ff9e0 fix: quests exceptions error 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin b3b374b3cd fix: update quest [explore] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 5ca187651d fix: update quest answer file [get-started] 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin f2ca4a26ac fix: update quest's exceptions 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 462f17a779 fix: update quest [cast] 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 3deabb25a9 fix: update quest [get-started] 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 3dad7ddccb fix: revise quest content [get-started] (1 to be fixed) 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 25888a1465 fix: modify component name and title (HintPart=>DescriptionPart) 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 3559814f55 fix: quest error [remove] 2024-07-03 00:44:30 +08:00
Chen, Tai-Lin 8142ff816c Merge branch 'fix-current-quests' of https://github.com/lancatlin/linux-odyssey into fix-current-quests 2024-07-03 00:34:39 +08:00
Chen, Tai-Lin 9c160253c9 fix: update quest [read] 2024-07-03 00:34:33 +08:00
Chen, Tai-Lin 80e1a40b6e fix: update quest [path] 2024-07-03 00:34:33 +08:00
Chen, Tai-Lin 2b70f3c7cd fix: update quest [move] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 2e29919ad9 fix: update quest [helloworld] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin ca79042a3b fix: update quest [get-started] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin c8d2320bf5 fix: update quest [explore] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 441909efd5 fix: update quest [remove] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 90e3fc2bd0 fix: update quest [create] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 598141d735 fix: update quest [cast] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin e11432be45 fix: update quest [copy] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 2c1fe7bb96 fix: quests exceptions error 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 8c54bfe192 fix: update quest [explore] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin d292195302 fix: update quest answer file [get-started] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 6bbf70a258 fix: update quest's exceptions 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 526b856de7 fix: update quest [cast] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 1a20ffb295 fix: update quest [get-started] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 4efa9b8831 fix: revise quest content [get-started] (1 to be fixed) 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 3944e91d0c fix: modify component name and title (HintPart=>DescriptionPart) 2024-07-03 00:34:32 +08:00
wancat c30b4a7c6e feat: add isGuest migration script 2024-07-03 00:34:32 +08:00
wancat 14cd16b611 feat: add isGuest field in user model 2024-07-03 00:34:32 +08:00
wancat b07b60f6bd feat: add register guest button 2024-07-03 00:34:32 +08:00
wancat 9d1bed7543 feat: add register-guest api 2024-07-03 00:34:32 +08:00
wancat 715352f77c feat: install date-fns 2024-07-03 00:34:32 +08:00
Annie Huang c4550a395a fix map view 2024-07-03 00:34:32 +08:00
dependabot[bot] 5795cb16b8 build(deps): bump tar from 6.2.0 to 6.2.1
Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 691a6dcb25 fix: update quest [read] 2024-07-03 00:32:04 +08:00
Chen, Tai-Lin 979ea72f3a fix: update quest [path] 2024-07-03 00:31:57 +08:00
Chen, Tai-Lin f9e40edb8a fix: update quest [move] 2024-07-03 00:31:46 +08:00
Chen, Tai-Lin 276bfee5ed fix: update quest [helloworld] 2024-07-03 00:31:40 +08:00
Chen, Tai-Lin f03c359f89 fix: update quest [get-started] 2024-07-03 00:31:30 +08:00
Chen, Tai-Lin 7db08a5cda fix: update quest [explore] 2024-07-03 00:31:20 +08:00
Chen, Tai-Lin b19f0da678 fix: update quest [remove] 2024-07-03 00:31:10 +08:00
Chen, Tai-Lin c87eef233c fix: update quest [create] 2024-07-03 00:30:59 +08:00
Chen, Tai-Lin dabc40eb1e fix: update quest [cast] 2024-07-03 00:30:44 +08:00
Chen, Tai-Lin 0efe2235f8 fix: update quest [copy] 2024-07-03 00:30:37 +08:00
Chen, Tai-Lin 9d45993d0d fix: quests exceptions error 2024-07-02 22:32:00 +08:00
Lin, Hung-Shin 99871fa299 Fix exception not showing bug (#143) 2024-07-02 11:58:27 +08:00
wancat 248271355f fix: restore quests changes to prevent conflicts 2024-07-02 11:57:55 +08:00
Chen, Tai-Lin 79f9cb3e95 fix: update quest [explore] 2024-07-02 02:09:09 +08:00
Chen, Tai-Lin 47c7ff04a9 fix: update quest answer file [get-started] 2024-07-02 00:49:42 +08:00
Chen, Tai-Lin f831f4d4e5 fix: update quest's exceptions 2024-07-01 23:39:58 +08:00
Chen, Tai-Lin 29799faa79 fix: update quest [cast] 2024-07-01 23:10:46 +08:00
Chen, Tai-Lin 2727c9f4bb fix: update quest [get-started] 2024-07-01 22:38:42 +08:00
wancat 988cb65b51 fix: wrong exception condition 2024-07-01 10:41:48 +08:00
wancat 4324932fc9 fix: exception not showing 2024-07-01 10:41:26 +08:00
wancat e9b49502ab fix: progress not update when completing quest 2024-06-28 16:42:15 +08:00
Lin, Hung-Shin d5519e7157 Fix duplicate responses (#140) 2024-06-28 16:19:48 +08:00
wancat ed596c245d fix: duplicate response 2024-06-28 16:17:28 +08:00
Lin, Hung-Shin 57c080d88b Add start session button (#134)
- Add new file StartButton
- Covering Hint part and Terminal part
- Load instruction before pressing button
- Load session after pressing the button
2024-06-28 16:15:26 +08:00
wancat d795dbd788 refactor: Remove commented out code and unused imports 2024-06-28 16:14:26 +08:00
wancat 218d433c14 fix: remove unused logging 2024-06-28 16:05:45 +08:00
wancat 48835f8830 feat: change e2e to not required 2024-06-28 11:41:12 +08:00
Chen, Tai-Lin 9ed726d597 fix: revise quest content [get-started] (1 to be fixed) 2024-06-28 02:25:34 +08:00
Chen, Tai-Lin fcb52b9d9e fix: modify component name and title (HintPart=>DescriptionPart) 2024-06-28 02:14:22 +08:00
wancat dcd8f441f8 feat: echo start after creating session 2024-06-27 12:14:15 +08:00
wancat 94ef3749f5 fix: socket & terminal states across quests 2024-06-27 12:04:35 +08:00
wancat 3398d18055 feat: load session and display start button if not exists 2024-06-27 11:55:34 +08:00
wancat c64043316e feat: replace getOrCreateActive session to getActiveSession 2024-06-27 11:54:28 +08:00
wancat 7cdc542c19 refactor: rewrite session store using pinia and fix start button 2024-06-27 11:23:21 +08:00
wancat 78b9529699 refactor: rewrite map page using user profile store 2024-06-27 10:52:02 +08:00
wancat 8a31a58c56 refactor: rewrite user profile store using pinia 2024-06-27 10:48:08 +08:00
wancat 103ce4774c refactor: rename dev docker compose file and remove unused services 2024-06-27 10:31:26 +08:00
Annie Huang 4e6602d82f change post to get (not yet success) 2024-06-27 10:30:00 +08:00
Annie Huang 2eaf3c8f24 auto type echo start 2024-06-27 10:30:00 +08:00
Annie Huang 15465feda9 auto connect session, create session after button 2024-06-27 10:30:00 +08:00
Annie Huang ea2ba2c7a0 load quest info before start 2024-06-27 10:30:00 +08:00
Annie Huang 0b136a830a add start button
- new stage will load previous stored data at first
2024-06-27 10:30:00 +08:00
Lin, Hung-Shin 3848ca6941 116 guest account (#137)
* add isGuest flag in User model
* add register guest api
* add continue as guest button in frontend
2024-06-27 10:03:04 +08:00
wancat 443e810471 feat: add isGuest migration script 2024-06-17 20:12:32 +08:00
wancat 198dcae3f8 feat: add isGuest field in user model 2024-06-17 19:50:58 +08:00
wancat 7347c37a9e feat: add register guest button 2024-06-17 19:49:00 +08:00
wancat 1d725d82ae feat: add register-guest api 2024-06-17 19:23:30 +08:00
wancat b5c601f4a9 feat: install date-fns 2024-06-17 19:23:23 +08:00
Annie Huang 5069ac12da fix map view 2024-04-11 23:39:57 +08:00
Lin, Hung-Shin cec4d86058 build(deps): bump tar from 6.2.0 to 6.2.1 (#131)
Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/isaacs/node-tar/commit/bef7b1e4ffab822681fea2a9b22187192ed14717"><code>bef7b1e</code></a>
6.2.1</li>
<li><a
href="https://github.com/isaacs/node-tar/commit/fe8cd57da5686f8695415414bda49206a545f7f7"><code>fe8cd57</code></a>
prevent extraction in excessively deep subfolders</li>
<li><a
href="https://github.com/isaacs/node-tar/commit/fe7ebfdcede1f8a2e65db12e19ecc4b3a9934648"><code>fe7ebfd</code></a>
remove security.md</li>
<li>See full diff in <a
href="https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tar&package-manager=npm_and_yarn&previous-version=6.2.0&new-version=6.2.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2024-04-11 22:39:51 +08:00
dependabot[bot] 93b0bae204 build(deps): bump tar from 6.2.0 to 6.2.1
Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-11 11:56:58 +00:00
Lin, Hung-Shin 4713b11c95 Fix e2e Testing (#130)
**Fix e2e broken caused by website modification**

- Add back "GameTitle" for **Linux Odyssey** in header
- Delete echo hint in quest **move** (It stucks the testing)
- Modify paths for several testing describes
- Fix type problems in command.ts
2024-04-11 15:19:38 +08:00
Annie Huang 36e6bc10d3 fix: e2e broken caused by website modification 2024-04-09 11:32:58 +08:00
Annie Huang 580d5fc3f8 Feat landing page - Mobile Optimisation (#112)
This optimisation is for mobile usage and also make header a better
component.
- Header Restruction
- Menu pop up when using smaller screen
- Seperate header components and other components
2024-04-07 23:49:53 +08:00
Annie Huang d8392ec3f6 delete redundant code 2024-04-07 23:44:41 +08:00
Annie Huang 5f99f42f5f fix type problems 2024-04-07 20:41:17 +08:00
Annie Huang bdf757c2d4 fix: header components in the middle 2024-04-07 20:40:44 +08:00
Annie Huang 453bfb7f1e delete redundant code 2024-04-06 22:06:48 +08:00
wancat 3b470ed952 fix: session store type 2024-04-06 22:06:48 +08:00
Annie Huang b4af2d46d3 fix hint display problems 2024-04-06 22:06:48 +08:00
Annie Huang 0e96e30b96 categorise components 2024-04-06 22:06:48 +08:00
Annie Huang 3e3ac58c6b Restruct heading 2024-04-06 22:06:48 +08:00
wancat 9f690548cf fix: session store type 2024-04-06 22:06:48 +08:00
Annie Huang cd2ec6df75 fix hint display problems 2024-04-06 22:06:48 +08:00
Annie Huang 8e7b5f2552 modify menu position while open 2024-04-06 22:06:48 +08:00
Annie Huang 385c6bc110 delete testing id 2024-04-06 22:06:48 +08:00
wancat 092cf284dd fix: session store type 2024-04-06 22:06:48 +08:00
Annie Huang 7806852cab fix hint display problems 2024-04-06 22:06:48 +08:00
Annie Huang 1e31cf3be5 fix file locations and transfer to ts 2024-04-06 22:06:29 +08:00
Annie Huang aa903fbacf fix file position 2024-04-06 22:06:29 +08:00
Annie Huang df980a7d56 delete redundant code 2024-04-06 22:06:29 +08:00
Annie Huang 980d0497f4 fix header justify end property 2024-04-06 22:06:29 +08:00
Annie Huang e37a23c34f mobile menu (but not justify end while bigger screen, still solving) 2024-04-06 22:06:29 +08:00
Annie Huang f4fc6de4cc canceling [..vh] 2024-04-06 22:06:29 +08:00
Annie Huang 9bfc6614ca hide some text when screen is small 2024-04-06 22:06:29 +08:00
Annie Huang 8c4a9ea871 add header to login and register page, modified header class but not fit for all circumstances 2024-04-06 22:06:29 +08:00
Annie Huang ad29137b86 categorise components - change path 2024-04-06 22:06:29 +08:00
Annie Huang b5e49547ea categorise components 2024-04-06 22:06:29 +08:00
Annie Huang 9a4f9b958a Restruct heading 2024-04-06 22:06:29 +08:00
Annie Huang 29602785cd rwd player opinions 2024-04-06 22:05:59 +08:00
70 changed files with 1494 additions and 1376 deletions

No files matched your search

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