Files
devplacepy/devplacepy/docs_api/groups/battles.py
T
blindxfishandClaude Fable 5 50baf9d6f1 Fix battles join docs param location and the fight-flow ticker assertion
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

150 lines
6.1 KiB
Python

# retoor <retoor@molodetz.nl>
from devplacepy.services.opinionwar import rules
from .._shared import endpoint, field
FILTER_KEYS = ["active", "ended", "mine"]
SAMPLE_WAR = {
"uid": "0198f2c0-2222-7aaa-8bbb-000000000002",
"post_uid": "0198f2c0-3333-7aaa-8bbb-000000000003",
"post_url": "/posts/8bbb000000000003-tabs-or-spaces",
"post_title": "Tabs or spaces?",
"faction_a": "Tabs",
"faction_b": "Spaces",
"hp_a": 12548,
"hp_b": 7362,
"pct_a": 63,
"pct_b": 37,
"leader": "a",
"fighter_count": 42,
"status": "active",
"winner": "",
"ends_at": "2026-08-27T12:00:00+00:00",
"ends_in": "2d 14h 32m",
"last_seq": 87,
"fight_cost": rules.FIGHT_COST_COINS,
"top_contributors": [
{"username": "code_warrior", "faction": "a", "hp": 982},
],
"recent_events": [
{"seq": 87, "kind": "fight", "message": "code_warrior dealt 300 HP for Tabs", "faction": "a"},
],
"viewer": {
"faction": "a",
"hp": 256,
"rank": 7,
"can_fight": True,
"next_fight_at": "",
},
}
GROUP = {
"slug": "battles",
"title": "Opinion Wars",
"intro": f"""
# Opinion Wars
An Opinion War is a week-long two-faction battle attached to a post. The creator names
exactly two factions when creating the post (the `war_faction_a` / `war_faction_b` fields
on `POST /posts/create`); from that moment the battle runs for exactly
{rules.WAR_DURATION_DAYS} days.
Any signed-in member joins one of the two factions and may **fight** once every
{rules.FIGHT_COOLDOWN_HOURS} hours per battle. A fight costs {rules.FIGHT_COST_COINS}
Code Farm coins and deals deterministic, level-weighted damage for the fighter's faction:
`{rules.BASE_DAMAGE} + {rules.LEVEL_DAMAGE_STEP} * min(level, {rules.LEVEL_DAMAGE_CAP})`
HP, so a level 1 member deals {rules.damage_for(1)} HP and the bonus caps at
{rules.damage_for(rules.LEVEL_DAMAGE_CAP)} HP. There is no randomness. Switching factions
is allowed at any time; damage already dealt stays with the faction it was dealt to.
When the week is over the faction with more HP wins. Resolution is evaluated lazily on
read (no background clock): the first read after the deadline freezes the totals, awards
XP (participation for every fighter with at least one fight, a bonus for the winning
side, a bonus for the single top damage dealer) and notifies every fighter. Equal totals
are a draw with participation XP only.
Every battle keeps an ordered event log (kinds `join`, `switch`, `fight`, `lead_change`,
`result`) replayable with the `after` cursor; live frames are also published on the
pub/sub topic `public.battle.{{uid}}`.
All endpoints negotiate HTML or JSON. POST bodies are form encoded. Action POSTs answer
`{{"ok": true, "redirect": "...", "data": {{...}}}}`; a refused action (cooldown, missing
coins, ended battle) answers `400` as `{{"error": {{"status": 400, "message": "..."}}}}`.
""",
"endpoints": [
endpoint(
id="battles-list",
method="GET",
path="/battles",
title="Battle listing",
summary="Opinion Wars with HP totals, filter counts and the viewer's faction state.",
auth="public",
negotiation=True,
params=[
field("search", "query", "string", False, "tabs", "Match a faction name or the creator's username."),
field("filter", "query", "enum", False, "active", "Which battles to list.", options=FILTER_KEYS),
field("page", "query", "integer", False, "1", "1-based page number."),
],
sample_response={"battles": [SAMPLE_WAR], "counts": {"active": 3, "ended": 12, "mine": 1}},
),
endpoint(
id="battles-get",
method="GET",
path="/battles/{uid}",
title="Battle state",
summary="One battle's full serialized state, resolving it first when its week is over.",
auth="public",
params=[
field("uid", "path", "string", True, SAMPLE_WAR["uid"], "Battle uid."),
],
sample_response=SAMPLE_WAR,
),
endpoint(
id="battles-events",
method="GET",
path="/battles/{uid}/events",
title="Battle events",
summary="The ordered battle event log, replayable incrementally with the after cursor.",
auth="public",
params=[
field("uid", "path", "string", True, SAMPLE_WAR["uid"], "Battle uid."),
field("after", "query", "integer", False, "0", "Return only events with a seq greater than this."),
field("limit", "query", "integer", False, "500", "Maximum events to return."),
],
sample_response={"events": SAMPLE_WAR["recent_events"], "status": "active"},
),
endpoint(
id="battles-join",
method="POST",
path="/battles/{uid}/join",
title="Join or switch faction",
summary="Join faction a or b, or switch an existing fighter to the other side.",
auth="user",
encoding="form",
params=[
field("uid", "path", "string", True, SAMPLE_WAR["uid"], "Battle uid."),
field("faction", "form", "enum", True, "a", "Which side to join or switch to.", options=["a", "b"]),
],
sample_response={"ok": True, "redirect": SAMPLE_WAR["post_url"], "data": {"war": SAMPLE_WAR}},
),
endpoint(
id="battles-fight",
method="POST",
path="/battles/{uid}/fight",
title="Fight",
summary=(
f"Spend {rules.FIGHT_COST_COINS} Code Farm coins and deal level-weighted HP damage "
f"for your faction. Once per {rules.FIGHT_COOLDOWN_HOURS} hours per battle."
),
auth="user",
encoding="form",
params=[
field("uid", "path", "string", True, SAMPLE_WAR["uid"], "Battle uid."),
],
sample_response={"ok": True, "redirect": SAMPLE_WAR["post_url"], "data": {"war": SAMPLE_WAR, "damage": 300}},
),
],
}