DC Cartel Mafia — Game-Logic Graph Model

The rules of городская мафия (city mafia) as an explicit graph, so every interaction — and every overturn — is visible in one place instead of buried in imperative code.

Drafted 2026-06-29 · Updated 2026-07-01 (Nicholas resolved P1 / P2 / Q4r) · Source of truth for lib/services/night_resolver.dart, a new lib/services/win_checker.dart, and the phase_script.dart cue cards. Built from a full-codebase extraction + a 20-agent adversarial verification pass (12 stress scenarios). Supersedes the prose-only artifacts/game-rules-spec.html for the resolution layer.

How to read this

The game is not one graph. It is a small stack of structures, each the right shape for its job. Forcing them into one picture is itself a modeling error. The five layers:

  1. Night-order DAG — the fixed sequence roles act in (time).
  2. Override lattice — the directed "A overturns B" graph. This is the heart of the question "how does one rule overturn another."
  3. Propagation graph — player-to-player death spread (the Lover link).
  4. Win predicate — a boolean over the surviving counts (not a graph).
  5. Day-phase FSM — the sequential game loop (not an override graph).

⚠ Headline defect this model fixes

The only win logic that exists today is a text cue card in phase_script.dart:132 that reads "Black wins when blacks ≥ reds." Its own header comment calls the script "canonical Russian sport-mafia (FSM) script." For city mafia that rule is wrong on two counts: it counts the Maniac as black-for-parity, and it compares against reds instead of all other living players. This is the class of mistake that mis-rules a live game. The corrected predicate is in Layer 4.

Roles & teams

CardTeamWakesNight action → host records
CitizenREDNoNone. Speaks + votes by day.
SheriffREDYesChecks one player → host signals RED / BLACK.
DoctorREDYesHeals one player (self OK, not 2 nights running) → full-night protection.
Mistress (Lover / Любовница)REDYesVisits one player (self OK, not 2 nights running) → grants vote-immunity; drags that player into death if she dies (immunity does not stop the drag).
MafiaBLACKYesCollective kill (no misses — one number, always a kill).
DonBLACKYes ×2Final say on the kill; then checks one player → host confirms Sheriff / not.
ManiacBLACK solo factionYesKills one player, indiscriminately. Does NOT count as mafia for parity.

Q1 resolved (2026-06-29): the Don asks "is X the Sheriff?" (→ Sheriff / not); the Sheriff asks "is X red or black?" (→ red / black). The code already implements both (runner_screen.dart:803–811). Composition: black cards (mafia + Don + Maniac) = 30% of the table, the Maniac being its own black card.

Layer 1 — Night-order DAG

A strict sequence each night. The two checks (Don, Sheriff) are information only: they change no state. Only a live target can be killed or visited; a stale/dead target is a no-op (no phantom death).

1 · Mafiakill 2 · Doncheck 3 · Sheriffcheck 4 · Doctorheal 5 · Mistressvisit 6 · Maniackill Morningresolve
Topological order. The Morning node runs the override lattice (Layer 2) over everything recorded.

Layer 2 — The override / "overturn" lattice

Nodes are effects and states; a directed edge A → B means "A overturns / negates B." The Morning resolve applies effects in a fixed precedence so higher effects cancel lower ones — the same pattern every serious card-game engine uses. This is the picture that answers "how can one thing overturn another."

Doctor healfull-night protect Immunity cardsurvives a night KILL Vote-immunityfrom Mistress visit Mafia kill Maniac kill Lover-drag death Day vote-out DEATH (final set) SURVIVESsaved / immune / vote-immune negatesnegates* negatesnegates negates
protection kill / death propagation vote * edge gated by a config flag
Protections (top) overturn deaths (middle). A death that no protection negates flows to the DEATH set.

