Tjuvpakk Backend

API Reference — World of Mythos

Authentication
POST /claim_name Register or update a player name

Registers a new player name linked to an email, or updates the email if the name was previously unclaimed.

Request Body

FieldTypeRequiredDescription
namestringYes3–12 characters, no spaces or special characters
emailstringYesValid email address (must contain @)

Responses

StatusBodyMeaning
200{"success": true}Name claimed or email updated
400{"error": "Name must be 3–12 characters long"}Invalid name format
400{"error": "Invalid email."}Invalid email format
409{"error": "Name already claimed."}Name belongs to another email
500{"error": "Server error."}Database error
POST /log_in Verify name + email credentials

Verifies that a name + email pair matches what is stored in the database.

Request Body

FieldTypeRequiredDescription
namestringYesRegistered name
emailstringYesAssociated email

Responses

StatusBodyMeaning
200{"success": true}Credentials verified
403{"error": "Email does not match."}Wrong email for that name
404{"error": "Name not found."}Name does not exist
500{"error": "Server error."}Database error
Lobby
POST /create_lobby Create a new game lobby

Creates a new game lobby. The creator becomes the admin.

Request Body

FieldTypeRequiredDescription
namestringYesPlayer name (validated against DB)
emailstringYesPlayer email (must match DB record)

Responses

StatusBodyMeaning
200{"lobby_id": "<3-char-id>"}Lobby created
400{"error": "..."}Invalid name format
403{"error": "This name is already claimed..."}Email mismatch
Note: The name "Verden" is reserved and always joins as a spectator.
POST /join_lobby/<lobby_id> Join an existing lobby

Joins an existing lobby as a player. Joining after round 1 has started results in spectator status.

Request Body

FieldTypeRequiredDescription
namestringYesPlayer name
emailstringYesPlayer email

Responses

StatusBodyMeaning
200{"status": "joined"}Successfully joined
400{"error": "Name taken"}Name already in lobby
403{"error": "This name is claimed..."}Email mismatch
404{"error": "Lobby not found"}Lobby ID does not exist
POST /kick_player/<lobby_id> Admin removes a player

Admin removes a player from the lobby before the game starts.

Request Body

FieldTypeRequiredDescription
adminstringYesName of the admin
targetstringYesName of player to kick

Responses

StatusBodyMeaning
200{"status": "Player kicked"}Player removed
400{"error": "Game already started"}Cannot kick mid-game
400{"error": "You can't kick yourself"}Self-kick attempt
403{"error": "You are not the admin"}Not the admin
404{"error": "Lobby not found"}Invalid lobby ID
404{"error": "Player not found"}Target not in lobby
POST /start_game/<lobby_id> Admin starts the game

Admin starts the game, advancing the lobby to round 1.

Request Body

FieldTypeRequiredDescription
adminstringYesName of the admin

Responses

StatusBodyMeaning
200{"status": "Game started"}Game started
400{"error": "Game already started"}Already in progress
403{"error": "Only admin can start the game"}Not admin
404{"error": "Lobby not found"}Invalid lobby ID
GET /get_state/<lobby_id> Full lobby state + game heartbeat ★ Key endpoint

The primary polling endpoint. Returns the complete current state of a lobby and drives automatic game logic — making it the heartbeat of every active game session.

Side-effects on every call. Even though this is a GET endpoint, calling it can mutate lobby state: it auto-starts scheduled fights, triggers AI moves, and resolves rounds on timeout. Clients should poll every 1–2 seconds while in an active game.

What happens on each call

The endpoint executes five stages before returning data:

