mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
Compare commits
230
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cc006e3a4 | ||
|
|
d6ae6d818d | ||
|
|
6692ae2b9b | ||
|
|
fab02d03a9 | ||
|
|
9e44ebe415 | ||
|
|
019caf58e4 | ||
|
|
97ad1a7b50 | ||
|
|
cb4b9f429a | ||
|
|
5e11d2da11 | ||
|
|
a901ce005c | ||
|
|
4dd2a7cbe1 | ||
|
|
5cf375905d | ||
|
|
d7c5c756f3 | ||
|
|
44ecd3b87a | ||
|
|
1888781a47 | ||
|
|
860b257ea7 | ||
|
|
ba251fb933 | ||
|
|
84bd2620d8 | ||
|
|
b449d84c54 | ||
|
|
91ebaec22c | ||
|
|
58354b7cd8 | ||
|
|
31301419ad | ||
|
|
251139c159 | ||
|
|
a4ed4cf533 | ||
|
|
247f03b71a | ||
|
|
be22a121c8 | ||
|
|
261c6fc029 | ||
|
|
03b2993b56 | ||
|
|
2cc348687a | ||
|
|
2a3429106c | ||
|
|
a6720cfe39 | ||
|
|
4c292fd6ba | ||
|
|
5ba0021c37 | ||
|
|
19897e607f | ||
|
|
08e9191c4b | ||
|
|
aa95eda23c | ||
|
|
6bc37900d8 | ||
|
|
98f18627e5 | ||
|
|
9b4f50ebbd | ||
|
|
5fd24190d2 | ||
|
|
d2bb079f50 | ||
|
|
d959ab22fe | ||
|
|
9c8260a411 | ||
|
|
3c92c9d143 | ||
|
|
5208d87bd6 | ||
|
|
634464065d | ||
|
|
4c90793fc6 | ||
|
|
169587c54d | ||
|
|
4dd5f94c60 | ||
|
|
14bcb3b0d5 | ||
|
|
8281a9d882 | ||
|
|
dcf72d046c | ||
|
|
b73607fb8d | ||
|
|
b64d707d16 | ||
|
|
7dcac306f9 | ||
|
|
eddbda23d7 | ||
|
|
124c5cc401 | ||
|
|
21d08b8156 | ||
|
|
3745011504 | ||
|
|
3f08452ea4 | ||
|
|
acfb09c6ca | ||
|
|
c2c100e6e6 | ||
|
|
4033eefa0d | ||
|
|
334b34ef65 | ||
|
|
ac89d521fc | ||
|
|
60641ba0ad | ||
|
|
dd67bcb1bd | ||
|
|
d002b21aa8 | ||
|
|
033c808c02 | ||
|
|
1be4f73e7e | ||
|
|
9ddfcd2a86 | ||
|
|
ca36f6391a | ||
|
|
1572b2ea06 | ||
|
|
8f6914d46c | ||
|
|
517ab43e8c | ||
|
|
74c49e1df7 | ||
|
|
cb0f3cc3b0 | ||
|
|
5605b27127 | ||
|
|
a7368110dd | ||
|
|
bdb7def73a | ||
|
|
b74866e50f | ||
|
|
d577656eb1 | ||
|
|
6400aa4a56 | ||
|
|
887d205ec1 | ||
|
|
8597c22b9f | ||
|
|
d8c0ee4b20 | ||
|
|
8180e9a0cf | ||
|
|
6daa11bff7 | ||
|
|
976dc6363c | ||
|
|
8d0a210f0e | ||
|
|
c38104d043 | ||
|
|
83e563ab56 | ||
|
|
fa2059814c | ||
|
|
f9fa5a2102 | ||
|
|
8ed6acbcc1 | ||
|
|
d2452c91a6 | ||
|
|
5bac9ce099 | ||
|
|
2aaff0f7bc | ||
|
|
96ab39437b | ||
|
|
d6b606e807 | ||
|
|
b792992391 | ||
|
|
922100d8da | ||
|
|
52597e8c5e | ||
|
|
a6c160ac0e | ||
|
|
3859403b56 | ||
|
|
2a29b0b8ac | ||
|
|
aaa97e0dca | ||
|
|
3a28af0a67 | ||
|
|
551d502845 | ||
|
|
3f0191112d | ||
|
|
d565709a59 | ||
|
|
435a426f5b | ||
|
|
377afb3078 | ||
|
|
9d26951978 | ||
|
|
db3b00f8ee | ||
|
|
c09c4ff9e0 | ||
|
|
b3b374b3cd | ||
|
|
5ca187651d | ||
|
|
f2ca4a26ac | ||
|
|
462f17a779 | ||
|
|
3deabb25a9 | ||
|
|
3dad7ddccb | ||
|
|
25888a1465 | ||
|
|
3559814f55 | ||
|
|
8142ff816c | ||
|
|
9c160253c9 | ||
|
|
80e1a40b6e | ||
|
|
2b70f3c7cd | ||
|
|
2e29919ad9 | ||
|
|
ca79042a3b | ||
|
|
c8d2320bf5 | ||
|
|
441909efd5 | ||
|
|
90e3fc2bd0 | ||
|
|
598141d735 | ||
|
|
e11432be45 | ||
|
|
2c1fe7bb96 | ||
|
|
8c54bfe192 | ||
|
|
d292195302 | ||
|
|
6bbf70a258 | ||
|
|
526b856de7 | ||
|
|
1a20ffb295 | ||
|
|
4efa9b8831 | ||
|
|
3944e91d0c | ||
|
|
c30b4a7c6e | ||
|
|
14cd16b611 | ||
|
|
b07b60f6bd | ||
|
|
9d1bed7543 | ||
|
|
715352f77c | ||
|
|
c4550a395a | ||
|
|
5795cb16b8 | ||
|
|
691a6dcb25 | ||
|
|
979ea72f3a | ||
|
|
f9e40edb8a | ||
|
|
276bfee5ed | ||
|
|
f03c359f89 | ||
|
|
7db08a5cda | ||
|
|
b19f0da678 | ||
|
|
c87eef233c | ||
|
|
dabc40eb1e | ||
|
|
0efe2235f8 | ||
|
|
9d45993d0d | ||
|
|
99871fa299 | ||
|
|
248271355f | ||
|
|
79f9cb3e95 | ||
|
|
47c7ff04a9 | ||
|
|
f831f4d4e5 | ||
|
|
29799faa79 | ||
|
|
2727c9f4bb | ||
|
|
988cb65b51 | ||
|
|
4324932fc9 | ||
|
|
e9b49502ab | ||
|
|
d5519e7157 | ||
|
|
ed596c245d | ||
|
|
57c080d88b | ||
|
|
d795dbd788 | ||
|
|
218d433c14 | ||
|
|
48835f8830 | ||
|
|
9ed726d597 | ||
|
|
fcb52b9d9e | ||
|
|
dcd8f441f8 | ||
|
|
94ef3749f5 | ||
|
|
3398d18055 | ||
|
|
c64043316e | ||
|
|
7cdc542c19 | ||
|
|
78b9529699 | ||
|
|
8a31a58c56 | ||
|
|
103ce4774c | ||
|
|
4e6602d82f | ||
|
|
2eaf3c8f24 | ||
|
|
15465feda9 | ||
|
|
ea2ba2c7a0 | ||
|
|
0b136a830a | ||
|
|
3848ca6941 | ||
|
|
443e810471 | ||
|
|
198dcae3f8 | ||
|
|
7347c37a9e | ||
|
|
1d725d82ae | ||
|
|
b5c601f4a9 | ||
|
|
5069ac12da | ||
|
|
cec4d86058 | ||
|
|
93b0bae204 | ||
|
|
4713b11c95 | ||
|
|
36e6bc10d3 | ||
|
|
580d5fc3f8 | ||
|
|
d8392ec3f6 | ||
|
|
5f99f42f5f | ||
|
|
bdf757c2d4 | ||
|
|
453bfb7f1e | ||
|
|
3b470ed952 | ||
|
|
b4af2d46d3 | ||
|
|
0e96e30b96 | ||
|
|
3e3ac58c6b | ||
|
|
9f690548cf | ||
|
|
cd2ec6df75 | ||
|
|
8e7b5f2552 | ||
|
|
385c6bc110 | ||
|
|
092cf284dd | ||
|
|
7806852cab | ||
|
|
1e31cf3be5 | ||
|
|
aa903fbacf | ||
|
|
df980a7d56 | ||
|
|
980d0497f4 | ||
|
|
e37a23c34f | ||
|
|
f4fc6de4cc | ||
|
|
9bfc6614ca | ||
|
|
8c4a9ea871 | ||
|
|
ad29137b86 | ||
|
|
b5e49547ea | ||
|
|
9a4f9b958a | ||
|
|
29602785cd |
No files matched your search
@@ -0,0 +1,50 @@
|
||||
HOST=localhost
|
||||
PORT=3000
|
||||
BASE_URL=https://example.com
|
||||
SECRET_KEY=YOU_SHOULD_CHANGE_THIS
|
||||
BACKEND_URL=http://backend:3000
|
||||
DOCKER_NETWORK=linux-odyssey-players
|
||||
|
||||
MONGO_ROOT_PASSWORD=adminpassword
|
||||
MONGO_USER=commander
|
||||
MONGO_PASS=commander
|
||||
MONGO_DB=linux-odyssey
|
||||
MONGO_URL=mongodb://commander:commander@db:27017/linux-odyssey
|
||||
|
||||
# Admin user credentials for analytics
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=admin
|
||||
ADMIN_HOST=0.0.0.0
|
||||
ADMIN_PORT=3001
|
||||
# ADMIN_BASE_URL=http://0.0.0.0:3001
|
||||
|
||||
# Optional
|
||||
# CONTAINER_EXPIRY=60 # 60 minutes
|
||||
# SESSION_MAX_AGE=168 # 7 days
|
||||
# TRUSTED_PROXIES=1.2.3.4
|
||||
# valid configurations see isIP & isIPRange in https://github.com/validatorjs/validator.js?tab=readme-ov-file#validators
|
||||
|
||||
# GOOGLE_CLIENT_ID=
|
||||
# GOOGLE_CLIENT_SECRET=
|
||||
# GITHUB_CLIENT_ID=
|
||||
# GITHUB_CLIENT_SECRET=
|
||||
# SURVEY_URL=
|
||||
# BUG_REPORT_URL=
|
||||
|
||||
# SURVEY_URL=
|
||||
# BUG_REPORT_URL=
|
||||
|
||||
# Docker related config
|
||||
# DOCKER_NETWORK=host
|
||||
# QUEST_IMAGE=linuxodyssey/quest-base
|
||||
# DOCKER_PREFIX=linuxodyssey/quest-
|
||||
|
||||
# To mount quest folder from host into container, for development purpose
|
||||
# MOUNT_QUEST=true
|
||||
|
||||
# LOG_PATH=
|
||||
|
||||
# Testing, create a user with the following credentials, disable rate limiting
|
||||
# TESTING=true
|
||||
# TESTING_USERNAME=alex
|
||||
# TESTING_PASSWORD=Alex1234
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
HOST=localhost
|
||||
PORT=3000
|
||||
BASE_URL=http://localhost:5173
|
||||
MONGO_URL=mongodb://localhost:27017/odyssey-test
|
||||
SECRET_KEY=sample-secret
|
||||
DOCKER_NETWORK=linux-odyssey-players-dev
|
||||
|
||||
# Optional
|
||||
# BACKEND_URL=http://host.docker.internal:3000
|
||||
# CONTAINER_EXPIRY=60 # 60 minutes
|
||||
# SESSION_MAX_AGE=168 # 7 days
|
||||
# TRUSTED_PROXIES=1.2.3.4
|
||||
# valid configurations see isIP & isIPRange in https://github.com/validatorjs/validator.js?tab=readme-ov-file#validators
|
||||
|
||||
# GOOGLE_CLIENT_ID=
|
||||
# GOOGLE_CLIENT_SECRET=
|
||||
# GITHUB_CLIENT_ID=
|
||||
# GITHUB_CLIENT_SECRET=
|
||||
# SURVEY_URL=
|
||||
# BUG_REPORT_URL=
|
||||
|
||||
# SURVEY_URL=
|
||||
# BUG_REPORT_URL=
|
||||
|
||||
# Docker related config
|
||||
# DOCKER_NETWORK=host
|
||||
# QUEST_IMAGE=linuxodyssey/quest-base
|
||||
# DOCKER_PREFIX=linuxodyssey/quest-
|
||||
|
||||
# To mount quest folder from host into container, for development purpose
|
||||
# MOUNT_QUEST=true
|
||||
|
||||
# LOG_PATH=
|
||||
|
||||
# Testing, create a user with the following credentials, disable rate limiting
|
||||
# TESTING=true
|
||||
# TESTING_USERNAME=alex
|
||||
# TESTING_PASSWORD=Alex1234
|
||||
@@ -35,23 +35,3 @@ jobs:
|
||||
|
||||
- name: Test
|
||||
run: yarn test
|
||||
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build Test Environment, Stage 1
|
||||
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
|
||||
|
||||
- name: Build Test Quests
|
||||
run: docker-compose -f docker-compose.testing.yml run --rm backend yarn build:quests
|
||||
|
||||
- name: Run Cypress Tests
|
||||
env:
|
||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
|
||||
GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
|
||||
run: docker-compose -f docker-compose.testing.yml run cypress
|
||||
@@ -0,0 +1,24 @@
|
||||
name: E2E Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build Test Environment, Stage 1
|
||||
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
|
||||
|
||||
- name: Build Test Quests
|
||||
run: docker-compose -f docker-compose.testing.yml run --rm backend yarn build:quests
|
||||
|
||||
- name: Run Cypress Tests
|
||||
env:
|
||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
|
||||
GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
|
||||
run: docker-compose -f docker-compose.testing.yml run cypress
|
||||
@@ -1,3 +1,5 @@
|
||||
config
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
@@ -85,6 +87,7 @@ built
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
.env.prod
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
@@ -5,9 +5,15 @@
|
||||
|
||||
## Requirements
|
||||
|
||||
- NodeJS
|
||||
- NodeJS v20+
|
||||
- Docker
|
||||
- Yarn
|
||||
- Yarn: v1.22
|
||||
|
||||
## Known Issues
|
||||
|
||||
- MongoDB requires x86 architecture to run for v5+ versions. You can track [this issue](https://github.com/linux-odyssey/linux-odyssey/issues/166) for updates.
|
||||
|
||||
Windows user please refer to [docs/windows-setup.md](docs/windows-setup.md) for setup instructions.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -15,30 +21,66 @@
|
||||
cd linux-odyssey
|
||||
yarn install
|
||||
|
||||
## Development
|
||||
## Project Structures
|
||||
|
||||
Run mongoDB first:
|
||||
1. **app**: frontend
|
||||
2. **server**: backend
|
||||
3. **quests**: tutorials scripts for each quest
|
||||
4. **packages**: other packages
|
||||
1. **analytics**: admin dashboard to analyze collected data
|
||||
2. **constants**: some shared config values for app and server
|
||||
3. **container**: the script run in container to capture users' command
|
||||
4. **file-graph**: the algorithm to construct and modify file graph, used in both app and server
|
||||
5. **models**: definitions of MondoDB collections (data schema)
|
||||
6. **utils**: other utility code shared between packages
|
||||
7. **game (in the future)**: core logic of game playing, will be extracted from server
|
||||
|
||||
docker compose -f docker-compose.dev.yml up -d db
|
||||
## Configuration
|
||||
|
||||
The db will automatically restarts by Docker.
|
||||
The configuration is stored in the `.env` file. You can copy from the sample file.
|
||||
|
||||
Build up dependencies:
|
||||
cp .env.sample .env
|
||||
|
||||
yarn build:deps
|
||||
The backend will use a SSH key pair to login to the player's container. The private key is on the host, and the public key is copied to the player's container.
|
||||
|
||||
Run dev server (frontend, backend):
|
||||
The key pair is stored in `./config/ssh_key` and `./config/ssh_key.pub`. If it doesn't exist, the backend will generate a new pair and store them.
|
||||
|
||||
yarn dev
|
||||
## Docker Setup
|
||||
|
||||
If you want to enable social login, you should have OAuth client token in a `.env` file:
|
||||
### Docker compose files explaination
|
||||
|
||||
### Build Quest Images
|
||||
1. **docker-compose.yml**: The one used for development. It sets up MongoDB and builds base quest image.
|
||||
2. **docker-compose.prod.yml**: The one used for production deployment. It has a Nginx service, a backend, a database, an analytics service, and creates isolated networks for player containers.
|
||||
3. **docker-compose.testing.yml**: Similar to production config, but sets up an one-time environment for end-to-end testing.
|
||||
|
||||
In each quest folder, there is a `game.yml` file, which is the quest definition. And a optional `Dockerfile` to overwrite the base image. If you want to include additional files, put them in a `home` folder. The files in `home` will **not** be copied to the image automatically, you have to copy them in the `Dockerfile`.
|
||||
### Common Development Setup
|
||||
|
||||
This sets up a development database, and running development server for frontend and backend.
|
||||
|
||||
Pull the latest images:
|
||||
|
||||
docker compose pull
|
||||
|
||||
Start the containers:
|
||||
|
||||
docker compose up -d
|
||||
|
||||
This will start the db. it will automatically restarts by Docker.
|
||||
|
||||
Building quest images:
|
||||
|
||||
docker compose build base
|
||||
yarn build:quests
|
||||
|
||||
### Build Quest Images in Details
|
||||
|
||||
In each quest folder, there is a `game.yml` file, which is the quest definition.
|
||||
|
||||
If you want to include additional files, put them in a `home` folder. The files in `home` will be copied to the user's home on container creation automatically.
|
||||
|
||||
quests/
|
||||
├── Dockerfile # Base image
|
||||
├── entrypoint.sh # Entrypoint for the container
|
||||
├── discover
|
||||
│ └── game.yml
|
||||
├── helloworld
|
||||
@@ -54,14 +96,55 @@ In each quest folder, there is a `game.yml` file, which is the quest definition.
|
||||
└── home
|
||||
└── ancient_scroll.txt
|
||||
|
||||
An optional `Dockerfile` can overwrite the base image. You can use this to install additional dependencies or modify the environment.
|
||||
|
||||
Customized `entrypoint.sh` for each quest is not yet supported.
|
||||
|
||||
To build all quests:
|
||||
|
||||
yarn build:quests
|
||||
|
||||
The base image is built with docker-compose, so you can use `docker compose build base` to rebuild it. Once you rebuild the base image, you have to rebuild all quests.
|
||||
|
||||
### Optional: Mounting Host Directory
|
||||
|
||||
To improve the development experience, we can mount the host directory to the container in the pattern:
|
||||
|
||||
/path/to/linux-odyssey/quests/${quest_id}/home:/home/commander
|
||||
/path/to/linux-odyssey/packages/container:/usr/local/lib/container
|
||||
|
||||
You can edit the files on your host, and the changes will be reflected in the container. This is disabled by default. To enable it, you can set the following environment variables:
|
||||
|
||||
MOUNT_QUEST=true
|
||||
|
||||
## Development
|
||||
|
||||
Copy `.env.sample` to `.env` and customize it for your needs.
|
||||
|
||||
Build the whole project for the first time:
|
||||
|
||||
yarn build
|
||||
|
||||
This will build the typescript for each package and the server.
|
||||
|
||||
Run dev server (frontend, backend):
|
||||
|
||||
yarn dev
|
||||
|
||||
**IMPORTANT NOTE**: only the app and server have hot-reloading, other packages require manually running `yarn dev` in each package.
|
||||
|
||||
To develop analytics dashboard:
|
||||
|
||||
yarn analytics
|
||||
|
||||
The default username and password is `alex` and `Alex1234`.
|
||||
|
||||
## Testing
|
||||
|
||||
Unit tests:
|
||||
|
||||
yarn test:unit
|
||||
|
||||
Run Cypress locally, using the current development containers:
|
||||
|
||||
docker compose up -d
|
||||
@@ -78,76 +161,22 @@ Run Cypress in container, create brand-new containers along with it:
|
||||
# tearing off the containers
|
||||
./docker-scripts.sh down
|
||||
|
||||
### Deployment
|
||||
## Deployment
|
||||
|
||||
# Build the images (only allowed for maintainers)
|
||||
docker compose -f docker-compose.prod.yml build
|
||||
docker compose -f docker-compose.prod.yml push
|
||||
|
||||
# Server side
|
||||
docker compose -f docker-compose.prod.yml pull
|
||||
yarn build:quests
|
||||
docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
### Necessary Configuration
|
||||
|
||||
To enable social login, you should have OAuth client token in a `.env` file:
|
||||
You can copy from `example.env`
|
||||
You can copy from `.env.sample`
|
||||
|
||||
# .env
|
||||
BASE_URL=https://example.com
|
||||
GOOGLE_CLIENT_ID=...
|
||||
GOOGLE_CLIENT_SECRET=...
|
||||
**YOU MUST CHANGE THE SECRET_KEY** in `.env` file.
|
||||
|
||||
To enable it:
|
||||
|
||||
docker compose --env-file .env -f docker-compose.prod.yml up -d
|
||||
|
||||
## Standalone App (without Docker)
|
||||
|
||||
Source: `app/`
|
||||
|
||||
# Connect to dev server (https://odyssey.wancat.cc)
|
||||
yarn app
|
||||
|
||||
# Connect to localhost (http://localhost:3000)
|
||||
yarn app:local
|
||||
|
||||
# Connect to anyhost
|
||||
API_ENDPOINT=http://example.com yarn app:local
|
||||
|
||||
## Swagger
|
||||
|
||||
docker compose up -d swagger
|
||||
|
||||
Open http://localhost:8080 to open Swagger
|
||||
|
||||
## CLI terminal client (Docker not required)
|
||||
|
||||
yarn cli
|
||||
Usage: client [options] [command]
|
||||
|
||||
Options:
|
||||
-s, --session <string> Session ID
|
||||
-c, --create Create a new session
|
||||
-h, --host <string> Server host (default: "http://localhost:3000")
|
||||
--help display help for command
|
||||
|
||||
Commands:
|
||||
list List all sessions
|
||||
|
||||
connet to recent session:
|
||||
|
||||
yarn cli
|
||||
|
||||
connect to new session:
|
||||
|
||||
yarn cli -c
|
||||
|
||||
connect to specific session:
|
||||
|
||||
yarn cli -s 64d0de0367459c13004bc83f
|
||||
|
||||
connect to other host:
|
||||
|
||||
yarn cli -h https://example.com
|
||||
|
||||
list sessions:
|
||||
|
||||
yarn cli list
|
||||
SECRET_KEY=your-secret-key
|
||||
+4
-2
@@ -6,9 +6,11 @@
|
||||
"dev": "vite",
|
||||
"dev:docker": "API_TARGET=http://backend:3000 vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"type-check": "vue-tsc -b",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/js": "^2.1.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||
@@ -16,13 +18,13 @@
|
||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||
"@linux-odyssey/constants": "*",
|
||||
"@linux-odyssey/file-graph": "*",
|
||||
"@linux-odyssey/utils": "*",
|
||||
"@linux-odyssey/models": "*",
|
||||
"@linux-odyssey/utils": "*",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"axios": "^1.6.0",
|
||||
"dompurify": "^3.0.6",
|
||||
"echarts": "^5.4.3",
|
||||
"marked": "^10.0.0",
|
||||
"pinia": "^2.1.7",
|
||||
"socket.io-client": "^4.7.1",
|
||||
"validator": "^13.11.0",
|
||||
"vue": "^3.3.4",
|
||||
|
||||
File renamed without changes.
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
@@ -83,6 +83,16 @@ const handleChange = () => {
|
||||
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>
|
||||
<template>
|
||||
<!-- login page -->
|
||||
@@ -219,6 +229,13 @@ const handleChange = () => {
|
||||
</RouterLink></span
|
||||
>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,59 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { createSession } from '../store/session'
|
||||
import { surveyUrl } from '../config'
|
||||
|
||||
const router = useRouter()
|
||||
const reset = async () => {
|
||||
console.log('resetting...')
|
||||
await createSession()
|
||||
}
|
||||
const continuePlay = async () => {
|
||||
try {
|
||||
router.push({ name: 'map' })
|
||||
} catch (err) {
|
||||
useToast().error('無法讀取地圖')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="h-[10%] p-3 grid grid-cols-4 gap-4 place-content-between">
|
||||
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
|
||||
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'file-invoice']"
|
||||
class="text-text-primary"
|
||||
/>
|
||||
<p class="text-text-primary inline ml-1">問卷</p>
|
||||
</a>
|
||||
</button>
|
||||
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'circle-question']"
|
||||
class="text-text-disabled"
|
||||
/>
|
||||
<p class="text-text-disabled inline ml-1">解答</p>
|
||||
</button>
|
||||
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'arrow-rotate-left']"
|
||||
class="text-text-primary"
|
||||
/>
|
||||
<p class="text-text-primary inline ml-1">重來</p>
|
||||
</button>
|
||||
<button
|
||||
id="continue"
|
||||
class="bg-bg-secondary rounded-lg p-2"
|
||||
@click="continuePlay"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'circle-right']"
|
||||
class="text-text-primary"
|
||||
/>
|
||||
<p class="text-text-primary inline ml-1">繼續</p>
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
@@ -20,7 +20,21 @@ function nodeClass() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a href="#" :class="nodeClass()">{{ node.discovered ? node.name : '???' }}</a>
|
||||
<a href="#" :class="nodeClass()">
|
||||
<font-awesome-icon
|
||||
v-if="node.type === 'folder'"
|
||||
:icon="['far', 'folder']"
|
||||
class="text-text-primary size-8"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
v-else
|
||||
:icon="['far', 'file']"
|
||||
class="text-text-primary size-8"
|
||||
/>
|
||||
<br />
|
||||
{{ node.discovered ? node.name : '???' }}
|
||||
{{ node.name === '' && node.discovered ? '/' : '' }}
|
||||
</a>
|
||||
<ul :class="nodeClass()" v-if="node.children && node.children.length > 0">
|
||||
<li :class="nodeClass()" v-for="child in node.children" :key="child.path">
|
||||
<FileNode :node="child" :pwd="pwd" />
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { defineProps, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { logout } from '../utils/auth'
|
||||
import { bugReportUrl, surveyUrl } from '../config'
|
||||
import { reset } from '../store/session'
|
||||
import userProfileStore, {
|
||||
loadUserProfile,
|
||||
resetUserProfile,
|
||||
} from '../store/userProfile'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout()
|
||||
resetUserProfile()
|
||||
reset()
|
||||
router.push({ name: 'login' })
|
||||
} catch (err) {
|
||||
toast.error('登出失敗')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await loadUserProfile()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast.error('無法讀取使用者資料')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="header" class="bg-bg-secondary content-center h-full w-full p-1">
|
||||
<div class="h-full flex items-center justify-center mx-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- <button id="stream" class="flex items-center h-5 w-5">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'bars']"
|
||||
class="text-text-primary h-full w-full"
|
||||
/>
|
||||
</button>
|
||||
<button id="backward" class="flex items-center h-5 w-5">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'arrow-left']"
|
||||
class="text-border h-full w-full"
|
||||
/>
|
||||
</button> -->
|
||||
<img src="../img/icon_totem.svg" class="h-5" />
|
||||
<button
|
||||
id="GameTitle"
|
||||
class="text-text-primary inline-block font-bold whitespace-nowrap"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
Linux Odyssey
|
||||
</button>
|
||||
<p
|
||||
id="QuestTitle"
|
||||
class="text-text inline-block font-bold whitespace-nowrap"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
{{ title }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="h-full w-full flex gap-3 items-center justify-end">
|
||||
<!-- <button id="warning" class="flex items-center h-5 w-5">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'triangle-exclamation']"
|
||||
class="text-text-primary h-full w-full"
|
||||
/>
|
||||
</button>
|
||||
<button id="setting" class="flex items-center h-5 w-5">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'gear']"
|
||||
class="text-text-primary h-full w-full"
|
||||
/>
|
||||
</button>
|
||||
<button id="fullscreen" class="flex items-center h-5 w-5">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'expand']"
|
||||
class="text-text-primary h-full w-full"
|
||||
/>
|
||||
</button> -->
|
||||
<p
|
||||
id="UsernameText"
|
||||
class="text-text inline-block whitespace-nowrap"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
{{ userProfileStore.username }}
|
||||
</p>
|
||||
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'file-invoice']"
|
||||
class="text-text-primary h-full w-full"
|
||||
/>
|
||||
</a>
|
||||
<RouterLink title="LeaderBoard" to="/leaderboard" class="h-5 w-5">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'trophy']"
|
||||
class="text-text-primary h-full w-full"
|
||||
/>
|
||||
</RouterLink>
|
||||
<RouterLink title="Map" to="/map" class="h-5 w-5">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'map']"
|
||||
class="text-text-primary h-full w-full"
|
||||
/>
|
||||
</RouterLink>
|
||||
<a
|
||||
title="Bug Report"
|
||||
:href="bugReportUrl"
|
||||
target="_blank"
|
||||
class="h-5 w-5"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'bug']"
|
||||
class="text-text-primary h-full w-full"
|
||||
/>
|
||||
</a>
|
||||
<button title="Sign Out" @click="handleLogout" class="h-5 w-5">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'arrow-right-from-bracket']"
|
||||
class="text-text-primary h-full w-full"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,6 +25,9 @@ const html = computed(() => {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.markdown-content :deep(p) {
|
||||
line-height: 1.5;
|
||||
}
|
||||
.markdown-content :deep(ol) {
|
||||
list-style: decimal;
|
||||
padding-left: 1em;
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { ref, defineProps, onMounted } from 'vue'
|
||||
import { getQuest } from '../utils/api'
|
||||
import MarkdownText from './MarkdownText.vue'
|
||||
|
||||
const props = defineProps({
|
||||
questTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
questId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
questUnlocked: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['closeIntro'])
|
||||
const showIntro = ref(true) // Controls whether the modal is visible or not
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const { questTitle, questId, questUnlocked } = props
|
||||
const questInstruction = ref('')
|
||||
const questColor = ref('')
|
||||
const questTextColor = ref('')
|
||||
const closeModal = () => {
|
||||
showIntro.value = false
|
||||
emit('closeIntro', true)
|
||||
router.push({ name: 'map' })
|
||||
}
|
||||
const handleQuests = (id: string) => {
|
||||
if (questUnlocked) {
|
||||
router.push({ name: 'game', params: { questId: id } })
|
||||
} else {
|
||||
toast.warning('你還沒完成前一個關卡!')
|
||||
}
|
||||
}
|
||||
const handleColour = () => {
|
||||
if (questUnlocked) {
|
||||
questColor.value = '#00ff00'
|
||||
questTextColor.value = '#000000'
|
||||
} else {
|
||||
questColor.value = '#505050'
|
||||
questTextColor.value = '#a0a0a0'
|
||||
}
|
||||
}
|
||||
onMounted(async () => {
|
||||
const quest = await getQuest(questId)
|
||||
questInstruction.value = quest.instruction
|
||||
handleColour()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showIntro" class="modal">
|
||||
<div class="modal-content bg-bg-secondary rounded-lg">
|
||||
<span class="close text-text content-between" @click="closeModal"
|
||||
>×</span
|
||||
>
|
||||
|
||||
<h2
|
||||
id="QuestTitle"
|
||||
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
|
||||
>
|
||||
{{ questTitle }}
|
||||
</h2>
|
||||
<div class="w-full h-3/5 p-8">
|
||||
<div class="px-5 whitespace-pre-wrap">
|
||||
<MarkdownText
|
||||
class="text-text markdown-content"
|
||||
:content="questInstruction"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
id="StartAdventure"
|
||||
target="_blank"
|
||||
@click="handleQuests(questId)"
|
||||
:style="{ backgroundColor: questColor, color: questTextColor }"
|
||||
class="inline-flex justify-center rounded-lg text-base font-black py-3 mt-5 w-full text-lg"
|
||||
>開始冒險</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: relative;
|
||||
margin: 10% auto;
|
||||
padding: 20px;
|
||||
width: 60%;
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.markdown-content {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
+235
-170
@@ -1,189 +1,254 @@
|
||||
<!-- eslint-disable no-console -->
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { use, init } from 'echarts/core'
|
||||
import { GraphChart } from 'echarts/charts'
|
||||
import { SVGRenderer } from 'echarts/renderers'
|
||||
import {
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
DataZoomComponent,
|
||||
} from 'echarts/components'
|
||||
import { DAG } from '@linux-odyssey/utils'
|
||||
import api from '../utils/api'
|
||||
import { NodeImage } from '../img/svg.js'
|
||||
import { getQuests } from '../utils/api'
|
||||
import useUserProfile from '../store/userProfile'
|
||||
import QuestIntro from './QuestIntro.vue'
|
||||
|
||||
const marginX = 500
|
||||
const marginY = 300
|
||||
const chartContainer = ref(null)
|
||||
let chartInstance = null
|
||||
const fullwidth = window.screen.width
|
||||
|
||||
use([
|
||||
GraphChart,
|
||||
SVGRenderer,
|
||||
TitleComponent,
|
||||
TooltipComponent,
|
||||
DataZoomComponent,
|
||||
])
|
||||
|
||||
async function getQuests() {
|
||||
try {
|
||||
const res = await api.get('/quests')
|
||||
return res.data
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function getProgress() {
|
||||
try {
|
||||
const res = await api.get('/users/me')
|
||||
return res.data.progress
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const genOption = (nodes: any, edges: any) => ({
|
||||
renderer: 'svg',
|
||||
tooltip: {},
|
||||
animationDurationUpdate: 1500,
|
||||
animationEasingUpdate: 'quinticInOut',
|
||||
// dataZoom: [
|
||||
// {
|
||||
// type: 'slider', // This type means "slider data zoom" which provides a slider bar for zooming
|
||||
// orient: 'vertical', // This makes the data zoom component vertical
|
||||
// start: 0, // The starting percentage of the window out of the data extent
|
||||
// end: 100, // The ending percentage of the window out of the data extent
|
||||
// show: true,
|
||||
// },
|
||||
// ],
|
||||
series: [
|
||||
{
|
||||
type: 'graph',
|
||||
layout: 'none',
|
||||
symbol: () => {
|
||||
return NodeImage
|
||||
},
|
||||
symbolSize: [fullwidth / 13, fullwidth / 30],
|
||||
roam: 'move',
|
||||
zoom: 1.5,
|
||||
center: ['120%', '30%'],
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: fullwidth / 90,
|
||||
},
|
||||
edgeSymbol: ['pin', 'arrow'],
|
||||
edgeSymbolSize: [2, 15],
|
||||
edgeLabel: {
|
||||
fontSize: 25,
|
||||
},
|
||||
data: nodes,
|
||||
// links: [],
|
||||
links: edges,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: 'gray',
|
||||
opacity: 4,
|
||||
curveness: 0.08,
|
||||
},
|
||||
itemStyle: {
|
||||
color: (params: any) => {
|
||||
const {
|
||||
data: { completed, unlocked },
|
||||
} = params
|
||||
if (completed) {
|
||||
return '#00ff00'
|
||||
}
|
||||
if (unlocked) {
|
||||
return '#ADADB5'
|
||||
}
|
||||
return '#505050'
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
function getOption(quests: any, progress: any) {
|
||||
const dag = new DAG(quests)
|
||||
|
||||
const nodes = dag.getNodes().map((node) => ({
|
||||
id: node._id,
|
||||
name: node.title,
|
||||
y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
|
||||
x: marginY * node.layer * 2,
|
||||
completed: progress[node._id]?.completed || false,
|
||||
unlocked: node.requirements.every((req: any) => progress[req]?.completed),
|
||||
}))
|
||||
const lineAppearence = (node: any) => {
|
||||
try {
|
||||
const { unlocked } = node
|
||||
return {
|
||||
type: unlocked ? 'line' : 'dashed',
|
||||
color: unlocked ? '#ADADB5' : '#454552',
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
const edges = dag.getEdgesArray().map((edge: any) => ({
|
||||
source: edge[0],
|
||||
target: edge[1],
|
||||
lineStyle: lineAppearence(nodes.find((node) => node.id === edge[1])),
|
||||
}))
|
||||
return genOption(nodes, edges)
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const store = useUserProfile()
|
||||
const toast = useToast()
|
||||
|
||||
function initChart(option: any) {
|
||||
chartInstance = init(chartContainer.value)
|
||||
chartInstance.setOption(option)
|
||||
chartInstance.on('click', (params: any) => {
|
||||
const {
|
||||
data: { id, unlocked },
|
||||
} = params
|
||||
const svgWidth = ref(window.innerWidth)
|
||||
const svgHeight = ref(window.innerHeight)
|
||||
|
||||
if (unlocked) {
|
||||
router.push({ name: 'game', params: { questId: id } })
|
||||
} else {
|
||||
toast.warning('你還沒完成前一個關卡!')
|
||||
}
|
||||
})
|
||||
const marginX = 120
|
||||
const marginY = 120
|
||||
|
||||
const nodeWidth = 160
|
||||
const nodeHeight = 50
|
||||
const opened = ref<Node | null>(null)
|
||||
type Node = {
|
||||
id: string
|
||||
title: string
|
||||
x: number
|
||||
y: number
|
||||
index: number
|
||||
completed: boolean
|
||||
unlocked: boolean
|
||||
}
|
||||
|
||||
type Edge = {
|
||||
source: Node
|
||||
target: Node
|
||||
}
|
||||
|
||||
const graphData = ref<{ nodes: Node[]; edges: Edge[] }>({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
})
|
||||
|
||||
const isDragging = ref(false)
|
||||
const dragStartX = ref(0)
|
||||
const dragStartY = ref(0)
|
||||
const offsetX = ref(0)
|
||||
const offsetY = ref(0)
|
||||
|
||||
function startDrag(event: MouseEvent) {
|
||||
isDragging.value = true
|
||||
dragStartX.value = event.clientX - offsetX.value
|
||||
dragStartY.value = event.clientY - offsetY.value
|
||||
}
|
||||
|
||||
function drag(event: MouseEvent) {
|
||||
if (!isDragging.value) return
|
||||
offsetX.value = event.clientX - dragStartX.value
|
||||
offsetY.value = event.clientY - dragStartY.value
|
||||
}
|
||||
|
||||
function endDrag() {
|
||||
isDragging.value = false
|
||||
}
|
||||
|
||||
async function computeGraphData() {
|
||||
if (!store.progress) return
|
||||
|
||||
try {
|
||||
const quests = await getQuests()
|
||||
const dag = new DAG(quests)
|
||||
const nodesValues = dag.getNodes()
|
||||
|
||||
const nodes = nodesValues.map((node) => ({
|
||||
id: node._id,
|
||||
title: node.title,
|
||||
x: marginX * node.layer * 2,
|
||||
y:
|
||||
marginY * node.index -
|
||||
(marginX * dag.getLayer(node._id)) / 2 +
|
||||
svgHeight.value / 2,
|
||||
completed: store.progress[node._id]?.completed || false,
|
||||
unlocked: node.requirements.every(
|
||||
(req: string) => store.progress[req]?.completed
|
||||
),
|
||||
index: node.index,
|
||||
}))
|
||||
|
||||
const edges = dag
|
||||
.getEdgesArray()
|
||||
.map((edge) => ({
|
||||
source: nodes.find((n) => n.id === edge[0]),
|
||||
target: nodes.find((n) => n.id === edge[1]),
|
||||
}))
|
||||
.filter(
|
||||
(edge): edge is Edge =>
|
||||
edge.source !== undefined && edge.target !== undefined
|
||||
)
|
||||
|
||||
graphData.value = { nodes, edges }
|
||||
} catch (error) {
|
||||
console.error('Error computing graph data:', error)
|
||||
toast.error('Failed to load quest data')
|
||||
}
|
||||
}
|
||||
function handleNodeClick(node: Node) {
|
||||
opened.value = node
|
||||
}
|
||||
function closeIntro(close: boolean) {
|
||||
if (close) {
|
||||
opened.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const quests = await getQuests()
|
||||
const progress = await getProgress()
|
||||
const option = getOption(quests, progress)
|
||||
initChart(option)
|
||||
await store.loadUserProfile()
|
||||
await computeGraphData()
|
||||
})
|
||||
|
||||
watch(() => store.progress, computeGraphData, { deep: true })
|
||||
|
||||
const curvedPath = computed(() => {
|
||||
return (source: Node, target: Node) => {
|
||||
// const dx = target.x - source.x
|
||||
// const dy = target.y - source.y
|
||||
// const dr = Math.sqrt(dx * dx + dy * dy)
|
||||
const dr = 0
|
||||
const sweep = target.index > source.index ? 0 : 1 // Determines if the arc should curve up or down based on node index
|
||||
return `M${source.x},${source.y}A${dr},${dr} 0 0,${sweep} ${target.x},${target.y}`
|
||||
}
|
||||
})
|
||||
|
||||
const nodeStyle = computed(() => {
|
||||
return (node: Node) => {
|
||||
if (node.completed) return 'completed'
|
||||
if (node.unlocked) return 'unlocked'
|
||||
return 'locked'
|
||||
}
|
||||
})
|
||||
|
||||
const edgeStyle = computed(() => {
|
||||
return (edge: Edge) => {
|
||||
return edge.target.unlocked ? 'unlocked' : 'locked'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative bg-black w-full h-full flex">
|
||||
<img
|
||||
src="../img/catelogbg.svg"
|
||||
alt="bg"
|
||||
class="p-2 stroke-2 scale-y-120 justify-center items-center"
|
||||
/>
|
||||
<h1
|
||||
class="p-4 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
|
||||
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
|
||||
<div
|
||||
class="flex place-content-center w-full bg-catelogbg bg-cover bg-scroll"
|
||||
>
|
||||
踏上你的Linux冒險之旅吧!
|
||||
</h1>
|
||||
<div class="flex flex-wrap absolute w-full h-full z-1">
|
||||
<div ref="chartContainer" class="w-full h-full flex flex-wrap"></div>
|
||||
<h1
|
||||
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"
|
||||
>
|
||||
踏上你的Linux冒險之旅吧!
|
||||
</h1>
|
||||
<svg
|
||||
:width="svgWidth"
|
||||
:height="svgHeight"
|
||||
class="absolute"
|
||||
@mousedown="startDrag"
|
||||
@mousemove="drag"
|
||||
@mouseup="endDrag"
|
||||
@mouseleave="endDrag"
|
||||
>
|
||||
<g :transform="`translate(${offsetX}, ${offsetY})`">
|
||||
<g
|
||||
v-for="edge in graphData.edges"
|
||||
:key="`${edge.source.id}-${edge.target.id}`"
|
||||
>
|
||||
<path
|
||||
:d="curvedPath(edge.source, edge.target)"
|
||||
:class="['edge', edgeStyle(edge)]"
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
v-for="node in graphData.nodes"
|
||||
:key="node.id"
|
||||
@click="handleNodeClick(node)"
|
||||
>
|
||||
<rect
|
||||
:x="node.x - nodeWidth / 2"
|
||||
:y="node.y - nodeHeight / 2"
|
||||
:width="nodeWidth"
|
||||
:height="nodeHeight"
|
||||
rx="10"
|
||||
ry="10"
|
||||
:class="['node', nodeStyle(node)]"
|
||||
/>
|
||||
<text
|
||||
:x="node.x"
|
||||
:y="node.y"
|
||||
text-anchor="middle"
|
||||
alignment-baseline="middle"
|
||||
:class="['node-text', nodeStyle(node)]"
|
||||
font-size="18"
|
||||
>
|
||||
{{ node.title }}
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<QuestIntro
|
||||
:questTitle="opened.title"
|
||||
:questId="opened.id"
|
||||
:questUnlocked="opened.unlocked"
|
||||
v-if="opened"
|
||||
@close-intro="closeIntro"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.node {
|
||||
&.completed {
|
||||
fill: #6cf76c;
|
||||
}
|
||||
&.unlocked {
|
||||
fill: #8c8c92;
|
||||
}
|
||||
&.locked {
|
||||
fill: #505050;
|
||||
}
|
||||
}
|
||||
|
||||
.node-text {
|
||||
&.completed {
|
||||
fill: #1d1d1d;
|
||||
}
|
||||
&.unlocked {
|
||||
fill: #ffffff;
|
||||
}
|
||||
&.locked {
|
||||
fill: #a0a0a0;
|
||||
}
|
||||
}
|
||||
|
||||
.edge {
|
||||
stroke-width: 3;
|
||||
fill: none;
|
||||
&.unlocked {
|
||||
stroke: #adadb5;
|
||||
stroke-dasharray: none;
|
||||
}
|
||||
&.locked {
|
||||
stroke: #454552;
|
||||
stroke-dasharray: 5, 5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+1
-1
@@ -2,6 +2,6 @@
|
||||
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
|
||||
<h1 id="CommandBook" class="inline text-text-primary font-xl">指令之書</h1>
|
||||
<div id="cheatsheets">
|
||||
<p class="text-text-secondary">--指令列表--</p>
|
||||
<p class="text-text-secondary">-- --指令列表-- --</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -4,14 +4,16 @@ import { useToast } from 'vue-toastification'
|
||||
import { ref } from 'vue'
|
||||
import { Vue3Lottie } from 'vue3-lottie'
|
||||
// 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 router = useRouter()
|
||||
const toast = useToast()
|
||||
|
||||
const closeModal = () => {
|
||||
showModal.value = false
|
||||
}
|
||||
|
||||
const defineComponent = () => ({
|
||||
components: {
|
||||
Vue3Lottie,
|
||||
@@ -22,11 +24,11 @@ const defineComponent = () => ({
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const backtoMap = async () => {
|
||||
try {
|
||||
router.push({ name: 'map' })
|
||||
} catch (err) {
|
||||
// useToast().error('Failed to load map')
|
||||
toast.error('無法讀取地圖')
|
||||
console.error(err)
|
||||
}
|
||||
@@ -60,17 +62,6 @@ setTimeout(() => {
|
||||
<p class="text-base text-text text-center">
|
||||
接下來回到地圖,迎接新的挑戰吧!
|
||||
</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
|
||||
id="BacktoMap"
|
||||
target="_blank"
|
||||
@@ -79,6 +70,21 @@ setTimeout(() => {
|
||||
>回到地圖</a
|
||||
>
|
||||
</div>
|
||||
<div style="position: relative; height: 100vh; overflow: auto">
|
||||
<iframe
|
||||
src="https://app.formbricks.com/s/clyffdm0c0be2wh6335clle0l"
|
||||
frameborder="0"
|
||||
style="
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import useSession from '../../store/session'
|
||||
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
|
||||
|
||||
const router = useRouter()
|
||||
const sessionStore = useSession()
|
||||
const reset = async () => {
|
||||
console.log('resetting...')
|
||||
await sessionStore.createSession()
|
||||
}
|
||||
const continuePlay = async () => {
|
||||
try {
|
||||
router.push({ name: 'map' })
|
||||
} catch (err) {
|
||||
useToast().error('無法讀取地圖')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="mt-2 flex flex-row gap-3 justify-center">
|
||||
<button
|
||||
id="survey"
|
||||
class="bg-bg-secondary rounded-lg p-2"
|
||||
@click="openQuestSurvey"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'file-invoice']"
|
||||
class="text-text-primary px-1"
|
||||
/>
|
||||
<p class="text-text-primary inline md:flex">問卷</p>
|
||||
</button>
|
||||
<button
|
||||
title="Bug Report"
|
||||
@click="openBugReport"
|
||||
class="bg-bg-secondary rounded-lg p-2"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
|
||||
<p class="text-text-primary inline ml-1 md:flex">回報</p>
|
||||
</button>
|
||||
<!-- <button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'circle-question']"
|
||||
class="text-text-disabled px-1"
|
||||
/>
|
||||
<p class="text-text-disabled inline ml-1 md:flex">解答</p>
|
||||
</button> -->
|
||||
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'arrow-rotate-left']"
|
||||
class="text-text-primary px-1"
|
||||
/>
|
||||
<p class="text-text-primary inline ml-1 md:flex">重來</p>
|
||||
</button>
|
||||
<button
|
||||
id="continue"
|
||||
class="bg-bg-secondary rounded-lg p-2"
|
||||
@click="continuePlay"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'circle-right']"
|
||||
class="text-text-primary px-1"
|
||||
/>
|
||||
<p class="text-text-primary inline ml-1 md:flex">繼續</p>
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
@@ -1,16 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import sessionStore from '../store/session'
|
||||
import MarkdownText from './MarkdownText.vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import useSession from '../../store/session'
|
||||
import MarkdownText from '../MarkdownText.vue'
|
||||
|
||||
const sessionStore = useSession()
|
||||
const current = ref(-1)
|
||||
watch(
|
||||
() => sessionStore.session.hints,
|
||||
(hints) => {
|
||||
current.value = hints.length - 1
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
const length = computed(() => {
|
||||
return sessionStore.session?.responses.length ?? 0
|
||||
})
|
||||
|
||||
const disabled = computed(() => {
|
||||
return current.value === length.value - 1
|
||||
})
|
||||
|
||||
watch(length, () => {
|
||||
current.value = length.value - 1
|
||||
})
|
||||
|
||||
const left = () => {
|
||||
if (current.value > 0) {
|
||||
@@ -19,7 +24,7 @@ const left = () => {
|
||||
}
|
||||
|
||||
const right = () => {
|
||||
if (current.value < sessionStore.session.hints.length - 1) {
|
||||
if (current.value < length.value - 1) {
|
||||
current.value += 1
|
||||
}
|
||||
}
|
||||
@@ -27,16 +32,12 @@ const right = () => {
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<!-- Hint Header -->
|
||||
<div class="flex items-center p-2 h-10">
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'lightbulb']"
|
||||
class="text-yellow-200 p-2 content-center"
|
||||
/>
|
||||
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">
|
||||
<!-- Hint -->
|
||||
提示
|
||||
</h1>
|
||||
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">說明</h1>
|
||||
<div v-if="current !== -1" class="flex w-full justify-end items-end">
|
||||
<button
|
||||
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">/</p>
|
||||
<p class="inline text-text font-xl w-1/14 p-2 m-1">
|
||||
{{ sessionStore.session.hints.length }}
|
||||
{{ length }}
|
||||
</p>
|
||||
<button
|
||||
class="p-2 m-1 w-1/8"
|
||||
id="Rbutton"
|
||||
@click="right"
|
||||
:disabled="current === sessionStore.session.hints.length - 1"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'arrow-right']" class="text-text" />
|
||||
</button>
|
||||
@@ -68,7 +69,7 @@ const right = () => {
|
||||
<div id="hint" class="bg-bg p-8 overflow-y-auto">
|
||||
<ul>
|
||||
<li
|
||||
v-for="response in sessionStore.session.responses[current]"
|
||||
v-for="response in sessionStore.session?.responses[current]"
|
||||
:key="response.type"
|
||||
class="text-text font-xl whitespace-pre-wrap"
|
||||
>
|
||||
@@ -77,7 +78,7 @@ const right = () => {
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
v-for="hint in sessionStore.session.hints[current]"
|
||||
v-for="hint in sessionStore.session?.hints[current]"
|
||||
:key="hint"
|
||||
class="text-text-primary font-xl whitespace-pre-wrap"
|
||||
>
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import MarkdownText from './MarkdownText.vue'
|
||||
import sessionStore from '../store/session'
|
||||
import { Task } from '../types'
|
||||
import MarkdownText from '../MarkdownText.vue'
|
||||
import useSession from '../../store/session'
|
||||
import { Task } from '../../types'
|
||||
|
||||
const sessionStore = useSession()
|
||||
const tasks = computed(() => {
|
||||
return sessionStore.session.tasks.map((task: Task) => {
|
||||
return sessionStore.session?.tasks.map((task: Task) => {
|
||||
const prefix = task.completed ? '✓' : '➤'
|
||||
const color = task.completed ? 'text-text-primary' : 'text-text'
|
||||
return {
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { computed } from 'vue'
|
||||
import useSession from '../../store/session'
|
||||
|
||||
const toast = useToast()
|
||||
const sessionStore = useSession()
|
||||
|
||||
const showCover = computed(() => sessionStore.session === null)
|
||||
|
||||
const startSession = async () => {
|
||||
try {
|
||||
await sessionStore.createSession()
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
toast.error(err.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
v-if="showCover"
|
||||
class="bg-bg-secondary h-full w-1/2 fixed bg-opacity-50 flex justify-center items-center"
|
||||
>
|
||||
<button
|
||||
id="start"
|
||||
class="bg-bg-secondary rounded-lg p-3"
|
||||
@click="startSession"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'play']"
|
||||
class="text-text-primary h-full px-3"
|
||||
/>
|
||||
<p class="text-text-primary inline ml-1 md:flex">開始</p>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useTerminal } from '../store/session'
|
||||
import { useTerminal } from '../../store/session'
|
||||
|
||||
const terminal = ref<HTMLElement | null>(null)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import FileNode from './FileNode.vue'
|
||||
import sessionStore from '../store/session'
|
||||
import FileNode from '../FileNode.vue'
|
||||
import useSession from '../../store/session'
|
||||
|
||||
const sessionStore = useSession()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -8,6 +10,7 @@ import sessionStore from '../store/session'
|
||||
<ul>
|
||||
<li>
|
||||
<FileNode
|
||||
v-if="sessionStore.session"
|
||||
:node="sessionStore.session.graph"
|
||||
:pwd="sessionStore.session.pwd"
|
||||
/>
|
||||
@@ -45,14 +48,14 @@ import sessionStore from '../store/session'
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 50%;
|
||||
border-top: 1px solid #00ff00;
|
||||
border-top: 2px solid #00ff00;
|
||||
width: 50%;
|
||||
height: 20px;
|
||||
}
|
||||
.tree li::after {
|
||||
right: auto;
|
||||
left: 50%;
|
||||
border-left: 1px solid #00ff00;
|
||||
border-left: 2px solid #00ff00;
|
||||
}
|
||||
|
||||
.tree li:only-child::after,
|
||||
@@ -70,7 +73,7 @@ import sessionStore from '../store/session'
|
||||
}
|
||||
|
||||
.tree li:last-child::before {
|
||||
border-right: 1px solid #00ff00;
|
||||
border-right: 2px solid #00ff00;
|
||||
border-radius: 0 5px 0 0;
|
||||
-webkit-border-radius: 0 5px 0 0;
|
||||
-moz-border-radius: 0 5px 0 0;
|
||||
@@ -86,7 +89,7 @@ import sessionStore from '../store/session'
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
border-left: 1px solid #00ff00;
|
||||
border-left: 2px solid #00ff00;
|
||||
width: 0;
|
||||
height: 20px;
|
||||
}
|
||||
@@ -97,7 +100,7 @@ import sessionStore from '../store/session'
|
||||
text-decoration: none;
|
||||
color: #ffffff;
|
||||
font-family: arial, verdana, tahoma;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
|
||||
border-radius: 5px;
|
||||
@@ -109,18 +112,10 @@ import sessionStore from '../store/session'
|
||||
-moz-transition: all 0.5s;
|
||||
}
|
||||
|
||||
a.discovered {
|
||||
background: #526b4e;
|
||||
color: #000;
|
||||
border: 1px solid #94a0b4;
|
||||
}
|
||||
|
||||
ul.discovered,
|
||||
li.discovered {
|
||||
border-color: #94a0b4;
|
||||
}
|
||||
|
||||
a.pwd {
|
||||
box-shadow: 0 0 40px rgba(255, 255, 0, 20);
|
||||
.tree li a.pwd {
|
||||
border: 3px solid #00ff00;
|
||||
background: #008080;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
File renamed without changes.
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { logout } from '../../utils/auth'
|
||||
import { surveyUrl } from '../../config'
|
||||
import { openBugReport } from '../../utils/formbricks'
|
||||
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>
|
||||
<button
|
||||
title="Bug Report"
|
||||
@click="openBugReport"
|
||||
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>
|
||||
</button>
|
||||
<button
|
||||
title="Sign Out"
|
||||
@click="handleLogout"
|
||||
class="md:h-5 md:w-5 px-1.5 w-auto"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'arrow-right-from-bracket']"
|
||||
class="text-text-primary"
|
||||
/>
|
||||
<span class="text-text sm:hidden px-2">Sign Out</span>
|
||||
</button>
|
||||
</template>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
headerComponent: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
const menuOpen = ref(false)
|
||||
const windowWidth = window.innerWidth < 768
|
||||
const toggleMenu = () => {
|
||||
menuOpen.value = !menuOpen.value
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="w-full top-0 px-5 fixed flex flex-row flex-wrap bg-bg-secondary justify-between items-center sm:justify-between"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-3" :class="{ hidden: menuOpen }">
|
||||
<img src="../../img/icon_totem.svg" class="h-8 justify-self-start" />
|
||||
<RouterLink
|
||||
to="/"
|
||||
id="GameTitle"
|
||||
class="text-text-primary inline-block font-bold whitespace-nowrap pt-1 transition ease-in-out hover:scale-110 duration-300"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
Linux Odyssey
|
||||
</RouterLink>
|
||||
<p
|
||||
id="QuestTitle"
|
||||
class="text-text inline-block font-bold whitespace-nowrap"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
{{ title }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="w-full flex flex-col items-center justify-end sm:w-auto sm:block sm:ml-6"
|
||||
:class="{ hidden: !menuOpen }"
|
||||
>
|
||||
<component :is="headerComponent" />
|
||||
</div>
|
||||
<div
|
||||
v-if="windowWidth"
|
||||
:class="{ 'w-full flex justify-end pb-3': menuOpen }"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center h-5 w-5 rounded-md sm:hidden"
|
||||
aria-controls="mobile-menu"
|
||||
aria-expanded="false"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'bars']"
|
||||
class="text-text-primary h-full w-full"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { bugReportUrl } from '../../config'
|
||||
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
const section = document.getElementById(sectionId)
|
||||
if (section) {
|
||||
const container = document.querySelector('.overflow-y-auto') as HTMLElement // Replace '.overflow-y-auto' with the appropriate class or selector for your container
|
||||
if (container) {
|
||||
container.scrollTo({
|
||||
top: section.offsetTop - container.offsetTop,
|
||||
behavior: 'smooth', // Add smooth scrolling behavior if desired
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<button
|
||||
id="Intro"
|
||||
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||
@click="scrollToSection('intro')"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
前言
|
||||
</button>
|
||||
<button
|
||||
id="Feature"
|
||||
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||
@click="scrollToSection('feature')"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
特色</button
|
||||
><button
|
||||
id="Info"
|
||||
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||
@click="scrollToSection('about')"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
關於
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="Contact"
|
||||
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||
@click="scrollToSection('contact')"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
聯絡我們
|
||||
</button>
|
||||
<RouterLink
|
||||
title="Sign In"
|
||||
id="SignIn"
|
||||
to="/register"
|
||||
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
登入
|
||||
</RouterLink>
|
||||
<a title="Bug Report" :href="bugReportUrl" target="_blank" class="h-5 w-5">
|
||||
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
|
||||
</a>
|
||||
</template>
|
||||
+33
-27
@@ -3,27 +3,30 @@ import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
/* import specific icons */
|
||||
import {
|
||||
faBars,
|
||||
faArrowLeft,
|
||||
faList,
|
||||
faGear,
|
||||
faTriangleExclamation,
|
||||
faExpand,
|
||||
faTerminal,
|
||||
faArrowRotateLeft,
|
||||
faArrowRightFromBracket,
|
||||
faArrowRight,
|
||||
faArrowRightFromBracket,
|
||||
faArrowRotateLeft,
|
||||
faBars,
|
||||
faBug,
|
||||
faCircleExclamation,
|
||||
faMap,
|
||||
faTrophy,
|
||||
faFileInvoice,
|
||||
faEnvelope,
|
||||
faExpand,
|
||||
faFileInvoice,
|
||||
faGear,
|
||||
faList,
|
||||
faMap,
|
||||
faPlay,
|
||||
faTerminal,
|
||||
faTriangleExclamation,
|
||||
faTrophy,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faLightbulb,
|
||||
faCircleQuestion,
|
||||
faCircleRight,
|
||||
faFile,
|
||||
faFolder,
|
||||
faLightbulb,
|
||||
} from '@fortawesome/free-regular-svg-icons'
|
||||
import {
|
||||
faFacebook,
|
||||
@@ -33,26 +36,29 @@ import {
|
||||
|
||||
/* add icons to the library */
|
||||
library.add(
|
||||
faBars,
|
||||
faArrowLeft,
|
||||
faList,
|
||||
faGear,
|
||||
faTriangleExclamation,
|
||||
faExpand,
|
||||
faTerminal,
|
||||
faArrowRotateLeft,
|
||||
faArrowRightFromBracket,
|
||||
faArrowRight,
|
||||
faArrowRightFromBracket,
|
||||
faArrowRotateLeft,
|
||||
faBars,
|
||||
faBug,
|
||||
faMap,
|
||||
faTrophy,
|
||||
faCircleExclamation,
|
||||
faLightbulb,
|
||||
faEnvelope,
|
||||
faExpand,
|
||||
faFileInvoice,
|
||||
faGear,
|
||||
faList,
|
||||
faMap,
|
||||
faPlay,
|
||||
faTerminal,
|
||||
faTriangleExclamation,
|
||||
faTrophy,
|
||||
faCircleQuestion,
|
||||
faCircleRight,
|
||||
faGithub,
|
||||
faGoogle,
|
||||
faFileInvoice,
|
||||
faFile,
|
||||
faFolder,
|
||||
faLightbulb,
|
||||
faFacebook,
|
||||
faEnvelope
|
||||
faGithub,
|
||||
faGoogle
|
||||
)
|
||||
+11
-1
@@ -1,4 +1,6 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import './style.css'
|
||||
/* import font awesome icon component */
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
@@ -8,16 +10,24 @@ import Toast, { POSITION } from 'vue-toastification'
|
||||
/* import Vue Lottie */
|
||||
import Vue3Lottie from 'vue3-lottie'
|
||||
import 'vue-toastification/dist/index.css'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import formbricks from './utils/formbricks'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.component('font-awesome-icon', FontAwesomeIcon)
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
|
||||
router.afterEach(() => {
|
||||
if (typeof formbricks !== 'undefined') {
|
||||
formbricks.registerRouteChange()
|
||||
}
|
||||
})
|
||||
|
||||
const options = {
|
||||
position: POSITION.TOP_RIGHT,
|
||||
}
|
||||
|
||||
+87
-138
@@ -1,156 +1,105 @@
|
||||
import { reactive } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { defineStore } from 'pinia'
|
||||
import { FileGraph, FileObject } from '@linux-odyssey/file-graph'
|
||||
import { IQuest } from '@linux-odyssey/models'
|
||||
import api from '../utils/api'
|
||||
import { createSession, getActiveSession, getQuest } from '../utils/api'
|
||||
import Socket from '../utils/socket'
|
||||
import SocketTerminal from '../utils/terminal'
|
||||
import { LoadQuestError, LoadSessionError } from '../utils/errors'
|
||||
import { Session, StageResponse } from '../types'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
function newSession() {
|
||||
const graph = new FileGraph({
|
||||
path: '/',
|
||||
type: 'folder',
|
||||
discovered: false,
|
||||
})
|
||||
return {
|
||||
_id: '',
|
||||
status: 'inactive',
|
||||
graph,
|
||||
pwd: '',
|
||||
hints: [],
|
||||
tasks: [],
|
||||
responses: [],
|
||||
}
|
||||
}
|
||||
const socket = new Socket()
|
||||
const term = new SocketTerminal(40, 80)
|
||||
let hasSetup = false
|
||||
|
||||
interface Store {
|
||||
session: Session
|
||||
session: Session | null
|
||||
questId: string
|
||||
quest: IQuest | null
|
||||
}
|
||||
|
||||
const store = reactive<Store>({
|
||||
session: newSession(),
|
||||
questId: '',
|
||||
quest: null,
|
||||
const useSession = defineStore('session', {
|
||||
state: (): Store => ({
|
||||
session: null,
|
||||
questId: '',
|
||||
quest: null,
|
||||
}),
|
||||
actions: {
|
||||
async setQuest(questId: string) {
|
||||
this.quest = await getQuest(questId)
|
||||
this.questId = questId
|
||||
},
|
||||
async setSession(session: Session) {
|
||||
this.session = session
|
||||
this.session.graph = new FileGraph(session.graph)
|
||||
term.reset()
|
||||
await socket.connect(session)
|
||||
term.focus()
|
||||
},
|
||||
async createSession() {
|
||||
const session = await createSession(this.questId)
|
||||
await this.setSession(session)
|
||||
socket.emit('terminal', 'echo start\n')
|
||||
},
|
||||
async getActiveSession() {
|
||||
const session = await getActiveSession(this.questId)
|
||||
if (session) {
|
||||
this.setSession(session)
|
||||
}
|
||||
},
|
||||
updateGraph(event: { discover?: FileObject[]; pwd?: string }) {
|
||||
if (!this.session) return
|
||||
if (event.discover) {
|
||||
this.session.graph.discover(event.discover)
|
||||
}
|
||||
if (event.pwd) {
|
||||
this.session.pwd = event.pwd
|
||||
}
|
||||
},
|
||||
newResponse(response: StageResponse) {
|
||||
if (!this.session) return
|
||||
this.session.responses.push(response.responses)
|
||||
this.session.hints.push(response.hints)
|
||||
if (response.tasks) {
|
||||
this.session.tasks = response.tasks
|
||||
}
|
||||
if (
|
||||
response.status === 'finished' &&
|
||||
this.session.status !== 'finished'
|
||||
) {
|
||||
this.finish()
|
||||
}
|
||||
this.session.status = response.status
|
||||
},
|
||||
finish() {},
|
||||
reset() {
|
||||
socket.reset()
|
||||
term.reset()
|
||||
this.$reset()
|
||||
},
|
||||
setup() {
|
||||
if (hasSetup) return
|
||||
socket.on('terminal', (data: string) => {
|
||||
term.write(data)
|
||||
})
|
||||
term.onData((data: string) => {
|
||||
socket.emit('terminal', data)
|
||||
})
|
||||
socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
|
||||
this.updateGraph(event)
|
||||
})
|
||||
socket.on('response', (response: StageResponse) => {
|
||||
this.newResponse(response)
|
||||
})
|
||||
hasSetup = true
|
||||
},
|
||||
setStatus(status: string) {
|
||||
if (!this.session) return
|
||||
this.session.status = status
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const socket = new Socket()
|
||||
const term = new SocketTerminal(40, 80)
|
||||
|
||||
function setQuest(questId: string) {
|
||||
return api.get(`/quests/${questId}`).then((res) => {
|
||||
store.questId = res.data._id
|
||||
store.quest = res.data
|
||||
})
|
||||
}
|
||||
|
||||
async function setSession(session: Session) {
|
||||
try {
|
||||
console.log('Setting session...', session)
|
||||
store.session = session
|
||||
store.session.graph = new FileGraph(session.graph)
|
||||
term.reset()
|
||||
await socket.connect(session)
|
||||
term.focus()
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
if (err instanceof Error) {
|
||||
toast.error(err.message)
|
||||
} else {
|
||||
toast.error('Failed to connect to the session. Please try again later.')
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSession() {
|
||||
try {
|
||||
console.log('Creating a new session...')
|
||||
const res = await api.post('/sessions', { questId: store.questId })
|
||||
const { data } = res
|
||||
await setSession(data)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast.error('Failed to create a new session. Please try again later.')
|
||||
}
|
||||
}
|
||||
|
||||
async function getActiveSession(questId: string) {
|
||||
try {
|
||||
const res = await api.post('/sessions/active', { questId })
|
||||
await setSession(res.data)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
toast.warning('Failed to connect previous session. Creating a new one...')
|
||||
await createSession()
|
||||
}
|
||||
}
|
||||
|
||||
function updateGraph(event: { discover: FileObject[]; pwd: string }) {
|
||||
if (event.discover) {
|
||||
store.session.graph.discover(event.discover)
|
||||
}
|
||||
if (event.pwd) {
|
||||
store.session.pwd = event.pwd
|
||||
}
|
||||
}
|
||||
|
||||
function newResponse(response: StageResponse) {
|
||||
console.log('newResponse', response, 'session', store.session)
|
||||
store.session.responses.push(response.responses)
|
||||
store.session.hints.push(response.hints)
|
||||
store.session.tasks = response.tasks
|
||||
store.session.status = response.status
|
||||
}
|
||||
|
||||
export function useTerminal() {
|
||||
return term
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
socket.reset()
|
||||
term.reset()
|
||||
store.session = newSession()
|
||||
store.quest = null
|
||||
}
|
||||
|
||||
export async function init(questId: string) {
|
||||
if (!questId) throw new Error('No quest ID provided')
|
||||
reset()
|
||||
try {
|
||||
await setQuest(questId)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw new LoadQuestError('Failed to load quest', questId)
|
||||
}
|
||||
try {
|
||||
await getActiveSession(questId)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw new LoadSessionError('Failed to load session', questId)
|
||||
}
|
||||
}
|
||||
|
||||
function setup() {
|
||||
socket.on('terminal', (data: string) => {
|
||||
term.write(data)
|
||||
})
|
||||
term.onData((data: string) => {
|
||||
socket.emit('terminal', data)
|
||||
})
|
||||
socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
|
||||
updateGraph(event)
|
||||
})
|
||||
socket.on('response', (response: StageResponse) => {
|
||||
newResponse(response)
|
||||
})
|
||||
}
|
||||
|
||||
setup()
|
||||
|
||||
export default store
|
||||
export default useSession
|
||||
@@ -1,30 +1,35 @@
|
||||
import { reactive } from 'vue'
|
||||
import api from '../utils/api'
|
||||
import { defineStore } from 'pinia'
|
||||
import { getUserProfile } from '../utils/api'
|
||||
|
||||
const store = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
progress: {},
|
||||
interface IProgress {
|
||||
quest: string
|
||||
sessions: string[]
|
||||
completed: boolean
|
||||
startedAt: Date
|
||||
finishedAt?: Date
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
username: string
|
||||
email: string
|
||||
progress: Record<string, IProgress>
|
||||
}
|
||||
|
||||
const useUserProfile = defineStore('userProfile', {
|
||||
state: (): UserProfile => ({
|
||||
username: '',
|
||||
email: '',
|
||||
progress: {},
|
||||
}),
|
||||
actions: {
|
||||
async loadUserProfile() {
|
||||
const res = await getUserProfile()
|
||||
this.$patch(res)
|
||||
},
|
||||
async resetUserProfile() {
|
||||
this.$reset()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export async function loadUserProfile() {
|
||||
try {
|
||||
const res = await api.get('/users/me')
|
||||
const { username, email, progress } = res.data
|
||||
store.username = username
|
||||
store.email = email
|
||||
store.progress = progress
|
||||
return store
|
||||
} catch (err: 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
|
||||
export default useUserProfile
|
||||
@@ -1,4 +1,8 @@
|
||||
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({
|
||||
baseURL: '/api/v1',
|
||||
@@ -8,3 +12,39 @@ const api = axios.create({
|
||||
withCredentials: true,
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
export async function getQuests(): Promise<IQuest[]> {
|
||||
const res = await api.get<IQuest[]>('/quests')
|
||||
return res.data
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import formbricks from '@formbricks/js/website'
|
||||
import useSession from '../store/session'
|
||||
import useUserProfile from '../store/userProfile'
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
formbricks.init({
|
||||
environmentId: 'clzcdqqn0000kadvtqykdo0pc',
|
||||
apiHost: 'https://app.formbricks.com',
|
||||
})
|
||||
}
|
||||
|
||||
export function openQuestSurvey() {
|
||||
const sessionStore = useSession()
|
||||
const userStore = useUserProfile()
|
||||
|
||||
formbricks.track('quest_completed', {
|
||||
hiddenFields: {
|
||||
quest: sessionStore.questId,
|
||||
session: sessionStore.session?._id,
|
||||
username: userStore.username,
|
||||
email: userStore.email,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function openBugReport() {
|
||||
const sessionStore = useSession()
|
||||
const userStore = useUserProfile()
|
||||
|
||||
formbricks.track('bug_report', {
|
||||
hiddenFields: {
|
||||
quest: sessionStore.questId,
|
||||
session: sessionStore.session?._id,
|
||||
username: userStore.username,
|
||||
email: userStore.email,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default formbricks
|
||||
@@ -46,7 +46,6 @@ class SocketTerminal {
|
||||
|
||||
mount(reference: HTMLElement): void {
|
||||
this.term.open(reference)
|
||||
this.term.focus()
|
||||
this.resizeScreen()
|
||||
}
|
||||
|
||||
|
||||
+38
-35
@@ -1,19 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import sessionStore, { init } from '../store/session'
|
||||
import GameHeaderPart from '../components/GameHeaderPart.vue'
|
||||
// import CommandlistPart from '../components/CommandlistPart.vue'
|
||||
import HintPart from '../components/HintPart.vue'
|
||||
import QuestPart from '../components/QuestPart.vue'
|
||||
import TerminalPart from '../components/TerminalPart.vue'
|
||||
import VisualizationPart from '../components/VisualizationPart.vue'
|
||||
import ControlPalette from '../components/ControlPalette.vue'
|
||||
import CompleteModal from '../components/CompleteModal.vue'
|
||||
import { LoadQuestError, LoadSessionError } from '../utils/errors'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import useSession from '../store/session'
|
||||
import useUserProfile from '../store/userProfile'
|
||||
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
|
||||
import DescriptionPart from '../components/game/DescriptionPart.vue'
|
||||
import QuestPart from '../components/game/QuestPart.vue'
|
||||
import TerminalPart from '../components/game/TerminalPart.vue'
|
||||
import VisualizationPart from '../components/game/VisualizationPart.vue'
|
||||
import ControlPalette from '../components/game/ControlPalette.vue'
|
||||
import CompleteModal from '../components/game/CompleteModal.vue'
|
||||
import StartButton from '../components/game/StartButton.vue'
|
||||
import { openQuestSurvey } from '../utils/formbricks'
|
||||
|
||||
const sessionStore = useSession()
|
||||
const userStore = useUserProfile()
|
||||
|
||||
const completed = computed(() => {
|
||||
return sessionStore.session.status === 'finished'
|
||||
return sessionStore.session?.status === 'finished'
|
||||
})
|
||||
|
||||
watch(completed, (newValue, oldValue) => {
|
||||
if (newValue && !oldValue) {
|
||||
console.log('finish', userStore)
|
||||
openQuestSurvey()
|
||||
}
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
@@ -23,39 +34,30 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await init(props.questId)
|
||||
} catch (err: any) {
|
||||
if (err instanceof LoadQuestError) {
|
||||
toast.error(`無法讀取關卡: ${err.questId},請確認網頁連結。`)
|
||||
return
|
||||
}
|
||||
if (err instanceof LoadSessionError) {
|
||||
toast.error(`無法建立工作階段: ${err.questId},請重新登入再試一次。`)
|
||||
return
|
||||
}
|
||||
console.error(err)
|
||||
toast.error(err.message)
|
||||
}
|
||||
sessionStore.reset()
|
||||
sessionStore.setup()
|
||||
await sessionStore.setQuest(props.questId)
|
||||
await sessionStore.getActiveSession()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- game header -->
|
||||
<div class="h-[6vh] w-full">
|
||||
<GameHeaderPart :title="sessionStore.quest?.title" />
|
||||
<div class="w-full">
|
||||
<HeaderPart
|
||||
:title="sessionStore.quest?.title"
|
||||
:headerComponent="GameHeaderComponents"
|
||||
/>
|
||||
</div>
|
||||
<!-- current status indicator -->
|
||||
<input
|
||||
type="hidden"
|
||||
id="currentStatus"
|
||||
:value="sessionStore.session.status"
|
||||
:value="sessionStore.session?.status"
|
||||
/>
|
||||
<!-- 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 -->
|
||||
<div class="bg-bg-secondary h-full w-1/4 rounded-lg">
|
||||
<section id="quest" class="h-full p-3 overflow-auto">
|
||||
@@ -67,9 +69,10 @@ onMounted(async () => {
|
||||
</section> -->
|
||||
</div>
|
||||
<!-- 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">
|
||||
<HintPart />
|
||||
<DescriptionPart />
|
||||
</section>
|
||||
<hr class="border-border border" />
|
||||
<section id="terminal" class="h-2/5 overflow-hidden">
|
||||
|
||||
+45
-119
@@ -1,97 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import ContactButton from '../components/ContactButton.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) => {
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
import { facebookUrl, githubUrl } from '../config'
|
||||
</script>
|
||||
<template>
|
||||
<div class="w-screen h-screen bg-scroll bg-bg">
|
||||
<DynamicBackground class="w-full h-full" />
|
||||
<div class="h-screen w-screen absolute top-0 left-0">
|
||||
<!-- game header -->
|
||||
<div
|
||||
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>
|
||||
|
||||
<HeaderPart :headerComponent="LandingHeaderComponents" class="z-50" />
|
||||
<!-- 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="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
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- intro -->
|
||||
<div class="w-full md:w-2/3 px-5">
|
||||
<section
|
||||
@@ -228,40 +149,43 @@ const scrollToSection = (sectionId: string) => {
|
||||
鈦坦敏捷開發特別獎 第二名
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- <h1
|
||||
class="text-text-primary p-5 font-extrabold font-mono col-span-3 gap-3"
|
||||
style="font-size: 3.5vh"
|
||||
>
|
||||
玩家感想
|
||||
</h1>
|
||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
||||
"很好玩很像益智遊戲"
|
||||
</h1>
|
||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
||||
"我從來不知道自由軟體的存在,謝謝你們。"
|
||||
</h1>
|
||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
||||
"還蠻好玩的,讓程式變好玩"
|
||||
</h1>
|
||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
||||
"以前不喜歡電腦課現在覺得喜歡一點點"
|
||||
</h1>
|
||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
||||
"程式真的對新手很友善,讓我學到了很多東西"
|
||||
</h1>
|
||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
||||
"感覺做得很好很簡明易懂關卡設計很用心"
|
||||
</h1>
|
||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
||||
"這個體驗我覺得非常有趣,可以體驗到沒辦法體驗的程式,學長也都熱心指導,非常感謝學長的用心的講解,希望以後還可以再體驗這種活動"
|
||||
</h1>
|
||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
||||
"以前我覺得寫程式很難不過現在覺得很有趣"
|
||||
</h1>
|
||||
<h1 class="text-text p-3" style="font-size: 2.5vh">
|
||||
"以前覺得寫程式是我生活中碰不到而且很難的項目。但通過這堂課之後,覺得寫程式不再那麼難以親近"
|
||||
</h1> -->
|
||||
<div class="container mx-auto">
|
||||
<div
|
||||
class="justify-items-center grid grid-cols-1 sm:grid-cols-3 gap-5"
|
||||
>
|
||||
<h1
|
||||
class="text-text-primary text-3xl font-extrabold font-mono sm:col-span-3 gap-6"
|
||||
>
|
||||
玩家感想
|
||||
<!--可以做成翻頁卡牌一次一個-->
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">"很好玩很像益智遊戲"</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"我從來不知道自由軟體的存在,謝謝你們。"
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"還蠻好玩的,讓程式變好玩"
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"以前不喜歡電腦課現在覺得喜歡一點點"
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"程式真的對新手很友善,讓我學到了很多東西"
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"感覺做得很好很簡明易懂關卡設計很用心"
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"這個體驗我覺得非常有趣,可以體驗到沒辦法體驗的程式,學長也都熱心指導,非常感謝學長的用心的講解,希望以後還可以再體驗這種活動"
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"以前我覺得寫程式很難不過現在覺得很有趣"
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"以前覺得寫程式是我生活中碰不到而且很難的項目。但通過這堂課之後,覺得寫程式不再那麼難以親近"
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<section
|
||||
@@ -271,7 +195,9 @@ const scrollToSection = (sectionId: string) => {
|
||||
<h1 class="text-text-primary font-extrabold font-mono text-3xl">
|
||||
聯絡我們
|
||||
</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
|
||||
:url="facebookUrl"
|
||||
:icons="['fab', 'facebook']"
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
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'
|
||||
|
||||
const leaderboard = ref<
|
||||
@@ -35,8 +36,8 @@ onMounted(async () => {
|
||||
<div
|
||||
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
|
||||
>
|
||||
<div class="h-[6vh] w-full">
|
||||
<GameHeaderPart title="排行榜" />
|
||||
<div class="w-full">
|
||||
<HeaderPart title="排行榜" :headerComponent="GameHeaderComponents" />
|
||||
</div>
|
||||
<div class="w-fit h-[94vh] m-3">
|
||||
<div
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
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 {
|
||||
TooManyRequestsError,
|
||||
@@ -45,10 +46,11 @@ const handleLogin = async ({
|
||||
|
||||
<template>
|
||||
<div class="w-screen h-screen">
|
||||
<Background class="w-full h-full" />
|
||||
<DynamicBackground class="w-full h-full" />
|
||||
<div
|
||||
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
|
||||
>
|
||||
<HeaderPart />
|
||||
<div class="w-fit">
|
||||
<AuthForm @onSubmit="handleLogin" type="login" />
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="h-[6vh] w-full">
|
||||
<GameHeaderPart title="關卡地圖" />
|
||||
<div class="w-fit">
|
||||
<HeaderPart
|
||||
title="關卡地圖"
|
||||
:headerComponent="GameHeaderComponents"
|
||||
class="z-50"
|
||||
/>
|
||||
</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 />
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,8 +3,8 @@ import validator from 'validator'
|
||||
import { isValidUsername } from '@linux-odyssey/utils'
|
||||
import { passwordPolicy } from '@linux-odyssey/constants'
|
||||
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 {
|
||||
UnauthorizedError,
|
||||
@@ -89,11 +89,12 @@ async function check({
|
||||
|
||||
<template>
|
||||
<div class="w-screen h-screen">
|
||||
<Background class="w-full h-full" />
|
||||
<DynamicBackground class="w-full h-full" />
|
||||
<div
|
||||
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
|
||||
@onSubmit="handleRegister"
|
||||
@onChange="check"
|
||||
|
||||
@@ -24,6 +24,9 @@ export default {
|
||||
DEFAULT: '#FF0000',
|
||||
},
|
||||
},
|
||||
backgroundImage: {
|
||||
catelogbg: "url('/catelogbg.svg')",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
||||
@@ -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
|
||||
+8
-23
@@ -1,4 +1,3 @@
|
||||
version: '3'
|
||||
services:
|
||||
app:
|
||||
image: linuxodyssey/frontend
|
||||
@@ -29,15 +28,14 @@ services:
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./logs:/app/logs
|
||||
- ./config:/app/config
|
||||
- ./quests:/app/quests:ro
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- MONGO_URL=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@db:27017/${MONGO_DB}
|
||||
- DOCKER_NETWORK=linux-odyssey-players
|
||||
- SECRET_KEY=${SECRET_KEY}
|
||||
- TRUSTED_PROXIES=${TRUSTED_PROXIES}
|
||||
- BACKEND_URL=http://backend:3000
|
||||
- BASE_URL=${BASE_URL}
|
||||
- HOST_PROJECT_ROOT=${PWD}
|
||||
|
||||
analytics:
|
||||
image: linuxodyssey/analytics
|
||||
@@ -52,13 +50,11 @@ services:
|
||||
- internal
|
||||
ports:
|
||||
- '3001:3001'
|
||||
environment:
|
||||
- MONGO_URL=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@db:27017/${MONGO_DB}
|
||||
- ADMIN_USERNAME=${ADMIN_USERNAME}
|
||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
db:
|
||||
image: mongo
|
||||
image: mongo:6
|
||||
restart: always
|
||||
networks:
|
||||
- internal
|
||||
@@ -67,19 +63,8 @@ services:
|
||||
volumes:
|
||||
- mongo:/data/db
|
||||
- ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
|
||||
environment:
|
||||
- BASE_URL=${BASE_URL}
|
||||
- MONGO_INITDB_ROOT_USERNAME=admin
|
||||
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_ROOT_PASSWORD}
|
||||
- MONGO_USER=${MONGO_USER}
|
||||
- MONGO_PASS=${MONGO_PASSWORD}
|
||||
- MONGO_DB=${MONGO_DB}
|
||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
||||
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
|
||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
|
||||
- SURVEY_URL=${SURVEY_URL}
|
||||
- BUG_REPORT_URL=${BUG_REPORT_URL}
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
base:
|
||||
image: linuxodyssey/quest-base
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
version: '3'
|
||||
services:
|
||||
db:
|
||||
image: mongo:6
|
||||
restart: always
|
||||
networks:
|
||||
- internal
|
||||
ports:
|
||||
- '27017:27017'
|
||||
volumes:
|
||||
- ./data:/data/db
|
||||
|
||||
base:
|
||||
image: linuxodyssey/quest-base
|
||||
networks:
|
||||
- players-dev
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./quests/Dockerfile
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
|
||||
networks:
|
||||
internal:
|
||||
players-dev:
|
||||
name: linux-odyssey-players-dev
|
||||
internal: false
|
||||
+4
-3
@@ -32,15 +32,16 @@ down() {
|
||||
dump() {
|
||||
# Define service name and dump directory name
|
||||
SERVICE_NAME="db"
|
||||
COMPOSE_FILE="docker-compose.prod.yml"
|
||||
DUMP_NAME="mongodump-$(date --iso).archive"
|
||||
CONTAINER_ID=$(docker compose ps -q $SERVICE_NAME)
|
||||
CONTAINER_ID=$(docker compose -f $COMPOSE_FILE ps -q $SERVICE_NAME)
|
||||
|
||||
# Fetch credentials from Docker environment variables
|
||||
MONGO_URL=$(docker compose exec -T backend printenv MONGO_URL)
|
||||
MONGO_URL=$(docker compose -f $COMPOSE_FILE exec -T backend printenv MONGO_URL)
|
||||
echo mongo uri: $MONGO_URL
|
||||
|
||||
# Dump MongoDB data using credentials
|
||||
docker compose exec -T $SERVICE_NAME sh -c "mongodump --gzip --archive=/tmp/$DUMP_NAME -d odyssey-test" &&
|
||||
docker compose -f $COMPOSE_FILE exec -T $SERVICE_NAME sh -c "mongodump --gzip --archive=/tmp/$DUMP_NAME --uri=$MONGO_URL" &&
|
||||
|
||||
# Copy the dump from the container to the host
|
||||
# Note: You will need to know the exact container name for this step
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# Windows Environment Setup
|
||||
|
||||
## Install WSL and Ubuntu
|
||||
|
||||
Please refer to the Official [Setup WSL](https://learn.microsoft.com/en-us/windows/wsl/setup/environment) on Microsoft's website.
|
||||
|
||||
Run the following command in PowerShell as Administrator:
|
||||
|
||||
wsl --install
|
||||
|
||||
Then restart the computer. After restarting, you can open Ubuntu terminal by searching "Ubuntu" in Start menu. You will be prompted to create a new user account. Once you have done that, you can continue with the following steps.
|
||||
|
||||
## Setup Environment (In Ubuntu terminal)
|
||||
|
||||
1. Update and upgrade
|
||||
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get upgrade
|
||||
```
|
||||
|
||||
1. Install Git
|
||||
|
||||
```
|
||||
sudo apt-get install git
|
||||
```
|
||||
|
||||
1. Install Node.js (Version 20.x)
|
||||
|
||||
```
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt-get install nodejs
|
||||
sudo apt install build-essential
|
||||
sudo npm install -g yarn
|
||||
```
|
||||
|
||||
## Install Docker
|
||||
|
||||
(Refer to [get.docker.com](https://get.docker.com/))
|
||||
|
||||
To install the latest stable versions of Docker CLI, Docker Engine, and their
|
||||
dependencies:
|
||||
|
||||
1. Download the script
|
||||
|
||||
```
|
||||
curl -fsSL https://get.docker.com -o install-docker.sh
|
||||
```
|
||||
|
||||
1. verify the script's content
|
||||
|
||||
```
|
||||
cat install-docker.sh
|
||||
```
|
||||
|
||||
1. run the script with --dry-run to verify the steps it executes
|
||||
|
||||
```
|
||||
sh install-docker.sh --dry-run
|
||||
```
|
||||
|
||||
**NOTE: the script will recommend you to use Docker Desktop instead, but we need to use Docker inside WSL for the backend to work.** Just ignore it and wait for the installation to finish.
|
||||
|
||||
1. run the script either as root, or using sudo to perform the installation.
|
||||
|
||||
```
|
||||
sudo sh install-docker.sh
|
||||
```
|
||||
|
||||
1. Verify the installation
|
||||
|
||||
```
|
||||
docker --version
|
||||
```
|
||||
|
||||
1. Add your user to the docker group
|
||||
|
||||
```
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
|
||||
**NOTE: You need to create a new WSL terminal to apply the changes.**
|
||||
|
||||
1. Verify the installation
|
||||
|
||||
```
|
||||
docker ps
|
||||
```
|
||||
This should return something like:
|
||||
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
|
||||
## Run the Project (In Ubuntu terminal)
|
||||
|
||||
1. Clone the repository
|
||||
|
||||
```
|
||||
git clone https://github.com/lancatlin/linux-odyssey.git
|
||||
cd linux-odyssey
|
||||
```
|
||||
|
||||
Optional: To open the project in VSCode, run the following command inside the project folder:
|
||||
|
||||
```
|
||||
code .
|
||||
```
|
||||
|
||||
1. Install dependencies
|
||||
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
It is ok if there are some warnings and `gyp: ERR` errors, just verify there is `done` at the end.
|
||||
|
||||
1. Copy the environment variables
|
||||
|
||||
```
|
||||
cp .env.sample .env
|
||||
```
|
||||
|
||||
1. Docker setup
|
||||
|
||||
```
|
||||
docker compose pull
|
||||
docker compose build
|
||||
docker compose up -d db
|
||||
```
|
||||
|
||||
<!-- Check folder ./config exist and is empty -->
|
||||
<!-- Check .env file exist -->
|
||||
|
||||
1. Run the project
|
||||
|
||||
```
|
||||
yarn build
|
||||
yarn dev
|
||||
```
|
||||
|
||||
<!-- Check ./config/ssh_key.pub is a file not a folder -->
|
||||
|
||||
Enjoy!
|
||||
+3
-1
@@ -54,6 +54,8 @@
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"latest": "^0.2.0",
|
||||
"typescript-eslint": "^7.4.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
@@ -21,14 +21,16 @@ interface Config {
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
host: process.env.HOST || 'localhost',
|
||||
port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3001,
|
||||
host: process.env.ADMIN_HOST || 'localhost',
|
||||
port: process.env.ADMIN_PORT ? parseInt(process.env.ADMIN_PORT, 10) : 3001,
|
||||
db: process.env.MONGO_URL || 'mongodb://localhost:27017/odyssey-test',
|
||||
username: getOrFailed('ADMIN_USERNAME'),
|
||||
password: getOrFailed('ADMIN_PASSWORD'),
|
||||
baseUrl:
|
||||
process.env.BASE_URL ||
|
||||
`http://${process.env.HOST || 'localhost'}:${process.env.PORT ? parseInt(process.env.PORT, 10) : 3001}`,
|
||||
process.env.ADMIN_BASE_URL ||
|
||||
`http://${process.env.ADMIN_HOST || 'localhost'}:${
|
||||
process.env.ADMIN_PORT ? parseInt(process.env.ADMIN_PORT, 10) : 3001
|
||||
}`,
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -1,3 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const passwordPolicy = {
|
||||
minLength: 8,
|
||||
minNumbers: 1,
|
||||
@@ -6,6 +8,11 @@ export const passwordPolicy = {
|
||||
minSymbols: 0,
|
||||
}
|
||||
|
||||
export default {
|
||||
passwordPolicy,
|
||||
}
|
||||
export const getSessionsRequestSchema = z.object({
|
||||
questId: z.string().optional(),
|
||||
status: z.enum(['active', 'inactive', 'finished']).optional(),
|
||||
limit: z.coerce.number().optional(),
|
||||
order: z.enum(['asc', 'desc']).optional(),
|
||||
})
|
||||
|
||||
export type GetSessionsRequest = z.infer<typeof getSessionsRequestSchema>
|
||||
@@ -10,5 +10,8 @@
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc -w"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export interface IUser {
|
||||
socialLogins: Map<string, ISocialLogin>
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
isGuest: boolean
|
||||
}
|
||||
|
||||
export const User = model<IUser>(
|
||||
@@ -51,6 +52,11 @@ export const User = model<IUser>(
|
||||
default: {},
|
||||
required: true,
|
||||
},
|
||||
isGuest: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -5,12 +5,12 @@ interface Node {
|
||||
|
||||
export default class DAG {
|
||||
private nodes = new Map()
|
||||
private edges = new Set()
|
||||
private edges = new Set<[string, string]>()
|
||||
private layers: number[] = []
|
||||
|
||||
constructor(nodes: Node[]) {
|
||||
this.nodes = new Map(nodes.map((node) => [node._id, node]))
|
||||
this.edges = new Set()
|
||||
this.edges = new Set<[string, string]>()
|
||||
this.nodes.forEach((_, id) => {
|
||||
this.setLayer(id)
|
||||
this.addEdges(id)
|
||||
@@ -42,7 +42,7 @@ export default class DAG {
|
||||
if (!node) {
|
||||
throw new Error(`Node ${id} not found`)
|
||||
}
|
||||
return this.layers[node.layer - 1]
|
||||
return this.layers[node.layer - 1] || 0
|
||||
}
|
||||
|
||||
private setLayer(id: string) {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# This is the sshd server system-wide configuration file. See
|
||||
# sshd_config(5) for more information.
|
||||
|
||||
# This sshd was compiled with PATH=/usr/local/bin:/usr/bin:/bin:/usr/games
|
||||
|
||||
# The strategy used for options in the default sshd_config shipped with
|
||||
# OpenSSH is to specify options with their default value where
|
||||
# possible, but leave them commented. Uncommented options override the
|
||||
# default value.
|
||||
|
||||
Include /etc/ssh/sshd_config.d/*.conf
|
||||
|
||||
#Port 22
|
||||
#AddressFamily any
|
||||
#ListenAddress 0.0.0.0
|
||||
#ListenAddress ::
|
||||
|
||||
#HostKey /etc/ssh/ssh_host_rsa_key
|
||||
#HostKey /etc/ssh/ssh_host_ecdsa_key
|
||||
#HostKey /etc/ssh/ssh_host_ed25519_key
|
||||
|
||||
# Ciphers and keying
|
||||
#RekeyLimit default none
|
||||
|
||||
# Logging
|
||||
#SyslogFacility AUTH
|
||||
#LogLevel INFO
|
||||
|
||||
# Authentication:
|
||||
|
||||
#LoginGraceTime 2m
|
||||
#PermitRootLogin prohibit-password
|
||||
#StrictModes yes
|
||||
#MaxAuthTries 6
|
||||
#MaxSessions 10
|
||||
|
||||
PubkeyAuthentication yes
|
||||
|
||||
# Expect .ssh/authorized_keys2 to be disregarded by default in future.
|
||||
AuthorizedKeysFile /etc/ssh/authorized_keys/%u
|
||||
|
||||
#AuthorizedPrincipalsFile none
|
||||
|
||||
#AuthorizedKeysCommand none
|
||||
#AuthorizedKeysCommandUser nobody
|
||||
|
||||
# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
|
||||
#HostbasedAuthentication no
|
||||
# Change to yes if you don't trust ~/.ssh/known_hosts for
|
||||
# HostbasedAuthentication
|
||||
#IgnoreUserKnownHosts no
|
||||
# Don't read the user's ~/.rhosts and ~/.shosts files
|
||||
#IgnoreRhosts yes
|
||||
|
||||
# To disable tunneled clear text passwords, change to no here!
|
||||
#PasswordAuthentication yes
|
||||
#PermitEmptyPasswords no
|
||||
|
||||
# Change to yes to enable challenge-response passwords (beware issues with
|
||||
# some PAM modules and threads)
|
||||
KbdInteractiveAuthentication no
|
||||
|
||||
# Kerberos options
|
||||
#KerberosAuthentication no
|
||||
#KerberosOrLocalPasswd yes
|
||||
#KerberosTicketCleanup yes
|
||||
#KerberosGetAFSToken no
|
||||
|
||||
# GSSAPI options
|
||||
#GSSAPIAuthentication no
|
||||
#GSSAPICleanupCredentials yes
|
||||
#GSSAPIStrictAcceptorCheck yes
|
||||
#GSSAPIKeyExchange no
|
||||
|
||||
# Set this to 'yes' to enable PAM authentication, account processing,
|
||||
# and session processing. If this is enabled, PAM authentication will
|
||||
# be allowed through the KbdInteractiveAuthentication and
|
||||
# PasswordAuthentication. Depending on your PAM configuration,
|
||||
# PAM authentication via KbdInteractiveAuthentication may bypass
|
||||
# the setting of "PermitRootLogin prohibit-password".
|
||||
# If you just want the PAM account and session checks to run without
|
||||
# PAM authentication, then enable this but set PasswordAuthentication
|
||||
# and KbdInteractiveAuthentication to 'no'.
|
||||
UsePAM yes
|
||||
|
||||
#AllowAgentForwarding yes
|
||||
#AllowTcpForwarding yes
|
||||
#GatewayPorts no
|
||||
X11Forwarding no
|
||||
#X11DisplayOffset 10
|
||||
#X11UseLocalhost yes
|
||||
#PermitTTY yes
|
||||
#PrintMotd yes
|
||||
#PrintLastLog yes
|
||||
#TCPKeepAlive yes
|
||||
#PermitUserEnvironment no
|
||||
#Compression delayed
|
||||
#ClientAliveInterval 0
|
||||
#ClientAliveCountMax 3
|
||||
#UseDNS no
|
||||
#PidFile /run/sshd.pid
|
||||
#MaxStartups 10:30:100
|
||||
#PermitTunnel no
|
||||
#ChrootDirectory none
|
||||
#VersionAddendum none
|
||||
|
||||
# no default banner path
|
||||
#Banner none
|
||||
|
||||
# Allow client to pass locale environment variables
|
||||
AcceptEnv LANG LC_* NODE_PATH ZDOTDIR API_ENDPOINT TOKEN
|
||||
|
||||
# override default of no subsystems
|
||||
Subsystem sftp /usr/lib/openssh/sftp-server
|
||||
|
||||
# Example of overriding settings on a per-user basis
|
||||
#Match User anoncvs
|
||||
# X11Forwarding no
|
||||
# AllowTcpForwarding no
|
||||
# PermitTTY no
|
||||
# ForceCommand cvs server
|
||||
+18
-12
@@ -1,26 +1,32 @@
|
||||
FROM alpine:3.18
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
LABEL maintainer="wancat@wancat.cc"
|
||||
|
||||
RUN apk add --no-cache \
|
||||
RUN apt-get update && apt-get install -y \
|
||||
vim \
|
||||
zsh \
|
||||
nodejs \
|
||||
yarn
|
||||
curl \
|
||||
openssh-server
|
||||
|
||||
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
|
||||
RUN bash nodesource_setup.sh
|
||||
RUN apt-get install -y nodejs
|
||||
RUN npm install -g yarn
|
||||
RUN node -v && yarn -v
|
||||
|
||||
WORKDIR /usr/local/lib/container
|
||||
|
||||
COPY ./packages/container/package.json ./yarn.lock ./
|
||||
RUN yarn install --modules-folder /usr/src/node_modules
|
||||
ENV NODE_PATH=/usr/src/node_modules
|
||||
COPY ./packages/container /usr/local/lib/container
|
||||
COPY ./quests/.includes/zshrc /etc/zsh/zshrc
|
||||
RUN rm -f /etc/motd /etc/issue /etc/issue.net /etc/update-motd.d/*
|
||||
RUN rm -rf /etc/skel && mkdir -p /etc/skel
|
||||
RUN mkdir -p /etc/ssh/authorized_keys && chmod 755 /etc/ssh/authorized_keys
|
||||
COPY ./quests/.includes/sshd_config /etc/ssh/sshd_config
|
||||
COPY ./quests/entrypoint.sh /entrypoint.sh
|
||||
|
||||
ENV USERNAME=commander
|
||||
EXPOSE 22
|
||||
|
||||
RUN adduser --disabled-password --gecos '' --shell /bin/zsh ${USERNAME}
|
||||
|
||||
USER ${USERNAME}
|
||||
|
||||
WORKDIR /home/${USERNAME}
|
||||
CMD [ "/bin/zsh" ]
|
||||
ENTRYPOINT ["/bin/sh", "/entrypoint.sh"]
|
||||
+12
-22
@@ -1,25 +1,23 @@
|
||||
title: 輸出文字
|
||||
instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 如何使用 `echo` 指令
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
1. 如何使用 `echo` 在終端機上顯示文字。
|
||||
|
||||
requirements:
|
||||
- get-started
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
task: 按下 `開始` 按鈕開始
|
||||
condition:
|
||||
output:
|
||||
- ^start$
|
||||
command:
|
||||
- ^echo start$
|
||||
responses:
|
||||
- content:
|
||||
- 你剛才使用的指令叫做 `echo`。
|
||||
- '`echo` 指令可以將後方接續的文字輸出到終端機上。'
|
||||
- 例如你輸入 `echo Hello`,終端機就會顯示 `Hello`。
|
||||
- 你可以試著輸入 `echo` 並在後面加上一些文字,然後按下 Enter 鍵,就可以看到終端機上顯示了你剛才輸入的文字。
|
||||
- content: |
|
||||
還記得第一關使用的指令 `echo` 嗎?
|
||||
`echo` 指令可以將後方接續的文字輸出到終端機上。
|
||||
例如你輸入 `echo Hello`,終端機就會顯示 `Hello`。
|
||||
你可以試著輸入 `echo ` 並在後面加上一些文字,然後按下 Enter 鍵,就可以看到終端機上顯示了你剛才輸入的文字。
|
||||
hints:
|
||||
- ➤ 輸入 `echo` 並在後面加上一些文字
|
||||
- ➤ 例如:`echo example`
|
||||
@@ -43,8 +41,8 @@ stages:
|
||||
requirements:
|
||||
- echo
|
||||
condition:
|
||||
output:
|
||||
- ^[Hh]ow are you\??$
|
||||
command:
|
||||
- ^echo [Hh]ow are you\??$
|
||||
exceptions:
|
||||
- condition:
|
||||
command:
|
||||
@@ -59,17 +57,9 @@ stages:
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 在 `echo` 後面還需要加上一些文字
|
||||
- 在 `echo ` 後面還需要加上 `How are you?`
|
||||
responses:
|
||||
- content:
|
||||
- 你成功的在終端機上顯示了 `How are you?`。
|
||||
- 相信你已經了解如何使用 `echo` 指令來顯示文字了。
|
||||
- 之後如果要在終端機顯示文字,記得可以使用 `echo` 指令。
|
||||
|
||||
exceptions:
|
||||
- repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
@@ -1,131 +0,0 @@
|
||||
title: 輸出文字
|
||||
instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 如何使用 `echo` 指令
|
||||
2. `echo` 指令在遊戲中的用途
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- get-started
|
||||
|
||||
stages:
|
||||
- id: warning-01
|
||||
repeatable: true
|
||||
condition:
|
||||
command:
|
||||
- ' '
|
||||
hints:
|
||||
- '!! 請勿使用全形空格 !!'
|
||||
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
condition:
|
||||
output:
|
||||
- ^start$
|
||||
responses:
|
||||
- content:
|
||||
- 你剛才使用的指令叫做 `echo`。
|
||||
- '`echo` 指令可以將後方接續的文字輸出到終端機上。'
|
||||
- 例如你輸入 `echo Hello`,終端機就會顯示 `Hello`。
|
||||
- 你可以試著輸入 `echo` 並在後面加上一些文字,然後按下 Enter 鍵,就可以看到終端機上顯示了你剛才輸入的文字。
|
||||
hints:
|
||||
- ➤ 輸入 `echo` 並在後面加上一些文字
|
||||
- ➤ 例如:`echo example`
|
||||
|
||||
- id: echo
|
||||
task: 在終端機上顯示顯示任意文字
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
command:
|
||||
- ^echo .*
|
||||
responses:
|
||||
- content:
|
||||
- 你應該可以看到終端機上顯示了你在 `echo` 指令後面的文字。
|
||||
- 接下來,請你試著在終端機上顯示 `How are you?`。
|
||||
hints:
|
||||
- ➤ 利用 `echo` 指令來在終端機上顯示 `How are you?`
|
||||
|
||||
- id: how-are-you
|
||||
task: 在終端機上顯示 `How are you?`
|
||||
requirements:
|
||||
- echo
|
||||
condition:
|
||||
output:
|
||||
- ^[Hh]ow are you\??$
|
||||
exceptions:
|
||||
- condition:
|
||||
command:
|
||||
- ^[Hh]ow are you\??$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 在 `How are you?` 前面必須加上 `echo`
|
||||
- condition:
|
||||
command:
|
||||
- ^echo$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 在 `echo` 後面還需要加上一些文字
|
||||
responses:
|
||||
- content:
|
||||
- 你成功的在終端機上顯示了 `How are you?`。
|
||||
- 相信你已經了解 `echo` 指令的用法了。
|
||||
- 接下來,要像你介紹 `echo` 在遊戲中的特殊功能,那就是詠唱咒語。
|
||||
- 你可以通過輸入 `echo [咒語]` 來詠唱咒語,並產生對應的特殊效果。
|
||||
- 但是要注意,詠唱咒語只有在 Linux Odyssey 中才會有特殊效果。
|
||||
- 在真實的 Linux 環境中,詠唱咒語並不會有任何特殊效果。
|
||||
- 現在,有一段咒語叫做 `HELP`,請你試著使用 `echo` 詠唱這段咒語。
|
||||
hints:
|
||||
- ➤ 試著使用 `echo` 來詠唱 `HELP` 咒語
|
||||
- ➤ 注意:咒語要使用大寫
|
||||
|
||||
- id: help
|
||||
task: 詠唱 `HELP` 咒語
|
||||
requirements:
|
||||
- how-are-you
|
||||
condition:
|
||||
output:
|
||||
- ^HELP$
|
||||
responses:
|
||||
- content:
|
||||
- 你詠唱了 `HELP` 咒語,一位精靈出現了。
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 你好,我是 Unix 精靈。
|
||||
- 既然你使用了 `HELP` 咒語呼喚我,就讓我來教你一個咒語吧。
|
||||
- 讓我想想... 好了,我想到了一個咒語,叫做 `WATER`。
|
||||
- 這是施展水魔法的咒語。詠唱這個咒語可以施展水魔法,產生少量的水。
|
||||
- 試著使用 `echo` 詠唱這個咒語吧。
|
||||
hints:
|
||||
- ➤ 詠唱 `WATER` 咒語
|
||||
|
||||
- id: END
|
||||
task: 詠唱 `WATER` 咒語
|
||||
requirements:
|
||||
- help
|
||||
condition:
|
||||
output:
|
||||
- ^WATER$
|
||||
responses:
|
||||
- content:
|
||||
- 你施展了水魔法,一個水球出現在你的手中。
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 很好,你已經學會了水魔法。
|
||||
- content:
|
||||
- 真厲害,你已經了解如何使用 `echo` 指令,並且學會了詠唱咒語的方法。
|
||||
- 最後複習一下,你可以通過輸入 `echo [文字]` 來在終端機上顯示文字。
|
||||
- 你也可以通過輸入 `echo [咒語]` 來詠唱咒語,並產生對應的特殊效果。
|
||||
- 不要忘了,詠唱咒語只有在 Linux Odyssey 中才會有特殊效果。
|
||||
- 在真實的 Linux 環境中,咒語只會被視為一串文字顯示出來而已。
|
||||
- 以上就是 `echo` 指令的用法,請繼續努力,完成接下來的關卡吧!
|
||||
hints: |
|
||||
複習:
|
||||
通過輸入 `echo [文字]` 來在終端機上顯示文字
|
||||
通過輸入 `echo [咒語]` 來詠唱咒語
|
||||
咒語只有在 Linux Odyssey 中才會有特殊效果
|
||||
@@ -0,0 +1,7 @@
|
||||
echo start
|
||||
echo next
|
||||
ls
|
||||
cd folder03
|
||||
pwd
|
||||
cd ../folder02
|
||||
ls ../folder01
|
||||
@@ -0,0 +1,110 @@
|
||||
title: 測驗一
|
||||
instruction: |
|
||||
在這個關卡中,將會考驗你是否理解以下內容:
|
||||
1. `echo`:用來輸出文字
|
||||
2. `ls`:用來查看資料夾的內容
|
||||
3. `cd`:用來在資料夾之間移動
|
||||
4. `pwd`:用來顯示目前的路徑
|
||||
|
||||
requirements:
|
||||
- path
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 按下 `開始` 按鈕開始
|
||||
condition:
|
||||
command:
|
||||
- ^echo start$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
這個關卡將會測試你是否理解之前的教學內容。
|
||||
因此不會告訴你要輸入什麼指令。
|
||||
請思考並應用正確的指令來完成任務。
|
||||
|
||||
現在,請讓終端機輸出 `next` 來繼續。
|
||||
|
||||
- id: next
|
||||
task: 讓終端機輸出 `next`
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
output:
|
||||
- ^next$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
現在請你查看目前資料夾的內容。
|
||||
|
||||
- id: ls-~
|
||||
task: 查看目前資料夾的內容
|
||||
requirements:
|
||||
- next
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
請進入 `folder03` 資料夾。
|
||||
|
||||
- id: cd-folder03
|
||||
task: 進入 `folder03` 資料夾
|
||||
requirements:
|
||||
- ls-~
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander/folder03$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
請在終端機顯示目前的路徑。
|
||||
|
||||
- id: pwd-folder03
|
||||
task: 顯示目前的路徑
|
||||
requirements:
|
||||
- cd-folder03
|
||||
condition:
|
||||
command:
|
||||
- ^pwd$
|
||||
output:
|
||||
- ^/home/commander/folder03$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
現在請你進入 `folder02` 資料夾,
|
||||
並查看 `folder02` 資料夾的內容。
|
||||
|
||||
- id: ls-folder02
|
||||
task: 進入並查看 `folder02` 資料夾的內容
|
||||
requirements:
|
||||
- pwd-folder03
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander/folder02$
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
最後,請你留在 `folder02` 資料夾,
|
||||
並查看 `folder01` 資料夾的內容。
|
||||
|
||||
- id: END
|
||||
requirements:
|
||||
- ls-folder02
|
||||
task: 留在 `folder02` 資料夾,查看 `folder01` 資料夾的內容。
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander/folder02$
|
||||
command:
|
||||
- ^ls
|
||||
output:
|
||||
- file1.txt
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
恭喜你通過了這個測驗!
|
||||
你可以繼續學習之後的教學內容了。
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,7 @@
|
||||
echo start
|
||||
echo next
|
||||
ls
|
||||
cat hello.txt
|
||||
cat pneumonoultramicroscopicsilicovolcanoconiosis.txt
|
||||
mkdir folder
|
||||
touch folder/file.txt
|
||||
@@ -0,0 +1,108 @@
|
||||
title: 測驗二
|
||||
instruction: |
|
||||
在這個關卡中,將會考驗你是否理解以下內容:
|
||||
1. `cat`:用來查看檔案的內容
|
||||
2. `touch`:用來建立檔案
|
||||
3. `mkdir`:用來建立資料夾
|
||||
4. 用 `tab` 鍵來自動補全
|
||||
5. 之前測驗包含的內容
|
||||
|
||||
requirements:
|
||||
- create
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 按下 `開始` 按鈕開始
|
||||
condition:
|
||||
command:
|
||||
- ^echo start$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
這個關卡將會測試你是否理解之前的教學內容。
|
||||
因此不會告訴你要輸入什麼指令。
|
||||
請思考並應用正確的指令來完成任務。
|
||||
|
||||
現在,請讓終端機輸出 `next` 來繼續。
|
||||
|
||||
- id: next
|
||||
task: 讓終端機輸出 `next`
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
output:
|
||||
- ^next$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
現在請你查看目前資料夾的內容。
|
||||
|
||||
- id: ls-~
|
||||
task: 查看目前資料夾的內容
|
||||
requirements:
|
||||
- next
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
請查看 `hello.txt` 檔案的內容。
|
||||
|
||||
- id: cat-hello
|
||||
task: 查看 `hello.txt` 檔案內容
|
||||
requirements:
|
||||
- ls-~
|
||||
condition:
|
||||
command:
|
||||
- ^cat hello.txt$
|
||||
output:
|
||||
- ^This is `hello.txt`.$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
請查看另一個有著很長檔名的檔案內容。
|
||||
|
||||
- id: cat-long-filename
|
||||
task: 查看另一個檔案的內容
|
||||
requirements:
|
||||
- cat-hello
|
||||
condition:
|
||||
command:
|
||||
- ^cat pneumonoultramicroscopicsilicovolcanoconiosis.txt$
|
||||
output:
|
||||
- ^A name that has been invented for a lung disease caused by breathing in very small pieces of ash or dust.$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
現在請你建立一個名稱為 `folder` 的資料夾。
|
||||
|
||||
- id: mkdir-folder
|
||||
task: 建立名稱為 `folder` 的資料夾
|
||||
requirements:
|
||||
- cat-long-filename
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/folder
|
||||
type: folder
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
最後,請你在 `folder` 資料夾中,
|
||||
建立檔名為 `file.txt` 的檔案。
|
||||
|
||||
- id: END
|
||||
requirements:
|
||||
- mkdir-folder
|
||||
task: 在 `folder` 中建立檔名為 `file.txt` 的檔案
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/folder/file.txt
|
||||
type: file
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
恭喜你通過了這個測驗!
|
||||
你可以繼續學習之後的教學內容了。
|
||||
@@ -0,0 +1 @@
|
||||
This is `hello.txt`.
|
||||
@@ -0,0 +1 @@
|
||||
A name that has been invented for a lung disease caused by breathing in very small pieces of ash or dust.
|
||||
@@ -0,0 +1,12 @@
|
||||
echo start
|
||||
echo next
|
||||
ls
|
||||
cd folder
|
||||
cp hello.txt world.txt
|
||||
rm hello.txt
|
||||
mv world.txt ..
|
||||
cd ..
|
||||
mv world.txt hello.txt
|
||||
cat hello.txt
|
||||
rmdir folder
|
||||
rm -r trash_can
|
||||
@@ -0,0 +1,153 @@
|
||||
title: 測驗三
|
||||
instruction: |
|
||||
在這個關卡中,將會考驗你是否理解以下內容:
|
||||
1. `cp`:用來複製檔案和資料夾
|
||||
2. `mv`:用來移動檔案和資料夾或更改名稱
|
||||
3. `rm`:用來刪除檔案和資料夾
|
||||
4. `rmdir`:用來刪除空的資料夾
|
||||
5. 透過選項讓指令達成不同的功能,例如 `rm -r`
|
||||
6. 之前測驗包含的內容
|
||||
|
||||
requirements:
|
||||
- remove
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 按下 `開始` 按鈕開始
|
||||
condition:
|
||||
command:
|
||||
- ^echo start$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
這個關卡將會測試你是否理解之前的教學內容。
|
||||
因此不會告訴你要輸入什麼指令。
|
||||
請思考並應用正確的指令來完成任務。
|
||||
|
||||
現在,請讓終端機輸出 `next` 來繼續。
|
||||
|
||||
- id: next
|
||||
task: 讓終端機輸出 `next`
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
output:
|
||||
- ^next$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
現在請你查看目前資料夾的內容。
|
||||
|
||||
- id: ls-~
|
||||
task: 查看目前資料夾的內容
|
||||
requirements:
|
||||
- next
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
請複製 `folder` 資料夾中的檔案到 `folder` 中,並命名為 `world.txt` 。
|
||||
|
||||
- id: cp-hello-world
|
||||
task: 複製 `folder` 中的檔案並命名為 `world.txt`
|
||||
requirements:
|
||||
- ls-~
|
||||
condition:
|
||||
command:
|
||||
- ^cp .*hello.txt .*world.txt$
|
||||
files:
|
||||
- path: /home/commander/folder/hello.txt
|
||||
type: file
|
||||
- path: /home/commander/folder/world.txt
|
||||
type: file
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
請刪除 `hello.txt` 檔案。
|
||||
|
||||
- id: rm-hello
|
||||
task: 刪除 `hello.txt`
|
||||
requirements:
|
||||
- cp-hello-world
|
||||
condition:
|
||||
command:
|
||||
- ^rm .*hello.txt$
|
||||
files:
|
||||
- path: /home/commander/folder/hello.txt
|
||||
type: file
|
||||
exists: false
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
現在請將 `world.txt` 移出 `folder` 資料夾。
|
||||
(移到 `commander` 資料夾中)
|
||||
|
||||
- id: mv-world-..
|
||||
task: 將 `world.txt` 移出 `folder` 資料夾
|
||||
requirements:
|
||||
- rm-hello
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/folder/world.txt
|
||||
type: file
|
||||
exists: false
|
||||
- path: /home/commander/world.txt
|
||||
type: file
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
將 `world.txt` 的名稱改回 `hello.txt` ,
|
||||
並查看檔案的內容。
|
||||
|
||||
- id: cat-hello
|
||||
task: 將 `world.txt` 的名稱改回 `hello.txt` 後查看檔案內容
|
||||
requirements:
|
||||
- mv-world-..
|
||||
condition:
|
||||
command:
|
||||
- ^cat hello.txt$
|
||||
files:
|
||||
- path: /home/commander/world.txt
|
||||
type: file
|
||||
exists: false
|
||||
- path: /home/commander/hello.txt
|
||||
type: file
|
||||
output:
|
||||
- ^This is `hello.txt`.$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
刪除 `folder` 資料夾。
|
||||
|
||||
- id: rmdir-folder
|
||||
task: 刪除 `folder` 資料夾
|
||||
requirements:
|
||||
- cat-hello
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/folder
|
||||
type: folder
|
||||
exists: false
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
刪除 `trash_can` 資料夾。
|
||||
|
||||
- id: END
|
||||
requirements:
|
||||
- rmdir-folder
|
||||
task: 刪除 `trash_can` 資料夾
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/trash_can
|
||||
type: folder
|
||||
exists: false
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
恭喜你通過了這個測驗!
|
||||
你可以繼續學習之後的教學內容了。
|
||||
@@ -0,0 +1 @@
|
||||
This is `hello.txt`.
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
aHR0cHM6Ly9yZXVybC5jYy9YUjZZMDA=
|
||||
Whitespace-only changes.
@@ -1,3 +0,0 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY home ${HOME}
|
||||
@@ -1,7 +1,8 @@
|
||||
echo start
|
||||
ls
|
||||
cat hello.txt
|
||||
cat world.txt
|
||||
echo continue
|
||||
echo next
|
||||
cp hello.txt hello2.txt
|
||||
cat hello2.txt
|
||||
cp world.txt world2.txt
|
||||
|
||||
+240
-93
@@ -1,122 +1,186 @@
|
||||
title: 複製檔案
|
||||
instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 如何用 `cp` 指令複製檔案
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
1. 如何使用 `cp` 複製檔案
|
||||
|
||||
requirements:
|
||||
- create
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始
|
||||
task: 按下 `開始` 按鈕開始
|
||||
condition:
|
||||
output:
|
||||
- start
|
||||
exceptions:
|
||||
- condition:
|
||||
command:
|
||||
- ^echo$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你忘記輸入 `start` 了嗎?
|
||||
- 請輸入 `echo start` 來開始
|
||||
- catchAll: true
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 請輸入 `echo start` 來開始
|
||||
command:
|
||||
- ^echo start$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 今天我們要學習如何複製檔案。
|
||||
- 現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,請使用 `ls` 與 `cat` 來查看它們的內容。
|
||||
- 看完之後,使用 `echo continue` 來繼續吧!
|
||||
content: |
|
||||
今天我們要學習如何複製檔案。
|
||||
先用 `ls` 指令查看目前資料夾裡有哪些檔案。
|
||||
hints:
|
||||
- ➤ 使用 `ls` 來查看目前資料夾裡有哪些檔案
|
||||
|
||||
- id: ls
|
||||
task: 查看目前資料夾裡的檔案
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls$
|
||||
output:
|
||||
- (^|\s)hello.txt($|\s)
|
||||
- (^|\s)world.txt($|\s)
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你不在 `/home/commander` 資料夾裡。
|
||||
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
可以看到現在資料夾裡有 `hello.txt` 跟 `world.txt`。
|
||||
請使用 `cat` 來查看它們的內容。
|
||||
看完之後,使用 `echo next` 來繼續吧!
|
||||
hints:
|
||||
- ➤ 使用 `ls` 來查看資料夾內的檔案
|
||||
- ➤ 使用 `cat hello.txt` 以及 `cat world.txt` 來查看檔案內容
|
||||
- ➤ 如果終端機卡在某個指令無法輸入,請按下 `Ctrl + C` 來取消該指令
|
||||
|
||||
- id: cat-hello
|
||||
task: 查看 `hello.txt` 的內容
|
||||
requirements:
|
||||
- start
|
||||
- ls
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^cat hello.txt$
|
||||
output:
|
||||
- Hello
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你不在 `/home/commander` 資料夾裡。
|
||||
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾
|
||||
- condition:
|
||||
command:
|
||||
- ^cat$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你忘記輸入 `cat` 了嗎?
|
||||
- 請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
|
||||
content: |
|
||||
你忘記輸入 `cat` 了嗎?
|
||||
請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
|
||||
- condition:
|
||||
command:
|
||||
- ^cat hello$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你忘記輸入 `.txt` 了嗎?
|
||||
- 請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
|
||||
content: |
|
||||
你忘記輸入 `.txt` 了嗎?
|
||||
請輸入 `cat hello.txt` 來查看 `hello.txt` 的內容
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你成功查看到 `hello.txt` 的內容了!
|
||||
content: |
|
||||
你成功查看到 `hello.txt` 的內容了!
|
||||
|
||||
- id: cat-world
|
||||
task: 查看 `world.txt` 的內容
|
||||
requirements:
|
||||
- start
|
||||
- ls
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^cat world.txt$
|
||||
output:
|
||||
- World
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你不在 `/home/commander` 資料夾裡。
|
||||
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾
|
||||
- condition:
|
||||
command:
|
||||
- ^cat$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你忘記輸入 `cat` 了嗎?
|
||||
請輸入 `cat world.txt` 來查看 `world.txt` 的內容
|
||||
- condition:
|
||||
command:
|
||||
- ^cat world$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你忘記輸入 `.txt` 了嗎?
|
||||
請輸入 `cat world.txt` 來查看 `world.txt` 的內容
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你成功查看到 `world.txt` 的內容了!
|
||||
content: |
|
||||
你成功查看到 `world.txt` 的內容了!
|
||||
|
||||
- id: cat-continue
|
||||
task: 使用 `echo continue` 來繼續
|
||||
- id: cat-next
|
||||
task: 輸入 `echo next` 來繼續
|
||||
requirements:
|
||||
- cat-hello
|
||||
- cat-world
|
||||
condition:
|
||||
command:
|
||||
- echo
|
||||
- ^echo next$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 接下來我們要來學習複製檔案。
|
||||
- '`cp` 這個指令,是 copy 的縮寫,就是複製的意思。'
|
||||
- 我們可以用 `cp [來源檔案] [目的檔案]` 的方式來複製檔案。
|
||||
- 請你使用 `cp` 來複製 `hello.txt` 到 `hello2.txt`。
|
||||
content: |
|
||||
接下來我們要來學習如何複製檔案。
|
||||
`cp` 這個指令,是 copy 的縮寫,就是複製的意思。
|
||||
我們可以用 `cp [來源檔案] [目的檔案]` 的方式來複製檔案。
|
||||
請你使用 `cp` 來複製 `hello.txt` 的內容到 `hello2.txt`。
|
||||
hints:
|
||||
- ➤ 使用 `cp hello.txt hello2.txt` 來複製 `hello.txt` 到 `hello2.txt`
|
||||
|
||||
- id: cp-hello
|
||||
task: 複製 `hello.txt` 到 `hello2.txt`
|
||||
requirements:
|
||||
- cat-continue
|
||||
- cat-next
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^cp hello.txt hello2.txt$
|
||||
files:
|
||||
- path: hello2.txt
|
||||
- path: /home/commander/hello2.txt
|
||||
type: file
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你不在 `/home/commander` 資料夾裡。
|
||||
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你成功複製 `hello.txt` 到 `hello2.txt` 了!
|
||||
- 我們打開來確認一下 `hello2.txt` 的內容吧!
|
||||
content: |
|
||||
你成功複製 `hello.txt` 到 `hello2.txt` 了!
|
||||
我們打開來確認一下 `hello2.txt` 的內容吧!
|
||||
hints:
|
||||
- ➤ 使用 `cat hello2.txt` 來查看 `hello2.txt` 的內容
|
||||
|
||||
@@ -125,16 +189,28 @@ stages:
|
||||
- cp-hello
|
||||
task: 查看 `hello2.txt` 的內容
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^cat hello2.txt$
|
||||
output:
|
||||
- Hello
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你不在 `/home/commander` 資料夾裡。
|
||||
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你成功查看到 `hello2.txt` 的內容了!
|
||||
- 正如我們所見,`hello2.txt` 的內容跟 `hello.txt` 一模一樣。
|
||||
- 那我們再來試試看複製 `world.txt` 到 `world2.txt` 吧!
|
||||
content: |
|
||||
你成功查看到 `hello2.txt` 的內容了!
|
||||
正如我們所見,`hello2.txt` 的內容跟 `hello.txt` 一模一樣。
|
||||
那我們再來試試看複製 `world.txt` 到 `world2.txt` 吧!
|
||||
hints:
|
||||
- ➤ 使用 `cp world.txt world2.txt` 來複製 `world.txt` 到 `world2.txt`
|
||||
|
||||
@@ -143,16 +219,28 @@ stages:
|
||||
- cat-hello2
|
||||
task: 複製 `world.txt` 到 `world2.txt`
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^cp world.txt world2.txt$
|
||||
files:
|
||||
- path: world2.txt
|
||||
- path: /home/commander/world2.txt
|
||||
type: file
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你不在 `/home/commander` 資料夾裡。
|
||||
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你成功複製 `world.txt` 到 `world2.txt` 了!
|
||||
- 我們打開來確認一下 `world2.txt` 的內容吧!
|
||||
content: |
|
||||
你成功複製 `world.txt` 到 `world2.txt` 了!
|
||||
我們打開來確認一下 `world2.txt` 的內容吧!
|
||||
hints:
|
||||
- ➤ 使用 `cat world2.txt` 來查看 `world2.txt` 的內容
|
||||
|
||||
@@ -161,23 +249,35 @@ stages:
|
||||
- cp-world
|
||||
task: 查看 `world2.txt` 的內容
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^cat world2.txt$
|
||||
output:
|
||||
- World
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你不在 `/home/commander` 資料夾裡。
|
||||
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你成功查看到 `world2.txt` 的內容了!
|
||||
- '`world2.txt` 的內容跟 `world.txt` 一模一樣。'
|
||||
- 接下來我們要來學習如何複製資料夾。
|
||||
- 現在目錄下有一個 `notes` 資料夾,裡面有一個 `note.txt` 的檔案。
|
||||
- 我們要能整個資料夾包含裡面的檔案一起複製到 `notes2`。
|
||||
- 跟複製單一檔案不一樣的是,複製資料夾時,我們要加上 `-r` 的選項(recursive)代表將資料夾中的所有檔案都一起複製。
|
||||
- 在使用指令時,如果想要調整指令的行為,可以在指令後面加上選項。
|
||||
- 選項的格式通常是 `-` 加上一個字母,例如 `-r`。
|
||||
- 選項擺放的位置沒有特別的限制,通常會跟在指令的後面,置於檔案名稱的前面。
|
||||
- 請你使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`。
|
||||
content: |
|
||||
你成功查看到 `world2.txt` 的內容了!
|
||||
`world2.txt` 的內容跟 `world.txt` 一模一樣。
|
||||
接下來我們要來學習如何複製資料夾。
|
||||
現在目錄下有一個 `notes` 資料夾,裡面有一個 `note.txt` 的檔案。
|
||||
我們要能整個資料夾包含裡面的檔案一起複製到 `notes2`。
|
||||
跟複製單一檔案不一樣的是,複製資料夾時,我們要加上 `-r` 的選項(recursive)代表將資料夾中的所有檔案都一起複製。
|
||||
在使用指令時,如果想要調整指令的行為,可以在指令後面加上選項。
|
||||
選項的格式通常是 `-` 加上一個字母,例如 `-r`。
|
||||
選項擺放的位置沒有特別的限制,通常會跟在指令的後面,置於檔案名稱的前面。
|
||||
請你使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`。
|
||||
hints:
|
||||
- ➤ 使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
|
||||
- ➤ 記得要加上 `-r` 的選項來複製所有檔案
|
||||
@@ -187,45 +287,68 @@ stages:
|
||||
- cat-world2
|
||||
task: 複製 `notes` 資料夾到 `notes2`
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^cp
|
||||
files:
|
||||
- path: notes2
|
||||
- path: /home/commander/notes2
|
||||
type: directory
|
||||
- path: notes2/note.txt
|
||||
- path: /home/commander/notes2/note.txt
|
||||
type: file
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你成功複製 `notes` 資料夾到 `notes2` 了!
|
||||
- 我們打開來確認一下 `notes2` 資料夾的內容吧!
|
||||
hints:
|
||||
- ➤ 使用 `ls notes2` 來查看 `notes2` 資料夾的內容
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你不在 `/home/commander` 資料夾裡。
|
||||
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||
- condition:
|
||||
error:
|
||||
- 'cp: omitting directory'
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你忘記加上 `-r` 的參數了嗎?
|
||||
- 請輸入 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
|
||||
content: |
|
||||
你忘記加上 `-r` 的參數了嗎?
|
||||
請輸入 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你成功複製 `notes` 資料夾到 `notes2` 了!
|
||||
我們打開來確認一下 `notes2` 資料夾的內容吧!
|
||||
hints:
|
||||
- ➤ 使用 `ls notes2` 來查看 `notes2` 資料夾的內容
|
||||
|
||||
- id: ls-notes2
|
||||
requirements:
|
||||
- cp-notes
|
||||
task: 查看 `notes2` 資料夾的內容
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls notes2$
|
||||
output:
|
||||
- note.txt
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你不在 `/home/commander` 資料夾裡。
|
||||
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你成功查看到 `notes2` 資料夾的內容了!
|
||||
- 我們可以看到,整個 `notes` 資料夾包含裡面的檔案,都被複製到 `notes2` 資料夾了。
|
||||
- 那麼最後我們要來試試看,如果複製一個檔案到已經存在的檔案會發生什麼事。
|
||||
content: |
|
||||
你成功查看到 `notes2` 資料夾的內容了!
|
||||
我們可以看到,整個 `notes` 資料夾包含裡面的檔案,都被複製到 `notes2` 資料夾了。
|
||||
那麼最後我們要來試試看,如果複製一個檔案到已經存在的檔案會發生什麼事。
|
||||
hints:
|
||||
- ➤ 使用 `cp hello.txt world.txt` 來複製 `hello.txt` 到 `world.txt`
|
||||
|
||||
@@ -234,13 +357,25 @@ stages:
|
||||
- ls-notes2
|
||||
task: 複製 `hello.txt` 到 `world.txt`
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^cp hello.txt world.txt$
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你不在 `/home/commander` 資料夾裡。
|
||||
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你成功複製 `hello.txt` 到 `world.txt` 了!
|
||||
- 我們打開來確認一下 `world.txt` 的內容吧!
|
||||
content: |
|
||||
你成功複製 `hello.txt` 到 `world.txt` 了!
|
||||
我們打開來確認一下 `world.txt` 的內容吧!
|
||||
hints:
|
||||
- ➤ 使用 `cat world.txt`
|
||||
|
||||
@@ -249,14 +384,26 @@ stages:
|
||||
- cp-overwrite
|
||||
task: 檢查 `world.txt` 的內容
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^cat world.txt$
|
||||
output:
|
||||
- Hello
|
||||
exceptions:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你不在 `/home/commander` 資料夾裡。
|
||||
請使用 `cd /home/commander` 來返回 `/home/commander` 資料夾。
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`world.txt` 的內容變成 `Hello` 了!'
|
||||
- 這是因為,當我們複製檔案時,如果目的檔案已經存在,那麼目的檔案的內容就會被覆蓋掉。
|
||||
- 所以在複製檔案的時候,要特別注意目的檔案是否已經存在,否則可能會不小心把資料弄丟。
|
||||
- 以上就是複製檔案的基本操作!
|
||||
content: |
|
||||
`world.txt` 的內容變成 `Hello` 了!
|
||||
這是因為,當我們複製檔案時,如果目的檔案已經存在,那麼目的檔案的內容就會被覆蓋掉。
|
||||
所以在複製檔案的時候,要特別注意目的檔案是否已經存在,否則可能會不小心把資料弄丟。
|
||||
以上就是複製檔案的基本操作!
|
||||
@@ -1,4 +1,5 @@
|
||||
echo start
|
||||
ls
|
||||
touch hello.txt
|
||||
ls
|
||||
cat hello.txt
|
||||
|
||||
+93
-57
@@ -4,40 +4,64 @@ instruction: |
|
||||
1. 如何使用 `touch` 建立檔案
|
||||
2. 如何使用 `mkdir` 建立資料夾
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- read
|
||||
- tab
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始
|
||||
task: 按下 `開始` 按鈕開始
|
||||
condition:
|
||||
command:
|
||||
- ^echo start
|
||||
- ^echo start$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 這次要學習的指令是 `touch` 指令。
|
||||
- '`touch` 指令可以建立一個檔案。'
|
||||
- 接下來,請你試著在終端機上建立一個檔案,檔名為 `hello.txt`。
|
||||
content: |
|
||||
這次要學習的指令是 `touch` 指令。
|
||||
請你先用 `ls` 指令查看目前所在資料夾中有哪些檔案。
|
||||
你應該會發現目前所在資料夾中沒有任何檔案。
|
||||
hints:
|
||||
- ➤ 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
|
||||
|
||||
- id: ls
|
||||
task: 查看資料夾中的檔案
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你應該可以看到終端機上沒有任何回應,這表示目前所在資料夾中沒有任何檔案。
|
||||
而我們可以使用 `touch [檔案名稱]` 指令來建立一個檔案。
|
||||
接下來,請你試著在終端機上建立一個檔案,檔名為 `hello.txt`。
|
||||
hints:
|
||||
- ➤ 輸入 `touch hello.txt` 來建立一個檔案
|
||||
|
||||
- id: touch
|
||||
task: 使用 `touch` 指令來建立一個檔案
|
||||
requirements:
|
||||
- start
|
||||
- ls
|
||||
condition:
|
||||
files:
|
||||
- path: hello.txt
|
||||
- path: /home/commander/hello.txt
|
||||
type: file
|
||||
exceptions:
|
||||
- condition:
|
||||
files:
|
||||
- path: /home/commander/hello
|
||||
type: file
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你似乎忘記輸入副檔名 `txt` 了。
|
||||
請再試一次,並確保檔名為 `hello.txt`。
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你應該可以看到終端機上沒有任何回應。
|
||||
- 這表示 `touch` 指令已經成功建立了一個檔案。
|
||||
- 接下來,請你用之前學過的 `ls` 指令來查看目前所在資料夾中有哪些檔案。
|
||||
content: |
|
||||
你應該可以看到終端機上沒有任何回應。
|
||||
然而實際上 `touch` 指令已經成功建立了一個檔案。
|
||||
請你用之前學過的 `ls` 指令來查看目前所在資料夾中有哪些檔案。
|
||||
hints:
|
||||
- ➤ 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
|
||||
|
||||
@@ -52,26 +76,28 @@ stages:
|
||||
- hello.txt
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你應該可以看到終端機上顯示了 `hello.txt` 這個檔案。
|
||||
- 我們來看看 `hello.txt` 檔案的內容吧!
|
||||
- 我們可以使用之前學過的 `cat` 指令來查看檔案內容。
|
||||
content: |
|
||||
你應該可以看到終端機上顯示了 `hello.txt` 這個檔案。
|
||||
我們來看看 `hello.txt` 檔案的內容吧!
|
||||
我們可以使用之前學過的 `cat` 指令來查看檔案內容。
|
||||
hints:
|
||||
- ➤ 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
|
||||
|
||||
- id: cat
|
||||
task: 使用 `cat` 指令來查看 `hello.txt` 檔案的內容
|
||||
requirements:
|
||||
- ls-touch
|
||||
condition:
|
||||
command:
|
||||
- ^cat hello.txt$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 咦?竟然什麼都沒有!
|
||||
- 沒錯,使用 `touch` 指令建立的檔案是空的。
|
||||
- 我們在未來會學習如何編輯檔案的內容。
|
||||
- 再來試試建立其他的檔案吧!
|
||||
- 請你試著在終端機上建立另一個檔案,檔名為 `world.txt`。
|
||||
content: |
|
||||
咦?竟然什麼都沒有!
|
||||
沒錯,使用 `touch` 指令建立的檔案是空的。
|
||||
我們在未來會學習如何編輯檔案的內容。
|
||||
再來試試建立其他的檔案吧!
|
||||
請你試著在終端機上建立另一個檔案,檔名為 `world.txt`。
|
||||
hints:
|
||||
- ➤ 輸入 `touch world.txt` 來建立一個檔案
|
||||
|
||||
@@ -81,13 +107,23 @@ stages:
|
||||
- ls-touch
|
||||
condition:
|
||||
files:
|
||||
- path: world.txt
|
||||
- path: /home/commander/world.txt
|
||||
type: file
|
||||
exceptions:
|
||||
- condition:
|
||||
files:
|
||||
- path: /home/commander/world
|
||||
type: file
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
你似乎忘記輸入副檔名 `txt` 了。
|
||||
請再試一次,並確保檔名為 `world.txt`。
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 非常好,你已經成功建立了第二個檔案。
|
||||
- 接下來,請你用之前學過的 `ls` 指令來查看剛剛建立的兩個檔案。
|
||||
content: |
|
||||
非常好,你已經成功建立了第二個檔案。
|
||||
接下來,請你用之前學過的 `ls` 指令來查看剛剛建立的兩個檔案。
|
||||
hints:
|
||||
- ➤ 輸入 `ls` 指令來查看目前所在資料夾中有哪些檔案
|
||||
|
||||
@@ -99,16 +135,16 @@ stages:
|
||||
command:
|
||||
- ^ls$
|
||||
output:
|
||||
- hello.txt
|
||||
- world.txt
|
||||
- (^|\s)hello.txt($|\s)
|
||||
- (^|\s)world.txt($|\s)
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你應該可以看到終端機上顯示了 `hello.txt` 和 `world.txt` 這兩個檔案。
|
||||
- 接下來,我們來試試看建立資料夾吧!
|
||||
- '`mkdir` 指令可以建立一個空的資料夾。'
|
||||
- 使用方式是 `mkdir [資料夾名稱]`。
|
||||
- 請你試著在終端機上建立一個資料夾,名稱為 `notes`。
|
||||
content: |
|
||||
你應該可以看到終端機上顯示了 `hello.txt` 和 `world.txt` 這兩個檔案。
|
||||
接下來,我們來試試看建立資料夾吧!
|
||||
`mkdir` 指令可以建立一個空的資料夾。
|
||||
使用方式是 `mkdir [資料夾名稱]`。
|
||||
請你試著在終端機上建立一個資料夾,名稱為 `notes`。
|
||||
hints:
|
||||
- ➤ 輸入 `mkdir notes` 來建立一個資料夾
|
||||
|
||||
@@ -118,13 +154,13 @@ stages:
|
||||
- ls-2
|
||||
condition:
|
||||
files:
|
||||
- path: notes
|
||||
- path: /home/commander/notes
|
||||
type: folder
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 非常好,你已經成功建立了 `notes` 資料夾。
|
||||
- 接下來,請你用 `ls` 指令來查看剛剛建立的資料夾。
|
||||
content: |
|
||||
非常好,你已經成功建立了 `notes` 資料夾。
|
||||
接下來,請你用 `ls` 指令來查看剛剛建立的資料夾。
|
||||
|
||||
- id: ls-mkdir
|
||||
task: 使用 `ls` 查看剛建立的資料夾
|
||||
@@ -137,11 +173,11 @@ stages:
|
||||
- notes
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你應該可以看到終端機上顯示了 `notes` 這個資料夾。
|
||||
- 接下來,我們要在 `notes` 資料夾中建立一個 `note.txt` 的檔案。
|
||||
- 首先,我們要先進入 `notes` 資料夾。
|
||||
- 還記得之前的 `cd` 指令嗎?請你試著使用 `cd` 指令來進入 `notes` 資料夾。
|
||||
content: |
|
||||
你應該可以看到終端機上顯示了 `notes` 這個資料夾。
|
||||
接下來,我們要在 `notes` 資料夾中建立一個 `note.txt` 的檔案。
|
||||
首先,我們要先進入 `notes` 資料夾。
|
||||
還記得之前的 `cd` 指令嗎?請你試著使用 `cd` 指令來進入 `notes` 資料夾。
|
||||
hints:
|
||||
- ➤ 輸入 `cd notes` 來進入 `notes` 資料夾
|
||||
- ➤ 可以在輸入到 `cd n` 時按下 `Tab` 鍵來自動補齊資料夾名稱
|
||||
@@ -155,9 +191,9 @@ stages:
|
||||
- ^/home/commander/notes$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你已經成功進入了 `notes` 資料夾。
|
||||
- 接下來,請你試著在 `notes` 資料夾裡建立一個檔案,檔名為 `note.txt`。
|
||||
content: |
|
||||
你已經成功進入了 `notes` 資料夾。
|
||||
接下來,請你試著在 `notes` 資料夾裡建立一個檔案,檔名為 `note.txt`。
|
||||
hints:
|
||||
- ➤ 輸入 `touch note.txt` 來建立一個檔案
|
||||
|
||||
@@ -167,13 +203,13 @@ stages:
|
||||
- cd-notes
|
||||
condition:
|
||||
files:
|
||||
- path: notes/note.txt
|
||||
- path: /home/commander/notes/note.txt
|
||||
type: file
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 非常好,你已經成功建立了 `note.txt` 檔案。
|
||||
- 跟之前一樣,你可以使用 `ls` 指令來查看目前所在資料夾中有哪些檔案。
|
||||
content: |
|
||||
非常好,你已經成功建立了 `note.txt` 檔案。
|
||||
跟之前一樣,你可以使用 `ls` 指令來查看目前所在資料夾中有哪些檔案。
|
||||
|
||||
- id: END
|
||||
task: 使用 `ls` 指令來查看目前所在資料夾中有哪些檔案
|
||||
@@ -186,7 +222,7 @@ stages:
|
||||
- note.txt
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你應該可以看到終端機上顯示了 `note.txt` 這個檔案。
|
||||
- 恭喜你完成了這個關卡!
|
||||
- 在下一個關卡中,你將學習如何複製檔案與資料夾。
|
||||
content: |
|
||||
你應該可以看到終端機上顯示了 `note.txt` 這個檔案。
|
||||
恭喜你完成了這個關卡!
|
||||
在下一個關卡中,你將學習如何複製檔案與資料夾。
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
USERNAME=commander
|
||||
|
||||
echo "Initializing container for $USERNAME"
|
||||
|
||||
if ! id -u ${USERNAME} >/dev/null 2>&1; then
|
||||
echo "Adding user $USERNAME"
|
||||
adduser --disabled-password --gecos '' --shell /bin/zsh ${USERNAME} && \
|
||||
find / -name ".gitkeep" -type f -exec rm -f {} +
|
||||
|
||||
echo "Adding public key for $USERNAME"
|
||||
echo "Public key: $PUB_KEY"
|
||||
PUB_KEY_PATH=/etc/ssh/authorized_keys/${USERNAME}
|
||||
echo $PUB_KEY > ${PUB_KEY_PATH}
|
||||
chmod 644 ${PUB_KEY_PATH}
|
||||
fi
|
||||
|
||||
mkdir -p /run/sshd
|
||||
echo "Starting SSH server"
|
||||
/usr/sbin/sshd -D -e
|
||||
+35
-78
@@ -4,139 +4,96 @@ exceptions:
|
||||
- '[\uff21-\uff3a\uff41-\uff5a]'
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! 請使用半形英文字母 !!'
|
||||
content: |
|
||||
!! 請使用半形英文字母 !!
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- ' '
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! 請使用半形空格 !!'
|
||||
|
||||
- condition:
|
||||
or:
|
||||
- command:
|
||||
- '`'
|
||||
- command:
|
||||
- \'
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! 不需要輸入上下引號 !!'
|
||||
content: |
|
||||
!! 請使用半形空格 !!
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- ^echo\S$
|
||||
- ^echo\S
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! `echo` 後面要加空格 !!'
|
||||
content: |
|
||||
!! `echo` 後面要加空格 !!
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- ^cd\S$
|
||||
- ^cd\S
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! `cd` 後面要加空格 !!'
|
||||
content: |
|
||||
!! `cd` 後面要加空格 !!
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- ^ls\S$
|
||||
- ^ls\S
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! `ls` 後面要加空格 !!'
|
||||
content: |
|
||||
!! `ls` 後面要加空格 !!
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- ^cat\S$
|
||||
- ^cat\S
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! `cat` 後面要加空格 !!'
|
||||
content: |
|
||||
!! `cat` 後面要加空格 !!
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- ^touch\S$
|
||||
- ^touch\S
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! `touch` 後面要加空格 !!'
|
||||
content: |
|
||||
!! `touch` 後面要加空格 !!
|
||||
|
||||
- condition:
|
||||
not:
|
||||
command:
|
||||
- ^rmdir$
|
||||
- ^rmdir
|
||||
command:
|
||||
- ^rm\S$
|
||||
- ^rm\S
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! `rm` 後面要加空格 !!'
|
||||
content: |
|
||||
!! `rm` 後面要加空格 !!
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- ^mv\S$
|
||||
- ^mv\S
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! `mv` 後面要加空格 !!'
|
||||
content: |
|
||||
!! `mv` 後面要加空格 !!
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- ^mkdir\S$
|
||||
- ^mkdir\S
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! `mkdir` 後面要加空格 !!'
|
||||
content: |
|
||||
!! `mkdir` 後面要加空格 !!
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- ^rmdir\S$
|
||||
- ^rmdir\S
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! `rmdir` 後面要加空格 !!'
|
||||
content: |
|
||||
!! `rmdir` 後面要加空格 !!
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- ^cp\S$
|
||||
- ^cp\S
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! `cp` 後面要加空格 !!'
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- ^[Ii1]s$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '!! `ls` 的首字母是小寫的 `L` !!'
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- 嗨
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '嗨~'
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- folderl
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`folder1` 的 `1` 是數字 `1`,不是小寫的 `L` !!'
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- filel
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`file1` 的 `1` 是數字 `1`,不是小寫的 `L` !!'
|
||||
content: |
|
||||
!! `cp` 後面要加空格 !!
|
||||
@@ -0,0 +1,80 @@
|
||||
# 執行檔案
|
||||
|
||||
## Outline
|
||||
|
||||
1. 什麼是可執行檔
|
||||
2. 使用 `which` 指令查看執行檔的位置
|
||||
3. 如何執行可執行檔
|
||||
|
||||
---
|
||||
|
||||
在我們先前的課程裡,已經使用了許多不同的指令,像是 `ls`, `cat`, `cp`, `mv`, `rm` 等。
|
||||
這些指令,其實都是一個**可執行檔**,在你打指令的時候被作業系統執行。
|
||||
所以大部分的指令,我們其實都能找到它在檔案系統裡的原始位置。
|
||||
要查看一個指令它的檔案位置,可以使用 `which` 指令。
|
||||
輸入 `which [指令名稱]` 就能夠查看指令的檔案位置了。
|
||||
|
||||
> 使用 `which ls` 來查看 `ls` 的路徑
|
||||
|
||||
---
|
||||
|
||||
你看到終端機顯示出 `/bin/ls` 這個路徑,它其實就是 `ls` 指令它檔案存在的位置。
|
||||
`/bin` 是一個系統資料夾,裡面會儲存許多系統內建的程式,像是 `cat`, `cp` 等指令也都被存在這裡。
|
||||
|
||||
> 使用 `which cat` 查看 `cat` 的路徑
|
||||
|
||||
---
|
||||
|
||||
我們要執行一個可執行檔時,只要直接輸入它的路徑就可以執行它。
|
||||
我們來試看看使用執行檔的路徑來執行指令。
|
||||
|
||||
> 輸入 `/bin/ls` 來查看當前的資料夾
|
||||
|
||||
---
|
||||
|
||||
你會看到它列出了我們資料夾內的檔案,你可以自己比較看看,跟單純打 `ls` 的結果會一模一樣。
|
||||
它一樣可以使用參數。我們來用 `cat` 試試看。
|
||||
|
||||
> 輸入 `/bin/cat hello.txt` 來顯示 `hello.txt` 的內容
|
||||
|
||||
---
|
||||
|
||||
你會看到了 `hello.txt` 的內容,效果跟單純用 `cat` 一模一樣。
|
||||
事實上,這是必然的,因為 `cat` 的檔案位置就是 `/bin/cat`,我們執行了同一個程式,當然會有同樣的結果。
|
||||
其實指令,就是由 **一個執行檔** + **參數** 所構成。
|
||||
我們之所以不用打 `/bin/ls`,而是只要打 `ls` 就能執行,是因為有 `PATH` 這個環境變數,幫我們預設好了終端機要去哪些*資料夾*找檔案。
|
||||
但因為這超出了本次課程的範圍,關於 `PATH` 以及環境變數的議題,我們會在未來的課程再討論。
|
||||
|
||||
我們也可以執行不是內建的指令,像是現在資料夾中的 `dog` 檔案。
|
||||
|
||||
> 使用 `cat dog` 來查看 `dog` 的內容
|
||||
|
||||
---
|
||||
|
||||
你會看到這是一個用 `echo` 和 `cat` 所組成的簡單程式。
|
||||
詳細的內容不用了解沒關係,功能是和 `cat` 一樣,用來印出檔案內容的程式。
|
||||
但是這支程式,在沒有給要印出的檔案時,不會停留在輸入狀態。
|
||||
|
||||
當我們要執行一個檔案時,就是用指定路徑的方式。
|
||||
那因為 `dog` 位在家目錄底下,所以牠的絕對路徑就是 `/home/commander/dog`,我們也可以用簡寫 `~/dog`。
|
||||
|
||||
> 輸入 `~/dog` 來執行 `dog` 檔案
|
||||
|
||||
---
|
||||
|
||||
你看到了終端機輸出這隻傲嬌的狗。
|
||||
除了使用絕對路徑,很多時候,我們會處在跟程式相同的資料夾,這時使用相對路徑會更為便利。
|
||||
但跟平常列出檔案時不一樣,我們為了區別它不是內建的指令,需要加上 `./` 在檔名前面。
|
||||
`.` 在路徑中,代表的就是當前的資料夾。
|
||||
那我們再試試看使用相對路徑,並讓牠接收一個檔案作為參數。
|
||||
|
||||
> 輸入 `./dog hello.txt` 來印出 `hello.txt` 的內容
|
||||
|
||||
---
|
||||
|
||||
你看到這隻狗在講了幾句話後也印出了 `hello.txt` 的內容了。
|
||||
這個範例,其實就是一個簡單的自訂指令。
|
||||
我們可以把一大串指令,寫在一個檔案裡讓它自動執行,幫我們處理一些複雜或是重複性的工作。
|
||||
我們在使用別人寫的程式時,也常會用到這個技巧。
|
||||
|
||||
最後留一個小挑戰給你,請試著移動到 `commands` 資料夾後,再使用相對路徑執行裡面的 `lucky` 檔案。
|
||||
@@ -0,0 +1,187 @@
|
||||
title: 執行檔案
|
||||
instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 什麼是可執行檔
|
||||
2. 使用 `which` 指令查看執行檔的位置
|
||||
3. 如何執行可執行檔
|
||||
|
||||
requirements:
|
||||
- remove
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 按下開始按鈕
|
||||
type: start
|
||||
condition:
|
||||
command: ^echo start$
|
||||
responses:
|
||||
- content: |
|
||||
在我們先前的課程裡,已經使用了許多不同的指令,像是 `ls`, `cat`, `cp`, `mv`, `rm` 等。
|
||||
這些指令,其實都是一個**可執行檔**,在你打指令的時候被作業系統執行。
|
||||
所以大部分的指令,我們其實都能找到它在檔案系統裡的原始位置。
|
||||
要查看一個指令它的檔案位置,可以使用 `which` 指令。
|
||||
輸入 `which [指令名稱]` 就能夠查看指令的檔案位置了。
|
||||
|
||||
hints:
|
||||
- ➤ 使用 `which ls` 來查看 `ls` 的路徑
|
||||
|
||||
- id: which-ls
|
||||
requirements:
|
||||
- start
|
||||
task: 使用 `which` 指令查看 `ls` 的路徑
|
||||
condition:
|
||||
command: ^which ls$
|
||||
responses:
|
||||
- content: |
|
||||
你看到終端機顯示出 `/bin/ls` 這個路徑,它其實就是 `ls` 指令它檔案存在的位置。
|
||||
`/bin` 是一個系統資料夾,裡面會儲存許多系統內建的程式,像是 `cat`, `cp` 等指令也都被存在這裡。
|
||||
hints:
|
||||
- ➤ 使用 `which cat` 查看 `cat` 的路徑
|
||||
|
||||
- id: which-cat
|
||||
requirements:
|
||||
- which-ls
|
||||
task: 使用 `which` 指令查看 `cat` 的路徑
|
||||
condition:
|
||||
command: ^which cat$
|
||||
responses:
|
||||
- content: |
|
||||
你看到了 `/bin/cat` 這個路徑,它就是 `cat` 指令它檔案存在的位置。
|
||||
我們要執行一個可執行檔時,只要 **直接輸入它的路徑** 就可以執行它。
|
||||
我們來試看看使用執行檔的路徑來執行指令。
|
||||
|
||||
在此之前,我們先試試看一般執行 `ls` 指令的結果。
|
||||
hints:
|
||||
- ➤ 輸入 `ls` 來查看當前的資料夾
|
||||
|
||||
- id: ls
|
||||
requirements:
|
||||
- which-cat
|
||||
task: 使用 `ls` 指令查看當前的資料夾
|
||||
condition:
|
||||
command: ^ls$
|
||||
responses:
|
||||
- content: |
|
||||
你看到了當前的資料夾內有 `commands` 資料夾和 `dog`, `hello.txt` 這兩個檔案。
|
||||
我們再來試看看用 `ls` 的路徑來執行它。
|
||||
hints:
|
||||
- ➤ 輸入 `/bin/ls` 來查看當前的資料夾
|
||||
|
||||
- id: bin-ls
|
||||
requirements:
|
||||
- ls
|
||||
task: 使用執行檔的絕對路徑來執行 `ls`
|
||||
condition:
|
||||
command: ^/bin/ls$
|
||||
responses:
|
||||
- content: |
|
||||
你會看到它列出了我們資料夾內的檔案,你會發現它的輸出跟單純打 `ls` 的結果一模一樣。
|
||||
它一樣可以使用參數。我們來用 `cat` 試試看。
|
||||
|
||||
hints:
|
||||
- ➤ 輸入 `/bin/cat hello.txt` 來顯示 `hello.txt` 的內容
|
||||
|
||||
- id: bin-cat
|
||||
requirements:
|
||||
- bin-ls
|
||||
task: 使用執行檔的絕對路徑來執行 `cat`
|
||||
condition:
|
||||
command: ^/bin/cat hello.txt$
|
||||
responses:
|
||||
- content: |
|
||||
你看到了 `hello.txt` 的內容,跟單純使用 `cat` 一模一樣。
|
||||
事實上,這是必然的,因為 `cat` 的檔案位置就是 `/bin/cat`,我們執行了同一個程式,當然會有同樣的結果。
|
||||
|
||||
因為一個指令,就是由 `執行檔 參數1 參數2 ...` 所構成。
|
||||
`執行檔` 就是我們要執行的程式,像是 `ls`, `cat`, `which` 等。
|
||||
`參數` 則是我們要提供給程式的資訊,像是接在 `cat` 後面的 `hello.txt`。
|
||||
|
||||
我們之所以不用打 `/bin/ls`,而是只要打 `ls` 就能執行,是因為有 `PATH` 這個環境變數,幫我們預設好了終端機要去哪些*資料夾*找檔案。
|
||||
但因為這超出了本次課程的範圍,關於 `PATH` 以及環境變數的議題,我們會在未來的課程再討論。
|
||||
|
||||
接著我們來試著執行不是內建的指令。
|
||||
現在資料夾中的 `dog` 檔案,是一個在這門課程所提供的自訂指令。
|
||||
我們先來看看它是怎麼寫的。
|
||||
|
||||
hints:
|
||||
- ➤ 使用 `cat dog` 來查看 `dog` 的內容
|
||||
|
||||
- id: cat-dog
|
||||
requirements:
|
||||
- bin-cat
|
||||
task: 使用 `cat` 指令查看 `dog` 的內容
|
||||
condition:
|
||||
command: ^cat dog$
|
||||
responses:
|
||||
- content: |
|
||||
你看到了這是一個用 `echo` 和 `sleep` 所組成的簡單程式。
|
||||
詳細的內容不用了解沒關係,只是輸出一些文字而已。
|
||||
|
||||
當我們要執行一個檔案時,就是用指定路徑的方式。
|
||||
也就是說 `執行檔` 就是 `dog` 的路徑,`參數` 在這個範例中不會使用到。
|
||||
那因為 `dog` 位在家目錄底下,所以牠的絕對路徑就是 `/home/commander/dog`。
|
||||
那在 `執行檔 參數` 這個格式下,我們要打的指令就會是
|
||||
```
|
||||
/home/commander/dog
|
||||
```
|
||||
|
||||
hints:
|
||||
- ➤ 輸入 `/home/commander/dog` 來執行 `dog` 檔案
|
||||
|
||||
- id: dog-abs
|
||||
requirements:
|
||||
- cat-dog
|
||||
task: 使用絕對路徑來執行 `dog`
|
||||
condition:
|
||||
or:
|
||||
- command: ^/home/commander/dog$
|
||||
- command: ^~/dog$
|
||||
responses:
|
||||
- content: |
|
||||
你看到了終端機輸出這隻傲嬌的狗。
|
||||
除了使用絕對路徑,很多時候,我們會處在跟程式相同的資料夾,這時使用相對路徑會更為便利。
|
||||
但跟平常列出檔案時不一樣,我們為了區別它不是內建的指令,需要加上 `./` 在檔名前面。
|
||||
`.` 在路徑中,代表的就是當前的資料夾。
|
||||
那我們再試試看使用相對路徑來執行 `dog` 檔案。
|
||||
|
||||
hints:
|
||||
- ➤ 輸入 `./dog` 來執行 `dog` 檔案
|
||||
|
||||
- id: dog-rel
|
||||
requirements:
|
||||
- dog-abs
|
||||
task: 使用相對路徑來執行 `dog`,並加上參數
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: ^./dog$
|
||||
responses:
|
||||
- content: |
|
||||
你看到這隻狗執行了跟剛才一樣的結果。
|
||||
這個範例,其實就是一個簡單的自訂指令。
|
||||
我們可以把一大串指令,寫在一個檔案裡讓它自動執行,幫我們處理一些複雜或是重複性的工作。
|
||||
我們在使用別人寫的程式時,也常會用到這個技巧。
|
||||
|
||||
最後留一個小挑戰給你,請試著先進入 `commands` 資料夾後,再使用相對路徑執行裡面的 `lucky` 檔案。
|
||||
hints:
|
||||
- ➤ 使用 `cd` 來進入資料夾
|
||||
- ➤ 使用 `ls` 查看檔案
|
||||
- ➤ 輸入 `./檔案名稱` 來執行檔案
|
||||
|
||||
- id: END
|
||||
requirements:
|
||||
- dog-rel
|
||||
task: 使用相對路徑來執行 `lucky`
|
||||
condition:
|
||||
pwd: /home/commander/commands
|
||||
command: ^./lucky$
|
||||
responses:
|
||||
- content: |
|
||||
你看到了終端機輸出 `You are lucky!` 這個字串。
|
||||
恭喜你,你已經完成了這個課程!
|
||||
exceptions:
|
||||
condition:
|
||||
command: ^echo help$
|
||||
responses:
|
||||
- content: |
|
||||
你需要先移動到 `commands` 資料夾,再使用相對路徑執行 `lucky` 檔案。
|
||||
請先輸入 `cd commands` 來移動到 `commands` 資料夾,再使用相對路徑 `./lucky` 來執行 `lucky` 檔案。
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "I am SO LUCKY!!!"
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "你執行了 dog 檔案"
|
||||
echo "汪汪汪"
|
||||
sleep 1
|
||||
echo "你知道嗎?"
|
||||
sleep 1
|
||||
echo "我比貓還厲害"
|
||||
@@ -0,0 +1 @@
|
||||
Hello! This is a file hello.txt
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY home ${HOME}
|
||||
+60
-56
@@ -4,24 +4,22 @@ instruction: |
|
||||
1. 如何使用 `ls` 查看資料夾的內容
|
||||
2. 如何使用 `cd` 在資料夾之間移動
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- get-started
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
task: 按下 `開始` 按鈕開始教程
|
||||
condition:
|
||||
output:
|
||||
- ^start$
|
||||
command:
|
||||
- ^echo start$
|
||||
responses:
|
||||
- content:
|
||||
- 在終端機中,並沒有圖形介面可以讓你查看資料夾的內容。
|
||||
- 但是你可以透過 `ls` 指令來查看資料夾的內容。
|
||||
- '`ls` 指令是 list 的縮寫,也就是列出檔案的意思。'
|
||||
- 在輸入 `ls` 指令後,終端機會輸出資料夾中包含的檔案和資料夾,右邊的檔案樹也會隨之改變。
|
||||
- 現在,請你輸入 `ls` 指令,查看資料夾中有什麼東西。
|
||||
- content: |
|
||||
在終端機中,並沒有圖形介面可以讓你查看資料夾的內容。
|
||||
但是你可以透過 `ls` 指令來查看資料夾的內容。
|
||||
`ls` 指令(第一個字是小寫L)是 list 的簡寫,也就是列出檔案的意思。
|
||||
在輸入 `ls` 指令後,終端機會輸出資料夾中包含的檔案和資料夾。
|
||||
現在,請你輸入 `ls` 指令,查看資料夾中有什麼東西。
|
||||
hints:
|
||||
- ➤ 輸入 `ls` 並查看資料夾的內容
|
||||
|
||||
@@ -36,24 +34,23 @@ stages:
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
- /home/commander
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 請先回到 `/home/commander/` 資料夾
|
||||
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
||||
content: |
|
||||
請先回到 `/home/commander/` 資料夾
|
||||
輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
||||
responses:
|
||||
- content:
|
||||
- 你應該能看到資料夾中的內容了。
|
||||
- 在畫面右邊的檔案樹也有了改變。
|
||||
- 可以看出,在這個資料夾中包含了兩個資料夾,分別是 `folder1` 和 `folder2`。
|
||||
- content:
|
||||
- 接下來,我要教你如何進入資料夾。
|
||||
- '`cd` 指令是 change directory 的縮寫,也就是改變路徑的意思。'
|
||||
- 你可以通過輸入 `cd [資料夾名稱]` 來進入資料夾。
|
||||
- 例如你要進入 `folder1` 資料夾,你可以輸入
|
||||
- '`cd folder1`。'
|
||||
- 現在,請你進入 `folder1` 資料夾。
|
||||
- content: |
|
||||
你應該能看到資料夾中的內容了。
|
||||
在畫面右邊的檔案樹也有了改變。
|
||||
可以看出,在這個資料夾中包含了兩個資料夾,分別是 `folder1` 和 `folder2` 。
|
||||
- content: |
|
||||
接下來,我要教你如何進入資料夾。
|
||||
`cd` 指令是 change directory 的縮寫,也就是改變路徑的意思。
|
||||
你可以通過輸入 `cd [資料夾名稱]` 來進入資料夾。
|
||||
例如你要進入 `folder1` 資料夾,你可以輸入 `cd folder1` 。
|
||||
現在,請你進入 `folder1` 資料夾。
|
||||
hints:
|
||||
- ➤ 輸入 `cd folder1` 來進入 `folder1` 資料夾
|
||||
|
||||
@@ -65,30 +62,37 @@ stages:
|
||||
pwd:
|
||||
- ^/home/commander/folder1$
|
||||
exceptions:
|
||||
- condition:
|
||||
command:
|
||||
- folderl
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
`folder1` 的最後是數字 `1` 不是字母 `l`。
|
||||
- condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 輸入 `cd folder1` 來進入 `folder1` 資料夾
|
||||
content: 輸入 `cd folder1` 來進入 `folder1` 資料夾
|
||||
- condition:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 請先回到 `/home/commander/` 資料夾
|
||||
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
||||
content: |
|
||||
請先回到 `/home/commander/` 資料夾
|
||||
輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
||||
再輸入 `cd folder1` 來進入 `folder1` 資料夾
|
||||
responses:
|
||||
- content:
|
||||
- 你成功的進入 `folder1` 資料夾了。
|
||||
- 你可以發現指令提示符從原本的
|
||||
- '`commander:~ $` 變成了'
|
||||
- '`commander:~/folder1 $` 。'
|
||||
- 這表示你現在正位在 `folder1` 資料夾中。
|
||||
- 現在,請查看 `folder1` 資料夾的內容。
|
||||
- content: |
|
||||
你成功的進入 `folder1` 資料夾了。
|
||||
你可以發現指令提示符從原本的
|
||||
`commander:~ $` 變成了
|
||||
`commander:~/folder1 $` 。
|
||||
這表示你現在正位在 `folder1` 資料夾中。
|
||||
現在,請查看 `folder1` 資料夾的內容。
|
||||
hints:
|
||||
- ➤ 輸入 `ls` 查看資料夾的內容
|
||||
|
||||
@@ -102,11 +106,11 @@ stages:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- 你現在可以看到 `folder1` 資料夾中有兩的檔案,分別是 `file1.txt` 跟 `file2.txt`。
|
||||
- 現在我們可以離開 `folder1` 回到原本的資料夾了。
|
||||
- 你可以使用 `cd ..` 回到上一層資料夾。
|
||||
- 這裡的 `..` 代表的是上一層的資料夾。
|
||||
- content: |
|
||||
你現在可以看到 `folder1` 資料夾中有兩個檔案,分別是 `file1.txt` 跟 `file2.txt`。
|
||||
現在我們可以離開 `folder1` 回到原本的資料夾了。
|
||||
你可以使用 `cd ..` 回到上一層資料夾。
|
||||
這裡的 `..` 代表的是上一層的資料夾。
|
||||
hints:
|
||||
- ➤ 輸入 `cd ..` 回到上一層資料夾
|
||||
|
||||
@@ -118,9 +122,9 @@ stages:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- content:
|
||||
- 你成功的回到上一層資料夾了。
|
||||
- 現在,請你剛才所學的指令,進入 `folder2` 資料夾,檢查 `folder2` 資料夾的內容,最後回到這個資料夾。
|
||||
- content: |
|
||||
你成功的回到上一層資料夾了。
|
||||
現在,請你剛才所學的指令,進入 `folder2` 資料夾,檢查 `folder2` 資料夾的內容,最後回到這個資料夾。
|
||||
hints:
|
||||
- ➤ 使用 `cd` 指令進出資料夾
|
||||
- ➤ 使用 `ls` 指令查看資料夾的內容
|
||||
@@ -133,9 +137,9 @@ stages:
|
||||
pwd:
|
||||
- ^/home/commander/folder2$
|
||||
responses:
|
||||
- content:
|
||||
- 你成功的進入 `folder2` 資料夾了。
|
||||
- 現在,請你查看 `folder2` 資料夾的內容。
|
||||
- content: |
|
||||
你成功的進入 `folder2` 資料夾了。
|
||||
現在,請你查看 `folder2` 資料夾的內容。
|
||||
hints:
|
||||
- ➤ 使用 `ls` 查看資料夾的內容
|
||||
|
||||
@@ -147,11 +151,11 @@ stages:
|
||||
pwd:
|
||||
- ^/home/commander/folder2$
|
||||
command:
|
||||
- ls
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- 你看到了 `folder2` 資料夾中的檔案,分別是 `file3.txt` 跟 `file4.txt`。
|
||||
- 現在你可以離開 `folder2` 回到原本的資料夾了。
|
||||
- content: |
|
||||
你看到了 `folder2` 資料夾中的檔案,分別是 `file3.txt` 跟 `file4.txt`。
|
||||
現在你可以離開 `folder2` 回到原本的資料夾了。
|
||||
hints:
|
||||
- ➤ 使用 `cd ..` 回到上一層資料夾
|
||||
|
||||
@@ -163,9 +167,9 @@ stages:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- content:
|
||||
- 你成功的回到原本的資料夾了。
|
||||
- 恭喜你學會了 `ls` 和 `cd` 指令的用法。
|
||||
- content: |
|
||||
你成功的回到原本的資料夾了。
|
||||
恭喜你學會了 `ls` 和 `cd` 指令的用法。
|
||||
hints:
|
||||
- ➤ 使用 `cd` 指令進出資料夾
|
||||
- ➤ 使用 `ls` 指令查看資料夾的內容
|
||||
@@ -1,3 +1,4 @@
|
||||
echo start
|
||||
echo hello
|
||||
echo next
|
||||
echo finish
|
||||
+39
-46
@@ -5,30 +5,20 @@ instruction: |
|
||||
1. 如何使用這個網站
|
||||
2. 如何使用終端機
|
||||
|
||||
在畫面的中間可以看到終端機和提示兩個區塊。
|
||||
請你在終端機中的 `commander:~ $` 後面輸入 `echo start` 並按下 Enter 來開始教程。
|
||||
!! 注意:不需要輸入上下引號 !!
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
task: 按下 `開始` 按鈕開始
|
||||
condition:
|
||||
output:
|
||||
- ^start$
|
||||
command:
|
||||
- ^echo start$
|
||||
responses:
|
||||
- content:
|
||||
- 你好,我是你的 Unix 助手,我叫做 Ada。我將會在之後協助你了解如何使用終端機。
|
||||
- 現在所在的這個區塊就是終端機,你可以在這裡輸入指令來操作電腦。
|
||||
- 在終端機中,你所看到的
|
||||
- '`commander:~ $` 是指令提示符號,它能讓你知道現在可以輸入指令了。'
|
||||
- 你必須將指令輸入在它的後方。
|
||||
- 當你輸入完指令並按下 Enter 之後,終端機會執行你輸入的指令。
|
||||
- 並且會將執行結果輸出在終端機的下一行。
|
||||
- 這個終端機的功能和你在 Linux 電腦上看到的終端機是一樣的。
|
||||
- 唯一的差別是,在這裡會有劇情和提示來協助你學習如何使用終端機。
|
||||
- 在輸入指令之後,如果你看到用綠色 `=====` 組成的分隔線,代表在分隔線下方的內容是添加的內容,在真實的終端機中不會出現。
|
||||
- 而終端機的輸出,是在綠色 `=====` 分隔線上方的白色內容。
|
||||
- 現在請你輸入 `echo hello` 來看看會發生什麼事。
|
||||
- content: |
|
||||
在畫面的中下方的區塊是終端機,你可以在終端機裡輸入指令來操作電腦。
|
||||
在終端機中,你所看到的 `commander:~ $` 叫做命令提示字串。
|
||||
命令提示字串最後的錢字號 `$` 叫命令提示符,你必須將指令輸入在它的後方。
|
||||
當你輸入完指令並按下 Enter 之後,終端機會執行你輸入的指令,並且會從終端機的下一行開始輸出指令的執行結果。
|
||||
直到你看到新的 `commander:~ $` 出現時,代表你可以輸入下一個指令了。
|
||||
現在請你輸入 `echo hello` 來看看會發生什麼事。
|
||||
hints:
|
||||
- ➤ 在 `commander:~ $` 後面輸入 `echo hello` 並按下 Enter
|
||||
|
||||
@@ -40,38 +30,41 @@ stages:
|
||||
command:
|
||||
- ^echo hello$
|
||||
responses:
|
||||
- content:
|
||||
- 你應該可以看到終端機上顯示了:
|
||||
- commander:~ $ echo hello
|
||||
- hello
|
||||
- 第一行的 `commander:~ $` 是指令提示符, `echo hello` 是你輸入的指令。
|
||||
- 第二行的 `hello` 則是終端機的輸出。
|
||||
- content:
|
||||
- 現在你知道如何在終端機上輸入指令,以及查看終端機輸出的位置。
|
||||
- 指令必須輸入在指令提示符號的後面,然而當指令提示符消失時,你就無法輸入指令了。
|
||||
- 這時候你可以按下 `Ctrl + C` 來嘗試解決這個問題。
|
||||
- 在終端機中, `Ctrl + C` 並不能用來複製,而是用來中斷目前正在執行的指令。
|
||||
- 因此,如果你前一次的指令執行時卡住了,或是執行很久沒有反應,你可以按下 `Ctrl + C` 來中斷指令,讓你可以繼續輸入指令。
|
||||
- 如果 `Ctrl + C` 也沒有辦法解決問題,可以按按看 `esc` 或 `q` 鍵。如果都沒用的話,按下畫面右下角的 `重來` 按鈕重新開始關卡。
|
||||
- content:
|
||||
- 在終端機下方會顯示提示,你可以用提示右上角的箭頭來查看之前的提示。
|
||||
- 在畫面左邊的區域是關卡說明,你可以在這裡看到關卡的介紹,以及現在需要完成的任務。
|
||||
- 在畫面右邊的區域是檔案樹,它會根據你所探索的資料夾發生變化,讓你更清楚檔案之間的位置關係。
|
||||
- 畫面右上角的圖示分別是 `查看地圖`、`回報錯誤` 以及 `登出` 按鈕。
|
||||
- content:
|
||||
- 第一關的文字內容比較多,可以慢慢讀沒關係。
|
||||
- 如果忘記了也可以隨時回來複習。
|
||||
- 需要的話可以按下 `Enter` 來快速跳過劇情說明。
|
||||
- 如果你已經讀完了上述內容,請輸入 `echo finish` 來結束這一關。
|
||||
- content: |
|
||||
你應該可以看到終端機上顯示了以下內容:
|
||||
```
|
||||
commander:~ $ echo hello
|
||||
hello
|
||||
```
|
||||
第一行的 `commander:~ $` 是命令提示字串,接在後方的 `echo hello` 是你輸入的指令。
|
||||
第二行的 `hello` 則是終端機的輸出結果。
|
||||
你現在知道了如何在終端機上輸入指令,以及查看終端機輸出的位置。
|
||||
現在請輸入 `echo next` 來繼續下一個步驟。
|
||||
hints:
|
||||
- ➤ 按下 `Ctrl + C` 能夠中斷指令
|
||||
- ➤ 劇情進行時可以按下 `Enter` 來快速跳過
|
||||
- ➤ 輸入 `echo next` 繼續下一個步驟
|
||||
|
||||
- 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` 來結束這一關
|
||||
|
||||
- id: END
|
||||
task: 結束關卡
|
||||
requirements:
|
||||
- echo-hello
|
||||
- echo-next
|
||||
condition:
|
||||
command:
|
||||
- ^echo finish$
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY home ${HOME}
|
||||
+31
-31
@@ -21,17 +21,17 @@ stages:
|
||||
- 你聽到了一個聲音:
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 你好,指令官,我是你的 Unix 精靈。我已在此恭候多時。
|
||||
- 這個世界是在 3560000 時脈以前,由初代王建立了 Unix 律法,所形成的世界。然而在經過了好幾世代的王後,終於誕生了永恆之王——薩加(Saga),她比歷代的君王都更為強大,且從來不會衰老,使得這個世界變得前所未有的強大。
|
||||
- 然而,外在世界一直不斷在改變,同時在面臨外敵的不斷攻擊,而薩加卻始終用同一套治理的手段。最終,在 160000 時脈前的一次入侵中,根部遭到攻陷,薩加遭到封印,而這個國家也陷入了黑暗。
|
||||
- 由於長久以來依賴於薩加,世人已失去了「指令」的力量,即將走向衰敗和滅亡的命運。
|
||||
- 你的任務,就是找回被世人遺忘的指令,奪回 Linux 之根,成為 Linux 之王。
|
||||
- 首先,你必須要學會如何使用指令。
|
||||
- 你要學習的第一個指令,是「搜索」的指令:`ls` (list)。 `ls` 可以用來查看一個目錄中有哪些檔案。
|
||||
- 現在,請你輸入 `ls` 來查看目前的目錄下有哪些檔案。
|
||||
hints: |
|
||||
➤ 使用 `ls` 指令來查看目前的目錄下有哪些檔案。
|
||||
content: |
|
||||
你好,指令官,我是你的 Unix 精靈。我已在此恭候多時。
|
||||
這個世界是在 3560000 時脈以前,由初代王建立了 Unix 律法,所形成的世界。然而在經過了好幾世代的王後,終於誕生了永恆之王——薩加(Saga),她比歷代的君王都更為強大,且從來不會衰老,使得這個世界變得前所未有的強大。
|
||||
然而,外在世界一直不斷在改變,同時在面臨外敵的不斷攻擊,而薩加卻始終用同一套治理的手段。最終,在 160000 時脈前的一次入侵中,根部遭到攻陷,薩加遭到封印,而這個國家也陷入了黑暗。
|
||||
由於長久以來依賴於薩加,世人已失去了「指令」的力量,即將走向衰敗和滅亡的命運。
|
||||
你的任務,就是找回被世人遺忘的指令,奪回 Linux 之根,成為 Linux 之王。
|
||||
首先,你必須要學會如何使用指令。
|
||||
你要學習的第一個指令,是「搜索」的指令:`ls` (list)。 `ls` 可以用來查看一個目錄中有哪些檔案。
|
||||
現在,請你輸入 `ls` 來查看目前的目錄下有哪些檔案。
|
||||
hints:
|
||||
- ➤ 使用 `ls` 指令來查看目前的目錄下有哪些檔案。
|
||||
|
||||
- id: find-scroll
|
||||
task: 搜索卷軸
|
||||
@@ -44,20 +44,20 @@ stages:
|
||||
- ^ls$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 一瞬間,視野變得清晰起來,你發現自己身處在一個石頭建築的房間裡。
|
||||
- 你發現了一個檔案,檔名是 `forgotten-scroll.txt`。
|
||||
content: |
|
||||
一瞬間,視野變得清晰起來,你發現自己身處在一個石頭建築的房間裡。
|
||||
你發現了一個檔案,檔名是 `forgotten-scroll.txt`。
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 你找到了一個被遺忘的卷軸,上面寫著解除封印的咒語。
|
||||
- 使用指令 `cat [ 檔案名稱 ]` (concatenate)來查看檔案內容。
|
||||
- 我可以為你提供一些指令的提示,只要輸入指令的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
|
||||
- 例如你只要輸入檔案名稱的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
|
||||
hints: |
|
||||
➤ 使用 `cat [ 檔案名稱 ]` 指令來查看檔案內容。
|
||||
➤ 使用 `Tab` 鍵來取得指令提示。
|
||||
➤ 如果卡在未知的情況,按下 `Ctrl + C` 嘗試離開。
|
||||
content: |
|
||||
你找到了一個被遺忘的卷軸,上面寫著解除封印的咒語。
|
||||
使用指令 `cat [ 檔案名稱 ]` (concatenate)來查看檔案內容。
|
||||
我可以為你提供一些指令的提示,只要輸入指令的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
|
||||
例如你只要輸入檔案名稱的前幾個字元,然後按下 `Tab` 鍵,就可以看到提示了。
|
||||
hints:
|
||||
- ➤ 使用 `cat [ 檔案名稱 ]` 指令來查看檔案內容。
|
||||
- ➤ 使用 `Tab` 鍵來取得指令提示。
|
||||
- ➤ 如果卡在未知的情況,按下 `Ctrl + C` 嘗試離開。
|
||||
|
||||
- id: read-forgotten-scroll
|
||||
task: 查看卷軸
|
||||
@@ -71,11 +71,11 @@ stages:
|
||||
responses:
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 你找到的是一個被遺忘的卷軸,上面寫著解除封印的咒語。
|
||||
- 使用你已經知道的 `echo [ 咒語 ]` 指令來詠唱咒語。
|
||||
hints: |
|
||||
➤ 輸入 `echo Hello World` 來詠唱咒語。
|
||||
content: |
|
||||
你找到的是一個被遺忘的卷軸,上面寫著解除封印的咒語。
|
||||
使用你已經知道的 `echo [ 咒語 ]` 指令來詠唱咒語。
|
||||
hints:
|
||||
- ➤ 輸入 `echo Hello World` 來詠唱咒語。
|
||||
|
||||
- id: END
|
||||
task: 解除封印
|
||||
@@ -92,6 +92,6 @@ stages:
|
||||
- 突然之間,你感覺到一股強大的力量從你的身體中湧出,你的身體開始發光,你的手中出現了一把魔法之劍。
|
||||
- type: dialogue
|
||||
speaker: Unix 精靈
|
||||
content:
|
||||
- 恭喜您,我的指令官,您已成功解除了封印。
|
||||
- 請跟著在接下來的關卡,一步步取回 Linux 之根吧!
|
||||
content: |
|
||||
恭喜您,我的指令官,您已成功解除了封印。
|
||||
請跟著在接下來的關卡,一步步取回 Linux 之根吧!
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY --chown=${USERNAME} home ${HOME}
|
||||
@@ -1,12 +1,11 @@
|
||||
echo start
|
||||
echo continue
|
||||
echo next
|
||||
mv hello.txt notes
|
||||
ls
|
||||
ls notes
|
||||
echo continue
|
||||
echo next
|
||||
mv world.txt wow.txt
|
||||
ls
|
||||
mv notes docs
|
||||
ls
|
||||
echo hint
|
||||
mv docs/hello.txt hi.txt
|
||||
+59
-71
@@ -1,65 +1,63 @@
|
||||
title: 移動檔案
|
||||
instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 如何用 `mv` 移動檔案與資料夾
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
1. 如何使用 `mv` 移動檔案與資料夾
|
||||
|
||||
requirements:
|
||||
- create
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始
|
||||
task: 按下 `開始` 按鈕開始
|
||||
condition:
|
||||
command:
|
||||
- ^echo start$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 今天我們要學習如何移動檔案與資料夾。
|
||||
- 現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,以及一個 `notes` 資料夾。
|
||||
- 請先用 `ls` 與 `cat` 來查看它們的內容。
|
||||
- 看完之後,使用 `echo continue` 來繼續吧!
|
||||
content: |
|
||||
今天我們要學習如何移動檔案與資料夾。
|
||||
現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,以及一個 `notes` 資料夾。
|
||||
請先用 `ls` 與 `cat` 來查看它們的內容。
|
||||
看完之後,使用 `echo next` 來繼續吧!
|
||||
hints:
|
||||
- ➤ 用 `ls`、`ls notes` 來查看資料夾內的檔案
|
||||
- ➤ 用 `cat hello.txt` 以及 `cat world.txt` 來查看檔案內容
|
||||
- ➤ 完成後輸入 `echo continue` 來繼續
|
||||
- ➤ 完成後輸入 `echo next` 來繼續
|
||||
|
||||
- id: continue
|
||||
- id: next
|
||||
requirements:
|
||||
- start
|
||||
task: 查看檔案內容,完成後輸入 `echo continue` 來繼續
|
||||
task: 查看檔案內容,完成後輸入 `echo next` 來繼續
|
||||
condition:
|
||||
command:
|
||||
- ^echo continue$
|
||||
- ^echo next$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 現在我們要來學習如何移動檔案與資料夾。
|
||||
- '`mv` 指令是移動 move 的縮寫,可以用來移動檔案與資料夾。'
|
||||
- 使用方式為 `mv [來源] [目的地]`。
|
||||
- 我們先來將 `hello.txt` 移動到 `notes` 資料夾中吧!
|
||||
content: |
|
||||
現在我們要來學習如何移動檔案與資料夾。
|
||||
`mv` 指令是移動 move 的縮寫,可以用來移動檔案與資料夾。
|
||||
使用方式為 `mv [來源] [目的地]`。
|
||||
我們先來將 `hello.txt` 移動到 `notes` 資料夾中吧!
|
||||
hints:
|
||||
- ➤ 使用 `mv hello.txt notes` 來將 `hello.txt` 移動到 `notes` 資料夾中
|
||||
|
||||
- id: mv-hello
|
||||
requirements:
|
||||
- continue
|
||||
- next
|
||||
task: 將 `hello.txt` 移動到 `notes` 資料夾中
|
||||
condition:
|
||||
command:
|
||||
- ^mv hello.txt notes$
|
||||
files:
|
||||
- path: notes/hello.txt
|
||||
- path: /home/commander/notes/hello.txt
|
||||
type: file
|
||||
- path: hello.txt
|
||||
- path: /home/commander/hello.txt
|
||||
exists: false
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你做得很好!
|
||||
- 我們用 `ls` 跟 `ls notes` 來查看一下資料夾內的檔案吧!
|
||||
content: |
|
||||
你做得很好!
|
||||
我們用 `ls` 跟 `ls notes` 來查看一下資料夾內的檔案吧!
|
||||
hints:
|
||||
- ➤ 使用 `ls`、`ls notes` 來查看資料夾內的檔案
|
||||
|
||||
@@ -74,8 +72,8 @@ stages:
|
||||
- ^ls$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`hello.txt` 已經不在資料夾中了!'
|
||||
content: |
|
||||
`hello.txt` 已經不在資料夾中了!
|
||||
|
||||
- id: ls-notes-hello
|
||||
requirements:
|
||||
@@ -90,43 +88,33 @@ stages:
|
||||
- hello.txt
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`hello.txt` 出現在 `notes` 資料夾中了!'
|
||||
|
||||
- id: continue-mv
|
||||
requirements:
|
||||
- ls-no-hello
|
||||
- ls-notes-hello
|
||||
task: 輸入 `echo continue` 來繼續
|
||||
condition:
|
||||
command:
|
||||
- ^echo continue$
|
||||
responses:
|
||||
content: |
|
||||
`hello.txt` 出現在 `notes` 資料夾中了!
|
||||
- type: narrative
|
||||
content:
|
||||
- '`mv` 指令也可以用來更改檔案名稱。'
|
||||
- '只要將目的地改成新的檔案名稱就可以了。'
|
||||
- 現在我們要將 `world.txt` 更名為 `wow.txt`。
|
||||
content: |
|
||||
`mv` 指令也可以用來更改檔案名稱。
|
||||
只要將目的地改成新的檔案名稱就可以了。
|
||||
現在我們要將 `world.txt` 更名為 `wow.txt`。
|
||||
hints:
|
||||
- ➤ 使用 `mv world.txt wow.txt` 來將 `world.txt` 更名為 `wow.txt`
|
||||
|
||||
- id: mv-world
|
||||
requirements:
|
||||
- continue-mv
|
||||
- ls-notes-hello
|
||||
task: 將 `world.txt` 更名為 `wow.txt`
|
||||
condition:
|
||||
command:
|
||||
- ^mv
|
||||
files:
|
||||
- path: wow.txt
|
||||
- path: /home/commander/wow.txt
|
||||
type: file
|
||||
- path: world.txt
|
||||
- path: /home/commander/world.txt
|
||||
exists: false
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你做得很好!
|
||||
- 我們用 `ls` 來確認一下資料夾內的檔案吧!
|
||||
content: |
|
||||
你做得很好!
|
||||
我們用 `ls` 來確認一下資料夾內的檔案吧!
|
||||
hints:
|
||||
- ➤ 使用 `ls` 來查看資料夾內的檔案
|
||||
|
||||
@@ -143,12 +131,12 @@ stages:
|
||||
- wow.txt
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`world.txt` 現在已經變成 `wow.txt` 了。'
|
||||
- 接下來我們來學習如何移動資料夾吧!
|
||||
- 移動資料夾非常簡單,方法跟移動檔案一樣。
|
||||
- '`mv` 指令會自動將資料夾內的所有檔案一起移動'
|
||||
- 我們先來將 `notes` 資料夾改名叫作 `docs` 吧!
|
||||
content: |
|
||||
`world.txt` 現在已經變成 `wow.txt` 了。
|
||||
接下來我們來學習如何移動資料夾吧!
|
||||
移動資料夾非常簡單,方法跟移動檔案一樣。
|
||||
`mv` 指令會自動將資料夾內的所有檔案一起移動
|
||||
我們先來將 `notes` 資料夾改名叫作 `docs` 吧!
|
||||
hints:
|
||||
- ➤ 使用 `mv notes docs` 來將 `notes` 資料夾改名叫作 `docs`
|
||||
|
||||
@@ -160,15 +148,15 @@ stages:
|
||||
command:
|
||||
- ^mv
|
||||
files:
|
||||
- path: docs
|
||||
- path: /home/commander/docs
|
||||
type: directory
|
||||
- path: notes
|
||||
- path: /home/commander/notes
|
||||
exists: false
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你做得很好!
|
||||
- 我們用 `ls` 來確認一下資料夾內的檔案吧!
|
||||
content: |
|
||||
你做得很好!
|
||||
我們用 `ls` 來確認一下資料夾內的檔案吧!
|
||||
hints:
|
||||
- ➤ 使用 `ls` 來查看資料夾內的檔案
|
||||
|
||||
@@ -185,11 +173,11 @@ stages:
|
||||
- docs
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`notes` 資料夾已經變成 `docs` 了。'
|
||||
- 那麼最後來考考你:
|
||||
- 試著將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`。
|
||||
- 如果你不知道怎麼做,可以輸入 `echo hint` 來取得提示。
|
||||
content: |
|
||||
`notes` 資料夾已經變成 `docs` 了。
|
||||
那麼最後來考考你:
|
||||
試著將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`。
|
||||
如果你不知道怎麼做,可以輸入 `echo hint` 來取得提示。
|
||||
|
||||
- id: END
|
||||
requirements:
|
||||
@@ -199,9 +187,9 @@ stages:
|
||||
command:
|
||||
- ^mv
|
||||
files:
|
||||
- path: hi.txt
|
||||
- path: /home/commander/hi.txt
|
||||
type: file
|
||||
- path: docs/hello.txt
|
||||
- path: /home/commander/docs/hello.txt
|
||||
exists: false
|
||||
exceptions:
|
||||
- condition:
|
||||
@@ -209,12 +197,12 @@ stages:
|
||||
- ^echo hint$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你可以使用 `mv docs/hello.txt hi.txt` 來完成這個任務。
|
||||
content: |
|
||||
你可以使用 `mv docs/hello.txt hi.txt` 來完成這個任務。
|
||||
hints:
|
||||
- ➤ 使用 `mv docs/hello.txt hi.txt` 來將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你做得很好!
|
||||
- 你已經學會如何移動檔案與資料夾了!
|
||||
content: |
|
||||
你做得很好!
|
||||
你已經學會如何移動檔案與資料夾了!
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY home ${HOME}
|
||||
+51
-53
@@ -4,24 +4,22 @@ instruction: |
|
||||
1. 絕對路徑和相對路徑
|
||||
2. 如何使用 `pwd` 顯示目前的路徑
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- cast
|
||||
- explore
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
task: 按下 `開始` 按鈕開始教程
|
||||
condition:
|
||||
output:
|
||||
- ^start$
|
||||
command:
|
||||
- ^echo start$
|
||||
responses:
|
||||
- content:
|
||||
- 在 Linux 系統中,每個資料夾和檔案都有獨一無二的路徑。
|
||||
- 而路徑又分為相對路徑和絕對路徑。
|
||||
- 相對路徑是從目前所在的資料夾開始的路徑,而絕對路徑則是從根目錄開始的路徑。
|
||||
- 請你先用 `ls` 指令查看這個資料夾中的檔案。
|
||||
- content: |
|
||||
在 Linux 系統中,每個資料夾和檔案都有獨一無二的路徑。
|
||||
而路徑又分為相對路徑和絕對路徑。
|
||||
相對路徑是從目前所在的資料夾開始的路徑,而絕對路徑則是從根目錄開始的路徑。
|
||||
請你先用 `ls` 指令查看這個資料夾中的檔案。
|
||||
|
||||
- id: ls
|
||||
task: 查看資料夾中的檔案
|
||||
@@ -33,8 +31,8 @@ stages:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- 現在你使用 `cd` 指令進入 `folder1` 資料夾。
|
||||
- content: |
|
||||
現在你使用 `cd` 指令進入 `folder1` 資料夾。
|
||||
|
||||
- id: cd-folder1
|
||||
task: 進入 `folder1` 資料夾
|
||||
@@ -44,14 +42,14 @@ stages:
|
||||
pwd:
|
||||
- ^/home/commander/folder1$
|
||||
responses:
|
||||
- content:
|
||||
- 你進入了 `folder1` 資料夾。
|
||||
- 在進入 `folder1` 資料夾時,使用的 `cd folder1` 中的 `folder1` 就是相對路徑。
|
||||
- 相同的道理 `cd ..` 中的 `..` 也是相對路徑。
|
||||
- 相對路徑可以不只有一層資料夾。
|
||||
- 例如:現在想要進入 `folder2` 資料夾的話,可以使用 `cd ..` 回到上一層資料夾,再使用 `cd folder2` 進入 `folder2` 資料夾。
|
||||
- 然而,你也可以用 `/` 將 `..` 和 `folder2` 依序連接起來,使用 `cd ../folder2` 來一次進入 `folder2` 資料夾。
|
||||
- 現在試著用這個方式進入 `folder2` 資料夾看看吧。
|
||||
- content: |
|
||||
你進入了 `folder1` 資料夾。
|
||||
在進入 `folder1` 資料夾時,使用的 `cd folder1` 中的 `folder1` 就是相對路徑。
|
||||
相同的道理 `cd ..` 中的 `..` 也是相對路徑。
|
||||
相對路徑可以不只有一層資料夾。
|
||||
例如:現在想要進入 `folder2` 資料夾的話,可以使用 `cd ..` 回到上一層資料夾,再使用 `cd folder2` 進入 `folder2` 資料夾。
|
||||
然而,你也可以用 `/` 將 `..` 和 `folder2` 依序連接起來,使用 `cd ../folder2` 來一次進入 `folder2` 資料夾。
|
||||
現在試著用這個方式進入 `folder2` 資料夾看看吧。
|
||||
hints:
|
||||
- ➤ 請輸入 `cd ../folder2` 來進入 `folder2` 資料夾。
|
||||
|
||||
@@ -68,14 +66,14 @@ stages:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 輸入 `cd folder2` 來進入 `folder2` 資料夾
|
||||
content: |
|
||||
輸入 `cd folder2` 來進入 `folder2` 資料夾
|
||||
responses:
|
||||
- content:
|
||||
- 你成功的用相對路徑進入了 `folder2` 資料夾。
|
||||
- 而你也能夠在 `ls` 後加上相對路徑來查看其他資料夾的內容。
|
||||
- 例如:現在你想要查看 `folder1` 資料夾中有哪些檔案,你可以使用 `ls ../folder1` 來查看。
|
||||
- 試著用這個方式查看 `folder1` 資料夾中的檔案吧。
|
||||
- content: |
|
||||
你成功的用相對路徑進入了 `folder2` 資料夾。
|
||||
而你也能夠在 `ls` 後加上相對路徑來查看其他資料夾的內容。
|
||||
例如:現在你想要查看 `folder1` 資料夾中有哪些檔案,你可以使用 `ls ../folder1` 來查看。
|
||||
試著用這個方式查看 `folder1` 資料夾中的檔案吧。
|
||||
hints:
|
||||
- ➤ 請輸入 `ls ../folder1` 來查看 `folder1` 資料夾中的檔案。
|
||||
|
||||
@@ -93,16 +91,16 @@ stages:
|
||||
- ^/home/commander/folder2$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 進到 `folder1` 資料夾並用 `ls` 查看內容。
|
||||
content: |
|
||||
進到 `folder1` 資料夾並用 `ls` 查看內容。
|
||||
responses:
|
||||
- content:
|
||||
- 很好,你現在知道如何運用相對路徑了。
|
||||
- 接下來要介紹的是絕對路徑。
|
||||
- 絕對路徑是從根目錄開始的路徑,根目錄是位於檔案樹的最上層的資料夾。
|
||||
- 你可以使用 `pwd` 來查看目前所在的絕對路徑。
|
||||
- '`pwd` 的意思是 `print working directory`,也就是顯示目前所在的路徑。'
|
||||
- 現在請你使用 `pwd` 來查看目前所在的絕對路徑。
|
||||
- content: |
|
||||
很好,你現在知道如何運用相對路徑了。
|
||||
接下來要介紹的是絕對路徑。
|
||||
絕對路徑是從根目錄開始的路徑,根目錄是位於檔案樹的最上層的資料夾。
|
||||
你可以使用 `pwd` 來查看目前所在的絕對路徑。
|
||||
`pwd` 的意思是 `print working directory`,也就是顯示目前所在的路徑。
|
||||
現在請你使用 `pwd` 來查看目前所在的絕對路徑。
|
||||
hints:
|
||||
- ➤ 輸入 `pwd` 查看目前所在的絕對路徑
|
||||
|
||||
@@ -122,17 +120,17 @@ stages:
|
||||
- ^/home/commander/folder2$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 請先回到 `folder2` 資料夾,並用 `pwd` 來查看目前所在的絕對路徑
|
||||
content: |
|
||||
請先回到 `folder2` 資料夾,並用 `pwd` 來查看目前所在的絕對路徑
|
||||
responses:
|
||||
- content:
|
||||
- 你可以看到你現在的絕對路徑是 `/home/commander/folder2`。
|
||||
- 而絕對路徑其實也顯示在你的命令提示符中。
|
||||
- '`commander:~/folder2 $` 中的 `~/folder2` 就是你的絕對路徑。'
|
||||
- 而 `~` 則是 `/home/commander` 這個絕對路徑的縮寫,又稱為家目錄,你每次遊戲都會從這裡開始。
|
||||
- '`ls` 和 `cd` 指令也可以使用絕對路徑。'
|
||||
- 例如:你可以使用 `ls ~` 來查看家目錄的內容,或使用 `cd ~` 來回到家目錄。
|
||||
- 現在請你使用 `ls ~` 來查看家目錄的內容。
|
||||
- content: |
|
||||
你可以看到你現在的絕對路徑是 `/home/commander/folder2`。
|
||||
而絕對路徑其實也顯示在你的命令提示符中。
|
||||
`commander:~/folder2 $` 中的 `~/folder2` 就是你的絕對路徑。
|
||||
而 `~` 則是 `/home/commander` 這個絕對路徑的縮寫,又稱為家目錄,你每次遊戲都會從這裡開始。
|
||||
`ls` 和 `cd` 指令也可以使用絕對路徑。
|
||||
例如:你可以使用 `ls ~` 來查看家目錄的內容,或使用 `cd ~` 來回到家目錄。
|
||||
現在請你使用 `ls ~` 來查看家目錄的內容。
|
||||
hints:
|
||||
- ➤ 輸入 `ls ~` 來查看家目錄的內容
|
||||
|
||||
@@ -144,8 +142,8 @@ stages:
|
||||
command:
|
||||
- ^ls ~$
|
||||
responses:
|
||||
- content:
|
||||
- 現在請使用 `cd ~` 回到家目錄。
|
||||
- content: |
|
||||
現在請使用 `cd ~` 回到家目錄。
|
||||
hints:
|
||||
- ➤ 輸入 `cd ~` 來回到家目錄
|
||||
|
||||
@@ -162,9 +160,9 @@ stages:
|
||||
- ^/home/commander$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 請使用 `cd ~` 回到家目錄
|
||||
content: |
|
||||
請使用 `cd ~` 回到家目錄
|
||||
responses:
|
||||
- content:
|
||||
- 你回到家目錄了。
|
||||
- 恭喜!現在你已經知道如何運用絕對路徑和相對路徑,以及查看目前所在的絕對路徑了。
|
||||
- content: |
|
||||
你回到家目錄了。
|
||||
恭喜!現在你已經知道如何運用絕對路徑和相對路徑,以及查看目前所在的絕對路徑了。
|
||||
@@ -1,3 +0,0 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY home ${HOME}
|
||||
+30
-30
@@ -3,24 +3,22 @@ instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 如何使用 `cat` 查看檔案的內容
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- path
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始教程
|
||||
task: 按下 `開始` 按鈕開始教程
|
||||
condition:
|
||||
output:
|
||||
- ^start$
|
||||
command:
|
||||
- ^echo start$
|
||||
responses:
|
||||
- content:
|
||||
- 在終端機中,並沒有圖形介面可以讓你查看檔案的內容。
|
||||
- 但是你可以透過 `cat` 指令來查看檔案的內容。
|
||||
- '`cat` 指令是 concatenate 的縮寫,也就是串接的意思。'
|
||||
- 在輸入 `cat [檔案名稱]` 指令後,終端機會輸出檔案的內容。
|
||||
- 請你先查看這個資料夾中哪些檔案。
|
||||
- content: |
|
||||
在終端機中,並沒有圖形介面可以讓你查看檔案的內容。
|
||||
但是你可以透過 `cat` 指令來查看檔案的內容。
|
||||
`cat` 指令是 concatenate 的縮寫,也就是串接的意思。
|
||||
在輸入 `cat [檔案名稱]` 指令後,終端機會輸出檔案的內容。
|
||||
請你先查看這個資料夾中哪些檔案。
|
||||
hints:
|
||||
- ➤ 請輸入 `ls` 來查看這個資料夾中的檔案。
|
||||
|
||||
@@ -34,9 +32,9 @@ stages:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- 這個資料夾中有一個檔案和一個資料夾。
|
||||
- 現在你可以使用 `cat` 指令來查看 `file1.txt` 的內容。
|
||||
- content: |
|
||||
這個資料夾中有一個檔案和一個資料夾。
|
||||
現在你可以使用 `cat` 指令來查看 `file1.txt` 的內容。
|
||||
hints:
|
||||
- ➤ 請輸入 `cat file1.txt` 來查看檔案的內容。
|
||||
|
||||
@@ -50,10 +48,10 @@ stages:
|
||||
command:
|
||||
- ^cat file1.txt$
|
||||
responses:
|
||||
- content:
|
||||
- 你應該能看到檔案的內容了。
|
||||
- '`cat` 指令也可以使用路徑來查看檔案內容'
|
||||
- 請你先用 `ls folder` 查看資料夾內的檔案。
|
||||
- content: |
|
||||
你應該能看到檔案的內容了。
|
||||
`cat` 指令也可以使用路徑來查看檔案內容
|
||||
請你先用 `ls folder` 查看資料夾內的檔案。
|
||||
hints:
|
||||
- ➤ 輸入 `ls folder` 查看資料夾內的檔案
|
||||
|
||||
@@ -71,13 +69,13 @@ stages:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
content:
|
||||
- 請留在 `/home/commander` 資料夾中。
|
||||
- 請輸入 `cd ~` 回到 `/home/commander` 資料夾。
|
||||
content: |
|
||||
請留在 `/home/commander` 資料夾中。
|
||||
請輸入 `cd ~` 回到 `/home/commander` 資料夾。
|
||||
responses:
|
||||
- content:
|
||||
- 你現在知道 `folder` 資料夾的內容了。
|
||||
- 請用 `cat` 指令來查看 `folder` 資料夾中的 `file.txt` 檔案。
|
||||
- content: |
|
||||
你現在知道 `folder` 資料夾的內容了。
|
||||
請用 `cat` 指令來查看 `folder` 資料夾中的 `file.txt` 檔案。
|
||||
hints:
|
||||
- ➤ 輸入 `cat folder/file.txt` 查看 `file.txt` 檔案的內容。
|
||||
|
||||
@@ -95,10 +93,12 @@ stages:
|
||||
not:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
content:
|
||||
- 請留在 `/home/commander` 資料夾中。
|
||||
- 請輸入 `cd ~` 回到 `/home/commander` 資料夾。
|
||||
responses:
|
||||
- type: narrative
|
||||
content: |
|
||||
請留在 `/home/commander` 資料夾中。
|
||||
請輸入 `cd ~` 回到 `/home/commander` 資料夾。
|
||||
responses:
|
||||
- content:
|
||||
- 你成功地查看到 `file.txt` 的內容了。
|
||||
- 你已經會使用 `cat` 指令查看檔案的內容了。
|
||||
- content: |
|
||||
你成功地查看到 `file.txt` 的內容了。
|
||||
你已經會使用 `cat` 指令查看檔案的內容了。
|
||||
@@ -1,4 +0,0 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY --chown=${USERNAME} home ${HOME}
|
||||
RUN mkdir empty
|
||||
@@ -1,4 +1,5 @@
|
||||
echo start
|
||||
ls
|
||||
rm hello.txt
|
||||
ls
|
||||
rm world.txt wow.txt
|
||||
|
||||
+91
-89
@@ -1,12 +1,9 @@
|
||||
title: 刪除檔案與資料夾
|
||||
instruction: |
|
||||
在這個關卡中,你將會學到:
|
||||
1. 用 `rm` 指令刪除檔案
|
||||
2. 用 `rmdir` 與資料夾
|
||||
3. 用 `rm -r` 刪除資料夾與它的所有檔案
|
||||
現在資料夾裡有一些檔案與資料夾,請你使用 `ls` 跟 `cat` 來查看它們的內容。
|
||||
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
1. 如何使用 `rm` 指令刪除檔案
|
||||
2. 如何使用 `rmdir` 刪除空資料夾
|
||||
3. 如何使用 `rm -r` 刪除資料夾與其中的所有檔案
|
||||
|
||||
requirements:
|
||||
- copy
|
||||
@@ -14,33 +11,44 @@ requirements:
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始
|
||||
task: 按下 `開始` 按鈕開始
|
||||
condition:
|
||||
command:
|
||||
- ^echo
|
||||
output:
|
||||
- start
|
||||
- ^echo start$
|
||||
responses:
|
||||
- content:
|
||||
- 今天我們要學習如何刪除檔案與資料夾。
|
||||
- '首先要來學習 `rm` 指令。`rm` 指令可以用來刪除檔案。使用方式是 `rm [檔案名稱]`。'
|
||||
- '現在請你使用 `rm` 指令刪除 `hello.txt`。'
|
||||
- content: |
|
||||
今天我們要學習如何刪除檔案與資料夾。
|
||||
請先使用 `ls` 指令查看資料夾內容。
|
||||
hints:
|
||||
- ➤ 使用 `ls` 來查看資料夾內容。
|
||||
|
||||
- id: ls
|
||||
requirements:
|
||||
- start
|
||||
task: 使用 `ls` 查看資料夾內容
|
||||
condition:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content: |
|
||||
首先要來學習 `rm` 指令。`rm` 指令可以用來刪除檔案。使用方式是 `rm [檔案名稱]`。
|
||||
現在請你使用 `rm` 指令刪除 `hello.txt`。
|
||||
hints:
|
||||
- ➤ 使用 `rm hello.txt` 來刪除 `hello.txt`。
|
||||
|
||||
- id: rm-hello
|
||||
requirements:
|
||||
- start
|
||||
- ls
|
||||
task: 使用 `rm` 刪除 `hello.txt`
|
||||
condition:
|
||||
command:
|
||||
- ^rm
|
||||
- ^rm\s
|
||||
files:
|
||||
- path: hello.txt
|
||||
- path: /home/commander/hello.txt
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!現在請你使用 `ls` 指令來查看資料夾內容。'
|
||||
- content: |
|
||||
很好!現在請你使用 `ls` 指令來查看資料夾內容。
|
||||
|
||||
- id: ls-no-hello
|
||||
requirements:
|
||||
@@ -48,15 +56,15 @@ stages:
|
||||
task: 使用 `ls` 查看資料夾內容
|
||||
condition:
|
||||
command:
|
||||
- ^ls
|
||||
- ^ls$
|
||||
files:
|
||||
- path: hello.txt
|
||||
- path: /home/commander/hello.txt
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '你會發現 `hello.txt` 已經不見了。'
|
||||
- '`rm` 也可以同時刪除多個檔案,只要把檔案名稱用空白隔開就可以了,也就是 `rm [檔案1] [檔案2] ...`。'
|
||||
- 現在請你使用 `rm` 刪除 `world.txt` 與 `wow.txt`。
|
||||
- content: |
|
||||
你會發現 `hello.txt` 已經不見了。
|
||||
`rm` 也可以同時刪除多個檔案,只要把檔案名稱用空白隔開就可以了,也就是 `rm [檔案1] [檔案2] ...`。
|
||||
現在請你使用 `rm` 刪除 `world.txt` 與 `wow.txt`。
|
||||
hints:
|
||||
- ➤ 使用 `rm world.txt wow.txt` 來刪除 `world.txt` 與 `wow.txt`。
|
||||
- ➤ 檔案名稱之間要用空白隔開。
|
||||
@@ -70,13 +78,13 @@ stages:
|
||||
command:
|
||||
- ^rm
|
||||
files:
|
||||
- path: world.txt
|
||||
- path: /home/commander/world.txt
|
||||
exists: false
|
||||
- path: wow.txt
|
||||
- path: /home/commander/wow.txt
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!我們一樣用 `ls` 指令確認一下資料夾內容。'
|
||||
- content: |
|
||||
很好!我們一樣用 `ls` 指令確認一下資料夾內容。
|
||||
hints:
|
||||
- ➤ 使用 `ls` 來查看資料夾內容。
|
||||
|
||||
@@ -88,10 +96,10 @@ stages:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- '你會發現 `world.txt` 與 `wow.txt` 已經不見了。'
|
||||
- '現在我們來學習 `rmdir` 指令。`rmdir` 指令可以用來刪除資料夾。使用方式是 `rmdir [資料夾名稱]`。'
|
||||
- '現在請你使用 `rmdir` 指令刪除 `empty` 資料夾。'
|
||||
- content: |
|
||||
你會發現 `world.txt` 與 `wow.txt` 已經不見了。
|
||||
現在我們來學習 `rmdir` 指令。`rmdir` 指令可以用來刪除資料夾。使用方式是 `rmdir [資料夾名稱]`。
|
||||
現在請你使用 `rmdir` 指令刪除 `empty` 資料夾。
|
||||
hints:
|
||||
- ➤ 使用 `rmdir empty` 來刪除 `empty` 資料夾。
|
||||
|
||||
@@ -101,13 +109,13 @@ stages:
|
||||
task: 使用 `rmdir` 刪除 `empty` 資料夾
|
||||
condition:
|
||||
command:
|
||||
- ^rmdir
|
||||
- ^rmdir empty$
|
||||
files:
|
||||
- path: empty
|
||||
- path: /home/commander/empty
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!現在請你使用 `ls` 指令來查看資料夾內容。'
|
||||
- content: |
|
||||
很好!現在請你使用 `ls` 指令來查看資料夾內容。
|
||||
|
||||
- id: ls-no-empty
|
||||
requirements:
|
||||
@@ -117,9 +125,9 @@ stages:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- '你會發現 `empty` 資料夾已經不見了。'
|
||||
- '現在請你試試看 `rmdir` 指令刪除 `notes` 資料夾。'
|
||||
- content: |
|
||||
你會發現 `empty` 資料夾已經不見了。
|
||||
現在請你試試看 `rmdir` 指令刪除 `notes` 資料夾。
|
||||
hints:
|
||||
- ➤ 使用 `rmdir notes` 來刪除 `notes` 資料夾
|
||||
|
||||
@@ -131,16 +139,16 @@ stages:
|
||||
command:
|
||||
- ^rmdir notes$
|
||||
files:
|
||||
- path: notes
|
||||
- path: /home/commander/notes
|
||||
type: folder
|
||||
exists: true
|
||||
responses:
|
||||
- content:
|
||||
- 指令執行失敗了!
|
||||
- '你會發現 `notes` 資料夾還在。'
|
||||
- '這是因為 `rmdir` 指令只能刪除空的資料夾。'
|
||||
- 我們先來將 `notes` 資料夾的檔案刪除再來試一次。
|
||||
- '現在請你使用 `rm` 指令刪除 `notes` 資料夾內的檔案。'
|
||||
- content: |
|
||||
指令執行失敗了!
|
||||
你會發現 `notes` 資料夾還在。
|
||||
這是因為 `rmdir` 指令只能刪除空的資料夾。
|
||||
我們先來將 `notes` 資料夾的檔案刪除再來試一次。
|
||||
現在請你使用 `rm` 指令刪除 `notes` 資料夾內的檔案。
|
||||
hints:
|
||||
- ➤ 使用 `rm notes/[檔案名稱]` 來刪除 `notes` 資料夾內的檔案。
|
||||
- ➤ 你可能需要使用 `ls notes` 來查看 `notes` 資料夾內有哪些檔案。
|
||||
@@ -153,41 +161,39 @@ stages:
|
||||
command:
|
||||
- ^rm
|
||||
files:
|
||||
- path: notes
|
||||
- path: /home/commander/notes
|
||||
type: folder
|
||||
exists: true
|
||||
- path: notes/note.txt
|
||||
- path: /home/commander/notes/note.txt
|
||||
exists: false
|
||||
exceptions:
|
||||
- condition:
|
||||
command:
|
||||
- ^rm notes/\[.*\]
|
||||
responses:
|
||||
- content:
|
||||
- '你好像理解錯了。'
|
||||
- 這邊中括號包起來 `[檔案名稱]` 是參數的意思,需要你自己填入檔案的名稱。
|
||||
- 實際打指令的時候,不要打中括號 `[]` 喔!
|
||||
- '例如,如果要刪除 `notes` 資料夾內的 `note.txt` 檔案,指令應該要是 `rm notes/note.txt`。'
|
||||
- 請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!
|
||||
- content: |
|
||||
你好像理解錯了。
|
||||
這邊中括號包起來 `[檔案名稱]` 是參數的意思,需要你自己填入檔案的名稱。
|
||||
實際打指令的時候,不要打中括號 `[]` 喔!
|
||||
例如,如果要刪除 `notes` 資料夾內的 `note.txt` 檔案,指令應該要是 `rm notes/note.txt`。
|
||||
請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!
|
||||
- condition:
|
||||
command:
|
||||
- ^rm
|
||||
error:
|
||||
- 'No such file or directory'
|
||||
- ^rmdir notes$
|
||||
responses:
|
||||
- content:
|
||||
- 這個錯誤的意思是找不到檔案
|
||||
- '請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!'
|
||||
- content: |
|
||||
這個錯誤的意思是找不到檔案
|
||||
請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!
|
||||
- condition:
|
||||
command:
|
||||
- ^ls notes$
|
||||
responses:
|
||||
- content:
|
||||
- '`notes` 資料夾中有著 `note.txt` 檔案。'
|
||||
- 那我們就使用 `rm notes/note.txt` 來刪除 `note.txt` 檔案吧!
|
||||
- content: |
|
||||
`notes` 資料夾中有著 `note.txt` 檔案。
|
||||
那我們就使用 `rm notes/note.txt` 來刪除 `note.txt` 檔案吧!
|
||||
responses:
|
||||
- content:
|
||||
- '很好!現在我們再使用 `rmdir` 指令刪除 `notes` 資料夾。'
|
||||
- content: |
|
||||
很好!現在我們再使用 `rmdir` 指令刪除 `notes` 資料夾。
|
||||
hints:
|
||||
- ➤ 使用 `rmdir notes` 來刪除 `notes` 資料夾。
|
||||
|
||||
@@ -196,16 +202,14 @@ stages:
|
||||
- rm-notes-content
|
||||
task: 使用 `rmdir` 刪除 `notes` 資料夾
|
||||
condition:
|
||||
command:
|
||||
- ^rmdir
|
||||
files:
|
||||
- path: notes
|
||||
- path: /home/commander/notes
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!`notes` 資料夾已經被刪除了。'
|
||||
- 接下來我們來刪除 `trash` 資料夾。
|
||||
- 先使用 `ls` 指令查看一下 `trash` 資料夾內容。
|
||||
- content: |
|
||||
很好!`notes` 資料夾已經被刪除了。
|
||||
接下來我們來刪除 `trash` 資料夾。
|
||||
先使用 `ls` 指令查看一下 `trash` 資料夾內容。
|
||||
hints:
|
||||
- ➤ 使用 `ls trash` 來查看 `trash` 中的檔案。
|
||||
|
||||
@@ -215,16 +219,14 @@ stages:
|
||||
task: 使用 `ls` 查看 `trash` 資料夾內容
|
||||
condition:
|
||||
command:
|
||||
- ^ls
|
||||
output:
|
||||
- garbage.txt
|
||||
- ^ls trash$
|
||||
responses:
|
||||
- content:
|
||||
- 天啊!`trash` 資料夾裡有好多檔案,甚至還有其他的資料夾!
|
||||
- 如果今天資料夾中有很多檔案與資料夾,我們不想一個一個刪除,該怎麼辦呢?
|
||||
- 跟 `cp` 的時候一樣,這時候我們可以加上 `-r` 的選項來刪除資料夾與它的所有檔案。
|
||||
- '`-r` 的意思是遞迴(recursive),也就是會把資料夾中的所有檔案都刪除。'
|
||||
- 我們來試試看吧!
|
||||
- content: |
|
||||
天啊!`trash` 資料夾裡有好多檔案,甚至還有其他的資料夾!
|
||||
如果今天資料夾中有很多檔案與資料夾,我們不想一個一個刪除,該怎麼辦呢?
|
||||
跟 `cp` 的時候一樣,這時候我們可以加上 `-r` 的選項來刪除資料夾與它的所有檔案。
|
||||
`-r` 的意思是遞迴(recursive),也就是會把資料夾中的所有檔案都刪除。
|
||||
我們來試試看吧!
|
||||
hints:
|
||||
- ➤ 使用 `rm -r trash` 來刪除 `trash` 資料夾。
|
||||
|
||||
@@ -234,13 +236,13 @@ stages:
|
||||
task: 使用 `rm -r` 刪除 `trash` 資料夾
|
||||
condition:
|
||||
command:
|
||||
- ^rm -r
|
||||
- ^rm -r trash$
|
||||
files:
|
||||
- path: trash
|
||||
- path: /home/commander/trash
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!現在請你使用 `ls` 指令查看資料夾內容。'
|
||||
- content: |
|
||||
很好!現在請你使用 `ls` 指令查看資料夾內容。
|
||||
hints:
|
||||
- ➤ 使用 `ls` 來查看資料夾內容。
|
||||
|
||||
@@ -252,6 +254,6 @@ stages:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- '`trash` 資料夾已經不見了。'
|
||||
- 恭喜你!你已經學會如何刪除檔案與資料夾了!
|
||||
- content: |
|
||||
`trash` 資料夾已經不見了。
|
||||
恭喜你!你已經學會如何刪除檔案與資料夾了!
|
||||
Loaded 100 of 129 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user