diff --git a/app/src/components/QuestIntro.vue b/app/src/components/QuestIntro.vue index 20dac85..c76923c 100644 --- a/app/src/components/QuestIntro.vue +++ b/app/src/components/QuestIntro.vue @@ -6,7 +6,7 @@ import { ref, defineProps, onMounted } from 'vue' import MarkdownText from './MarkdownText.vue' import { trpc } from '../utils/trpc' -const { t } = useI18n() +const { t, locale } = useI18n() const props = defineProps({ questTitle: { @@ -52,8 +52,15 @@ const handleColour = () => { } } onMounted(async () => { - const quest = await trpc.quests.getQuestDetail.query(questId) - questInstruction.value = quest.instruction + try { + const quest = await trpc.quests.getQuestDetail.query({ + questId, + locale: locale.value, + }) + questInstruction.value = quest.instruction + } catch (error) { + toast.error('Failed to load quest detail:', error) + } handleColour() }) @@ -83,7 +90,7 @@ onMounted(async () => { 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" + class="inline-flex justify-center rounded-lg text-base font-black py-3 mt-5 w-full" > {{ t('quest.start') }} diff --git a/app/src/components/QuestMap.vue b/app/src/components/QuestMap.vue index 7237031..bf23cd0 100644 --- a/app/src/components/QuestMap.vue +++ b/app/src/components/QuestMap.vue @@ -7,7 +7,7 @@ import { trpc } from '../utils/trpc' import useUserProfile from '../store/userProfile' import QuestIntro from './QuestIntro.vue' -const { t } = useI18n() +const { t, locale } = useI18n() const store = useUserProfile() const toast = useToast() @@ -66,7 +66,8 @@ async function computeGraphData() { if (!store.progress) return try { - const quests = await trpc.quests.getQuests.query() + const quests = await trpc.quests.getQuests.query({ locale: locale.value }) + const dag = new DAG(quests) const nodesValues = dag.getNodes() @@ -117,6 +118,10 @@ onMounted(async () => { }) watch(() => store.progress, computeGraphData, { deep: true }) +watch(locale, () => { + opened.value = null + computeGraphData() +}) const curvedPath = computed(() => { return (source: Node, target: Node) => { diff --git a/app/src/components/game/ControlPalette.vue b/app/src/components/game/ControlPalette.vue index dd80732..9bd2542 100644 --- a/app/src/components/game/ControlPalette.vue +++ b/app/src/components/game/ControlPalette.vue @@ -4,15 +4,44 @@ import { useRouter } from 'vue-router' import { useToast } from 'vue-toastification' import { useSession } from '../../store/session' import { openBugReport, openQuestSurvey } from '../../utils/formbricks' +import { trpc } from '../../utils/trpc' +import type { QuestLocale } from '../../../../packages/constants' -const { t } = useI18n() +const { t, locale } = useI18n() +const toast = useToast() const router = useRouter() const sessionStore = useSession() const reset = async () => { console.log('resetting...') - await sessionStore.createSession() + const selectedLocale = locale.value as QuestLocale + const questId = sessionStore.session?.quest + + if (!questId) return + + try { + const quests = await trpc.quests.getQuests.query({ + locale: selectedLocale, + }) + + console.log({ + selectedLocale, + questId, + availableQuestIds: quests.map((quest) => quest.id), + }) + + if (!quests.some((quest) => quest.id === questId)) { + toast.warning(t('game.language_unavailable')) + return + } + + await sessionStore.createSession() + } catch (error) { + console.error('Failed to reset quest:', error) + toast.error(t('game.reset_failed')) + } } + const continuePlay = async () => { try { router.push({ name: 'map' }) diff --git a/app/src/components/header/LanguageSelector.vue b/app/src/components/header/LanguageSelector.vue index 5b2ad59..81011ac 100644 --- a/app/src/components/header/LanguageSelector.vue +++ b/app/src/components/header/LanguageSelector.vue @@ -1,12 +1,17 @@ diff --git a/packages/constants/index.ts b/packages/constants/index.ts index c555e8e..75eefca 100644 --- a/packages/constants/index.ts +++ b/packages/constants/index.ts @@ -16,3 +16,10 @@ export const getSessionsRequestSchema = z.object({ }) export type GetSessionsRequest = z.infer + +export const SUPPORTED_LOCALES = ['zh-TW', 'en'] as const +export const LOCALE_NAME_MAP: Record = { + 'zh-TW': '繁體中文', + en: 'English', +} +export type QuestLocale = (typeof SUPPORTED_LOCALES)[number] diff --git a/packages/models/session.ts b/packages/models/session.ts index 30f7d4e..9872be8 100644 --- a/packages/models/session.ts +++ b/packages/models/session.ts @@ -1,4 +1,6 @@ import { model, Schema, Types } from 'mongoose' +import type { QuestLocale } from '../constants' +import { SUPPORTED_LOCALES } from '../constants' export interface INode { path: string @@ -27,6 +29,7 @@ export interface ISession { _id: Types.ObjectId user: Types.ObjectId quest: string + locale: QuestLocale containerId: string | null containerName: string | null status: 'active' | 'finished' | 'inactive' @@ -52,6 +55,11 @@ export const Session = model( ref: 'Quest', required: true, }, + locale: { + type: String, + enum: SUPPORTED_LOCALES, + required: true, + }, containerId: String, containerName: String, status: { diff --git a/quests/checkpoint-01/answer.sh b/quests/checkpoint-01/en/answer.sh similarity index 100% rename from quests/checkpoint-01/answer.sh rename to quests/checkpoint-01/en/answer.sh diff --git a/quests/checkpoint-01/en/game.yml b/quests/checkpoint-01/en/game.yml new file mode 100644 index 0000000..3715866 --- /dev/null +++ b/quests/checkpoint-01/en/game.yml @@ -0,0 +1,92 @@ +id: checkpoint-01 +title: Checkpoint 1 +image: base +instruction: | + This quest tests your understanding of: + 1. `echo`: print text + 2. `ls`: list the contents of a folder + 3. `cd`: move between folders + 4. `pwd`: show your current path + +requirements: + - path + +stages: + - id: start + name: Click `Start` to begin + condition: + command: echo start + response: + content: | + This quest checks what you have learned in the previous lessons. + You will need to choose the commands yourself. + Think about which commands can help you complete each task. + + To continue, make the terminal print `next`. + + - id: next + name: Make the terminal print `next` + requirements: + - start + condition: + output: next + response: + content: | + Now list the contents of your current folder. + + - id: ls-home + name: List the contents of the current folder + requirements: + - next + condition: + pwd: /home/commander + command: ls + response: + content: | + Enter the `folder03` folder. + + - id: cd-folder03 + name: Enter `folder03` + requirements: + - ls-home + condition: + pwd: /home/commander/folder03 + response: + content: | + Show your current path in the terminal. + + - id: pwd-folder03 + name: Show your current path + requirements: + - cd-folder03 + condition: + command: pwd + output: /home/commander/folder03 + response: + content: | + Now enter the `folder02` folder + and list its contents. + + - id: ls-folder02 + name: Enter `folder02` and list its contents + requirements: + - pwd-folder03 + condition: + pwd: /home/commander/folder02 + command: ls + response: + content: | + Finally, stay in `folder02` + and list the contents of `folder01`. + + - id: finish + name: Stay in `folder02` and list the contents of `folder01` + requirements: + - ls-folder02 + condition: + pwd: /home/commander/folder02 + output: file1.txt + response: + content: | + Congratulations! You passed this checkpoint! + You are ready to move on to the next lessons. diff --git a/quests/checkpoint-01/home/folder01/file1.txt b/quests/checkpoint-01/en/home/folder01/file1.txt similarity index 100% rename from quests/checkpoint-01/home/folder01/file1.txt rename to quests/checkpoint-01/en/home/folder01/file1.txt diff --git a/quests/checkpoint-01/home/folder02/file2.txt b/quests/checkpoint-01/en/home/folder02/file2.txt similarity index 100% rename from quests/checkpoint-01/home/folder02/file2.txt rename to quests/checkpoint-01/en/home/folder02/file2.txt diff --git a/quests/checkpoint-01/home/folder03/file3.txt b/quests/checkpoint-01/en/home/folder03/file3.txt similarity index 100% rename from quests/checkpoint-01/home/folder03/file3.txt rename to quests/checkpoint-01/en/home/folder03/file3.txt diff --git a/quests/checkpoint-01/zh-TW/answer.sh b/quests/checkpoint-01/zh-TW/answer.sh new file mode 100644 index 0000000..cee7368 --- /dev/null +++ b/quests/checkpoint-01/zh-TW/answer.sh @@ -0,0 +1,7 @@ +echo start +echo next +ls +cd folder03 +pwd +cd ../folder02 +ls ../folder01 \ No newline at end of file diff --git a/quests/checkpoint-01/game.yml b/quests/checkpoint-01/zh-TW/game.yml similarity index 100% rename from quests/checkpoint-01/game.yml rename to quests/checkpoint-01/zh-TW/game.yml diff --git a/quests/explore/home/folder1/file1.txt b/quests/checkpoint-01/zh-TW/home/folder01/file1.txt similarity index 100% rename from quests/explore/home/folder1/file1.txt rename to quests/checkpoint-01/zh-TW/home/folder01/file1.txt diff --git a/quests/explore/home/folder1/file2.txt b/quests/checkpoint-01/zh-TW/home/folder02/file2.txt similarity index 100% rename from quests/explore/home/folder1/file2.txt rename to quests/checkpoint-01/zh-TW/home/folder02/file2.txt diff --git a/quests/explore/home/folder2/file3.txt b/quests/checkpoint-01/zh-TW/home/folder03/file3.txt similarity index 100% rename from quests/explore/home/folder2/file3.txt rename to quests/checkpoint-01/zh-TW/home/folder03/file3.txt diff --git a/quests/checkpoint-02/answer.sh b/quests/checkpoint-02/en/answer.sh similarity index 100% rename from quests/checkpoint-02/answer.sh rename to quests/checkpoint-02/en/answer.sh diff --git a/quests/checkpoint-02/en/game.yml b/quests/checkpoint-02/en/game.yml new file mode 100644 index 0000000..4c3a733 --- /dev/null +++ b/quests/checkpoint-02/en/game.yml @@ -0,0 +1,97 @@ +id: checkpoint-02 +title: Checkpoint 2 +image: base +instruction: | + This quest tests your understanding of: + 1. `cat`: display the contents of a file + 2. `touch`: create a file + 3. `mkdir`: create a folder + 4. Use the `Tab` key for autocompletion + 5. Topics covered in the previous checkpoint + +requirements: + - create + +stages: + - id: start + name: Click `Start` to begin + condition: + command: echo start + response: + content: | + This quest checks what you have learned in the previous lessons. + You will need to choose the commands yourself. + Think about which commands can help you complete each task. + + To continue, make the terminal print `next`. + + - id: next + name: Make the terminal print `next` + requirements: + - start + condition: + output: next + response: + content: | + Now list the contents of your current folder. + + - id: ls-home + name: List the contents of the current folder + requirements: + - next + condition: + pwd: /home/commander + command: ls + response: + content: | + Display the contents of `hello.txt`. + + - id: cat-hello + name: Display the contents of `hello.txt` + requirements: + - ls-home + condition: + command: cat hello.txt + output: This is `hello.txt`. + response: + content: | + Display the contents of the other file, which has a very long name. + + - id: cat-long-filename + name: Display the contents of the other file + 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. + response: + content: | + Now create a folder named `folder`. + + - id: mkdir-folder + name: Create a folder named `folder` + requirements: + - cat-long-filename + condition: + files: + - path: /home/commander/folder + type: directory + exists: true + response: + content: | + Finally, inside the `folder` folder, + create a file named `file.txt`. + + - id: finish + name: Create `file.txt` inside `folder` + requirements: + - mkdir-folder + condition: + files: + - path: /home/commander/folder/file.txt + type: file + exists: true + response: + content: | + Congratulations! You passed this checkpoint! + You are ready to move on to the next lessons. diff --git a/quests/checkpoint-02/home/hello.txt b/quests/checkpoint-02/en/home/hello.txt similarity index 100% rename from quests/checkpoint-02/home/hello.txt rename to quests/checkpoint-02/en/home/hello.txt diff --git a/quests/checkpoint-02/home/pneumonoultramicroscopicsilicovolcanoconiosis.txt b/quests/checkpoint-02/en/home/pneumonoultramicroscopicsilicovolcanoconiosis.txt similarity index 100% rename from quests/checkpoint-02/home/pneumonoultramicroscopicsilicovolcanoconiosis.txt rename to quests/checkpoint-02/en/home/pneumonoultramicroscopicsilicovolcanoconiosis.txt diff --git a/quests/checkpoint-02/zh-TW/answer.sh b/quests/checkpoint-02/zh-TW/answer.sh new file mode 100644 index 0000000..314821b --- /dev/null +++ b/quests/checkpoint-02/zh-TW/answer.sh @@ -0,0 +1,7 @@ +echo start +echo next +ls +cat hello.txt +cat pneumonoultramicroscopicsilicovolcanoconiosis.txt +mkdir folder +touch folder/file.txt \ No newline at end of file diff --git a/quests/checkpoint-02/game.yml b/quests/checkpoint-02/zh-TW/game.yml similarity index 100% rename from quests/checkpoint-02/game.yml rename to quests/checkpoint-02/zh-TW/game.yml diff --git a/quests/checkpoint-03/home/folder/hello.txt b/quests/checkpoint-02/zh-TW/home/hello.txt similarity index 100% rename from quests/checkpoint-03/home/folder/hello.txt rename to quests/checkpoint-02/zh-TW/home/hello.txt diff --git a/quests/checkpoint-02/zh-TW/home/pneumonoultramicroscopicsilicovolcanoconiosis.txt b/quests/checkpoint-02/zh-TW/home/pneumonoultramicroscopicsilicovolcanoconiosis.txt new file mode 100644 index 0000000..7689edb --- /dev/null +++ b/quests/checkpoint-02/zh-TW/home/pneumonoultramicroscopicsilicovolcanoconiosis.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. diff --git a/quests/checkpoint-03/answer.sh b/quests/checkpoint-03/en/answer.sh similarity index 100% rename from quests/checkpoint-03/answer.sh rename to quests/checkpoint-03/en/answer.sh diff --git a/quests/checkpoint-03/en/game.yml b/quests/checkpoint-03/en/game.yml new file mode 100644 index 0000000..32ca74a --- /dev/null +++ b/quests/checkpoint-03/en/game.yml @@ -0,0 +1,140 @@ +id: checkpoint-03 +title: Checkpoint 3 +image: base +instruction: | + This quest tests your understanding of: + 1. `cp`: copy files and folders + 2. `mv`: move or rename files and folders + 3. `rm`: delete files and folders + 4. `rmdir`: delete empty folders + 5. Use options to change a command's behavior, such as `rm -r` + 6. Topics covered in the previous checkpoints + +requirements: + - remove + +stages: + - id: start + name: Click `Start` to begin + condition: + command: echo start + response: + content: | + This quest checks what you have learned in the previous lessons. + You will need to choose the commands yourself. + Think about which commands can help you complete each task. + + To continue, make the terminal print `next`. + + - id: next + name: Make the terminal print `next` + requirements: + - start + condition: + output: next + response: + content: | + Now list the contents of your current folder. + + - id: ls-home + name: List the contents of the current folder + requirements: + - next + condition: + pwd: /home/commander + command: ls + response: + content: | + Copy the file inside `folder` to a new file named `world.txt` in the same folder. + + - id: cp-hello-world + name: Copy the file inside `folder` as `world.txt` + requirements: + - ls-home + condition: + files: + - path: /home/commander/folder/hello.txt + type: file + exists: true + - path: /home/commander/folder/world.txt + type: file + exists: true + response: + content: | + Delete the `hello.txt` file inside `folder`. + + - id: rm-hello + name: Delete `hello.txt` inside `folder` + requirements: + - cp-hello-world + condition: + files: + - path: /home/commander/folder/hello.txt + type: file + exists: false + response: + content: | + Now move `world.txt` out of `folder`. + (Move it into your home folder, `/home/commander`.) + + - id: mv-world-out + name: Move `world.txt` out of `folder` + requirements: + - rm-hello + condition: + files: + - path: /home/commander/folder/world.txt + type: file + exists: false + - path: /home/commander/world.txt + type: file + exists: true + response: + content: | + Rename `world.txt` back to `hello.txt` + and display its contents. + + - id: cat-hello + name: Rename `world.txt` to `hello.txt` and display its contents + requirements: + - mv-world-out + condition: + command: cat hello.txt + files: + - path: /home/commander/world.txt + type: file + exists: false + - path: /home/commander/hello.txt + type: file + exists: true + output: This is `hello.txt`. + response: + content: | + Delete the `folder` folder. + + - id: rmdir-folder + name: Delete `folder` + requirements: + - cat-hello + condition: + files: + - path: /home/commander/folder + type: directory + exists: false + response: + content: | + Delete the `trash_can` folder. + + - id: finish + name: Delete `trash_can` + requirements: + - rmdir-folder + condition: + files: + - path: /home/commander/trash_can + type: directory + exists: false + response: + content: | + Congratulations! You passed this checkpoint! + You are ready to move on to the next lessons. diff --git a/quests/checkpoint-03/en/home/folder/hello.txt b/quests/checkpoint-03/en/home/folder/hello.txt new file mode 100644 index 0000000..3bd5213 --- /dev/null +++ b/quests/checkpoint-03/en/home/folder/hello.txt @@ -0,0 +1 @@ +This is `hello.txt`. diff --git a/quests/checkpoint-03/home/trash_can/garbage.txt b/quests/checkpoint-03/en/home/trash_can/garbage.txt similarity index 100% rename from quests/checkpoint-03/home/trash_can/garbage.txt rename to quests/checkpoint-03/en/home/trash_can/garbage.txt diff --git a/quests/checkpoint-03/home/trash_can/rubbish.txt b/quests/checkpoint-03/en/home/trash_can/rubbish.txt similarity index 100% rename from quests/checkpoint-03/home/trash_can/rubbish.txt rename to quests/checkpoint-03/en/home/trash_can/rubbish.txt diff --git a/quests/checkpoint-03/home/trash_can/secret/letter.txt b/quests/checkpoint-03/en/home/trash_can/secret/letter.txt similarity index 100% rename from quests/checkpoint-03/home/trash_can/secret/letter.txt rename to quests/checkpoint-03/en/home/trash_can/secret/letter.txt diff --git a/quests/checkpoint-03/home/trash_can/trash.txt b/quests/checkpoint-03/en/home/trash_can/trash.txt similarity index 100% rename from quests/checkpoint-03/home/trash_can/trash.txt rename to quests/checkpoint-03/en/home/trash_can/trash.txt diff --git a/quests/checkpoint-03/zh-TW/answer.sh b/quests/checkpoint-03/zh-TW/answer.sh new file mode 100644 index 0000000..3b39b6d --- /dev/null +++ b/quests/checkpoint-03/zh-TW/answer.sh @@ -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 \ No newline at end of file diff --git a/quests/checkpoint-03/game.yml b/quests/checkpoint-03/zh-TW/game.yml similarity index 100% rename from quests/checkpoint-03/game.yml rename to quests/checkpoint-03/zh-TW/game.yml diff --git a/quests/checkpoint-03/zh-TW/home/folder/hello.txt b/quests/checkpoint-03/zh-TW/home/folder/hello.txt new file mode 100644 index 0000000..3bd5213 --- /dev/null +++ b/quests/checkpoint-03/zh-TW/home/folder/hello.txt @@ -0,0 +1 @@ +This is `hello.txt`. diff --git a/quests/remove/home/trash/garbage.txt b/quests/checkpoint-03/zh-TW/home/trash_can/garbage.txt similarity index 100% rename from quests/remove/home/trash/garbage.txt rename to quests/checkpoint-03/zh-TW/home/trash_can/garbage.txt diff --git a/quests/remove/home/trash/rubbish.txt b/quests/checkpoint-03/zh-TW/home/trash_can/rubbish.txt similarity index 100% rename from quests/remove/home/trash/rubbish.txt rename to quests/checkpoint-03/zh-TW/home/trash_can/rubbish.txt diff --git a/quests/checkpoint-03/zh-TW/home/trash_can/secret/letter.txt b/quests/checkpoint-03/zh-TW/home/trash_can/secret/letter.txt new file mode 100644 index 0000000..b95980b --- /dev/null +++ b/quests/checkpoint-03/zh-TW/home/trash_can/secret/letter.txt @@ -0,0 +1 @@ +aHR0cHM6Ly9yZXVybC5jYy9YUjZZMDA= diff --git a/quests/explore/home/folder2/file4.txt b/quests/checkpoint-03/zh-TW/home/trash_can/trash.txt similarity index 100% rename from quests/explore/home/folder2/file4.txt rename to quests/checkpoint-03/zh-TW/home/trash_can/trash.txt diff --git a/quests/copy/answer.sh b/quests/copy/en/answer.sh similarity index 100% rename from quests/copy/answer.sh rename to quests/copy/en/answer.sh diff --git a/quests/copy/en/game.yml b/quests/copy/en/game.yml new file mode 100644 index 0000000..e8ecf01 --- /dev/null +++ b/quests/copy/en/game.yml @@ -0,0 +1,348 @@ +id: copy +title: Copying Files +image: base +instruction: | + In this quest, you will learn: + 1. How to copy files with `cp` + +requirements: + - create + +stages: + - id: start + name: Click `Start` to begin + condition: + command: echo start + response: + content: | + Today we will learn how to copy files. + First, use `ls` to see which files are in your current folder. + hint: Use `ls` to list the files in your current folder + + - id: ls + name: List the files in the current folder + requirements: + - start + condition: + pwd: /home/commander + command: ls + exceptions: + - id: wrong_directory + condition: + not: + pwd: /home/commander + response: + content: | + You are outside the `/home/commander` folder. + Use `cd /home/commander` to return to `/home/commander`. + response: + content: | + You can see `hello.txt` and `world.txt` in this folder. + Use `cat` to display their contents. + When you have finished, enter `echo next` to continue! + hint: | + Use `cat hello.txt` and `cat world.txt` to display the file contents + If a command is keeping the terminal busy, press `Ctrl + C` to cancel it + + - id: cat-hello + name: Display the contents of `hello.txt` + requirements: + - ls + condition: + pwd: /home/commander + command: cat hello.txt + output: Hello + exceptions: + - id: wrong_directory + condition: + not: + pwd: /home/commander + response: + content: | + You are outside the `/home/commander` folder. + Use `cd /home/commander` to return to `/home/commander` + - id: missing_command + condition: + command: cat + response: + content: | + To display this file, add its filename after `cat`. + If `cat` is waiting for input, press `Ctrl + C` to cancel it. + Enter `cat hello.txt` to display the contents of `hello.txt` + - id: missing_extension + condition: + command: cat hello + response: + content: | + Did you forget the `.txt` extension? + Enter `cat hello.txt` to display the contents of `hello.txt` + response: + content: | + You have displayed the contents of `hello.txt`! + Here is what it contains: + ``` + Hello + ``` + + - id: cat-world + name: Display the contents of `world.txt` + requirements: + - ls + condition: + pwd: /home/commander + command: cat world.txt + output: World + exceptions: + - id: wrong_directory + condition: + not: + pwd: /home/commander + response: + content: | + You are outside the `/home/commander` folder. + Use `cd /home/commander` to return to `/home/commander` + - id: missing_command + condition: + command: cat + response: + content: | + To display this file, add its filename after `cat`. + If `cat` is waiting for input, press `Ctrl + C` to cancel it. + Enter `cat world.txt` to display the contents of `world.txt` + - id: missing_extension + condition: + command: cat world + response: + content: | + Did you forget the `.txt` extension? + Enter `cat world.txt` to display the contents of `world.txt` + response: + content: | + You have displayed the contents of `world.txt`! + Here is what it contains: + ``` + World + ``` + + - id: cat-next + name: Enter `echo next` to continue + requirements: + - cat-hello + - cat-world + condition: + command: echo next + response: + content: | + Next, let's learn how to copy files. + `cp` is short for copy. + To copy a file, use `cp [source file] [destination file]`. + Use `cp` to copy `hello.txt` to `hello2.txt`. + hint: Use `cp hello.txt hello2.txt` to copy `hello.txt` to `hello2.txt` + + - id: cp-hello + name: Copy `hello.txt` to `hello2.txt` + requirements: + - cat-next + condition: + pwd: /home/commander + command: cp hello.txt hello2.txt + files: + - path: /home/commander/hello2.txt + type: file + exists: true + exceptions: + - id: wrong_directory + condition: + not: + pwd: /home/commander + response: + content: | + You are outside the `/home/commander` folder. + Use `cd /home/commander` to return to `/home/commander`. + response: + content: | + You have copied `hello.txt` to `hello2.txt`! + Let's check the contents of `hello2.txt`. + hint: Use `cat hello2.txt` to display the contents of `hello2.txt` + + - id: cat-hello2 + name: Display the contents of `hello2.txt` + requirements: + - cp-hello + condition: + pwd: /home/commander + command: cat hello2.txt + output: Hello + exceptions: + - id: wrong_directory + condition: + not: + pwd: /home/commander + response: + content: | + You are outside the `/home/commander` folder. + Use `cd /home/commander` to return to `/home/commander`. + response: + content: | + You have displayed the contents of `hello2.txt`! + As you can see, `hello2.txt` contains exactly the same text as `hello.txt`. + Now try copying `world.txt` to `world2.txt`! + hint: Use `cp world.txt world2.txt` to copy `world.txt` to `world2.txt` + + - id: cp-world + name: Copy `world.txt` to `world2.txt` + requirements: + - cat-hello2 + condition: + pwd: /home/commander + command: cp world.txt world2.txt + files: + - path: /home/commander/world2.txt + type: file + exists: true + exceptions: + - id: wrong_directory + condition: + not: + pwd: /home/commander + response: + content: | + You are outside the `/home/commander` folder. + Use `cd /home/commander` to return to `/home/commander`. + response: + content: | + You have copied `world.txt` to `world2.txt`! + Let's check the contents of `world2.txt`. + hint: Use `cat world2.txt` to display the contents of `world2.txt` + + - id: cat-world2 + name: Display the contents of `world2.txt` + requirements: + - cp-world + condition: + pwd: /home/commander + command: cat world2.txt + output: World + exceptions: + - id: wrong_directory + condition: + not: + pwd: /home/commander + response: + content: | + You are outside the `/home/commander` folder. + Use `cd /home/commander` to return to `/home/commander`. + response: + content: | + You have displayed the contents of `world2.txt`! + `world2.txt` contains exactly the same text as `world.txt`. + Next, let's learn how to copy folders. + Your current folder contains a `notes` folder with a file named `note.txt` inside. + We want to copy the entire folder, including its contents, to `notes2`. + To copy a folder, add the `-r` option (recursive). This copies the folder and everything inside it. + Options let you change how a command behaves. + An option often starts with `-` followed by a letter, such as `-r`. + Options usually go after the command name and before the filenames. + Use `cp -r notes notes2` to copy `notes` to `notes2`. + hint: | + Use `cp -r notes notes2` to copy `notes` to `notes2` + Remember to include `-r` to copy all the contents + + - id: cp-notes + name: Copy the `notes` folder to `notes2` + requirements: + - cat-world2 + condition: + pwd: /home/commander + command: cp -r notes notes2 + files: + - path: /home/commander/notes2 + type: directory + exists: true + - path: /home/commander/notes2/note.txt + type: file + exists: true + exceptions: + - id: wrong_directory + condition: + not: + pwd: /home/commander + response: + content: | + You are outside the `/home/commander` folder. + Use `cd /home/commander` to return to `/home/commander`. + - id: missing_recursive + condition: + command: cp notes notes2 + response: + content: | + Did you forget the `-r` option? + Enter `cp -r notes notes2` to copy `notes` to `notes2` + response: + content: | + You have copied the `notes` folder to `notes2`! + The entire `notes` folder, including its files, has been copied to `notes2`. + `cp -r` copies everything in a folder, including files inside any nested folders. + + Finally, let's see what happens when you copy a file to a destination that already exists. + Copy `hello.txt` to `world.txt` and see what happens. + hint: Use `cp hello.txt world.txt` to copy `hello.txt` to `world.txt` + + - id: cp-overwrite + name: Copy `hello.txt` to `world.txt` + requirements: + - cp-notes + condition: + pwd: /home/commander + command: cp hello.txt world.txt + exceptions: + - id: wrong_directory + condition: + not: + pwd: /home/commander + response: + content: | + You are outside the `/home/commander` folder. + Use `cd /home/commander` to return to `/home/commander`. + response: + content: | + You have copied `hello.txt` to `world.txt`! + Let's check the contents of `world.txt`. + hint: Use `cat world.txt` + + - id: check-world + name: Check the contents of `world.txt` + requirements: + - cp-overwrite + condition: + pwd: /home/commander + command: cat world.txt + output: Hello + exceptions: + - id: wrong_directory + condition: + not: + pwd: /home/commander + response: + content: | + You are outside the `/home/commander` folder. + Use `cd /home/commander` to return to `/home/commander`. + response: + content: | + `world.txt` now contains `Hello`! + When the destination file already exists, copying another file to it overwrites its contents. + Check whether the destination already exists before copying, so you do not accidentally lose data. + That covers the basics of copying files! + Enter `echo finish` to complete this quest. + + - id: finish + name: Congratulations! All tasks completed! + requirements: + - check-world + condition: + command: echo finish + response: + content: | + Congratulations! You have completed all the tasks! + You now know how to copy files and folders. + In the next quest, you will learn how to move files and folders. diff --git a/quests/copy/home/hello.txt b/quests/copy/en/home/hello.txt similarity index 100% rename from quests/copy/home/hello.txt rename to quests/copy/en/home/hello.txt diff --git a/quests/copy/home/notes/note.txt b/quests/copy/en/home/notes/note.txt similarity index 100% rename from quests/copy/home/notes/note.txt rename to quests/copy/en/home/notes/note.txt diff --git a/quests/copy/home/world.txt b/quests/copy/en/home/world.txt similarity index 100% rename from quests/copy/home/world.txt rename to quests/copy/en/home/world.txt diff --git a/quests/copy/zh-TW/answer.sh b/quests/copy/zh-TW/answer.sh new file mode 100644 index 0000000..bef48aa --- /dev/null +++ b/quests/copy/zh-TW/answer.sh @@ -0,0 +1,13 @@ +echo start +ls +cat hello.txt +cat world.txt +echo next +cp hello.txt hello2.txt +cat hello2.txt +cp world.txt world2.txt +cat world2.txt +cp -r notes notes2 +ls notes2 +cp hello.txt world.txt +cat world.txt \ No newline at end of file diff --git a/quests/copy/game.yml b/quests/copy/zh-TW/game.yml similarity index 100% rename from quests/copy/game.yml rename to quests/copy/zh-TW/game.yml diff --git a/quests/move/home/hello.txt b/quests/copy/zh-TW/home/hello.txt similarity index 100% rename from quests/move/home/hello.txt rename to quests/copy/zh-TW/home/hello.txt diff --git a/quests/move/home/notes/note.txt b/quests/copy/zh-TW/home/notes/note.txt similarity index 100% rename from quests/move/home/notes/note.txt rename to quests/copy/zh-TW/home/notes/note.txt diff --git a/quests/move/home/world.txt b/quests/copy/zh-TW/home/world.txt similarity index 100% rename from quests/move/home/world.txt rename to quests/copy/zh-TW/home/world.txt diff --git a/quests/create/answer.sh b/quests/create/en/answer.sh similarity index 100% rename from quests/create/answer.sh rename to quests/create/en/answer.sh diff --git a/quests/create/en/game.yml b/quests/create/en/game.yml new file mode 100644 index 0000000..921ce01 --- /dev/null +++ b/quests/create/en/game.yml @@ -0,0 +1,165 @@ +id: create +title: Creating Files and Folders +image: base +instruction: | + In this quest, you will learn: + 1. How to create files with `touch` + 2. How to create folders with `mkdir` + +requirements: + - tab + +stages: + - id: start + name: Click `Start` to begin + condition: + command: echo start + response: + content: | + This time, we will learn about the `touch` command. + First, use `ls` to list the files in your current folder. + You should find that the folder has no files. + hint: Enter `ls` to list the files in your current folder + + - id: ls + name: List the files in the folder + requirements: + - start + condition: + command: ls + response: + content: | + The terminal shows no output, which means there are no visible files in this folder. + You can create a file with `touch [filename]`. + Try creating a file named `hello.txt` in the terminal. + hint: Enter `touch hello.txt` to create a file + + - id: touch + name: Create a file with `touch` + requirements: + - ls + condition: + files: + - path: /home/commander/hello.txt + type: file + exists: true + exceptions: + - id: missing_extension + condition: + files: + - path: /home/commander/hello + type: file + exists: true + response: + content: | + It looks like you forgot the `.txt` extension. + Try again and make sure the filename is `hello.txt`. + response: + content: | + The terminal should show no output. + The file tree on the right now shows the new `hello.txt` file. + Let's look inside `hello.txt`! + Use the `cat` command you learned earlier to display its contents. + hint: Enter `cat hello.txt` to display the contents of `hello.txt` + + - id: cat + name: Display the contents of `hello.txt` with `cat` + requirements: + - touch + condition: + command: cat hello.txt + response: + content: | + Huh? There is nothing there! + That's right. A new file created with `touch` is empty. + We will learn how to edit file contents in a future lesson. + Let's create another file! + Try creating another file named `world.txt` in the terminal. + hint: Enter `touch world.txt` to create a file + + - id: touch-2 + name: Create another file named `world.txt` with `touch` + requirements: + - cat + condition: + files: + - path: /home/commander/world.txt + type: file + exists: true + exceptions: + - id: missing_extension + condition: + files: + - path: /home/commander/world + type: file + exists: true + response: + content: | + It looks like you forgot the `.txt` extension. + Try again and make sure the filename is `world.txt`. + response: + content: | + Great! You have created your second file. + The file tree now shows both `hello.txt` and `world.txt`. + Next, let's create a folder! + The `mkdir` command creates an empty folder. + Use `mkdir [folder name]`. + Try creating a folder named `notes` in the terminal. + hint: Enter `mkdir notes` to create a folder + + - id: mkdir + name: Create the `notes` folder with `mkdir` + requirements: + - touch-2 + condition: + files: + - path: /home/commander/notes + type: directory + exists: true + response: + content: | + Great! You have created the `notes` folder. + Next, we will create a file named `note.txt` inside `notes`. + First, enter the `notes` folder. + Remember the `cd` command? Try using it to enter `notes`. + hint: | + Enter `cd notes` to enter the `notes` folder + After typing `cd n`, press `Tab` to complete the folder name + + - id: cd-notes + name: Enter the `notes` folder + requirements: + - mkdir + condition: + pwd: /home/commander/notes + response: + content: | + You have entered the `notes` folder. + Now try creating a file named `note.txt` inside `notes`. + hint: Enter `touch note.txt` to create a file + + - id: touch-note + name: Create `note.txt` inside `notes` + requirements: + - cd-notes + condition: + files: + - path: /home/commander/notes/note.txt + type: file + exists: true + response: + content: | + Great! You have created `note.txt`. + Congratulations! You have completed this quest! + In the next quest, you will learn how to copy files and folders. + Enter `echo finish` to complete this quest. + + - id: END + name: Complete the quest + requirements: + - touch-note + condition: + command: echo finish + response: + content: | + Congratulations! You have completed this quest! diff --git a/quests/create/zh-TW/answer.sh b/quests/create/zh-TW/answer.sh new file mode 100644 index 0000000..b0367e1 --- /dev/null +++ b/quests/create/zh-TW/answer.sh @@ -0,0 +1,12 @@ +echo start +ls +touch hello.txt +ls +cat hello.txt +touch world.txt +ls +mkdir notes +ls +cd notes +touch note.txt +ls \ No newline at end of file diff --git a/quests/create/game.yml b/quests/create/zh-TW/game.yml similarity index 100% rename from quests/create/game.yml rename to quests/create/zh-TW/game.yml diff --git a/quests/execute/en/execute.md b/quests/execute/en/execute.md new file mode 100644 index 0000000..2b7877e --- /dev/null +++ b/quests/execute/en/execute.md @@ -0,0 +1,79 @@ +# Running Executable Files + +## Outline + +1. What an executable file is +2. How to find an executable with `which` +3. How to run an executable file + +--- + +In earlier lessons, you used commands such as `ls`, `cat`, `cp`, `mv`, and `rm`. +These commands are **executable files** that the operating system runs when you enter them. +For most commands, you can find the corresponding file in the file system. +Use `which` to find the file associated with a command. +Enter `which [command name]` to display its path. + +> Use `which ls` to find the path to `ls` + +--- + +The terminal displays `/usr/bin/ls`, the location of the `ls` executable. +`/usr/bin` is a system folder containing many standard programs, including commands such as `cat` and `cp`. + +> Use `which cat` to find the path to `cat` + +--- + +To run an executable file, enter its path directly. +Let's try running a command using the path to its executable. + +> Enter `/usr/bin/ls` to list the contents of your current folder + +--- + +It lists the files in your folder, just as entering `ls` does. You can compare the results yourself. +You can still pass arguments. Let's try this with `cat`. + +> Enter `/usr/bin/cat hello.txt` to display the contents of `hello.txt` + +--- + +You can see the contents of `hello.txt`, just as you would with `cat`. +This makes sense: `cat` is located at `/usr/bin/cat`, so both commands run the same program. +A command like this consists of **an executable** plus **arguments**. +You can enter `ls` instead of `/usr/bin/ls` because the `PATH` environment variable tells the terminal which *folders* to search for programs. +`PATH` and environment variables are beyond this lesson's scope. We will cover them in a future lesson. + +You can also run custom programs, such as the `dog` file in your current folder. + +> Use `cat dog` to display the contents of `dog` + +--- + +This is a simple program built from `echo` and `sleep` commands. +You do not need to understand every detail yet. It prints a few lines of text, with short pauses between them. + +To run a file, specify its path. +Since `dog` is in your home folder, its absolute path is `/home/commander/dog`. You can also use the shorter form `~/dog`. + +> Enter `~/dog` to run the `dog` file + +--- + +The terminal displays a few words from this dog with an attitude. +When you are in the same folder as a program, a relative path is often more convenient than an absolute path. +To run a program in your current folder, put `./` before its filename instead of entering just its name. +In a path, `.` means the current folder. +Now try running `dog` using a relative path. + +> Enter `./dog` to run the `dog` file + +--- + +The dog produces the same output as before. +This is an example of a simple custom command. +You can put a sequence of commands in a file and run them together to handle complex or repetitive tasks. +This technique is also useful when running programs written by other people. + +Here is a final challenge: enter the `commands` folder, then run the `lucky` file inside it using a relative path. diff --git a/quests/execute/en/game.yml b/quests/execute/en/game.yml new file mode 100644 index 0000000..7670a8c --- /dev/null +++ b/quests/execute/en/game.yml @@ -0,0 +1,236 @@ +id: execute +title: Running Executable Files +image: base +instruction: | + In this quest, you will learn: + 1. What an executable file is + 2. How to find an executable with `which` + 3. How to run an executable file + +requirements: + - remove + +stages: + - id: start + name: Click Start + condition: + command: echo start + response: + content: | + In earlier lessons, you used commands such as `ls`, `cat`, `cp`, `mv`, and `rm`. + These commands are **executable files** that the operating system runs when you enter them. + For most commands, you can find the corresponding file in the file system. + Use `which` to find the file associated with a command. + Enter `which [command name]` to display its path. + hint: Use `which ls` to find the path to `ls` + + - id: which-ls + name: Find the path to `ls` with `which` + requirements: + - start + condition: + command: which ls + response: + content: | + The terminal displays `/usr/bin/ls`, the location of the `ls` executable. + `/usr/bin` is a system folder containing many standard programs. + + Next, let's find the path to `cat`. + hint: Use `which cat` to find the path to `cat` + + - id: which-cat + name: Find the path to `cat` with `which` + requirements: + - which-ls + condition: + command: which cat + response: + content: | + You can see `/usr/bin/cat`, which is also in the `/usr/bin` folder. + To run an executable file, **enter its path directly**. + Let's try running a command using the path to its executable. + + First, let's see what happens when you run `ls` normally. + hint: Enter `ls` to list the contents of your current folder + + - id: ls + name: List the current folder with `ls` + requirements: + - which-cat + condition: + command: ls + response: + content: | + Your current folder contains the `commands` folder and two files, `dog` and `hello.txt`. + Now try running `ls` using its absolute path, `/usr/bin/ls`. + hint: Enter `/usr/bin/ls` to list the contents of your current folder + + - id: bin-ls + name: Run `ls` using its absolute path + requirements: + - ls + condition: + command: /usr/bin/ls + exceptions: + - id: exception-ls + condition: + command: ls + response: + content: | + Here, we are practicing running `ls` using its absolute path. + Enter `/usr/bin/ls` to list the contents of your current folder. + response: + content: | + It lists the same files as `ls`. Both commands run the program at `/usr/bin/ls`, so the results are the same. + + You can still pass arguments. Try using `/usr/bin/cat` to display the contents of `hello.txt`. + hint: Enter `/usr/bin/cat hello.txt` + + - id: bin-cat + name: Run `cat` using its absolute path + requirements: + - bin-ls + condition: + command: /usr/bin/cat hello.txt + response: + content: | + You can see the contents of `hello.txt`, just as you would with `cat`. + Both commands run the program at `/usr/bin/cat`, so the results are the same. + + A command usually has the form `command argument1 argument2 ...`. + The `command` is the name or path of the program to run, such as `ls`, `cat`, or `which`. + The `arguments` provide information to the program, such as `hello.txt` after `cat`. + + You can enter `ls` instead of `/usr/bin/ls` because an environment variable called `PATH` tells the shell which folders to search for programs. + We will learn more about `PATH` in a future lesson. + + Now let's try running a custom command. + The file named `dog` in this folder is a custom command provided for this lesson. + First, let's see what is written inside it. + hint: Use `cat dog` to display the contents of `dog` + + - id: cat-dog + name: Display the contents of `dog` with `cat` + requirements: + - bin-cat + condition: + command: cat dog + response: + content: | + This is a simple program built from `echo` and `sleep` commands. + You do not need to understand every detail yet. It just prints a few lines of text. + + To run a file, specify its path. + Here, the first part of the command is the path to `dog`. This example does not use any arguments. + Since `dog` is in your home folder, its absolute path is `/home/commander/dog`. + Using the format `executable arguments`, the command is: + ``` + /home/commander/dog + ``` + hint: Enter `/home/commander/dog` to run the `dog` file + + - id: dog-abs + name: Run `dog` using an absolute path + requirements: + - cat-dog + condition: + or: + - command: /home/commander/dog + - command: ~/dog + exceptions: + - id: exception-dog + condition: + command: dog + error: 'command not found: dog' + response: + content: | + `dog` cannot be run by name here because it is not in the command search path. Specify the path to its executable file. + Here, we are practicing running `dog` using an absolute path. + Enter `/home/commander/dog` to run the `dog` file. + response: + content: | + The terminal displays a few words from this dog with an attitude. + When you are in the same folder as a program, a relative path is often more convenient than an absolute path. + To run a program in your current folder, put `./` before its filename instead of entering just its name. + In a path, `.` means the current folder. + Now try running `dog` using a relative path. + hint: Enter `./dog` to run the `dog` file + + - id: dog-rel + name: Run `dog` using a relative path + requirements: + - dog-abs + condition: + pwd: /home/commander + command: ./dog + exceptions: + - id: exception-dog-2 + condition: + command: dog + error: 'command not found: dog' + response: + content: | + `dog` cannot be run by name here because it is not in the command search path. Specify the path to its executable file. + Here, we are practicing running `dog` using a relative path. + Enter `./dog` to run the `dog` file. + response: + content: | + The dog produces the same output as before. + This is an example of a simple custom command. + You can put a sequence of commands in a file and run them together to handle complex or repetitive tasks. + This technique is also useful when running programs written by other people. + + Here is a final challenge: enter the `commands` folder, then run the `lucky` file inside it using a relative path. + hint: | + Use `cd` to enter a folder + Use `ls` to list the files + Enter `./filename` to run a file + + - id: execute-lucky + name: Run `lucky` using a relative path + requirements: + - dog-rel + condition: + pwd: /home/commander/commands + command: ./lucky + response: + content: | + The terminal displays `I am SO LUCKY!!!`, the message printed by `lucky`. + Congratulations! You have completed this lesson! + Enter `echo finish` to complete this quest. + exceptions: + - id: exception-lucky + condition: + command: lucky + error: 'command not found: lucky' + response: + content: | + `lucky` cannot be run by name here because it is not in the command search path. Specify the path to its executable file. + + - id: exception-lucky-2 + condition: + pwd: /home/commander + command: ./lucky + error: 'no such file or directory' + response: + content: | + You are outside the `commands` folder, so `./lucky` does not point to the file yet. + First, enter `cd commands` to move into `commands`. Then enter `./lucky` to run `lucky` using a relative path. + - id: help + condition: + command: echo help + response: + content: | + Enter the `commands` folder before running `lucky` using a relative path. + First, enter `cd commands` to move into `commands`. Then enter `./lucky` to run `lucky` using a relative path. + + - id: END + name: Congratulations! All tasks completed! + requirements: + - execute-lucky + condition: + command: echo finish + response: + content: | + Congratulations! You have completed this lesson! + You now know how to run executable files. diff --git a/quests/execute/home/commands/lucky b/quests/execute/en/home/commands/lucky old mode 100755 new mode 100644 similarity index 100% rename from quests/execute/home/commands/lucky rename to quests/execute/en/home/commands/lucky diff --git a/quests/execute/en/home/dog b/quests/execute/en/home/dog new file mode 100644 index 0000000..f4204ce --- /dev/null +++ b/quests/execute/en/home/dog @@ -0,0 +1,8 @@ +#!/bin/sh + +echo "You ran the dog file" +echo "Woof woof woof" +sleep 1 +echo "You know what?" +sleep 1 +echo "I am better than a cat" diff --git a/quests/execute/home/hello.txt b/quests/execute/en/home/hello.txt similarity index 100% rename from quests/execute/home/hello.txt rename to quests/execute/en/home/hello.txt diff --git a/quests/execute/execute.md b/quests/execute/zh-TW/execute.md similarity index 100% rename from quests/execute/execute.md rename to quests/execute/zh-TW/execute.md diff --git a/quests/execute/game.yml b/quests/execute/zh-TW/game.yml similarity index 100% rename from quests/execute/game.yml rename to quests/execute/zh-TW/game.yml diff --git a/quests/execute/zh-TW/home/commands/lucky b/quests/execute/zh-TW/home/commands/lucky new file mode 100755 index 0000000..78771c2 --- /dev/null +++ b/quests/execute/zh-TW/home/commands/lucky @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "I am SO LUCKY!!!" diff --git a/quests/execute/home/dog b/quests/execute/zh-TW/home/dog similarity index 100% rename from quests/execute/home/dog rename to quests/execute/zh-TW/home/dog diff --git a/quests/execute/zh-TW/home/hello.txt b/quests/execute/zh-TW/home/hello.txt new file mode 100644 index 0000000..2d9bd63 --- /dev/null +++ b/quests/execute/zh-TW/home/hello.txt @@ -0,0 +1 @@ +Hello! This is a file hello.txt diff --git a/quests/explore/answer.sh b/quests/explore/en/answer.sh similarity index 100% rename from quests/explore/answer.sh rename to quests/explore/en/answer.sh diff --git a/quests/explore/en/game.yml b/quests/explore/en/game.yml new file mode 100644 index 0000000..5732275 --- /dev/null +++ b/quests/explore/en/game.yml @@ -0,0 +1,161 @@ +id: explore +image: base +title: Exploring Files +instruction: | + In this quest, you will learn: + 1. How to list folder contents with `ls` + 2. How to move between folders with `cd` + +requirements: + - get-started + +stages: + - id: start + name: Click `Start` to begin the lesson + condition: + command: ^echo start$ + response: + content: | + The terminal has no graphical interface for browsing folder contents. + Instead, you can use `ls` to list what is inside a folder. + `ls` is short for list. Its first character is a lowercase L. + When you enter `ls`, the terminal lists the files and folders in your current folder. + Now enter `ls` to see what is in this folder. + hint: Enter `ls` to list the contents of the folder + + - id: ls + name: List the files in the folder + requirements: + - start + condition: + command: ^ls$ + exceptions: + - id: wrong_directory + condition: + not: + pwd: /home/commander + response: + type: narrative + content: | + First, return to `/home/commander/` + Enter `cd ~` to return to `/home/commander/` + response: + content: | + You should now see the contents of the folder. + The file tree on the right has changed too. + This folder contains two folders named `folder1` and `folder2`. + + Next, let's learn how to enter a folder. + `cd` is short for change directory, which means changing your current folder. + Enter `cd [folder name]` to move into a folder. + For example, enter `cd folder1` to move into `folder1`. + Now enter the `folder1` folder. + hint: Enter `cd folder1` to move into `folder1` + + - id: cd-folder1 + name: Enter `folder1` + requirements: + - ls + condition: + pwd: /home/commander/folder1 + exceptions: + - id: wrong_spelling + condition: + command: ^folderl$ + response: + type: narrative + content: | + `folder1` ends with the number `1`, not the letter `l`. + - id: wrong_directory + condition: + pwd: /home/commander + response: + type: narrative + content: Enter `cd folder1` to move into `folder1` + - id: not_in_home + condition: + not: + pwd: /home/commander + response: + type: narrative + content: | + First, return to `/home/commander/` + Enter `cd ~` to return to `/home/commander/` + Then enter `cd folder1` to move into `folder1` + response: + content: | + You have entered the `folder1` folder. + Notice that the command prompt has changed from + `commander:~ $` to + `commander:~/folder1 $`. + This tells you that you are now in `folder1`. + Now list the contents of `folder1`. + hint: Enter `ls` to list the contents of the folder + + - id: ls-folder1 + name: List the contents of `folder1` + requirements: + - cd-folder1 + condition: + pwd: /home/commander/folder1 + command: ^ls$ + response: + content: | + You can see two files in `folder1`, named `file1.txt` and `file2.txt`. + Now let's leave `folder1` and return to the original folder. + Use `cd ..` to move up to the parent folder. + Here, `..` means the parent folder. + hint: Enter `cd ..` to move up to the parent folder + + - id: cd_up + name: Return to the parent folder + requirements: + - ls-folder1 + condition: + pwd: /home/commander + response: + content: | + You have returned to the parent folder. + Use the commands you just learned to enter `folder2`, list its contents, and return to this folder. + hint: | + Use `cd` to move between folders + Use `ls` to list the contents of a folder + + - id: cd-folder2 + name: Enter `folder2` + requirements: + - cd_up + condition: + pwd: /home/commander/folder2 + response: + content: | + You have entered the `folder2` folder. + Now list the contents of `folder2`. + hint: Use `ls` to list the contents of the folder + + - id: ls-folder2 + name: List the contents of `folder2` + requirements: + - cd-folder2 + condition: + pwd: /home/commander/folder2 + command: ^ls$ + response: + content: | + You can see two files in `folder2`, named `file3.txt` and `file4.txt`. + Now leave `folder2` and return to the original folder. + hint: Use `cd ..` to move up to the parent folder + + - id: END + name: Return to the original folder + requirements: + - ls-folder2 + condition: + pwd: /home/commander + response: + content: | + You have returned to the original folder. + Congratulations! You now know how to use `ls` and `cd`. + hint: | + Use `cd` to move between folders + Use `ls` to list the contents of a folder diff --git a/quests/path/home/folder1/file1.txt b/quests/explore/en/home/folder1/file1.txt similarity index 100% rename from quests/path/home/folder1/file1.txt rename to quests/explore/en/home/folder1/file1.txt diff --git a/quests/path/home/folder1/file2.txt b/quests/explore/en/home/folder1/file2.txt similarity index 100% rename from quests/path/home/folder1/file2.txt rename to quests/explore/en/home/folder1/file2.txt diff --git a/quests/path/home/folder2/file3.txt b/quests/explore/en/home/folder2/file3.txt similarity index 100% rename from quests/path/home/folder2/file3.txt rename to quests/explore/en/home/folder2/file3.txt diff --git a/quests/path/home/folder2/file4.txt b/quests/explore/en/home/folder2/file4.txt similarity index 100% rename from quests/path/home/folder2/file4.txt rename to quests/explore/en/home/folder2/file4.txt diff --git a/quests/explore/zh-TW/answer.sh b/quests/explore/zh-TW/answer.sh new file mode 100644 index 0000000..9a36ff0 --- /dev/null +++ b/quests/explore/zh-TW/answer.sh @@ -0,0 +1,8 @@ +echo start +ls +cd folder1 +ls +cd .. +cd folder2 +ls +cd .. \ No newline at end of file diff --git a/quests/explore/game.yml b/quests/explore/zh-TW/game.yml similarity index 100% rename from quests/explore/game.yml rename to quests/explore/zh-TW/game.yml diff --git a/quests/remove/home/empty/.gitkeep b/quests/explore/zh-TW/home/folder1/file1.txt similarity index 100% rename from quests/remove/home/empty/.gitkeep rename to quests/explore/zh-TW/home/folder1/file1.txt diff --git a/quests/remove/home/trash/secrets/envelope.txt b/quests/explore/zh-TW/home/folder1/file2.txt similarity index 100% rename from quests/remove/home/trash/secrets/envelope.txt rename to quests/explore/zh-TW/home/folder1/file2.txt diff --git a/quests/explore/zh-TW/home/folder2/file3.txt b/quests/explore/zh-TW/home/folder2/file3.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/explore/zh-TW/home/folder2/file4.txt b/quests/explore/zh-TW/home/folder2/file4.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/get-started/answer.sh b/quests/get-started/en/answer.sh similarity index 100% rename from quests/get-started/answer.sh rename to quests/get-started/en/answer.sh diff --git a/quests/get-started/en/game.yml b/quests/get-started/en/game.yml new file mode 100644 index 0000000..8845a97 --- /dev/null +++ b/quests/get-started/en/game.yml @@ -0,0 +1,76 @@ +id: get-started +image: base +title: Terminal Basics +instruction: | + Welcome to Linux Odyssey! + This quest introduces the basics of using the terminal. + In this quest, you will learn: + 1. How to use this website + 2. How to use the terminal + 3. How to use the `echo` command + +stages: + - id: start + name: Click `Start` to begin + condition: + command: ^echo start$ + response: + content: | + Hello, brave adventurer! + Welcome to Linux Odyssey, an adventure filled with terminal magic! + Think of the terminal as your magic wand. You control the computer with **text spells**, a useful skill for any aspiring programmer! + The area below is your stage for casting spells: the terminal. Ready to cast your first one? + Enter `echo hello` below, then press Enter to see what happens! + + - id: echo-hello + name: Enter `echo hello` in the terminal + requirements: + - start + condition: + command: ^echo hello$ + response: + content: | + Congratulations on running your first command! + You should see this in the terminal: + ``` + commander:~ $ echo hello + hello + ``` + The prompt `commander:~ $` means the computer is waiting for a command. + After you enter `echo hello`, the computer prints "hello" on the next line. + + A command is like a sentence telling the computer what to do. + Take `echo hello`, for example: + - `echo` tells the computer to print something + - `hello` is the text to print + So when you enter `echo hello`, the computer prints "hello". + + Now try making the terminal display the text `next`. + If you get stuck, click the `Hint` button for help. + hint: | + Enter `echo next` to continue to the next step. + + - id: echo-next + name: Enter `echo next` in the terminal + requirements: + - echo-hello + condition: + command: ^echo next$ + response: + content: | + Congratulations! You now know how to use `echo`. + You might wonder why we need a command like this. + Although `echo` is simple, printing text is a basic building block for many tasks. + In this adventure, we will also use `echo` to trigger events. + That is all for this quest. Enter `echo finish` to complete it. + + - id: END + name: Finish the quest + requirements: + - echo-next + condition: + command: ^echo finish$ + response: + content: | + Congratulations! You have completed this quest! + You have learned how to use `echo` and what it does. diff --git a/quests/get-started/zh-TW/answer.sh b/quests/get-started/zh-TW/answer.sh new file mode 100644 index 0000000..cbe1543 --- /dev/null +++ b/quests/get-started/zh-TW/answer.sh @@ -0,0 +1,4 @@ +echo start +echo hello +echo next +echo finish \ No newline at end of file diff --git a/quests/get-started/game.yml b/quests/get-started/zh-TW/game.yml similarity index 100% rename from quests/get-started/game.yml rename to quests/get-started/zh-TW/game.yml diff --git a/quests/move/answer.sh b/quests/move/en/answer.sh similarity index 100% rename from quests/move/answer.sh rename to quests/move/en/answer.sh diff --git a/quests/move/en/game.yml b/quests/move/en/game.yml new file mode 100644 index 0000000..76a266d --- /dev/null +++ b/quests/move/en/game.yml @@ -0,0 +1,136 @@ +id: move +title: Moving Files +image: base +instruction: | + In this quest, you will learn: + 1. How to move files and folders with `mv` + +requirements: + - create + +stages: + - id: start + name: Click `Start` to begin + condition: + command: echo start + response: + content: | + Today we will learn how to move files and folders. + Your current folder contains `hello.txt`, `world.txt`, and a folder named `notes`. + First, use `ls` and `cat` to explore their contents. + When you have finished, enter `echo next` to continue! + hint: | + Use `ls` and `ls notes` to list the files in the folders + Use `cat hello.txt` and `cat world.txt` to display the file contents + When you have finished, enter `echo next` to continue + + - id: next + requirements: + - start + name: Explore the files, then enter `echo next` to continue + condition: + command: echo next + response: + content: | + Now let's learn how to move files and folders. + `mv` is short for move. It moves files and folders. + Use `mv [source] [destination]`. + Let's start by moving `hello.txt` into the `notes` folder! + hint: Use `mv hello.txt notes` to move `hello.txt` into `notes` + + - id: mv-hello + requirements: + - next + name: Move `hello.txt` into `notes` + condition: + command: mv hello.txt notes + files: + - path: /home/commander/notes/hello.txt + type: file + exists: true + - path: /home/commander/hello.txt + type: file + exists: false + response: + content: | + Well done! + `hello.txt` has been moved into `notes`! + + `mv` can also rename files! + Just use the new name as the destination. + Now rename `world.txt` to `wow.txt`. + hint: Use `mv world.txt wow.txt` to rename `world.txt` to `wow.txt` + + - id: mv-world + requirements: + - mv-hello + name: Rename `world.txt` to `wow.txt` + condition: + command: mv world.txt wow.txt + files: + - path: /home/commander/wow.txt + type: file + exists: true + - path: /home/commander/world.txt + type: file + exists: false + response: + content: | + `world.txt` is now named `wow.txt`. + + Next, let's learn how to move folders! + Moving a folder works the same way as moving a file. + `mv` automatically moves everything inside the folder too. + First, rename the `notes` folder to `docs`! + hint: Use `mv notes docs` to rename `notes` to `docs` + + - id: mv-notes + requirements: + - mv-world + name: Rename `notes` to `docs` + condition: + command: mv notes docs + files: + - path: /home/commander/docs + type: directory + exists: true + - path: /home/commander/notes + type: directory + exists: false + response: + content: | + The `notes` folder is now named `docs`. + Here is a final challenge: + Move `docs/hello.txt` to your home folder and rename it to `hi.txt`. + hint: Use `mv docs/hello.txt hi.txt` to move the file to your home folder as `hi.txt` + + - id: mv-docs-hello + requirements: + - mv-notes + name: Move `docs/hello.txt` to your home folder as `hi.txt` + condition: + command: mv docs/hello.txt hi.txt + files: + - path: /home/commander/hi.txt + type: file + exists: true + - path: /home/commander/docs/hello.txt + type: file + exists: false + response: + content: | + Well done! + You now know how to move files and folders! + + Enter `echo finish` to complete this quest. + + - id: END + requirements: + - mv-docs-hello + name: Complete the quest + condition: + command: echo finish + response: + content: | + You now know how to move files and folders! + Congratulations on completing this quest! diff --git a/quests/remove/home/hello.txt b/quests/move/en/home/hello.txt similarity index 100% rename from quests/remove/home/hello.txt rename to quests/move/en/home/hello.txt diff --git a/quests/remove/home/notes/note.txt b/quests/move/en/home/notes/note.txt similarity index 100% rename from quests/remove/home/notes/note.txt rename to quests/move/en/home/notes/note.txt diff --git a/quests/remove/home/world.txt b/quests/move/en/home/world.txt similarity index 100% rename from quests/remove/home/world.txt rename to quests/move/en/home/world.txt diff --git a/quests/move/zh-TW/answer.sh b/quests/move/zh-TW/answer.sh new file mode 100644 index 0000000..a24458b --- /dev/null +++ b/quests/move/zh-TW/answer.sh @@ -0,0 +1,11 @@ +echo start +echo next +mv hello.txt notes +ls +ls notes +echo next +mv world.txt wow.txt +ls +mv notes docs +ls +mv docs/hello.txt hi.txt \ No newline at end of file diff --git a/quests/move/game.yml b/quests/move/zh-TW/game.yml similarity index 100% rename from quests/move/game.yml rename to quests/move/zh-TW/game.yml diff --git a/quests/move/zh-TW/home/hello.txt b/quests/move/zh-TW/home/hello.txt new file mode 100644 index 0000000..e965047 --- /dev/null +++ b/quests/move/zh-TW/home/hello.txt @@ -0,0 +1 @@ +Hello diff --git a/quests/move/zh-TW/home/notes/note.txt b/quests/move/zh-TW/home/notes/note.txt new file mode 100644 index 0000000..77c6f9e --- /dev/null +++ b/quests/move/zh-TW/home/notes/note.txt @@ -0,0 +1 @@ +Note diff --git a/quests/move/zh-TW/home/world.txt b/quests/move/zh-TW/home/world.txt new file mode 100644 index 0000000..216e97c --- /dev/null +++ b/quests/move/zh-TW/home/world.txt @@ -0,0 +1 @@ +World diff --git a/quests/path/answer.sh b/quests/path/en/answer.sh similarity index 100% rename from quests/path/answer.sh rename to quests/path/en/answer.sh diff --git a/quests/path/en/game.yml b/quests/path/en/game.yml new file mode 100644 index 0000000..95c6c8f --- /dev/null +++ b/quests/path/en/game.yml @@ -0,0 +1,146 @@ +id: path +title: File Paths +image: base +instruction: | + In this quest, you will learn: + 1. Absolute and relative paths + 2. How to show your current path with `pwd` + +requirements: + - explore + +stages: + - id: start + name: Click `Start` to begin the lesson + condition: + command: echo start + response: + content: | + In Linux, a path describes the location of a file or folder. + Paths can be relative or absolute. + A relative path starts from your current folder. An absolute path starts from the root folder. + First, use `ls` to list the files in this folder. + + - id: ls + name: List the files in the folder + requirements: + - start + condition: + pwd: /home/commander + command: ls + response: + content: | + Now use `cd` to enter the `folder1` folder. + + - id: cd-folder1 + name: Enter `folder1` + requirements: + - ls + condition: + pwd: /home/commander/folder1 + response: + content: | + You have entered `folder1`. + In `cd folder1`, the `folder1` part is a relative path. + Likewise, `..` in `cd ..` is a relative path. + A relative path can span more than one level of folders. + For example, to enter `folder2` from here, you could use `cd ..` to go up one level, then `cd folder2` to enter it. + You can also join `..` and `folder2` with `/` and enter `cd ../folder2` to get there in one command. + Try entering `folder2` this way. + hint: Enter `cd ../folder2` to move into `folder2`. + + - id: cd-folder2 + name: Enter `folder2` + requirements: + - cd-folder1 + condition: + pwd: /home/commander/folder2 + exceptions: + - id: not_in_folder2 + condition: + pwd: /home/commander + response: + content: Enter `cd folder2` to move into `folder2` + response: + content: | + You have entered `folder2` using a relative path. + You can also add a relative path after `ls` to list the contents of another folder. + For example, use `ls ../folder1` to see which files are in `folder1`. + Try listing the files in `folder1` this way. + hint: Enter `ls ../folder1` to list the files in `folder1`. + + - id: ls-folder1 + name: List the files in `folder1` + requirements: + - cd-folder2 + condition: + output: file1.txt + exceptions: + - id: not_in_folder2 + condition: + not: + pwd: /home/commander/folder2 + response: + content: Return to `folder2` with `cd ~/folder2`, then use `ls ../folder1` to list the contents of `folder1` without leaving `folder2`. + response: + content: | + Great! You now know how to use relative paths. + Next, let's look at absolute paths. + An absolute path starts from the root folder, which is at the top of the file tree. + Use `pwd` to display the absolute path to your current folder. + `pwd` stands for `print working directory`: it displays the path to the folder you are in. + Now use `pwd` to display your current absolute path. + hint: Enter `pwd` to display your current absolute path + + - id: pwd + name: Display your current absolute path + requirements: + - ls-folder1 + condition: + pwd: /home/commander/folder2 + command: pwd + exceptions: + - id: not_in_folder2 + condition: + not: + pwd: /home/commander/folder2 + response: + content: Return to `folder2`, then use `pwd` to display your current absolute path + response: + content: | + Your current absolute path is `/home/commander/folder2`. + The command prompt also shows your current location. + In `commander:~/folder2 $`, `~/folder2` is a shortened form of the path to your current folder. + `~` stands for `/home/commander`, your home folder. Each game starts here. + You can use absolute paths with `ls` and `cd` too. + For example, use `ls ~` to list your home folder or `cd ~` to return to it. + Now use `ls ~` to list the contents of your home folder. + hint: Enter `ls ~` to list the contents of your home folder + + - id: ls_home + name: List the contents of your home folder + requirements: + - pwd + condition: + command: ls ~ + response: + content: | + Now use `cd ~` to return to your home folder. + hint: Enter `cd ~` to return to your home folder + + - id: END + name: Return to your home folder + requirements: + - ls_home + condition: + command: cd ~ + exceptions: + - id: already_home + condition: + pwd: /home/commander + response: + content: Use `cd ~` to return to your home folder + response: + content: | + You have returned to your home folder. + Congratulations! You now know how to use absolute and relative paths and display your current absolute path. diff --git a/quests/path/en/home/folder1/file1.txt b/quests/path/en/home/folder1/file1.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/path/en/home/folder1/file2.txt b/quests/path/en/home/folder1/file2.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/path/en/home/folder2/file3.txt b/quests/path/en/home/folder2/file3.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/path/en/home/folder2/file4.txt b/quests/path/en/home/folder2/file4.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/path/zh-TW/answer.sh b/quests/path/zh-TW/answer.sh new file mode 100644 index 0000000..bf3a210 --- /dev/null +++ b/quests/path/zh-TW/answer.sh @@ -0,0 +1,8 @@ +echo start +ls +cd folder1 +cd ../folder2 +ls ../folder1 +pwd +ls ~ +cd ~ \ No newline at end of file diff --git a/quests/path/game.yml b/quests/path/zh-TW/game.yml similarity index 100% rename from quests/path/game.yml rename to quests/path/zh-TW/game.yml diff --git a/quests/path/zh-TW/home/folder1/file1.txt b/quests/path/zh-TW/home/folder1/file1.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/path/zh-TW/home/folder1/file2.txt b/quests/path/zh-TW/home/folder1/file2.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/path/zh-TW/home/folder2/file3.txt b/quests/path/zh-TW/home/folder2/file3.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/path/zh-TW/home/folder2/file4.txt b/quests/path/zh-TW/home/folder2/file4.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/read/answer.sh b/quests/read/en/answer.sh similarity index 100% rename from quests/read/answer.sh rename to quests/read/en/answer.sh diff --git a/quests/read/en/game.yml b/quests/read/en/game.yml new file mode 100644 index 0000000..e27aaba --- /dev/null +++ b/quests/read/en/game.yml @@ -0,0 +1,93 @@ +id: read +title: Reading File Contents +image: base +instruction: | + In this quest, you will learn: + 1. How to display file contents with `cat` + +requirements: + - path + +stages: + - id: start + name: Click `Start` to begin the lesson + condition: + command: echo start + response: + content: | + The terminal has no graphical interface for viewing file contents. + Instead, you can use `cat` to display them. + `cat` is short for concatenate, which means joining things together. + Enter `cat [filename]` to print a file's contents in the terminal. + First, see which files are in this folder. + hint: Enter `ls` to list the files in this folder. + + - id: ls_home + name: List the files in the folder + requirements: + - start + condition: + pwd: /home/commander + command: ls + response: + content: | + This folder contains one file and one folder. + Now use `cat` to display the contents of `file1.txt`. + hint: Enter `cat file1.txt` to display the file contents. + + - id: cat-file1 + name: Display the contents of `file1.txt` + requirements: + - ls_home + condition: + pwd: /home/commander + command: cat file1.txt + response: + content: | + You should now see the file's contents. + You can also give `cat` a path to a file in another folder. + First, use `ls folder` to list the files inside `folder`. + hint: Enter `ls folder` to list the files inside the folder + + - id: ls-folder + name: List the contents of `folder` + requirements: + - cat-file1 + condition: + pwd: /home/commander + command: ls folder + exceptions: + - id: not_in_home + condition: + not: + pwd: /home/commander + response: + content: | + Stay in the `/home/commander` folder. + Enter `cd ~` to return to `/home/commander`. + response: + content: | + You now know what is inside `folder`. + Use `cat` to display the contents of `file.txt` inside `folder`. + hint: Enter `cat folder/file.txt` to display the contents of `file.txt`. + + - id: END + name: Display the contents of `file.txt` + requirements: + - ls-folder + condition: + pwd: /home/commander + command: cat folder/file.txt + exceptions: + - id: not_in_home + condition: + not: + pwd: /home/commander + response: + content: | + Stay in the `/home/commander` folder. + Enter `cd ~` to return to `/home/commander`. + response: + content: | + You have displayed the contents of `file.txt`. + You now know how to display file contents with `cat`. diff --git a/quests/read/home/file1.txt b/quests/read/en/home/file1.txt similarity index 100% rename from quests/read/home/file1.txt rename to quests/read/en/home/file1.txt diff --git a/quests/read/home/folder/file.txt b/quests/read/en/home/folder/file.txt similarity index 100% rename from quests/read/home/folder/file.txt rename to quests/read/en/home/folder/file.txt diff --git a/quests/read/zh-TW/answer.sh b/quests/read/zh-TW/answer.sh new file mode 100644 index 0000000..c70c380 --- /dev/null +++ b/quests/read/zh-TW/answer.sh @@ -0,0 +1,5 @@ +echo start +ls +cat file1.txt +ls folder +cat folder/file.txt \ No newline at end of file diff --git a/quests/read/game.yml b/quests/read/zh-TW/game.yml similarity index 100% rename from quests/read/game.yml rename to quests/read/zh-TW/game.yml diff --git a/quests/read/zh-TW/home/file1.txt b/quests/read/zh-TW/home/file1.txt new file mode 100644 index 0000000..b3cfc35 --- /dev/null +++ b/quests/read/zh-TW/home/file1.txt @@ -0,0 +1 @@ +This is file1.txt diff --git a/quests/read/zh-TW/home/folder/file.txt b/quests/read/zh-TW/home/folder/file.txt new file mode 100644 index 0000000..597d692 --- /dev/null +++ b/quests/read/zh-TW/home/folder/file.txt @@ -0,0 +1 @@ +This is file.txt \ No newline at end of file diff --git a/quests/remove/answer.sh b/quests/remove/en/answer.sh similarity index 100% rename from quests/remove/answer.sh rename to quests/remove/en/answer.sh diff --git a/quests/remove/en/game.yml b/quests/remove/en/game.yml new file mode 100644 index 0000000..e327ec5 --- /dev/null +++ b/quests/remove/en/game.yml @@ -0,0 +1,202 @@ +id: remove +title: Deleting Files and Folders +image: base +instruction: | + In this quest, you will learn: + 1. How to delete files with `rm` + 2. How to delete empty folders with `rmdir` + 3. How to delete folders and their contents with `rm -r` + +requirements: + - copy + - move + +stages: + - id: start + name: Click `Start` to begin + condition: + command: echo start + response: + content: | + Today we will learn how to delete files and folders. + Your current folder contains `hello.txt`, `world.txt`, and a folder named `notes`. + First, use `ls` to list them. + When you have finished, enter `echo next` to continue! + hint: | + Use `ls` to list the files in the folder + When you have finished, enter `echo next` to continue + + - id: next + requirements: + - start + name: Explore the files, then enter `echo next` to continue + condition: + command: echo next + response: + content: | + Now let's learn how to delete files. + `rm` is short for remove. It deletes files. + Use `rm [filename]`. + Let's start by deleting `hello.txt`! + hint: Use `rm hello.txt` to delete `hello.txt` + + - id: rm-hello + requirements: + - next + name: Delete `hello.txt` + condition: + command: rm hello.txt + files: + - path: /home/commander/hello.txt + type: file + exists: false + response: + content: | + `hello.txt` is no longer in the folder! + + `rm` can delete several files at once. Separate their names with spaces. + Use `rm [file1] [file2] ...`. + Now use `rm` to delete `world.txt` and `wow.txt`. + hint: | + Use `rm world.txt wow.txt` to delete `world.txt` and `wow.txt` + Separate the filenames with a space + The order of the names does not matter + + - id: rm-world-wow + requirements: + - rm-hello + name: Delete `world.txt` and `wow.txt` + condition: + files: + - path: /home/commander/world.txt + type: file + exists: false + - path: /home/commander/wow.txt + type: file + exists: false + response: + content: | + `world.txt` and `wow.txt` are now gone. + + Next, let's learn how to delete folders. + `rmdir` deletes empty folders. + Use `rmdir [folder name]`. + Let's start by deleting the `empty` folder! + hint: Use `rmdir empty` to delete the `empty` folder + + - id: rm-empty + requirements: + - rm-world-wow + name: Delete the `empty` folder + condition: + command: rmdir empty + files: + - path: /home/commander/empty + type: directory + exists: false + response: + content: | + The `empty` folder is now gone. + Now try using `rmdir` to delete the `notes` folder. + hint: Use `rmdir notes` to delete the `notes` folder + + - id: rm-notes-failed + requirements: + - rm-empty + name: Try deleting the `notes` folder + condition: + command: rmdir notes + error: "rmdir: failed to remove 'notes': Directory not empty" + response: + content: | + The command failed! + The `notes` folder is still there. + This is because `rmdir` only deletes empty folders. + Let's delete the files inside `notes` and try again. + Now use `rm` to delete the files inside `notes`. + hint: | + Use `rm notes/[filename]` to delete a file inside `notes` + You may need to use `ls notes` to see which files are inside + Or use Tab to complete the filename + + - id: rm-notes-content + requirements: + - rm-notes-failed + name: Delete the files inside `notes` + condition: + command: rm notes/note.txt + files: + - path: /home/commander/notes/note.txt + type: file + exists: false + exceptions: + - id: hint + condition: + command: echo hint + response: + content: | + Use `rm notes/note.txt` to delete `note.txt` inside `notes`. + hint: Use `rm notes/note.txt` to delete `note.txt` inside `notes` + response: + content: | + Great! Now use `rmdir` again to delete the `notes` folder. + hint: Use `rmdir notes` to delete the `notes` folder + + - id: rm-notes + requirements: + - rm-notes-content + name: Delete the `notes` folder + condition: + command: rmdir notes + files: + - path: /home/commander/notes + type: directory + exists: false + response: + content: | + Great! The `notes` folder has been deleted. + Next, let's delete the `trash` folder. + First, use `ls` to see what is inside `trash`. + hint: Use `ls trash` to list the files inside `trash` + + - id: ls-trash + requirements: + - rm-notes + name: List the contents of `trash` + condition: + command: ls trash + response: + content: | + Wow! `trash` contains lots of files, and even more folders! + What if a folder has many files and subfolders and we want to delete them all at once? + Just as with `cp`, add the `-r` option to delete the folder and everything inside it. + `-r` means recursive: the command also deletes the contents of any nested folders. + Let's try it! + hint: Use `rm -r trash` to delete the `trash` folder + + - id: rm-trash + requirements: + - ls-trash + name: Delete `trash` and its contents + condition: + command: rm -r trash + files: + - path: /home/commander/trash + type: directory + exists: false + response: + content: | + The `trash` folder and everything inside it are now gone. + Congratulations! You now know how to delete files and folders! + Enter `echo finish` to complete this quest. + + - id: END + requirements: + - rm-trash + name: Complete the quest + condition: + command: echo finish + response: + content: | + You now know how to delete files and folders! + Congratulations on completing this quest! diff --git a/quests/remove/en/home/empty/.gitkeep b/quests/remove/en/home/empty/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quests/remove/en/home/hello.txt b/quests/remove/en/home/hello.txt new file mode 100644 index 0000000..e965047 --- /dev/null +++ b/quests/remove/en/home/hello.txt @@ -0,0 +1 @@ +Hello diff --git a/quests/remove/en/home/notes/note.txt b/quests/remove/en/home/notes/note.txt new file mode 100644 index 0000000..77c6f9e --- /dev/null +++ b/quests/remove/en/home/notes/note.txt @@ -0,0 +1 @@ +Note diff --git a/quests/remove/en/home/trash/garbage.txt b/quests/remove/en/home/trash/garbage.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/remove/en/home/trash/rubbish.txt b/quests/remove/en/home/trash/rubbish.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/remove/en/home/trash/secrets/envelope.txt b/quests/remove/en/home/trash/secrets/envelope.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/remove/en/home/world.txt b/quests/remove/en/home/world.txt new file mode 100644 index 0000000..216e97c --- /dev/null +++ b/quests/remove/en/home/world.txt @@ -0,0 +1 @@ +World diff --git a/quests/remove/home/wow.txt b/quests/remove/en/home/wow.txt similarity index 100% rename from quests/remove/home/wow.txt rename to quests/remove/en/home/wow.txt diff --git a/quests/remove/zh-TW/answer.sh b/quests/remove/zh-TW/answer.sh new file mode 100644 index 0000000..9d69429 --- /dev/null +++ b/quests/remove/zh-TW/answer.sh @@ -0,0 +1,14 @@ +echo start +ls +rm hello.txt +ls +rm world.txt wow.txt +ls +rmdir empty +ls +rmdir notes +rm notes/note.txt +rmdir notes +ls trash +rm -r trash +ls \ No newline at end of file diff --git a/quests/remove/game.yml b/quests/remove/zh-TW/game.yml similarity index 100% rename from quests/remove/game.yml rename to quests/remove/zh-TW/game.yml diff --git a/quests/remove/zh-TW/home/empty/.gitkeep b/quests/remove/zh-TW/home/empty/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quests/remove/zh-TW/home/hello.txt b/quests/remove/zh-TW/home/hello.txt new file mode 100644 index 0000000..e965047 --- /dev/null +++ b/quests/remove/zh-TW/home/hello.txt @@ -0,0 +1 @@ +Hello diff --git a/quests/remove/zh-TW/home/notes/note.txt b/quests/remove/zh-TW/home/notes/note.txt new file mode 100644 index 0000000..77c6f9e --- /dev/null +++ b/quests/remove/zh-TW/home/notes/note.txt @@ -0,0 +1 @@ +Note diff --git a/quests/remove/zh-TW/home/trash/garbage.txt b/quests/remove/zh-TW/home/trash/garbage.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/remove/zh-TW/home/trash/rubbish.txt b/quests/remove/zh-TW/home/trash/rubbish.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/remove/zh-TW/home/trash/secrets/envelope.txt b/quests/remove/zh-TW/home/trash/secrets/envelope.txt new file mode 100644 index 0000000..e69de29 diff --git a/quests/remove/zh-TW/home/world.txt b/quests/remove/zh-TW/home/world.txt new file mode 100644 index 0000000..216e97c --- /dev/null +++ b/quests/remove/zh-TW/home/world.txt @@ -0,0 +1 @@ +World diff --git a/quests/remove/zh-TW/home/wow.txt b/quests/remove/zh-TW/home/wow.txt new file mode 100644 index 0000000..18bd653 --- /dev/null +++ b/quests/remove/zh-TW/home/wow.txt @@ -0,0 +1 @@ +Wow \ No newline at end of file diff --git a/quests/tab/en/game.yml b/quests/tab/en/game.yml new file mode 100644 index 0000000..ec3aea2 --- /dev/null +++ b/quests/tab/en/game.yml @@ -0,0 +1,174 @@ +id: tab +title: Tab Completion +image: base +instruction: | + In this quest, you will learn: + + 1. How to complete paths with Tab + 2. How to handle multiple matching names + + Click the button in the middle to start this quest. + +requirements: + - read + +stages: + - id: start + name: Click Start to begin the quest + condition: + command: echo start + response: + content: | + In the terminal, you work by entering text. + Typing takes time, and a single typo can make a command fail. + Autocompletion helps you fill in file paths, command names, and supported options, saving time and avoiding typos. + + Before we start practicing, let's see which files are in your home folder. + hint: Use `ls` to list the files + + - id: ls + name: List the files with `ls` + requirements: + - start + condition: + command: ls + response: + content: | + This folder is full of very long and misspelled filenames. Typing them character by character would be painful. + Luckily, autocompletion makes even long filenames easy to enter. + + To use autocompletion, **type part of a filename, then press Tab**. + (Tab is the key to the left of Q.) + For example, type `Su` and press Tab to complete `SuperSecretPlansForWorldDomination.txt`. The terminal fills in the rest for you. + + Let's display the contents of `SuperSecretPlansForWorldDomination.txt`. + hint: | + Use `cat SuperSecretPlansForWorldDomination.txt` to display the file contents + Type `cat Su`, then press Tab to complete the filename. Press Enter to run the command + + - id: cat_super_secret + name: Display SuperSecretPlansForWorldDomination.txt with `cat` + requirements: + - ls + condition: + pwd: /home/commander + command: cat SuperSecretPlansForWorldDomination.txt + response: + content: | + The terminal completed `SuperSecretPlansForWorldDomination.txt` for you, saving you from typing a long name and making mistakes. + You can start with `S`, `Su`, `Sup`, `Super`, or even no characters at all. + However, the fewer characters you enter, the more likely they are to match several files. + We will practice handling multiple matches shortly. + + Autocompletion also helps with awkward spellings. Take `ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt`: anyone could make a mistake typing that! + Use `cat` again to display the contents of `ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt`. + hint: | + Enter `cat ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` to display the file contents + Type `cat Thsi`, then press Tab to complete the filename + Try pressing Tab after entering fewer characters and see what happens + + - id: cat_typo + name: Display ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt with `cat` + requirements: + - cat_super_secret + condition: + pwd: /home/commander + command: cat ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt + response: + content: | + You have opened the file with the misspelled name. + With autocompletion, typos, mixed capitalization, and random-looking names are much easier to handle. + Let's practice again. This time, list the files inside `ThingsIWillOrganizeLaterWhenIHaveTime`. + As before, type the first few characters, then press Tab. + hint: | + Enter `ls ThingsIWillOrganizeLaterWhenIHaveTime` to list the files inside the folder + Type `ls Thing`, then press Tab to complete the folder name + + - id: ls_things + name: List ThingsIWillOrganizeLaterWhenIHaveTime with `ls` + requirements: + - cat_typo + condition: + pwd: /home/commander + command: ls ThingsIWillOrganizeLaterWhenIHaveTime + response: + content: | + This folder contains three files: a guide to making sandwiches and two lists of things downloaded in the middle of the night. + The sandwich guide, `HowToProperlyMakeASandwich.txt`, is inside another folder, so one press of Tab will not complete the whole path. + This time, let's practice **completing a path in steps**. + + Type the start of the folder name and press Tab. Then type the start of the filename and press Tab again to complete `folder/filename`. + When you complete a folder name, the terminal adds `/` at the end for you. + Let's open `HowToProperlyMakeASandwich.txt`. + hint: | + Enter `cat ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt` to display the file contents + Type `cat Thin`, then press Tab to complete it to `cat ThingsIWillOrganizeLaterWhenIHaveTime/` + Next, type `How` after the folder path and press Tab to complete the full path + + - id: cat_sandwich + name: Display ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt with `cat` + requirements: + - ls_things + condition: + command: cat ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt + response: + content: | + You have read this very strict guide to making a sandwich. + Two files remain: `RandomStuffThatIDownloadedAt3AM.txt` and `RandomStuffThatIDownloadedAt4AM.txt`. + Their names are almost identical. They differ only at `3AM` and `4AM` before the `.txt` extension. + If you only type the first few characters, the terminal cannot tell which file you want. + This is a case of **multiple matches**. + + You can handle multiple matches in steps, just as you did with the folder path. + Type `Ran` and press Tab. The terminal completes the shared part of both names: `RandomStuffThatIDownloadedAt`. + Then type `3` or `4`, depending on which file you want, and press Tab again to complete the rest. + + _Completing a full path also takes steps: after completing the folder name, you still need to choose a file inside it._ + hint: | + Display the contents of `ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt` + Type `cat Thin`, then press Tab to complete it to `cat ThingsIWillOrganizeLaterWhenIHaveTime/` + Next, type `Ran` and press Tab to complete it to `cat ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt` + Then type `3` and press Tab to complete the full path. + + - id: cat_3am + name: Display ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt with `cat` + requirements: + - cat_sandwich + condition: + pwd: /home/commander + command: cat ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt + response: + content: | + You have read a list of things downloaded at 3 AM. Hmm... that is enough internet for today. + Here is a final challenge: enter `ThingsIWillOrganizeLaterWhenIHaveTime`, then display the contents of `RandomStuffThatIDownloadedAt4AM.txt`. + Use the autocompletion skills you learned today to complete the task. + hint: | + First, use `cd` to enter ThingsIWillOrganizeLaterWhenIHaveTime + Then use `cat` to display RandomStuffThatIDownloadedAt4AM.txt + Remember to use Tab completion to save time and avoid mistakes + + - id: cd_things + name: Enter ThingsIWillOrganizeLaterWhenIHaveTime with `cd` + requirements: + - cat_3am + condition: + pwd: /home/commander/ThingsIWillOrganizeLaterWhenIHaveTime + response: + content: | + Great! You have entered ThingsIWillOrganizeLaterWhenIHaveTime. + Now use `cat` to display the contents of `RandomStuffThatIDownloadedAt4AM.txt`. + Remember to use Tab completion! + + - id: END + name: Display RandomStuffThatIDownloadedAt4AM.txt with `cat` + requirements: + - cd_things + condition: + pwd: /home/commander/ThingsIWillOrganizeLaterWhenIHaveTime + command: cat RandomStuffThatIDownloadedAt4AM.txt + response: + content: | + Congratulations! You have completed all the tasks and practiced using Tab completion. + This skill will make working in the terminal much easier. + Whether you face long filenames, complex paths, or tricky spellings, Tab completion is there to help! + hint: All tasks completed! You can keep exploring or finish this quest. diff --git a/quests/tab/en/home/SuperSecretPlansForWorldDomination.txt b/quests/tab/en/home/SuperSecretPlansForWorldDomination.txt new file mode 100644 index 0000000..4e6f35b --- /dev/null +++ b/quests/tab/en/home/SuperSecretPlansForWorldDomination.txt @@ -0,0 +1,10 @@ +1. Collect all the socks in the world so nobody can find a matching pair. +2. Train an army of penguins to conquer the world with cuteness. +3. Invent a device that turns everyone's speech into chicken noises. +4. Replace all coffee with decaf and watch the world descend into chaos. +5. Erase every national border with a giant eraser and declare Earth one huge playground. +6. Make all politicians wear clown costumes when giving speeches. +7. Replace every math textbook with a cookbook and see what happens. +8. Invent a perfume that makes people tell the truth, then spray it at the UN General Assembly. +9. Use a giant magnet to collect all the metal on Earth and build a Disneyland in space. +10. If all else fails, flood the world's social media with cute cat videos until everyone surrenders. diff --git a/quests/tab/en/home/ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt b/quests/tab/en/home/ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt new file mode 100644 index 0000000..adb8eea --- /dev/null +++ b/quests/tab/en/home/ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt @@ -0,0 +1,9 @@ +How to Make a Sandwich "Properly": + +1. Slap your bread awake. It has been loafing around all day! +2. Choose your ingredients carefully. Each one must have its own personality. For example, pick a slice of ham that loves telling jokes. +3. Measure the thickness of each ingredient with a ruler. The margin of error must be no more than 1 mm. +4. Wear white gloves and laboratory goggles while assembling the sandwich to ensure the highest standards of hygiene. +5. Once finished, inspect every inch of the sandwich with a magnifying glass to make sure it is flawless. + +Remember: making a sandwich is an exact science. No mistakes or creativity allowed! diff --git a/quests/tab/en/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt b/quests/tab/en/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt new file mode 100644 index 0000000..63fed87 --- /dev/null +++ b/quests/tab/en/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt @@ -0,0 +1,5 @@ +Things I started downloading at 3 AM but never had time to organize. + +1. Blueprints for a magic wand that turns any food into pizza +2. A secret guide to learning ten alien languages in five minutes +3. A tutorial video on making fashionable hats from old socks (link no longer works) diff --git a/quests/tab/en/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt4AM.txt b/quests/tab/en/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt4AM.txt new file mode 100644 index 0000000..72f14a5 --- /dev/null +++ b/quests/tab/en/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt4AM.txt @@ -0,0 +1,5 @@ +Things I started downloading at 4 AM but never had time to organize. + +1. A paper explaining in detail why cats are the true rulers of Earth +2. A wikiHow article on repairing the International Space Station with chewing gum and paper clips +3. A complete course syllabus for training jellyfish as underwater detectives diff --git a/quests/tab/home/ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt b/quests/tab/en/home/ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt similarity index 100% rename from quests/tab/home/ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt rename to quests/tab/en/home/ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt diff --git a/quests/tab/en/tab.md b/quests/tab/en/tab.md new file mode 100644 index 0000000..2e18e14 --- /dev/null +++ b/quests/tab/en/tab.md @@ -0,0 +1,95 @@ +# Using Tab Completion + +In this quest, you will learn: + +1. How to complete paths with Tab +2. How to handle multiple matching names + +Click the button in the middle to start this quest. + +--- + +In the terminal, you work by entering text. +Typing takes time, and a single typo can make a command fail. +Autocompletion helps you fill in file paths, command names, and supported options, saving time and avoiding typos. + +Before we start practicing, let's see which files are in your home folder. + +> Use `ls` to list the files + +--- + +This folder is full of very long and misspelled filenames. Typing them character by character would be painful. +Luckily, autocompletion makes even long filenames easy to enter. + +To use autocompletion, **type part of a filename, then press Tab**. +(Tab is the key to the left of Q.) +For example, type `Su` and press Tab to complete `SuperSecretPlansForWorldDomination.txt`. The terminal fills in the rest for you. + +Let's display the contents of `SuperSecretPlansForWorldDomination.txt`. + +> Use `cat SuperSecretPlansForWorldDomination.txt` to display the file contents +> Type `cat Su`, then press Tab to complete the filename. Press Enter to run the command + +--- + +The terminal completed `SuperSecretPlansForWorldDomination.txt` for you, saving you from typing a long name and making mistakes. +You can start with `S`, `Su`, `Sup`, `Super`, or even no characters at all. +However, the fewer characters you enter, the more likely they are to match several files. +We will practice handling multiple matches shortly. + +Autocompletion also helps with awkward spellings. Take `ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt`: anyone could make a mistake typing that! +Use `cat` again to display the contents of `ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt`. + +> Enter `cat ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt` to display the file contents +> Type `cat Thsi`, then press Tab to complete the filename +> Try pressing Tab after entering fewer characters and see what happens + +--- + +You have opened the file with the misspelled name. +With autocompletion, typos, mixed capitalization, and random-looking names are much easier to handle. +Let's practice again. This time, list the files inside `ThingsIWillOrganizeLaterWhenIHaveTime`. +As before, type the first few characters, then press Tab. + +> Enter `ls ThingsIWillOrganizeLaterWhenIHaveTime` to list the files inside the folder +> Type `ls Thing`, then press Tab to complete the folder name + +--- + +This folder contains three files: a guide to making sandwiches and two lists of things downloaded in the middle of the night. +The sandwich guide, `HowToProperlyMakeASandwich.txt`, is inside another folder, so one press of Tab will not complete the whole path. +This time, let's practice **completing a path in steps**. + +Type the start of the folder name and press Tab. Then type the start of the filename and press Tab again to complete `folder/filename`. +When you complete a folder name, the terminal adds `/` at the end for you. +Let's open `HowToProperlyMakeASandwich.txt`. + +> Enter `cat ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt` to display the file contents +> Type `cat Thin`, then press Tab to complete it to `cat ThingsIWillOrganizeLaterWhenIHaveTime/` +> Next, type `How` after the folder path and press Tab to complete the full path + +--- + +You have read this very strict guide to making a sandwich. +Two files remain: `RandomStuffThatIDownloadedAt3AM.txt` and `RandomStuffThatIDownloadedAt4AM.txt`. +Their names are almost identical. They differ only at `3AM` and `4AM` before the `.txt` extension. +If you only type the first few characters, the terminal cannot tell which file you want. +This is a case of **multiple matches**. + +You can handle multiple matches in steps, just as you did with the folder path. +Type `Ran` and press Tab. The terminal completes the shared part of both names: `RandomStuffThatIDownloadedAt`. +Then type `3` or `4`, depending on which file you want, and press Tab again to complete the rest. + +_Completing a full path also takes steps: after completing the folder name, you still need to choose a file inside it._ + +> Enter `cat ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt` to display the file contents +> Type `cat Thin`, then press Tab to complete it to `cat ThingsIWillOrganizeLaterWhenIHaveTime/` +> Next, type `Ran` and press Tab to complete it to `cat ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt` +> Then type `3` and press Tab to complete the full path. + +--- + +You have read a list of things downloaded at 3 AM. Hmm... that is enough internet for today. +Here is a final challenge: enter `ThingsIWillOrganizeLaterWhenIHaveTime`, then display the contents of `RandomStuffThatIDownloadedAt4AM.txt`. +Use the autocompletion skills you learned today to complete the task. diff --git a/quests/tab/game.yml b/quests/tab/zh-TW/game.yml similarity index 100% rename from quests/tab/game.yml rename to quests/tab/zh-TW/game.yml diff --git a/quests/tab/home/SuperSecretPlansForWorldDomination.txt b/quests/tab/zh-TW/home/SuperSecretPlansForWorldDomination.txt similarity index 100% rename from quests/tab/home/SuperSecretPlansForWorldDomination.txt rename to quests/tab/zh-TW/home/SuperSecretPlansForWorldDomination.txt diff --git a/quests/tab/home/ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt b/quests/tab/zh-TW/home/ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt similarity index 100% rename from quests/tab/home/ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt rename to quests/tab/zh-TW/home/ThingsIWillOrganizeLaterWhenIHaveTime/HowToProperlyMakeASandwich.txt diff --git a/quests/tab/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt b/quests/tab/zh-TW/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt similarity index 100% rename from quests/tab/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt rename to quests/tab/zh-TW/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt3AM.txt diff --git a/quests/tab/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt4AM.txt b/quests/tab/zh-TW/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt4AM.txt similarity index 100% rename from quests/tab/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt4AM.txt rename to quests/tab/zh-TW/home/ThingsIWillOrganizeLaterWhenIHaveTime/RandomStuffThatIDownloadedAt4AM.txt diff --git a/quests/tab/zh-TW/home/ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt b/quests/tab/zh-TW/home/ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt new file mode 100644 index 0000000..f9aa76c --- /dev/null +++ b/quests/tab/zh-TW/home/ThsisAFlieNmaeWthAloOfTpyosThatisRleayHdToRaed.txt @@ -0,0 +1,3 @@ +Congrats! You fond a flie namd by somone who cleerly had a strok. If you can raed this, you mght be a geenius or jsut realy gud at Wheel of Fortnue. + +Remeber: freinds don't let freinds name files aftr drinkng too much. But hey, at lest you used tab compltion to get here! diff --git a/quests/tab/tab.md b/quests/tab/zh-TW/tab.md similarity index 100% rename from quests/tab/tab.md rename to quests/tab/zh-TW/tab.md diff --git a/server/src/api/controllers/commandController.ts b/server/src/api/controllers/commandController.ts index c49e80a..aeab0a3 100644 --- a/server/src/api/controllers/commandController.ts +++ b/server/src/api/controllers/commandController.ts @@ -54,7 +54,7 @@ export const newCommand = asyncHandler(async (req: Request, res: Response) => { session.lastActivityAt = new Date() - const quest = await questManager.get(session.quest) + const quest = await questManager.get(session.quest, session.locale) if (!quest) { res.status(404).json({ message: 'quest not found' }) return diff --git a/server/src/containers/docker.ts b/server/src/containers/docker.ts index ef4b84b..a3b03a4 100644 --- a/server/src/containers/docker.ts +++ b/server/src/containers/docker.ts @@ -1,5 +1,6 @@ import fs from 'fs/promises' import Docker from 'dockerode' +import type { QuestLocale } from '../../../packages/constants' import config, { getQuestImage } from '../config.js' import logger from '../utils/logger.js' @@ -36,19 +37,20 @@ const newContainerOptions = ( export async function createContainer( name: string, questId: string, + locale: QuestLocale, imageId: string ): Promise { const binds: string[] = [] - if (await questHomeExists(questId)) { + if (await questHomeExists(questId, locale)) { logger.info('Mounting quest home', questId) binds.push( - `${config.docker.hostProjectRoot}/quests/${questId}/home:/etc/skel:ro` + `${config.docker.hostProjectRoot}/quests/${questId}/${locale}/home:/etc/skel:ro` ) } if (config.docker.mountQuest && imageId !== 'base') { logger.info('Mounting quest directory', questId) binds.push( - `${config.docker.hostProjectRoot}/quests/${questId}/home:/home/commander` + `${config.docker.hostProjectRoot}/quests/${questId}/${locale}/home:/home/commander` ) } if (config.docker.mountCLI) { @@ -75,9 +77,11 @@ async function createNetworkIfNotExists(network: string) { } } -async function questHomeExists(imageId: string) { +async function questHomeExists(imageId: string, locale: QuestLocale) { try { - const stat = await fs.stat(`${config.projectRoot}/quests/${imageId}/home`) + const stat = await fs.stat( + `${config.projectRoot}/quests/${imageId}/${locale}/home` + ) return stat.isDirectory() } catch { return false diff --git a/server/src/models/quest.ts b/server/src/models/quest.ts index fa5f71d..cab531a 100644 --- a/server/src/models/quest.ts +++ b/server/src/models/quest.ts @@ -3,6 +3,8 @@ import fs from 'fs/promises' import path from 'path' import yaml from 'yaml' +import type { QuestLocale } from '../../../packages/constants' +import { SUPPORTED_LOCALES } from '../../../packages/constants' import { questSchema, globalExceptionSchema, @@ -36,16 +38,22 @@ class QuestValidationError extends Error { } } +function questImageId(id: string, locale: QuestLocale) { + return `${id}-${locale.toLowerCase()}` +} + class QuestManager { - private quests = new Map() + private quests = new Map>() private questDirectory = path.join(config.projectRoot, 'quests') - get(id: string) { - return this.quests.get(id) + get(id: string, locale: QuestLocale) { + return this.quests.get(id)?.get(locale) } - getAll() { + getAll(locale: QuestLocale) { return Array.from(this.quests.values()) + .map((versions) => versions.get(locale)) + .filter((quest): quest is IQuest => quest !== undefined) } private async mapQuests(callback: QuestCallback) { @@ -83,42 +91,119 @@ class QuestManager { async loadAndUpdateQuests() { const exceptions = await this.loadGlobalExceptions() - return this.mapQuests(async (id, questPath) => { - const files = await fs.readdir(questPath) - if (!files.includes('game.yml')) { - throw new QuestValidationError(`Missing game.yml`, id, null) + const loaded = new Map>() + + await this.mapQuests(async (id, questPath) => { + const entries = await fs.readdir(questPath, { withFileTypes: true }) + const versions = new Map() + + for (const locale of SUPPORTED_LOCALES) { + const localeEntry = entries.find((entry) => entry.name === locale) + + if (!localeEntry) { + logger.warn(`Missing locale "${locale}" for quest "${id}"`) + // eslint-disable-next-line no-continue + continue + } + + if (!localeEntry.isDirectory()) { + throw new QuestValidationError( + `${locale} exists but is not a directory`, + id, + null + ) + } + + const localePath = path.join(questPath, locale) + + try { + const files = await fs.readdir(localePath) + + if (!files.includes('game.yml')) { + throw new QuestValidationError( + `Missing ${locale}/game.yml`, + id, + null + ) + } + + const body = await fs.readFile( + path.join(localePath, 'game.yml'), + 'utf-8' + ) + + const questData = yaml.parse(body, { + merge: true, + }) + + const image = files.includes('Dockerfile') + ? questImageId(id, locale) + : 'base' + const quest = questSchema.parse({ + id, + image, + ...questData, + }) + quest.exceptions = [...exceptions] + versions.set(locale, quest) + } catch (error) { + logger.error(`Error parsing quest ${id} for locale ${locale}`, { + error, + }) + throw new QuestValidationError( + `Error parsing quest ${id} for locale ${locale}`, + id, + error + ) + } } - try { - const body = await fs.readFile( - path.join(questPath, 'game.yml'), - 'utf-8' - ) - - const questData = yaml.parse(body, { - merge: true, - }) - - const image = files.includes('Dockerfile') ? id : 'base' - const quest = questSchema.parse({ - id, - image, - ...questData, - }) - quest.exceptions = [...exceptions] - this.quests.set(id, quest) - } catch (error) { - logger.error(`Error parsing quest ${id}`, { error }) - throw new QuestValidationError(`Error parsing quest ${id}`, id, error) + if (versions.size > 0) { + loaded.set(id, versions) + } else { + logger.warn(`No valid locales found for quest "${id}"`) } }) + + this.quests = loaded } async getQuestDockerfiles() { const questlist = await this.mapQuests(async (id, questPath) => { - const files = await fs.readdir(questPath) - return files.includes('Dockerfile') ? { id, questPath } : null + const entries = await fs.readdir(questPath, { withFileTypes: true }) + const dockerfiles: { id: string; questPath: string }[] = [] + + for (const locale of SUPPORTED_LOCALES) { + const localeEntry = entries.find((entry) => entry.name === locale) + + if (!localeEntry) { + logger.warn(`Missing locale "${locale}" for quest "${id}"`) + // eslint-disable-next-line no-continue + continue + } + + if (!localeEntry.isDirectory()) { + throw new QuestValidationError( + `${locale} exists but is not a directory`, + id, + null + ) + } + + const localePath = path.join(questPath, locale) + const files = await fs.readdir(localePath) + + if (files.includes('Dockerfile')) { + dockerfiles.push({ + id: questImageId(id, locale), + questPath: localePath, + }) + } + } + + return dockerfiles.length > 0 ? dockerfiles : null }) + return questlist.filter((quest) => quest !== null) } } diff --git a/server/src/models/sessionManager.ts b/server/src/models/sessionManager.ts index 56ca590..c58266e 100644 --- a/server/src/models/sessionManager.ts +++ b/server/src/models/sessionManager.ts @@ -1,4 +1,6 @@ import { HydratedDocument } from 'mongoose' +import type { QuestLocale } from '../../../packages/constants' +import { LOCALE_NAME_MAP } from '../../../packages/constants' import { Session, UserProfile } from '../../../packages/models' import type { ISession, IUser } from '../../../packages/models' import { @@ -34,11 +36,12 @@ async function deactivateSessions(userId: string, quest: string) { export async function createNewSession( userId: string, - questId: string + questId: string, + locale: QuestLocale ): Promise { - const quest = await questManager.get(questId) + const quest = await questManager.get(questId, locale) if (!quest) { - throw new Error(`Quest ${questId} not found`) + throw new Error(`Quest ${questId} not found for ${LOCALE_NAME_MAP[locale]}`) } // deactivate all active sessions @@ -55,12 +58,18 @@ export async function createNewSession( const containerName = `quest-${quest.id}-${userProfile.user.username}-${Date.now()}` - const container = await createContainer(containerName, quest.id, quest.image) + const container = await createContainer( + containerName, + quest.id, + locale, + quest.image + ) await getAndStartContainer(container.id) const newSession = new Session({ user: userId, quest: quest.id, + locale, containerId: container.id, containerName, }) @@ -117,15 +126,16 @@ export async function finishSession( export async function isQuestUnlocked( userId: string, - questId: string + questId: string, + locale: QuestLocale ): Promise { const userProfile = await UserProfile.findOne({ user: userId }) if (!userProfile) { throw new Error(`UserProfile ${userId} not found`) } - const quest = await questManager.get(questId) + const quest = await questManager.get(questId, locale) if (!quest) { - throw new Error(`Quest ${questId} not found`) + throw new Error(`Quest ${questId} not found for ${LOCALE_NAME_MAP[locale]}`) } if (!quest.requirements) { return true diff --git a/server/src/routers/questRouter.ts b/server/src/routers/questRouter.ts index 7d00fdf..fc84084 100644 --- a/server/src/routers/questRouter.ts +++ b/server/src/routers/questRouter.ts @@ -2,26 +2,34 @@ import { TRPCError } from '@trpc/server' import { z } from 'zod' import { router, publicProcedure } from '../trpc.js' import { questManager } from '../models/quest.js' +import { SUPPORTED_LOCALES } from '../../../packages/constants' export const questRouter = router({ - getQuests: publicProcedure.query(async () => { - const quests = questManager.getAll() - return quests.map(({ title, id, requirements }) => ({ - id, - title, - requirements, - })) - }), - getQuestDetail: publicProcedure.input(z.string()).query(async (opts) => { - const questId = opts.input - const quest = questManager.get(questId) - if (!quest) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Quest not found.', - }) - } - const { id, title, instruction, requirements } = quest - return { id, title, instruction, requirements } - }), + getQuests: publicProcedure + .input(z.object({ locale: z.enum(SUPPORTED_LOCALES) })) + .query(async ({ input }) => { + const quests = questManager.getAll(input.locale) + + return quests.map(({ title, id, requirements }) => ({ + id, + title, + requirements, + })) + }), + + getQuestDetail: publicProcedure + .input(z.object({ questId: z.string(), locale: z.enum(SUPPORTED_LOCALES) })) + .query(async ({ input }) => { + const { questId, locale } = input + const quest = questManager.get(questId, locale) + + if (!quest) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Quest not found.', + }) + } + const { id, title, instruction, requirements } = quest + return { id, title, instruction, requirements } + }), }) diff --git a/server/src/routers/sessionRouter.ts b/server/src/routers/sessionRouter.ts index 3c5ecbf..0226597 100644 --- a/server/src/routers/sessionRouter.ts +++ b/server/src/routers/sessionRouter.ts @@ -1,5 +1,6 @@ import { TRPCError } from '@trpc/server' import { z } from 'zod' +import { SUPPORTED_LOCALES } from '../../../packages/constants' import { ISession, Session } from '../../../packages/models' import { GameSession, VoidFileExistenceChecker } from '../../../packages/game' import { createNewSession, isQuestUnlocked } from '../models/sessionManager.js' @@ -11,7 +12,7 @@ import { genJWT } from '../utils/auth' export type SessionDetail = Awaited> async function sessionDetail(session: ISession) { - const quest = questManager.get(session.quest) + const quest = questManager.get(session.quest, session.locale) if (!quest) { throw new TRPCError({ code: 'NOT_FOUND', @@ -31,6 +32,7 @@ async function sessionDetail(session: ISession) { _id: session._id.toString(), user: session.user.toString(), quest: session.quest, + locale: session.locale, status: session.status, createdAt: session.createdAt.toISOString(), lastActivityAt: session.lastActivityAt.toISOString(), @@ -45,12 +47,20 @@ async function sessionDetail(session: ISession) { export const sessionRouter = router({ createSession: protectedProcedure - .input(z.object({ questId: z.string() })) + .input( + z.object({ + questId: z.string(), + locale: z.enum(SUPPORTED_LOCALES), + }) + ) .mutation(async (opts) => { - const { questId } = opts.input + const { questId, locale } = opts.input const { user } = opts.ctx - if ((await isQuestUnlocked(user.id, questId)) || config.testing.enabled) { - const session = await createNewSession(user.id, questId) + if ( + (await isQuestUnlocked(user.id, questId, locale)) || + config.testing.enabled + ) { + const session = await createNewSession(user.id, questId, locale) return sessionDetail(session) } throw new TRPCError({ diff --git a/services/analytics/package.json b/services/analytics/package.json index 4542de4..6cf20dd 100644 --- a/services/analytics/package.json +++ b/services/analytics/package.json @@ -15,7 +15,8 @@ "express": "^4.20.0", "express-basic-auth": "^1.2.1", "express-handlebars": "^7.1.2", - "mongoose": "^8.24.1" + "mongoose": "^8.24.1", + "zod": "^3.24.2" }, "devDependencies": { "@swc/core": "^1.4.2",