AI-powered cooperative game delivered through Telegram bot. Each turn, an AI generates a unique story, personalized action images, and NPC interactions based on player choices.
The game is finite and can be lost. A doom clock (threat level 0-100) ticks every turn (+8 base, accelerated by hesitations, a critically damaged hull and mission stagnation) — at 100 the game ends. Hull damage accumulates turn over turn (hull 0 = ship destroyed), wounds escalate minor → moderate → critical (critical + any new wound kills), NPC loyalty drops with losses and can end in mutiny, and the crew is never replenished. The final verdict is computed by code from the end-state into five outcomes: triumph / victory / pyrrhic / stalemate / defeat.
graph TD
A[Telegram API] --> B[telegram-bot]
B --> C[game-server]
D[game-scheduler] --> C
C --> E[comfyui]
C --> G[llama.cpp<br/>external]
style B fill:#E1F5FE
style C fill:#E8F5E9
style E fill:#FFF3E0
style G fill:#E0E0E0
style D fill:#F3E5F5
| Service | Port | Description |
|---|---|---|
| game-server | 8000 | FastAPI backend with SQLite persistence |
| telegram-bot | N/A | Telegram bot interface |
| comfyui | 8188 | Image/Video generation backend |
| game-scheduler | N/A | Turn generation scheduler (run manually for debugging) |
FastAPI service that orchestrates the game:
- Turn story generation using OpenAI API
- NPC dialogue generation
- Personalized action-image generation via ComfyUI
- Player action processing
- Text/voice message handling
- SQLite persistence for game state
Ports: 8000
Key Endpoints:
POST /onboarding/start- Start onboarding for a playerPOST /onboarding/{session_id}/reroll- Reject the proposed character and roll anotherPOST /onboarding/{session_id}/complete- Accept the proposed characterGET /players/{player_id}/profile- Get player profileGET /game/current-turn- Get current turn's episodeGET /game/turn-deadline/{game_id}- Get the playable turn's deadlinePOST /game/remind-turn/{game_id}/{turn}- Send a T-2h/T-30m reminderPOST /game/actions- Submit player actionPOST /game/messages- Send message to game masterPOST /admin/continue-game- Generate the next turnGET /admin/win-rate- Outcome telemetry over ended gamesPOST /admin/generate-comic/{player_id}- Generate personalized comic
Player interface via Telegram:
/start- Begin onboarding or return to game/profile- Show player role and traits/turn- View current turn episode/help- Show help information- Interactive keyboards for action selection
- Voice message support
- Text chat with Game Master
Ports: None (outbound Telegram API only)
Scheduled task runner that triggers daily episode generation. Can be run manually for debugging.
Usage:
# Run single generation cycle for testing
GAME_SCHEDULER_MODE=single docker compose run --rm game-schedulerPorts: None
Image/video/3D generation backend. Requires GPU.
Ports: 8188
- Docker and Docker Compose
- NVIDIA GPU (for ComfyUI)
- NVIDIA Container Toolkit
- Telegram Bot Token (get from @BotFather)
- External
spark-networkDocker network - External
llama.cppservice running on port 8090
- Copy
.env.exampleto.env:
cp .env.example .env- Edit
.envand set your Telegram bot token:
TELEGRAM_BOT_TOKEN=your_bot_token_here- Build and start services:
docker compose up -d- Check logs:
docker compose logs -f game-server
docker compose logs -f telegram-bot- Run single generation cycle (for testing):
GAME_SCHEDULER_MODE=single docker compose run --rm game-scheduler- Player sends
/startto the bot - Bot starts an onboarding session (
POST /onboarding/start); the first player of a game also triggers the game concept (title + welcome + mission) - In the background the server rolls a random character — a free ship role, species (20% chance of a hybrid) and gender — generates all flavour text in one LLM call and renders the avatar via ComfyUI
- The bot shows a proposal card (avatar + role, species, gender, flavour) with Accept / Reroll buttons; already-rejected roles and species are not repeated while alternatives exist
- After 3 rerolls the next character is assigned automatically
- On accept (
POST /onboarding/{session_id}/complete) the profile is created; once enough players have joined (GAME_START_MIN_PLAYERS), the game starts automatically
A turn is driven by game-scheduler (the timer), produced by game-server
(LLM + image orchestration), and delivered to players by telegram-bot (its
push server on port 9090). There is no fixed daily clock — turns are generated
on a configurable interval (GAME_SCHEDULE, default 8h), and the previous
turn's outcome is published the moment all live players have chosen their
actions. Each turn has a deadline shown to players («Ход закроется: ...» UTC);
the scheduler sends T-2h/T-30m reminders and auto-picks actions for absentees
when the deadline passes.
sequenceDiagram
participant S as game-scheduler
participant GS as game-server
participant TB as telegram-bot
participant P as Players
Note over S: interval elapses (GAME_SCHEDULE)
S->>GS: GET /game/started (need >= 3 players)
S->>GS: GET /game/state (active? current turn N)
Note over S: if N>1: auto-pick actions for<br/>absentee players on turn N-1<br/>(LLM pick, or a recorded "delay")
S->>GS: POST /admin/continue-game?game_id=...&language=en&deadline=...
GS-->>S: 202 accepted (generation runs in background)
Note over S: T-2h / T-30m before the deadline:<br/>GET /game/turn-deadline, POST /game/remind-turn
Note over GS: background: LLM global circumstances -><br/>ComfyUI scene image -><br/>per-player briefings + 5 choices -><br/>NPC choices auto-selected -><br/>character/NPC images -> crew dialogues
opt turn N > 1
Note over GS: resolve & push turn N-1 outcome
GS->>GS: _analyze_turn_outcome(N-1)
GS->>TB: POST /push/outcome (results of last turn)
opt mission done / ship destroyed / crew wiped /<br/>threat 100 / NPC mutiny
GS->>TB: POST /push/game-over (finale)
GS->>TB: POST /push/game-summary (post-game stats)
end
end
GS->>TB: POST /push/briefings (new turn intro + choices + deadline)
TB->>P: deliver outcome (N-1), then briefing (N)
P->>TB: select an action
TB->>GS: POST /game/actions
Note over GS: record choice -> generate chosen-action image
opt all live players have chosen
GS->>GS: _analyze_turn_outcome(N) immediately
GS->>TB: POST /push/outcome
end
- Trigger —
game-schedulerfiresPOST /admin/continue-game. The endpoint returns202immediately; all generation runs in the background. - Global circumstances — LLM produces the turn's setting, narrative, conflict and a scene prompt.
- Scene & turn background images — ComfyUI renders the turn's briefing scene from that prompt, plus a character-free turn background (empty environment built from the setting + conflict) that is reused as the shared canvas for every character/action image of the turn.
- Briefings & choices — every live player and NPC gets a personalized
briefing with 5 action choices with hidden consequence kinds in a fixed
quota — 2 progress / 2 injury / 1 fatal (configurable via
GAME_TURN_PROGRESS_ACTIONS/GAME_TURN_INJURY_ACTIONS/GAME_TURN_FATAL_ACTIONS). NPC choices are auto-selected by the LLM; players' choices stay open until the turn deadline. - Character & NPC images — ComfyUI renders each player's personal briefing image as an identity-preserving edit: the player's avatar as reference image 1 placed into the turn background as reference image 2.
- Crew dialogues — LLM generates NPC banter for the turn.
- Game state advances — the turn record is created (with its deadline),
state moves to turn
N+1. - Previous turn outcome (turn
N > 1) —_analyze_turn_outcome(N-1)combines every player + NPC decision into one outcome narrative, applies the LLM's ship deltas (hull/shields/systems) to the persistent ship state, resolves wounds and deaths byentity_idthrough the rules ladder, ticks the doom clock and NPC loyalty, generates an outcome scene image, then pushes/push/outcome. The outcome risk is reweighted by code (damaged hull / high threat / reckless choices make catastrophes likelier). If the game ends (mission complete, hull 0, crew wiped, threat 100, or NPC mutiny), the code-computed verdict (triumph / victory / pyrrhic / stalemate / defeat) drives a finale pushed via/push/game-over, followed by a post-game stats summary via/push/game-summary. - New turn delivery —
/push/briefingssends the new turn's intro, choices and deadline to each player. (Outcomes ofN-1are always delivered before the briefing forN.)
- Players pick an action in the bot →
POST /game/actions. The server records the choice and kicks off a chosen-action image: an edit-model render placing the player's avatar (reference 1) into the turn's shared background (reference 2), staged per the action text. - The moment all live players have chosen, the outcome is analyzed immediately — it does not wait for the next scheduled tick. (The per-turn outcome analysis is idempotent, so a turn already resolved on time is a no-op at the start of the next turn.)
- Absentee players are handled by
game-scheduler: at the start of turnNit auto-selects an action for anyone who did not choose on turnN-1(POST /game/auto-action/{player}/{turn}, LLM-picked) before triggering the next turn. If the LLM pick fails, a delay (hesitation) is recorded honestly: it does not advance the mission and accelerates the doom clock.
/gm_start <game_id>— generate the first turn (POST /admin/start-game)./gm_continue <game_id>— generate the next turn immediately (POST /admin/continue-game), bypassing the scheduler./gm_turn <game_id>— regenerate the current turn (/admin/regenerate-turn)./gm_restart <game_id>,/gm_pause,/gm_schedule,/gm_status,/gm_kick,/gm_list,/gm_lang— additional game management commands.
At game start, free seats are filled with a bounded pool of exactly up to 4
NPCs (NPC_COUNT), preferring the key roles chief_engineer,
medical_officer, pilot and science_officer. A bounded pool keeps the
crew-wipe ending reachable — and since a kicked or dead player's seat is never
refilled, every loss is permanent.
- Chief Engineer - Technical systems maintenance
- Medical Officer - Crew health and wound treatment
- Pilot - Navigation and flight operations
- Science Officer - Research and analysis
NPCs have distinct personalities and speech styles. Each NPC also has a
loyalty score (0-100, starting at 70) maintained by code: deaths, heavy
hull damage and mission regressions lower it; progress and healing raise it.
Loyalty is visible to players in /game/team (loyalty band) and shapes NPC
decisions. When two or more active NPCs drop to loyalty <= 15, the crew
mutinies — one of the five ways the game can end.
All images are rendered by ComfyUI. Workflows are built programmatically in
game-server/image_generator.py (one builder per model family); per-kind
model routing lives in game-server/comfyui_config.py and is configured via
COMFYUI_TXT2IMG_MODEL / COMFYUI_AVATAR_MODEL / COMFYUI_EDIT_MODEL in
.env. Image prompts are LLM-authored from templates in
game-server/prompts.py.
- txt2img —
turn_background(the shared stage of the current LOCATION — generated with standable surfaces and character-scale anchor objects, cached per(game, location_id)so the world stays consistent while the crew stays put; it is also the briefing header image),splash,loading,finale. - avatar — RGBA character cutout on a transparent background
(
avatar,npc_avatar_*), currently MOSS-Image-SFT. Prompts carry a crew presentation contract (role uniform, ship-grade lighting, no civilian props). - identity-preserving edit —
character(personal briefing image),player_action/npc_action(chosen action),player_death: the avatar (reference image 1) is placed into the location background (reference image 2) following a short LLM-written staging instruction. For humanoid characters the LLM also picks a pose frompose_library.py(OpenPose skeletons rendered in pure Python) and the skeleton goes to the edit model as reference image 3 — "the character adopts the pose of Picture 3". - multi-reference —
bridge(crew on the bridge) andoutcome(turn outcome scene) render all crew avatars in one scene via Qwen-Image-2.1 multi-reference.
Global circumstances carry a stable location object (id, name,
description); the LLM is told to reuse the same location.id while the
crew stays in the same place, and the backdrop is cached per location.
Consistency comes from the shared location background, the avatar reference
and the pose skeleton; there is no fixed seed.
# Game Server API
cd game-server
pip install -r requirements.txt
uvicorn main:app --reload --host 0.0.0.0 --port 8000
# Telegram Bot
cd telegram-bot
pip install -r requirements.txt
export TELEGRAM_BOT_TOKEN=your_token
python bot.py
# Game Scheduler (for debugging)
cd game-scheduler
pip install -r requirements.txt
GAME_SCHEDULER_MODE=single python main.pyAfter running this, you must generate a new turn via Telegram:
Activate the project virtual environment and run:
cd game-server && ../.venv/bin/python -m unittest discover -s testsOr specific modules:
cd game-server && ../.venv/bin/python -m unittest tests.test_game_rules tests.test_mission_dbVisit http://localhost:8000/docs for Swagger UI.
Check game-server health:
curl http://localhost:8000/health
docker compose logs game-serverdocker compose logs comfyui
# Check if NVIDIA runtime is configured
nvidia-smi# Check if bot token is set
docker compose exec telegram-bot env | grep TELEGRAM
# Verify API connectivity
docker compose exec telegram-bot ping game-server| Variable | Default | Description |
|---|---|---|
| LLM_URL | http://llama.cpp:8090/v1 | LLM endpoint |
| LLM_API_KEY | placeholder-key-for-llama-cpp | Required by OpenAI client |
| COMFYUI_URL | http://comfyui:8188 | Image gen endpoint |
| TELEGRAM_BOT_TOKEN | (required) | Telegram bot token |
| GAME_SCHEDULE | 8h | Turn schedule: Nh/Nm/Ns, HH:MM, HH:MM,..., DAY-HH:MM |
| GAME_SCHEDULER_MODE | scheduled | single/simulation/scheduled |
- Voice message transcription
- 3D scene generation
- Video clips for key moments
- Multi-player cooperation
- Cross-group events
- Telegram Mini App integration