Opinion Wars: week-long two-faction battles on posts #173

Merged
retoor merged 2 commits from blindxfish/devplacepy:OpinionWar into master 2026-08-22 07:42:15 +02:00
2 Commits
Author SHA1 Message Date
blindxfishandClaude Fable 5 50baf9d6f1 Fix battles join docs param location and the fight-flow ticker assertion
DevPlace CI / test (pull_request) Has been cancelled
The auth matrix probes documented endpoints with their documented form
params; the faction param was declared with location body instead of
form, so the anonymous probe sent an empty body and hit 422
validation before the auth guard. And the first fight always triggers
a lead-change event that outranks the fight event at the top of the
ticker, so the e2e flow now asserts on the ticker as a whole.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 00:05:00 +02:00
blindxfishandClaude Fable 5 80956ce0f4 Add Opinion Wars: week-long two-faction battles attached to posts
A new post attachment type beside polls: the composer gains a Start
Opinion War builder (same disabled-inputs opt-in as the poll builder)
that names exactly two factions; the battle runs for exactly 7 days
from post creation. Members join a side, may defect at any time
(damage already dealt stays with the faction it was dealt to), and
fight once per 24 hours per battle. A fight spends 25 Code Farm coins
and deals deterministic level-weighted damage: 100 + 10 * min(level,
20) HP, so a newcomer deals 110 and a veteran caps at 300 - no
randomness anywhere.

The battle renders on the post card as a CSS pixel-art battlefield
(box-shadow sprites: castles, faction flags, marching soldiers, a
flickering campfire; steps() animation, disabled under reduced motion)
with live HP bars, a countdown, the viewer's faction strip, top
contributors and an event ticker. Live frames ride pub/sub on
public.battle.{uid} via a relay on the service-lock owner, with the
durable opinion_war_events trail (per-war atomic seq) as the source of
truth and a 15s incremental poller as fallback. /battles lists battles
with active/ended/mine filters, search and pagination.

Every mutation is a conditional UPDATE via conditional_update_row: the
fight sequence claims the cooldown first, then spends coins, then lands
the damage, compensating earlier steps on any later refusal so a crash
costs a turn, never coins. Resolution is lazy on read (no cron):
an exactly-once CAS computes the winner in the statement, awards XP
(participation, winner bonus, top damage dealer bonus; draws pay
participation only), emits the result event and notifies fighters. The
OpinionWarService backstop resolves unviewed wars and sends
fight-ready notifications, exactly-once via a marker CAS.

Fan-out: battle notification type, four badges, audit keys
(battle.create/join/switch/fight/resolve), Devii actions (join/fight
confirm-gated), API docs group, docs prose page, sitemap and topnav
entries, REPORTABLE_TARGETS registration, post-delete cascades,
README and nested CLAUDE.md documentation.

Verified with the four-layer procedure: property checks over the full
damage domain, 1200-step stateful fuzz (hp-sum invariant, coins never
negative, resolved totals frozen), and real 8-process races proving
exactly-once semantics for concurrent fights, double-spends across two
wars, resolution XP and double-joins. Persisted tests in
tests/unit/services/opinionwar, tests/api/battles, tests/e2e/battles
and tests/api/posts/create.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-20 23:30:02 +02:00