Mythos
My final-year project, and the first time I put an LLM in my own code. You type a chore, press Questify, and a model hands it back as a fantasy quest worth some XP.
const rawJSON = response.choices[0].message.contentconst questData = parseAndValidateQuestJSON(rawJSON)return { taskId, status: 'success', quest: { user: userId, originalTask: task._id, ...questData, isComplete: false, xpReward: Math.min(Math.max(questData.xp, 10), 150), },}Source: Project-RPG backend/controllers/questController.js, lines 59 to 72- Role
- Solo, full stack
- Timeline
- Nov 2024 to Apr 2025
- Status
- Never deployed
- Stack
- React, Express, MongoDB, OpenAI
Sources for these facts
- Role: Project-RPG git shortlog: 40 of 41 commits mine, 1 automated
- Timeline: Project-RPG git log: 41 commits, 12 Nov 2024 to 30 Apr 2025
- Status: frontend/src/api/*.ts call localhost:3001; no live URL found
- Stack: frontend/package.json; backend/package-lock.json
The short version
Mythos is a to-do app that hands your chores to an OpenAI model and gets them back as fantasy quests, with XP, levels, streaks and charts of your progress.
It was my first full-stack project and my first LLM integration. The part I'm still glad about: I didn't trust a word the model said.
What happens when you press Questify
- Source: frontend/src/pages/Adventure.tsx, line 125
You type a chore and press Questify.
- Source: Project-RPG backend/controllers/questController.js, line 37
Your tasks go to the model, at most 5 at a time.
- Source: Project-RPG backend/controllers/questController.js, lines 12 to 16 and 54
The model is told to answer in JSON: a quest title, a description and an XP number.
- Source: Project-RPG backend/controllers/questController.js, lines 59 to 70 and 207 to 222
The reply is cleaned, parsed and checked, and the XP is clamped to 10 to 150.
the hard part, below - Source: Project-RPG backend/controllers/questController.js, lines 91 to 95; backend/index.js, lines 75 to 110
The quests are saved in one insert and pushed live to every device you are signed in on.
For anyone who finds a to-do list dull
The README's whole pitch is turning "mundane tasks into exciting quests", and that is as far as the docs go.Source: Project-RPG README.md, line 3
I have no users or feedback to show you, so this page is about the build, not results.
Quests, levels, streaks, charts and live sync
- Questify
- Type a task and the model returns a quest with a title, a description and an XP reward.Source: Project-RPG backend/controllers/questController.js, lines 49 to 70
- Levels
- Level n needs n × 100 XP. Leftover XP carries over, and several level-ups in one go are handled in a loop.Source: Project-RPG backend/controllers/questController.js, lines 153 to 159
- Streaks
- Finish 3 quests in one day (UTC) and your streak goes up by one.Source: Project-RPG backend/controllers/questController.js, lines 161 to 184
- Live sync
- Socket.IO checks the same login token as the API, then puts each user in their own room, so updates only reach your devices.Source: backend/index.js, lines 63 to 110
- The Codex
- D3 charts of your progress, including cumulative XP with a 3-point moving average and zoom.Source: frontend/src/components/graphs/PlayerProgression.tsx, lines 26 to 60
- Sign-in
- A JWT in an httpOnly cookie with a 24-hour expiry, and bcrypt-hashed passwords of at least 8 characters.Source: backend/controllers/userController.js, lines 4 to 8; backend/models/User.js, lines 27 to 47 and 97 to 105
- The plumbing
- Swagger API docs, Docker setups for development and production, and a CI workflow on Node 18 and 20.Source: backend/config/swagger.js; docker-compose*.yml; .github/workflows/node.js.yml
The hard part: never trusting the model's reply
This is where my AI learning started. A language model doesn't return data. It returns text that usually looks like data. Mythos was going to add that number to someone's XP, so I treated every reply the way you'd treat a form filled in by a stranger.
I asked for JSON and switched on the API's JSON response format. Then, belt and braces, the code strips any Markdown code fences the model wraps around its answer, parses what is left, and throws if the title, the description or a numeric XP is missing.Source: Project-RPG backend/controllers/questController.js, lines 51 to 60 and 207 to 222
Last, the XP is clamped: that is line 70, highlighted at the top of this page. The prompt asks for a number from 0 to 150. Whatever comes back, the quest stores at least 10 and at most 150. The prompt is a request; the clamp is the rule.Source: Project-RPG backend/controllers/questController.js, lines 15 and 70
const jsonString = rawJSON .replace(/```json/g, '') .replace(/```/g, '') .trim() const data = JSON.parse(jsonString) if ( !data.questTitle || !data.questDescription || typeof data.xp !== 'number' ) { throw new Error('Invalid quest format from OpenAI') }Source: Project-RPG backend/controllers/questController.js, lines 208 to 220Batches that fail one task at a time
Questify can send several tasks at once. They go out at most 5 at a time, and each succeeds or fails on its own: a bad reply comes back as an error for that one task, tasks that already have a quest are skipped, and the rest are saved in a single database insert.Source: Project-RPG backend/controllers/questController.js, lines 25 to 96
XP you can only earn once
Completing a quest is one database update that only matches a quest that is yours and not yet complete. Press Complete twice and the second press finds nothing, so nobody is paid XP twice.Source: Project-RPG backend/controllers/questController.js, lines 141 to 148
Promise.allSettled, so one bad reply costs one quest
The obvious tool for firing off several calls is Promise.all, and it rejects the whole batch the moment one call fails. I used Promise.allSettled, which waits for every task and reports which ones failed.
The cost is some bookkeeping: the code sorts every result into created or skipped, and the response returns both lists. One sulking reply no longer costs you the other quests.Source: Project-RPG backend/controllers/questController.js, lines 38 and 81 to 102
Tests for the database models, none for the parser
- Three backend test files cover the database models for quests, users and tasks, written to run against an in-memory MongoDB.Source: backend/test/Quest.test.js, User.test.js, task.test.js; backend/jest.config.js
- Nothing tests the parser or the clamp, which is the part this page is proudest of.Source: backend/test/*.test.js
- A GitHub Actions workflow runs lint, tests and a build on Node 18 and 20. The setup file the tests rely on was never committed, so I can't show you a passing run.Source: .github/workflows/node.js.yml; backend/jest.config.js, line 7 (test/setup.js is not tracked)
What I'd own up to
- It was never deployed. There are Docker files for development and production, but the frontend calls http://localhost:3001 directly while the compose file runs the backend on port 3000.Source: frontend/src/api/authAPI.ts, line 10; taskQuestAPI.ts, line 4; userAPI.ts, line 5; docker-compose.yml
- The README promises more than the code delivers. Password reset, rate limiting and token refresh are listed; the user model has fields for a reset token, but no route uses them.Source: Project-RPG README.md, lines 170 to 201; backend/models/User.js, lines 59 to 66; backend/controllers
- Three files are empty: the Rewards page, the profile page and the navbar.Source: frontend/src/pages/Rewards.tsx, pages/profile.tsx, components/Navbar.tsx: 0 lines each
Next time: a schema, real tests and a little retrieval
What I'd change now I'm learning RAG and agents. Intentions, not things I have built yet.
- Define the quest shape once, as a schema, and use it both to ask the model and to check its reply, instead of hand-written ifs.
- Keep every reply that broke the parser and turn it into a test, so the guard is proven and not just written.
- Keep the model's raw reply when a task fails. Today it comes back as "API error" and the reply is gone.
- Give the model the player's past quests as context, so the world stays consistent. That is retrieval on a small scale, which is what I'm learning now.
- Try a model that plans first and then acts, splitting one big chore into smaller quests: a first, small agent.
- Deploy it, with the API address in config rather than hard-coded.
Where this sits in the index
The highlighted points are the chunks of my index that come from this page.
- 1Mythos: my first LLM call. Type a chore, get back a fantasy quest with XP.source: Project-RPG backend/controllers/questController.js
- 2Mythos checks the model's JSON and clamps its XP between 10 and 150.source: Project-RPG backend/controllers/questController.js, lines 51 to 70
- 3Final-year project at the University of Birmingham, 2024 to 2025.source: Project-RPG; git.cs.bham.ac.uk projects-2024-25