GET /get_state/<lobby_id> │ ├─ 1. Lobby lookup │ Look up lobby from in-memory dict (config.lobbies) │ └─ Not found → {"error": "Lobby not found"} │ ├─ 2. Boss fight auto-start (only if boss_fight=true AND round=0) │ Compare UTC now ≥ lobby.start_time │ If yes: │ round = 1 │ round_end_time = now + 40s │ Append "🔥 The boss-fight has begun!" to history │ Notify all players via their messages list │ ├─ 3. Gremlin fight auto-start (only if gremlin_fight=true AND round=0) │ Immediately: │ round = 1 │ round_end_time = now + 40s │ Notify players: "A Gremlin appeared in the forest!" │ ├─ 4. AI moves │ For each player in lobby.players: │ If player.bot == true AND alive: │ make_dummy_move(lobby, player) │ → Bot picks random action + resource + target │ If player.boss == true AND moves not yet submitted: │ If gremlin_fight: gremlin_take_action(lobby) │ Else: boss_take_action(lobby) │ → Boss picks target, applies boss-specific behaviour │ ├─ 5. Round timeout check │ If now ≥ round_end_time: │ If NOT gameover AND NOT round_locked: │ resolve_round(lobby_id) ← advances the game │ If round_locked: │ Skip (prevents double-resolve race condition) │ └─ 6. Return JSON state

Round resolution — what resolve_round() does

Triggered either by timeout (stage 5) or when all alive players have submitted (from submit_choice).

  1. Sets round_locked = True to prevent concurrent calls.
  2. Applies resource bonusesgain_hp restores HP, gain_coin adds coins, gain_attack raises attack.
  3. Executes the attack phase — attackers deal damage; defenders reduce incoming damage; raiders skip this phase.
  4. Executes the raid phase — raiders deal damage to the boss; boss defeat awards relics and sets raidwinner.
  5. Eliminates players with HP ≤ 0.
  6. Checks game-end conditions: one PvP survivor → set winner; boss HP ≤ 0 → set raidwinner.
  7. Advances round += 1, sets new round_end_time = now + 40s.
  8. Clears all per-round player state (submittedAction, submittedResource, messages).
  9. Resets round_locked = False and submittedBossMoves = False.
  10. If gameover, persists final stats to Supabase (games, game_player_stats, players tables).

Response schema

{
  "round":          2,
  "players":        [ ...player objects... ],
  "winner":         null,
  "raidwinner":     null,
  "pending_deny":   null,
  "deny_target":    null,
  "readyPlayers":   ["Alice", "Bob"],
  "history":        ["Alice created a lobby.", "Bob joined.", "..."],
  "round_end_time": "2024-01-01T12:00:40+00:00",
  "boss_fight":     false,
  "gremlin_fight":  false,
  "start_time":     "2024-01-01T12:00:00+00:00",
  "gameover":       false,
  "chat":           [ ...chat message objects... ]
}

Response field reference

FieldTypeDescription
roundinteger0 = waiting in lobby; 1+ = game in progress.
playersarrayAll player objects in the lobby (see Player Object below).
winnerstring | nullName of the PvP winner once one alive non-spectator player remains. null while game is ongoing.
raidwinnerstring | nullName of the player who dealt the killing blow to the raid boss. null otherwise.
pending_denystring | nullName of the player who currently holds the deny ability and must pick a target this round.
deny_targetstring | nullName of the player who was most recently denied their choices. Cleared each round.
readyPlayersstring[]Names of players who have submitted both action and resource this round.
historystring[]Ordered event log for the entire session. Human-readable strings, never cleared.
round_end_timeISO 8601UTC timestamp when the timer expires and auto-resolution fires.
boss_fightbooleantrue for cooperative raid lobbies.
gremlin_fightbooleantrue for solo gremlin encounters.
start_timeISO 8601UTC timestamp when a scheduled boss fight begins. Not meaningful for PvP lobbies.
gameoverbooleantrue once the game has ended.
chatarrayLobby chat messages. Max 100 entries kept.

Player Object fields

