Work grimsyndicate
game

Grim Syndicate

A dystopian sci-fi text RPG where every choice echoes through the grid.

Grim Syndicate is the cyberpunk instance of the XRPG Engine — same ruleset, same passkey auth, same persistent-world AI narration, but the world is the Grid: factions, runners, contracts, and consequences that follow you across sessions. Operators carve out reputation by whose toes they step on. The dice are deterministic; the city remembers.

Last updateMar 17, 2026 PrimaryPython
  • Python
  • FastAPI
  • PostgreSQL
  • SQLAlchemy
  • Jinja2
  • WebAuthn
  • OpenRouter
  • Apache
  • systemd
Grim Syndicate — A dystopian sci-fi text RPG where every choice echoes through the grid.
Grim Syndicate media
Grim Syndicate media
Grim Syndicate media
Grim Syndicate media

Grim Syndicate is the cyberpunk-flavored instance of the XRPG Engine — same Python/FastAPI codebase that ships Tulpa, but the world is a sprawling surveillance city where you run contracts, build a reputation, and answer to whichever faction owns whatever block you happen to sleep on. Every choice echoes through the grid; campaigns persist across logins; the deterministic ruleset lives underneath the AI narration.

What's a "syndicate"

The setting is structural. There is no single benevolent state — just nested syndicates of varying ambition, all running on the same fragile substrate. Players are operators-for-hire who pick sides one job at a time. Reputation is real, faction memory is persistent, and the engine's NPC + scenario layer carries consequences forward across sessions. Burn the wrong fixer and six weeks later their cousin won't sell you a battery.

Same engine, different rules on top

  • Engine layer (shared with Tulpa): passkey auth, character creation, energy economy, deterministic rolls, persistent world memory, PvP arena, encrypted per-instance config in the database. Read the engine write-up on XRPG Engine.
  • Game layer (Syndicate-specific): faction reputation, contract economy, Grid-themed locations and NPCs, cyberpunk gear/affix tables, narration tuning that leans noir. None of that lives in the engine — it's all config and content stacked on top of the same Python skeleton.

Multi-instance is the whole story

Tulpa and Grim Syndicate share a git repository. They differ in a per-deployment .instance file, a database, a domain, and the brand assets in /static/uploads. That's it. Adding another world is a config-and-art exercise. The reason this exhibit exists alongside Tulpa is to show what the engine looks like when the same skeleton is dressed for a different genre — the game design surface area is enormous, but the technical surface area is the engine.

Straight from the source

The project's own README.

Rendered in place — every link, image, and code block carried over from the repo. The page below is what a contributor would see opening the project for the first time.

Enemy & Encounter System — Design Document

Overview

Enemies are NPCs that players can fight, flee from, or negotiate with. They should feel like part of the world — tied to specific locations, scaled to the campaign's difficulty, and capable of dropping loot when defeated. The system builds on existing infrastructure (NPC model, fabricator, locations, scenarios) rather than introducing new tables.

What Already Exists

NPC Model (app/models/campaign.py)

  • is_enemy flag distinguishes hostile NPCs
  • Full stat block (STR, DEX, CON, INT, WIS, CHA) + level
  • stat_range JSONB for level-scaled stat generation
  • location_id — ties NPC to a specific location
  • loot_table JSONB — list of potential drops
  • groups JSONB — faction/species tags (e.g., ["goblin", "cave_dweller"])
  • equipment JSONB — what the NPC is wielding (informs combat descriptions)
  • disposition + personality_traits — how they behave

Location Model

  • type field (town, dungeon, wilderness, etc.)
  • connections to other locations
  • NPCs are associated via location_id

Scenario Model

  • type: "encounter" — one-time encounters tied to locations
  • trigger_conditions JSONB — when the encounter fires
  • rewards JSONB — what the player gets

Fabricator Service (app/services/fabricator.py)

  • LLM-based item generation already works
  • Same pattern can generate enemy NPCs on the fly

Engine (app/services/engine.py)

  • Already extracts intent (fight_melee, fight_ranged, etc.)
  • Already parses [ITEM_FOUND: ...] tags from narrator
  • Stat gains already tied to combat intents

Proposed Architecture

1. Enemy Encounter Flow

Player enters area → Engine checks location NPCs → Injects enemy context into prompt
    ↓
Player attacks → Intent = fight_* → Engine resolves combat outcome
    ↓
Narrator describes result → [ENEMY_DEFEATED: NPC Name] tag if won
    ↓
Engine processes tag → Rolls loot from NPC's loot_table → Grants items

2. Enemy Context Injection

When building the system prompt, the engine should load enemy NPCs associated with the player's current location and include them:

--- ENEMIES IN AREA ---
Goblin Scout (Level 2): STR 11, DEX 13, CON 10. Carries a crude dagger. Lurks near the entrance.
Cave Spider (Level 1): STR 8, DEX 14, CON 7. Venomous bite. Hangs from ceiling webs.

This gives the narrator awareness of what threats exist so it can introduce them naturally rather than making them up inconsistently.

Key file: app/services/engine.py_build_system_prompt()

3. Combat Resolution

