forked from retoor/devplacepy
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>
150 lines
6.1 KiB
Python
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}},
|
|
),
|
|
],
|
|
}
|