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>
140 lines
2.8 KiB
Python
140 lines
2.8 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import asyncio
|
|
import base64
|
|
import binascii
|
|
import hashlib
|
|
import html
|
|
import json
|
|
import logging
|
|
import re
|
|
import secrets
|
|
from datetime import datetime, timedelta, timezone
|
|
from decimal import Decimal
|
|
from typing import Optional
|
|
from urllib.parse import urlsplit
|
|
from passlib.hash import pbkdf2_sha256
|
|
from fastapi import Request, HTTPException, status
|
|
|
|
from devplacepy.cache import TTLCache
|
|
from devplacepy.database import (
|
|
get_table,
|
|
get_user_stars,
|
|
bump_cache_version,
|
|
sync_local_cache,
|
|
invalidate_admins_cache,
|
|
notification_enabled,
|
|
get_silenced_uids,
|
|
record_activity,
|
|
record_unique_activity,
|
|
)
|
|
from devplacepy.config import (
|
|
SESSION_MAX_AGE,
|
|
DEFAULT_CORRECTION_PROMPT,
|
|
DEFAULT_MODIFIER_PROMPT,
|
|
)
|
|
from devplacepy.services.background import background
|
|
|
|
from devplacepy.utils.passwords import (
|
|
hash_password,
|
|
verify_password,
|
|
hash_password_async,
|
|
verify_password_async,
|
|
create_session,
|
|
)
|
|
from devplacepy.utils.authcache import (
|
|
_user_cache,
|
|
_sync_auth_cache,
|
|
clear_user_cache,
|
|
clear_session_cache,
|
|
)
|
|
from devplacepy.utils.auth import (
|
|
_UNSET,
|
|
_user_from_session,
|
|
_user_from_api_key,
|
|
_user_from_basic,
|
|
_request_has_auth,
|
|
_resolve_user,
|
|
_user_from_devrant_token,
|
|
_user_from_access_token,
|
|
get_current_user,
|
|
)
|
|
from devplacepy.utils.guards import (
|
|
safe_next,
|
|
redirect_back,
|
|
cookie_secure,
|
|
client_ip,
|
|
require_user,
|
|
is_admin,
|
|
is_primary_admin,
|
|
require_admin,
|
|
not_found,
|
|
require_user_api,
|
|
)
|
|
from devplacepy.utils.text import (
|
|
strip_html,
|
|
_FLOAT_TOKEN,
|
|
plain_float,
|
|
_mark_floats,
|
|
pretty_json,
|
|
slugify,
|
|
generate_uid,
|
|
make_combined_slug,
|
|
time_ago,
|
|
extract_mentions,
|
|
format_date,
|
|
)
|
|
from devplacepy.utils.notifications import (
|
|
logger,
|
|
PUSH_ICON,
|
|
DEFAULT_PUSH_URL,
|
|
_push_tasks,
|
|
_safe_notify,
|
|
_schedule_push,
|
|
_schedule_telegram,
|
|
create_notification,
|
|
_deliver_notification,
|
|
create_mention_notifications,
|
|
_deliver_mention_notifications,
|
|
)
|
|
from devplacepy.utils.badges import (
|
|
LEVEL_BADGES,
|
|
BADGE_CATALOG,
|
|
BADGE_GROUPS,
|
|
get_badge,
|
|
build_achievements,
|
|
award_badge,
|
|
notify_badge,
|
|
)
|
|
from devplacepy.utils.rewards import (
|
|
LEVEL_XP,
|
|
XP_POST,
|
|
XP_COMMENT,
|
|
XP_PROJECT,
|
|
XP_GIST,
|
|
XP_UPVOTE,
|
|
XP_FOLLOW,
|
|
XP_QUIZ,
|
|
XP_QUIZ_PUBLISH,
|
|
XP_QUIZ_COMPLETE,
|
|
XP_BATTLE_PART,
|
|
XP_BATTLE_WIN,
|
|
XP_BATTLE_TOP,
|
|
level_for_xp,
|
|
award_xp,
|
|
_COUNT_MILESTONES,
|
|
_milestone_metrics,
|
|
check_milestone_badges,
|
|
award_rewards,
|
|
_apply_rewards,
|
|
ACHIEVEMENTS,
|
|
UNIQUE_ACTIONS,
|
|
track_action,
|
|
_apply_achievements,
|
|
)
|
|
from devplacepy.utils.accounts import (
|
|
_create_account,
|
|
register_account,
|
|
register_account_async,
|
|
)
|