Game about willpower, economy and control
Retain item note with auction
#60 Add engraving
Fix bad login check


browse  log 



You can also use your local clone with git send-email.

#Text relic game

builds.sr.ht status

Web based MUD with a focus on economy, willpower and control.

Deployed here: stuff.kyleperik.com/relic

Checkout the progress and roadmap here: ticket tracker

Check how to deploy for yourself here: docs/deploy.md

#Game Overview

The idea of the game is inspired by Lord of the Rings, and the mechanics are based on several games I've played including Offword Trading Company, Minecraft, and One Hour One Life

I put my raw notes on my initial plan here: docs/spec.md.

#How to Run Locally

#Devcontainer and Visual Studio Code


Make sure the docker daemon is running, should just need to turn on Docker Desktop.

Clone this repo and open the cloned repo in Visual Studio Code.

Visual Studio Code should prompt you to re-open the workspace in a container. If not, just hit F1 and type in: Dev Containers: Rebuild and Reopen in Container and hit enter.

After the devcontainer has been opened, press F5 to turn on Relic!

sr.ht requires SSH keys to push changes, follow this tutorial for setting up SSH keys on your sr.ht account. Then the key will need to be added to your SSH Agent, so the devcontainer can access it. Follow this tutorial (NOTE: this is a microsoft tutorial and they tell you to copy your github key, you probably named your private key file something else, so look for that file in $HOME/.ssh/)

#How to Setup Locally with venv

First make sure you have a version of python 3.10 or greater. Also make sure you have sqlite3 installed.

First, clone this repo.

Then initialize the database by running each of the migrations

python -m relic.scripts.init_db

Create a virtual environment (pip install virtualenv):

virtualenv .venv


. .venv/bin/activate

Install requirements

pip install -r requirements.txt

Run the server

flask --app relic/main.py --debug run -h -p 8080

It should be running locally on port 8080:

NOTE: As time goes on, changes can be made to event processing, invalidating the cache. If you're running into errors locally after a pulling from master, try hitting GET /regen_cache


If you have docker installed, you can also run the webserver that way.

docker compose up


You can fork the project and sent me patchsets, or reach out to me for write access.

Please make sure to run make lint to make sure everything is neat prior to committing.

You can do cp pre-commit .git/hooks/; chmod +x .git/hooks/pre-commit to enable a git pre-commit hook to disable committing code which hasn't been linted.

Run make lint-fix to automatically run black and isort over your codebase.

#Technical Overview

Checkout this video on how the state management system.

This project is made up of a single backend written with flask for a webserver and sqlite3 for the db.

The frontend is very basic with just some raw html and js and api calls and vanilla DOM manipulation.

The backend consists mainly of views that interact with "commands" and "actions".

The general flow is as follows

action sent from the ui ("use forest")
-> actions generate a message and commands ("used forest", [-1wp, +10g, ...])
-> commands are recorded and change the game state
-> frontend gets updated with new state


Actions are the input sent from the text box in the UI.

Most of the time, these actions are sent to POST /action (with action_type and args)

This data is passed to perform_action in service.py to be processed, then to the action logic of the action in action/ .

Here it finds any action registered with the @action decorator, and generates the message and commands within an ActionResult.

The function must be in the format (args: str, state: State) -> ActionResult, args being the rest of the string, and state being the current state of the game. ActionResult contains a message and optionally, "commands" to mutate state.

From here, any commands are then recorded and processed in the future to generate state as I'll talk about in the next section.


aka events

The game state can always be regenerated from the event logs, which is the source of truth.

The purpose of commands is to mutate state. This materializes in a set of functions in a similar way to how actions are registered.

Use the decorator @command(name, kind) to register a new command (also make sure to add one to commands in the frontend as well). Commands are pretty simple as they take in one argument which can be any json serializable type, an entity_id, and the current state. They return nothing, as they only mutate state.

One special thing about commands is that they each have a scope. This currently means commands can be specific to one player or one area.

The kind which a command is registered must correspond with either of these two player or area. This is for the purpose of allowing partial state aggregates, for example, if your player is in an area, they only need to receieve updates for that specific area. These commands are also stored in different tables player_events and area_events.


On page load, the frontend first hits /init which will initialize with a new player if needed in a random area. It recieves a full state load, including only state from the player's "memory".

The frontend is capable of syncronous multiplayer, because it polls on an interval for new changes. To do this, for each entity type/id, it stores the latest event. Then it calls /head to see if the latest events matches with the current ones. If not, it will get update state which have happened since the recorded event with /player/current or /area/current.

The frontend will then process any events to update the local state. Note currently polling is only done for areas at the moment, because player entities should not be changed by other players at the moment.