Resolution precedence (the order the lattice is folded)

  1. Direct kills, then the heal. Mafia + Maniac mark their live targets. A Doctor heal grants full-night protection to its target — it negates every heal-eligible kill on that player, not one "bullet." A heal always negates the mafia kill; it negates the maniac kill only when healCancelsManiacKill. (Adversarial #3: stated as full-night protection so a double-kill on a healed target can't slip through.)
  2. Immunity card. An immune player is removed from the death set for a night KILL (a mafia / Maniac shot) — not the day vote, not the Lover-drag (Nicholas, 2026-07-01). Applied before the drag so an immune Mistress never "dies" to a shot and therefore can't drag her partner.
  3. Lover-death drag. If the Mistress actually dies tonight, the live player she visited dies with her. The immunity card does NOT save that partner (Nicholas: the card stops night kills only); only a heal stops it, and only when loverDragSavable (or the house-rule immunitySavesFromDrag).
  4. Vote-immunity. A Mistress who survives the night grants her surviving visited player immunity from tomorrow's vote (not from night death; the immunity card itself does NOT block a day vote — Q4r, Nicholas). She may not visit the same player two nights running, self included — the same cooldown as the Doctor. (Adversarial #6: gate on the Mistress surviving, not her pre-resolution alive flag.)

Override edges (precise)

A overturns BConditionFlagCode
Doctor heal → Mafia killheal target = victimalwaysnight_resolver.dart:86–95
Doctor heal → Maniac killheal target = victimhealCancelsManiacKill (true):96
2nd unsaved kill → Doctor savesaved target still in deaths → save voidedalways:99–100
Immunity → mafia / Maniac killplayer.immune (a night KILL only)alwaysnight_resolver.dart:116–123
Immunity → Doctor save (credit)saved player was actually immune → save voidedalways:124
Immunity → Lover-dragimmune partner is still dragged — immunity does NOT negate the dragimmunitySavesFromDrag (false):150–157
Doctor heal → Lover-dragheal target = dragged partnerloverDragSavable (false):127–130
Vote-immunity → Day vote-outvisited survived; Mistress survivedloverGrantsVoteImmunity (true):136–142

Layer 3 — Propagation graph

Player-to-player death-spread edges. Today there is exactly one: the directional Lover link mistress → visited. It fires only when the Mistress herself dies (not when the visited dies — adversarial #12 confirmed the direction). If chained links ever appear, this becomes a reachability walk; the imperative if would not survive that, the graph does.

Mistress (dies) Visited partner drags into death unless heal-saved (loverDragSavable)

Layer 4 — Win predicate (corrected)

Evaluated after every night-morning and every day vote, over the surviving roster. Not a graph: a boolean over counts. Let M = living Mafia + Don (the Maniac is excluded), K = living Maniac (0/1), R = living reds, and "others" = everyone alive who is not Mafia/Don.

OutcomeCondition (correct, city mafia)What the old cue got wrong
RED winsall black are out: M = 0 AND K = 0(was roughly right)
MAFIA winsM ≥ (alive − M) — mafia ≥ all other living players (reds and the Maniac). Includes the Maniac-vs-Mafia 1-on-1 standoff: a draw scored as a black win (Nicholas), reason mafia-maniac-standoff.old: blacks ≥ reds — counted the Maniac as mafia, and used reds (not all others) as the denominator (adv. #8, #9)
MANIAC winslast black standing, 1-on-1 with a single civilian (or sole survivor): M = 0, K = 1, R ≤ 1. A Maniac 1-on-1 with the Mafia does NOT win — that is the black-win draw above.old: not modeled at all
continuenone of the above
resolution donerecount roster all black out?→ RED wins M ≥ alive−M ?→ MAFIA wins M=0,K=1,R≤1 ?→ MANIAC wins continue P1 RESOLVED (Nicholas, 2026-07-01): a Maniac 1-on-1 with the Mafia is a draw scored as a black win (maniacWinBeatsParity = false).
Excluding the Maniac from M resolves the coupled cases (adv. #8/#9). The rare Maniac-vs-Mafia 1-on-1 is a draw scored as a black win (P1 resolved, Nicholas 2026-07-01).

Layer 5 — Day-phase state machine

The teleprompter loop. Sequential, not an override graph. (Day-first: the town speaks before the first night.)

Day speech Vote Last word(if out) Night Morning Win-check (L4) no winner → next round (day starts one seat later) tie → re-speak + re-vote (ladder)

Resolved decisions (Nicholas, 2026-07-01)

Every previously-open edge is now pinned by the game master. The flags stay in AppConfig as house-rule switches, but the defaults below are the club's confirmed rules.

#DecisionFlag → valueNicholas's ruling
P1Maniac-solo vs Mafia-parity in coupled end-statesmaniacWinBeatsParity = falseThe Maniac wins ONLY 1-on-1 with a single civilian. A Maniac 1-on-1 with the Mafia is a draw scored as a black (Mafia) win.
P2No-repeat cooldown scope + who it bindsrepeatTargetScope = anyTarget; doctorNoRepeatTarget & mistressNoRepeatTarget = trueBoth the Doctor and the Mistress may not act on the same player two nights running, self included.
Q4rImmunity card scope vs the day vote and the dragimmunityBlocksVote = false; immunitySavesFromDrag = falseImmunity saves ONLY from a night kill. A player with immunity can still be voted out by day, and immunity does not stop the Lover-drag.

Verified scenario table

The adversarial stress cases (12 original + 3 from Nicholas's 2026-07-01 rulings). OK = the model produces the correct city-mafia outcome; FIX = a defect/clarification folded into this model.

#ScenarioCorrect outcomeStatus
1Mafia kills Mistress; she visited live Bboth die (kill + drag)OK
2Mafia kills Mistress; she is immunenobody dies (immunity before drag)OK
3Mafia + Maniac both hit A; Doctor heals AA lives — heal = full-night protectionFIX
4Maniac hits A; Doctor heals Aflag-dependent (healCancelsManiacKill)OK
5Doctor heals A two nights; Mafia kills A night 22nd heal is void (anyTarget) → A diesOK
6Mafia kills Mistress; she visited B; B healedMistress dies; drag savable per flagFIX
7Visited B survives; village votes B; immune B?Mistress vote-immunity stops it; the immunity card does NOT block the day voteOK
8Endgame: Maniac + 1 redManiac winsFIX
9Endgame: Maniac + 1 mafia + 1 civno win yet — parity excludes ManiacFIX
10Maniac kills a mafia memberlegal; mafioso diesOK
11Mafia kills A (immune); Doctor heals Bnobody diesOK
12Mistress visits B; Maniac kills BB dies; no drag (link is mistress→visited)OK
13Mafia kills Mistress; visited B is immuneboth die — immunity does NOT save the drag partnerOK
14Endgame: Maniac 1-on-1 with the last Mafiadraw scored as a black (Mafia) winOK
15Mistress visits B two nights running2nd visit is void — no vote-immunity, no dragOK

Config flags (flippable house rules)

FlagControlsDefault
healCancelsManiacKillcan a Doctor heal cancel a Maniac killtrue
loverGrantsVoteImmunityMistress visit → next-day vote-immunitytrue
loverDeathDragMistress death drags the visited playertrue
loverDragSavablecan a heal stop the dragfalse
doctorNoRepeatTargetDoctor can't heal the same player 2 nights runningtrue
mistressNoRepeatTargetMistress can't visit the same player 2 nights runningtrue
repeatTargetScopeno-repeat scope: selfOnly vs anyTargetanyTarget
maniacWinBeatsParitya living Maniac blocks the Mafia parity win (else the standoff is a black-win draw)false
immunityBlocksVoteimmunity card also blocks a day vote-outfalse
immunitySavesFromDragimmunity card saves the dragged Lover partnerfalse

Implementation map


Decisions & log