Combat should NOT be fully automated — the narrator still drives the story. But the engine should provide outcome hints based on stat comparison:

def _calculate_combat_advantage(character, enemy_npc):
    """Compare character stats to enemy. Returns advantage score (-1 to 1)."""
    # Weight stats by intent: fight_melee weights STR/CON, fight_ranged weights DEX, etc.
    # Factor in equipment bonuses
    # Factor in level difference
    # Add randomness (fate roll)
    # Return: positive = player favored, negative = enemy favored

This advantage score gets injected into the prompt as a hint:

[COMBAT HINT: Player has moderate advantage against Goblin Scout (stat edge + equipment)]

The narrator uses this to inform whether the player succeeds, partially succeeds, or fails — without dictating exact mechanics.

Key file: New function in app/services/engine.py

4. Enemy Defeat & Loot

New tag format for the narrator:

[ENEMY_DEFEATED: Goblin Scout]

Engine processing:

  1. Parse [ENEMY_DEFEATED: ...] tags (similar to _parse_item_tags)
  2. Look up the NPC by name in the current location
  3. Roll against the NPC's loot_table
  4. Use fabricator to generate any dropped items
  5. Award XP bonus based on enemy level

5. Loot Table Format

The NPC's loot_table JSONB:

[
    {"type": "weapon", "rarity": "common", "chance": 0.3, "context": "crude goblin dagger"},
    {"type": "consumable", "rarity": "common", "chance": 0.5, "context": "small health potion"},
    {"type": "misc", "rarity": "uncommon", "chance": 0.1, "context": "goblin ear trophy"}
]

Each entry has a chance (0-1 probability) and a context string passed to the fabricator for thematic item generation.

6. Enemy Fabrication

For areas without pre-built enemy NPCs, the fabricator can generate them:

async def fabricate_enemy(db, location, campaign, player_level):
    """Generate a thematic enemy NPC for a location."""
    # LLM prompt includes location type, campaign setting, player level
    # Returns NPC data: name, description, stats, loot_table, groups
    # Saved as NPC with is_fabricated=True, is_enemy=True

This parallels fabricate_item() but for NPCs. The fabricated enemy persists in the database so future players at the same location encounter the same creature.

7. Location-Enemy Association Rules

  • Dungeons: Higher enemy density, tougher enemies deeper in
  • Wilderness: Patrol encounters, wildlife scaled to area level
  • Towns: No random enemies (guards, thieves only via scenarios)
  • Boss rooms: Single powerful enemy tied to a scenario

Enemy groups help cluster related creatures:

  • All NPCs with groups: ["goblin"] in a dungeon might appear together
  • Location type influences which groups spawn there

8. Enemy Scaling

Enemies should scale relative to the campaign and location, not the player:

  • Campaign min_level / max_level sets the band
  • Location depth (distance from start) increases difficulty
  • NPC stat_range allows variance: {"STR": [10, 14], "DEX": [12, 16]}
  • Fabricated enemies use player level as a baseline but don't rubber-band

9. Death & Defeat

When a player loses a fight:

  • Energy drops to 0 (already tracked)
  • Character could be marked is_alive = False for permadeath campaigns
  • Or session gets a "defeated" state allowing retreat/recovery
  • Gold loss as a penalty (percentage-based)

Implementation Priority

Phase 1 — Enemy Context (next)

  • Load location NPCs where is_enemy=True in _build_system_prompt
  • Include enemy stats, equipment, and descriptions in narrator context
  • No mechanical combat resolution yet — narrator handles it narratively

Phase 2 — Combat Hints

  • Add _calculate_combat_advantage() to engine
  • Inject advantage hints into prompt for fight intents
  • Parse [ENEMY_DEFEATED: ...] tags

Phase 3 — Loot & XP

  • Roll NPC loot tables on defeat
  • Fabricate dropped items
  • Award XP bonus for combat victories
  • Track defeated enemies per session

Phase 4 — Enemy Fabrication

  • Add fabricate_enemy() to fabricator service
  • Auto-generate enemies for locations that have none
  • Factory UI for manually creating enemy NPCs (already possible via NPC creator)

Phase 5 — Advanced Combat

  • Turn-based resolution with stat comparison
  • Equipment modifier application
  • Spell effects in combat
  • Companion assistance
  • Death/defeat mechanics

Key Files

File Role
app/models/campaign.py NPC model (enemy stats, loot_table, location_id, groups)
app/services/engine.py System prompt, intent extraction, combat resolution
app/services/fabricator.py Item fabrication (extend for enemy fabrication)
app/api/game.py Session management, message handling
app/templates/pages/play_session.html Combat UI, enemy notifications

Open Questions

  • Should enemies respawn? Or once defeated, gone for that session?
  • How does multiplayer affect encounters (shared enemies)?
  • Should the narrator be given explicit HP for enemies, or keep it narrative?
  • How granular should combat turns be (per-message vs multi-round)?
  • Should some enemies be avoidable through non-combat means (stealth, persuasion)?

Gallery

The full set.

Build something like this

Want a tool like this for your shop?

We've shipped this kind of thing before. Twenty-minute intro call, no slides.