mischief: you can now pay to have someone's expedition ruined

Mischief Makers M1 — the core engine, Matrix-only. `!mischief send elite @user`
debits the buyer, tells the games room a hit is out, and an hour later a monster
from the target's own level bracket walks into whatever dungeon they're in.
Survive it and they keep a cut of the money and the buyer is named; don't, and
they wake up on a cart home.

The monster comes from the target's bracket zone pool, not the arena ladder and
not the dungeon they happen to be standing in — the same selection code the M0
pricing sweep ran through, so the fee table can't drift away from the fight it
priced.

Three things that are load-bearing and don't look it:

  * Survival is read off the target's HP, not PlayerWon. The engine's timeout is
    a retreat, not a lethal blow — somebody who ran out the clock with HP left
    held the thing off, and a bought monster that merely outlasted them hasn't
    earned a maiming.

  * Nobody dies for money, and that includes the party. The delivery skips
    closeOutZoneWin/Loss (the fight is extrinsic to the dungeon — crediting it
    would let a buyer unlock the target's kill-gated resources for them), so
    nothing else floors a downed seat. Without floorMischiefRoster on BOTH
    outcomes, a member the leader outlived is left alive at 0 HP, which every
    `HPCurrent <= 0` gate reads as broken rather than dead.

  * One live contract per target is a partial UNIQUE INDEX, not a read-then-write
    check. Placement holds only the *buyer's* lock, so two buyers racing at the
    same victim would both pass an in-code test. The loser is refunded.

Payouts are a percentage of the base fee, never of what the buyer actually paid —
the sign surcharge is a pure sink. Capped at 75%, so a survival purse is always
strictly less than the outlay and collusion loses to !baltransfer, which is free.
That cap is the entire anti-collusion story; no danger multiplier needed.

A crash between claiming a contract and closing it out used to be unrecoverable
in the design: the row would strand, the target could never be targeted again,
and the buyer's money was gone. The stale sweep refunds those in full — that one
is our fault, not a bet they lost.

Contract timestamps bind as Go time.Time, never CURRENT_TIMESTAMP. The driver
stores RFC3339 and SQLite's own stamp is space-separated; the two compare
lexicographically wrong.

Pete learns four mischief_* event types in a separate commit, and has to deploy
BEFORE this does — an unknown event_type is a 400, which retries and then parks
the bulletin forever.
This commit is contained in:
prosolis
2026-07-13 20:03:20 -07:00
parent 2ab6e7843a
commit 8fc5a82b83
9 changed files with 1890 additions and 7 deletions

View File

@@ -1734,6 +1734,44 @@ CREATE TABLE IF NOT EXISTS community_pot (
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Mischief Makers: a paid monster hit on a player who is out on an expedition.
-- The buyer's euros are debited at placement (paid); fee is the payout basis the
-- target's survival purse is a percentage of — the sign surcharge is a pure sink
-- and never inflates the purse, which is what keeps a payout strictly below what
-- the buyer spent (collusion loses to !baltransfer, which is free).
--
-- Lifecycle: open → delivering → delivered (outcome survived|downed) | fizzled.
-- Every transition is a conditional UPDATE, so a double-fire of the ticker or a
-- restart mid-delivery can never pay twice. No bootstrap — absent row == no
-- contract in flight.
CREATE TABLE IF NOT EXISTS mischief_contracts (
contract_id TEXT PRIMARY KEY,
buyer_id TEXT NOT NULL,
target_id TEXT NOT NULL,
tier TEXT NOT NULL,
fee INTEGER NOT NULL,
paid INTEGER NOT NULL,
status TEXT NOT NULL,
signed INTEGER NOT NULL DEFAULT 0,
escalation_count INTEGER NOT NULL DEFAULT 0,
blessing_count INTEGER NOT NULL DEFAULT 0,
source TEXT NOT NULL DEFAULT 'matrix',
outcome TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
window_ends_at DATETIME NOT NULL,
resolved_at DATETIME
);
-- One live contract per target, enforced by the database rather than by a
-- read-then-write check. The placement path holds only the BUYER's lock, so two
-- different buyers racing to hit the same person would both pass an in-code
-- "is one already live?" test. This partial unique index is what actually stops
-- the second insert; the loser is refunded.
CREATE UNIQUE INDEX IF NOT EXISTS idx_mischief_one_live_per_target
ON mischief_contracts(target_id) WHERE status IN ('open', 'delivering');
CREATE INDEX IF NOT EXISTS idx_mischief_target ON mischief_contracts(target_id, status);
CREATE INDEX IF NOT EXISTS idx_mischief_buyer ON mischief_contracts(buyer_id, created_at);
CREATE INDEX IF NOT EXISTS idx_mischief_due ON mischief_contracts(status, window_ends_at);
-- Babysitting Service
CREATE TABLE IF NOT EXISTS adventure_babysit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,