FieldTypeDescription
namestringDisplay name.
hpintegerCurrent hit points.
coinsintegerCurrent coin count.
attackintegerCurrent attack stat.
alivebooleanfalse once eliminated.
adminbooleantrue for the lobby creator.
botbooleantrue for AI dummy players.
bossbooleantrue for the raid boss entity.
spectatorbooleantrue for observers with no actions.
submittedActionstringCurrent round action ("" if not yet submitted). Cleared after round resolves.
submittedResourcestringCurrent round resource ("" if not yet submitted). Cleared after round resolves.
targetstring | nullChosen attack target.
messagesstring[]Private messages for this player this round. Cleared each round.
personal_historyarrayRound-by-round summary of this player's actions and outcomes.
idle_roundsintegerConsecutive rounds without a submission. Players with idle_rounds > 1 are skipped in resolution.
In-memory only. All lobby data lives in a Python dict (config.lobbies). It is not persisted to Supabase until the game ends. A server restart will lose all active lobbies.
GET /get_history/<lobby_id> Full event log for a lobby

Responses

StatusBodyMeaning
200{"history": ["...", "..."]}Event log returned
404{"error": "..."}Lobby not found
GET /get_player_messages/<lobby_id>/<player_name> Private messages for a player this round

Responses

StatusBodyMeaning
200{"player": "<name>", "messages": [...]}Messages returned
404{"error": "..."}Lobby or player not found
GET /get_player_history/<lobby_id>/<player_name> Player's personal round-by-round history

Responses

StatusBodyMeaning
200{"player": "<name>", "player_history": [...]}History returned
404{"error": "..."}Lobby or player not found
POST /submit_choice/<lobby_id> Submit action + resource for the round

Player submits their action and resource choice for the current round. If all alive players have now submitted, the round resolves immediately without waiting for the timer.

Request Body

FieldTypeRequiredDescription
playerstringYesPlayer name
actionstringYesattack, defend, or raid
resourcestringYesgain_hp, gain_coin, or gain_attack
targetstringNoTarget player name (required when action is attack)

Responses

StatusBodyMeaning
200{"status": "choice received"}Choice accepted
400{"error": "Invalid action"}Unknown action value
400{"error": "Invalid resource"}Unknown resource value
404{"error": "Lobby not found"}Invalid lobby ID
409{"error": "Invalid action: the game has ended"}Game is over
POST /submit_deny_target/<lobby_id> Choose a deny target

A player who holds the "deny" ability picks which opponent to block this round.

Request Body

FieldTypeRequiredDescription
playerstringYesMust match the name in pending_deny
targetstringYesPlayer to deny

Responses

StatusBodyMeaning
200{"success": true}Deny applied
400{"error": "Invalid"}Player is not the pending denier
POST /add_dummy Add an AI bot to the lobby

Request Body

FieldTypeRequiredDescription
namestringYesAdmin's name
lobby_idstringYesTarget lobby ID

Responses

StatusBodyMeaning
200{"status": "<bot_name> added"}Bot added
400{"error": "Dummy already exists"}Bot already in lobby
403{"error": "Only the admin can add a dummy."}Not admin
404{"error": "Lobby not found"}Invalid lobby ID
POST /send_message/<lobby_id> Send a lobby chat message

Request Body

FieldTypeRequiredDescription
namestringYesSender's name (must be in the lobby)
messagestringYesMessage content (max 200 characters)

Responses

StatusBodyMeaning
200{"status": "sent"}Message delivered
400{"error": "Message cannot be empty"}Empty body
400{"error": "Message too long..."}Exceeds 200 chars
403{"error": "You are not in this lobby"}Not a lobby member
404{"error": "Lobby not found"}Invalid lobby ID
Note: Chat appears in the chat array from get_state. Capped at 100 messages.
Raids
POST /get_raid_lobby Join or create a shared raid lobby

Joins an existing open raid lobby, or creates a new one if none is available.

Request Body

FieldTypeRequiredDescription
namestringYesPlayer name (must exist in DB)

Responses

