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
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.
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
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):
pip install -r requirements.txt
Run the server
flask --app relic/main.py --debug run -h 0.0.0.0 -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
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.
make lint-fix to automatically run black and isort over your codebase.
This project is made up of a single backend written with
flask for a webserver
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
service.py to be processed, then to the
action logic of the action in
Here it finds any action registered with the
@action decorator, and generates the
message and commands within an
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
From here, any commands are then recorded and processed in the future to generate state as I'll talk about in the next section.
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.
kind which a command is registered
must correspond with either of these two
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
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
/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
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.