StatusBodyMeaning
200{"lobby_id": "...", "start_time": "..."}Raid lobby joined/created
500{"error": "..."}Server/database error
Hades bosses: HP=8, DMG=2  |  Regular bosses: HP=30, DMG=4  |  Raid interval: 1 minute
GET /get_next_raid_time Scheduled start time of the next raid
{ "start_time": "<ISO 8601 datetime>" }
If the current raid has timed out (>2 min with no activity), it is marked as over and a new one is scheduled.
POST /create_gremlin_lobby Create a solo gremlin encounter

Request Body

FieldTypeRequiredDescription
namestringYesPlayer name

Responses

StatusBodyMeaning
200{"lobby_id": "..."}Gremlin lobby created
400{"error": "Name is required"}Missing name
Gremlin stats: HP=5, DMG=1. Starts immediately.
POST /get_player_relics Fetch relics collected by a player

Request Body

FieldTypeRequiredDescription
namestringYesPlayer name

Responses

StatusBodyMeaning
200{"relics": [...]}Relics returned
500{"error": "Failed to fetch relics"}Database error

Relic fields: id, boss_id, name, power-cathegory, flavour_text, created_at, count

Vault
POST /vault_check Validate an artifact code

Request Body

FieldTypeRequiredDescription
codestring or intYesNumeric passkey or the original finder's name

Responses

StatusBodyMeaning
200{"first": true}First person to find this
200{"first": false, "seen_before": N, "og_keyfinder": "..."}Already found before
401{"success": false}Invalid code
Each valid check increments the times_found counter. Numeric codes match passkey; string codes match og_keyfinder.
POST /vault_register_name Register first finder's name

Request Body

FieldTypeRequiredDescription
codestring or intYesArtifact passkey
namestringYesName to register

Responses

StatusBodyMeaning
200{"success": true}Name registered
401{"success": false}Artifact not found
403{"success": false}Already claimed by someone else
POST /vault_register_email Register first finder's email

Request Body

FieldTypeRequiredDescription
codestring or intYesArtifact passkey
emailstringYesEmail to register

Responses

StatusBodyMeaning
200{"success": true}Email registered
401{"success": false}Artifact not found
403{"success": false}Email already registered
Leaderboards
GET /leaderboards Top-5 rankings across four categories

Query Parameters

ParamTypeRequiredDescription
typestringNo"monthly" for current month; omit for all-time

Response (200)

{
  "top_wins":      [{ "name": "...", "wins": 5 }, ...],
  "top_kills":     [{ "name": "...", "kills": 12 }, ...],
  "top_played":    [{ "name": "...", "played_games": 20 }, ...],
  "top_raid_wins": [{ "name": "...", "raid_wins": 8 }, ...]
}
Monthly aggregates from game_player_stats. All-time reads totals from the players table.
City Chat
POST /send_city_message Send a message to a city chat channel

Request Body

FieldTypeRequiredDescription
namestringYesSender's player name
city_idint/stringYesID of the city channel
messagestringYesContent (max 200 characters)

Responses

StatusBodyMeaning
200{"status": "sent"}Message sent
400{"error": "name, city_id, and message are required"}Missing fields
400{"error": "Message too long..."}Exceeds 200 chars
City chat is in-memory only, not persisted. Max 200 messages per city.
GET /get_city_chat/<city_id> Fetch city chat history
{
  "city_id": 1,
  "chat": [
    { "sender": "Alice", "message": "Hello!", "timestamp": "2024-01-01T12:00:00+00:00" }
  ]
}
Game Constants
ROUND_DURATION
40s
Time per round before auto-resolve
BOSS_HP
30
Default raid boss hit points
BOSS_DMG
4
Default raid boss damage per round
HADES_HP
8
Hades boss hit points
HADES_DMG
2
Hades boss damage per round
GREMLIN_HP
5
Gremlin encounter hit points
GREMLIN_DMG
1
Gremlin damage per round
RAIDINTERVAL
1 min
Time between scheduled raids
Authentication Model
No tokens or sessions. Authentication works by verifying that the supplied name + email pair matches the record stored in the players database table. Unregistered names are created on first use via /claim_name. CORS is enabled globally for all origins.