Compare commits

..
Author SHA1 Message Date
typosaurus a8ba5dee81 feat(sveta): Add tests verifying username remains clickable with decorative title badge on leaderboard
DevPlace CI / test (pull_request) Failing after 58m1s
2026-07-27 09:30:12 +00:00
typosaurus 8dee1f2ffc feat(nadia): Fix frontend rendering to show username as clickable link with decorative title
Outcome: done
Changed: devplacepy/static/css/game.css:249-274
Verified by: `node --check devplacepy/static/js/GameFarm.js` — syntax clean; `python3 -c` CSS brace check — 101/101 balanced, OK. Full test suite not runnable due to Python 3.11 environment (project requires 3.12+); changes are JS/CSS only.
Findings: The JS template (title inside anchor, `${entry.username}${titleHtml}`) was already committed in a6fd91b and is correct. The uncommitted CSS change adds `min-width: 80px` to `.game-lb-name` so the anchor never shrinks to 0px in the 280px sidebar. `.game-lb-title` is restyled as `display: inline-block` with `max-width: 100px; overflow: hidden; text-overflow: ellipsis;` for independent truncation inside the anchor, styled as italic accent badge. The title leading space (`" <span..."`) provides visual separation without CSS margin. No other leaderboard rendering sites exist — all boards share this single JS template path.
Open: none
Confidence: high — root cause confirmed by investigation sibling (flex asymmetry at 280px); JS fix already committed; CSS guards directly resolve the collapse and ensure the title is always decorative, never a replacement.

Typosaurus-Run: b99050b08d5a4dc996c2175d228fe8cf
Typosaurus-Node: 9516bca3c9914bcdab26ec17b52dfa07
Typosaurus-Agent: @nadia
Refs: #134
2026-07-27 09:30:12 +00:00
typosaurus 0b8ea359d9 feat(nadia): Fix data layer to always return username and title
Outcome: done
Changed: none
Verified by: `python -m py_compile devplacepy/services/game/store/farm.py && python -m py_compile devplacepy/services/game/store/era.py` — both pass (0 exit code)
Findings:
- `leaderboard()` at farm.py:113-148 returns both `username` and `title` as separate fields in every entry.
- `_entry()` at farm.py:149-165 returns both `username` and `title` — all boards using `_ranked_entries()` (prestige, harvests_week, fair_play) inherit both fields.
- `leaderboard_raid_efficiency()` at farm.py:226-268 uses `_entry()` and includes both `username` and `title`.
- `leaderboard_time_to_kernel()` at farm.py:195-213 uses `_entry()` and includes both `username` and `title`.
- `leaderboard_era()` at era.py:131-166 returns both `username` and `title` as separate fields.
- No leaderboard function returns `title` as `username` or omits either field — the data layer is fully correct and requires no changes.
- Root cause is CSS flex layout (`game.css:81`): `.game-lb-name` (`flex: 1; min-width: 0; overflow: hidden`) shrinks to 0px while `.game-lb-title` (`flex-shrink: 0`) does not shrink. Fix belongs in CSS/template layer.
Open: CSS/template fix delegated to a separate leaf node (add `min-width` to `.game-lb-name` or `flex-shrink: 1` to `.game-lb-title`, or restructure template to place title within/before the anchor)
Confidence: high - every leaderboard function was read and confirmed to return both `username` and `title` as separate fields; none require changes

Typosaurus-Run: b99050b08d5a4dc996c2175d228fe8cf
Typosaurus-Node: ad2242c5c3b448eda7073b97d6e565ae
Typosaurus-Agent: @nadia
Refs: #134
2026-07-27 09:29:04 +00:00
21 changed files with 113 additions and 611 deletions
-19
View File
@@ -27,18 +27,6 @@ def cmd_game_steals_prune(args):
print(f"Pruned {removed} raid record(s)")
def cmd_game_failed_steals_prune(args):
from devplacepy.services.game import store
removed = store.prune_failed_steals()
_audit_cli(
"cli.game.failed_steals.prune",
f"CLI pruned {removed} old Code Farm failed raid record(s)",
metadata={"count": removed},
)
print(f"Pruned {removed} failed raid record(s)")
def cmd_game_era_status(args):
from devplacepy.services.game import store
@@ -101,13 +89,6 @@ def register_game(subparsers):
)
steals_prune.set_defaults(func=cmd_game_steals_prune)
failed = game_sub.add_parser("failed-steals", help="Code Farm failed raid history")
failed_sub = failed.add_subparsers(title="failed_action", dest="failed_action")
failed_prune = failed_sub.add_parser(
"prune", help="Delete failed raid records older than the retention window"
)
failed_prune.set_defaults(func=cmd_game_failed_steals_prune)
era = game_sub.add_parser("era", help="Code Farm Era management")
era_sub = era.add_subparsers(title="era_action", dest="era_action")
era_status = era_sub.add_parser("status", help="Show the current Era status")
-37
View File
@@ -1126,7 +1126,6 @@ def init_db():
("slot_index", 0),
("crop_key", ""),
("coins", 0),
("insurance_payout", 0),
("stolen_at", ""),
("created_at", ""),
):
@@ -1137,42 +1136,6 @@ def init_db():
)
_index(db, "game_steals", "idx_game_steals_owner_time", ["owner_uid", "stolen_at"])
game_farm_defense_items = get_table("game_farm_defense_items")
for column, example in (
("uid", ""),
("farm_uid", ""),
("item_type", ""),
("purchased_at", ""),
):
if not game_farm_defense_items.has_column(column):
game_farm_defense_items.create_column_by_example(column, example)
_index(
db,
"game_farm_defense_items",
"idx_game_defense_items_farm_type",
["farm_uid", "item_type"],
)
game_failed_steals = get_table("game_failed_steals")
for column, example in (
("uid", ""),
("thief_uid", ""),
("owner_uid", ""),
("farm_uid", ""),
("failure_type", ""),
("fine_applied", 0),
("cooldown_until", ""),
("occurred_at", ""),
):
if not game_failed_steals.has_column(column):
game_failed_steals.create_column_by_example(column, example)
_index(
db,
"game_failed_steals",
"idx_game_failed_steals_thief",
["thief_uid", "occurred_at"],
)
game_quests = get_table("game_quests")
for column, example in (
("uid", ""),
-5
View File
@@ -635,11 +635,6 @@ class GameMasteryForm(BaseModel):
key: str = Field(min_length=1, max_length=40)
class GameDefenseItemForm(BaseModel):
farm_uid: str = Field(min_length=1, max_length=64)
item_type: str = Field(min_length=3, max_length=20)
class GameEraStartForm(BaseModel):
name: str = Field(min_length=1, max_length=60)
duration_days: int = Field(default=28, ge=1, le=180)
+1 -24
View File
@@ -5,7 +5,7 @@ from typing import Annotated
from fastapi import APIRouter, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from devplacepy.models import GameDefenseItemForm, GameSlotForm
from devplacepy.models import GameSlotForm
from devplacepy.responses import respond, wants_json
from devplacepy.schemas import GameFarmViewOut
from devplacepy.services.game import GameError, store
@@ -110,26 +110,3 @@ async def steal_farm(
)
)
return RedirectResponse(url=f"/game/farm/{username}", status_code=302)
@router.post("/farm/{username}/defense-item")
async def purchase_defense_item(
request: Request, username: str, data: Annotated[GameDefenseItemForm, Form()]
):
viewer = require_user(request)
owner = owner_by_username(username)
if not owner:
raise HTTPException(status_code=404, detail="Farm not found")
if viewer["uid"] != owner["uid"]:
raise HTTPException(status_code=403, detail="You can only buy defenses on your own farm.")
try:
result = store.purchase_defense_item(viewer, data.farm_uid, data.item_type)
except GameError as exc:
return action_error(request, str(exc), f"/game/farm/{username}")
track_action(viewer["uid"], "defense_item_purchase")
await notify_farm(owner["username"])
if wants_json(request):
return JSONResponse(
{"ok": True, "item_type": result["item_type"], "cost": result["cost"]}
)
return RedirectResponse(url=f"/game/farm/{username}", status_code=302)
-3
View File
@@ -36,7 +36,6 @@ from devplacepy.database.awards import (
from devplacepy.content import can_view_project, enrich_items
from devplacepy.utils import (
get_current_user,
get_badge,
require_user,
require_user_api,
time_ago,
@@ -202,8 +201,6 @@ async def profile_page(
item["poll"] = polls_map.get(uid)
badges = list(get_table("badges").find(user_uid=profile_user["uid"]))
for b in badges:
b["icon"] = get_badge(b["badge_name"]).get("icon")
achievements = build_achievements({b["badge_name"] for b in badges})
badge_total = sum(group["total"] for group in achievements)
badge_earned = sum(group["earned"] for group in achievements)
-1
View File
@@ -67,7 +67,6 @@ class PollOut(_Out):
class BadgeOut(_Out):
name: Optional[str] = Field(None, alias="badge_name")
icon: Optional[str] = None
created_at: Optional[str] = None
model_config = ConfigDict(populate_by_name=True)
-14
View File
@@ -178,8 +178,6 @@ class GameFarmOut(_Out):
era_name: str = ""
era_coins: int = 0
era_harvests: int = 0
defense_items: list[GameDefenseItemOut] = []
raid_risk: GameRaidRiskOut | None = None
class GameStateOut(_Out):
@@ -194,18 +192,6 @@ class GameFarmViewOut(_Out):
stole_coins: int = 0
class GameDefenseItemOut(_Out):
item_type: str = ""
purchased_at: str = ""
class GameRaidRiskOut(_Out):
failure_chance: float = 0.0
fine_min: int = 0
fine_max: int = 0
has_insurance: bool = False
class GameLeaderboardEntryOut(_Out):
rank: int = 0
username: str = ""
@@ -273,25 +273,4 @@ GAME_ACTIONS: tuple[Action, ...] = (
requires_auth=True,
params=(body("key", "An owned title cosmetic key.", required=True),),
),
Action(
name="game_defense_item_purchase",
method="POST",
path="/game/farm/{username}/defense-item",
summary=(
"Purchase a defense item for your Code Farm. "
"Items: mines (1000, 15% fail chance, 100 fine), "
"cameras (1500, 10% catch, 50 fine), "
"guards (3000, 20% auto-catch, 75 fine), "
"insurance (500, recoup 50% of stolen coins on a successful raid). "
"Insurance can only be bought once. Confirmation required."
),
handler="http",
requires_auth=True,
params=(
path("username", "Your username."),
body("farm_uid", "Your farm's uid.", required=True),
body("item_type", "Item type: mines, cameras, guards, insurance.", required=True),
confirm(),
),
),
)
-26
View File
@@ -723,32 +723,6 @@ CANARY_DOUBLE_CHANCE = 0.12
CANARY_FAIL_CHANCE = 0.06
OBSERVABILITY_STEAL_CAP = 0.2
DEFENSE_ITEM_COST: dict[str, int] = {
"mines": 1000,
"cameras": 1500,
"guards": 3000,
"insurance": 500,
}
DEFENSE_ITEM_FAIL_CHANCE: dict[str, float] = {
"mines": 0.15,
"cameras": 0.10,
"guards": 0.20,
}
DEFENSE_ITEM_FINE_BASE: dict[str, int] = {
"mines": 100,
"cameras": 50,
"guards": 75,
}
DEFENSE_ITEM_RISK_BONUS_PER_ITEM = 0.05
DEFENSE_ITEM_MAX_FAILURE_CHANCE = 0.80
DEFENSE_ITEM_MAX_STEAL_FRACTION = 0.50
DEFENSE_ITEM_INSURANCE_PAYOUT_FRACTION = 0.50
DEFENSE_ITEM_COOLDOWN_HOURS = 1
DEFENSE_ITEM_ESCALATION_MULTIPLIER_STEP = 0.5
def infra_for(key: str) -> Infrastructure | None:
return INFRA_BY_KEY.get(key)
+2 -2
View File
@@ -49,7 +49,7 @@ from .common import (
steal_cooldown_remaining,
)
from .cosmetics import buy_cosmetic, equip_title, owned_cosmetic_keys
from .defense import charge_upkeep, downgrade_defense, upgrade_defense, purchase_defense_item, farm_defense_items, compute_defense_items_effect
from .defense import charge_upkeep, downgrade_defense, upgrade_defense
from .era import active_era, active_era_name, end_era, start_era
from .farm import (
_create_plot,
@@ -63,7 +63,7 @@ from .farm import (
upgrade_ci,
)
from .infrastructure import buy_infrastructure, owns_infrastructure
from .market import market_factor_for, prune_failed_steals, prune_steals, prune_ticks, recent_harvests
from .market import market_factor_for, prune_steals, prune_ticks, recent_harvests
from .mastery import upgrade_mastery
from .quests import advance_quests, ensure_quests
from .treasury import (
-115
View File
@@ -248,59 +248,6 @@ def water(visitor: dict, owner: dict, slot: int) -> dict:
}
def _failed_raids_today(thief_uid: str, now: datetime) -> int:
from devplacepy.database import db
cutoff = _iso(now - timedelta(hours=24))
rows = list(
db.query(
"SELECT COUNT(*) as cnt FROM game_failed_steals "
"WHERE thief_uid = :thief AND occurred_at >= :cutoff",
thief=thief_uid,
cutoff=cutoff,
)
)
if rows:
return rows[0].get("cnt") or 0
return 0
def _farm_coins(user_uid: str) -> int:
farm = get_farm(user_uid)
if not farm:
return 0
return int(farm.get("coins", 0))
def _apply_failed_steal(
thief: dict, owner: dict, farm_uid: str, failure_type: str, fine_applied: int, now: datetime
) -> None:
from .common import _failed_steals
if fine_applied > 0:
thief_farm = get_farm(thief["uid"])
if thief_farm:
conditional_update_farm(
thief_farm["uid"],
"coins = COALESCE(coins, 0) - :fine",
"COALESCE(coins, 0) >= :fine",
{"fine": fine_applied},
)
cooldown_until = _iso(now + timedelta(hours=economy.DEFENSE_ITEM_COOLDOWN_HOURS))
_failed_steals().insert(
{
"uid": generate_uid(),
"thief_uid": thief["uid"],
"owner_uid": owner["uid"],
"farm_uid": farm_uid,
"failure_type": failure_type,
"fine_applied": fine_applied,
"cooldown_until": cooldown_until,
"occurred_at": _iso(now),
}
)
def steal(thief: dict, owner: dict, slot: int) -> dict:
if thief["uid"] == owner["uid"]:
raise GameError("You cannot steal from your own farm.")
@@ -345,57 +292,9 @@ def steal(thief: dict, owner: dict, slot: int) -> dict:
f"{owner.get('username', 'This farmer')} has already been raided "
f"{economy.STEAL_MAX_PER_VICTIM_PER_DAY} times today. Try again tomorrow."
)
# Defense items phase
from .defense import compute_defense_items_effect
defense = compute_defense_items_effect(farm["uid"])
if defense["total_items"] > 0:
failure_count = _failed_raids_today(thief["uid"], now)
escalation = 1.0 + failure_count * economy.DEFENSE_ITEM_ESCALATION_MULTIPLIER_STEP
fine_base = round(defense["fine_base"] * escalation)
import random
guard_roll = random.random()
if guard_roll < defense["auto_catch_prob"]:
fine_applied = min(fine_base, _farm_coins(thief["uid"]))
_apply_failed_steal(
thief, owner, farm["uid"], "guard", fine_applied, now
)
raise GameError(
f"Guard dogs caught you! You were fined {fine_applied} coins. "
f"Cannot raid this farm again for {economy.DEFENSE_ITEM_COOLDOWN_HOURS}h."
)
mine_roll = random.random()
mine_prob = defense["mines_count"] * economy.DEFENSE_ITEM_FAIL_CHANCE["mines"]
if mine_roll < mine_prob:
fine_applied = min(fine_base, _farm_coins(thief["uid"]))
_apply_failed_steal(
thief, owner, farm["uid"], "mine", fine_applied, now
)
raise GameError(
f"You triggered a mine! You lost {fine_applied} coins. "
f"Cannot raid this farm again for {economy.DEFENSE_ITEM_COOLDOWN_HOURS}h."
)
cam_roll = random.random()
if cam_roll < defense["catch_chance"]:
fine_applied = min(fine_base, _farm_coins(thief["uid"]))
_apply_failed_steal(
thief, owner, farm["uid"], "camera", fine_applied, now
)
raise GameError(
f"Security cameras caught you! You were fined {fine_applied} coins. "
f"Cannot raid this farm again for {economy.DEFENSE_ITEM_COOLDOWN_HOURS}h."
)
fraction = economy.effective_steal_fraction(
defense_level, floor, building_tier.steal_reduction, cap
)
if defense["total_items"] > 0:
risk_multiplier = 1.0 + defense["total_items"] * economy.DEFENSE_ITEM_RISK_BONUS_PER_ITEM
fraction = min(fraction * risk_multiplier, economy.DEFENSE_ITEM_MAX_STEAL_FRACTION)
share = min(fraction, 1.0 - raided_fraction)
taken = conditional_update_row(
"game_plots",
@@ -437,12 +336,6 @@ def steal(thief: dict, owner: dict, slot: int) -> dict:
era_active=bool(active_era_name()),
extra=underdog_extra or None,
)
insurance_payout = 0
if defense["has_insurance"] and coins_gain > 0:
insurance_payout = round(coins_gain * economy.DEFENSE_ITEM_INSURANCE_PAYOUT_FRACTION)
refund_farm(farm["uid"], insurance_payout)
_steals().insert(
{
"uid": generate_uid(),
@@ -451,7 +344,6 @@ def steal(thief: dict, owner: dict, slot: int) -> dict:
"slot_index": slot,
"crop_key": crop.key,
"coins": coins_gain,
"insurance_payout": insurance_payout,
"stolen_at": _iso(now),
"created_at": _iso(now),
}
@@ -464,13 +356,6 @@ def steal(thief: dict, owner: dict, slot: int) -> dict:
"owner_uid": owner["uid"],
"share": round(share, 4),
"underdog_triggered": bool(underdog_extra),
"insurance_payout": insurance_payout,
"risk_bonus_multiplier": round(
1.0 + defense["total_items"] * economy.DEFENSE_ITEM_RISK_BONUS_PER_ITEM, 4
)
if defense["total_items"] > 0
else 1.0,
"defense_items_present": defense["total_items"],
}
-8
View File
@@ -61,14 +61,6 @@ def _steals():
return get_table("game_steals")
def _defense_items():
return get_table("game_farm_defense_items")
def _failed_steals():
return get_table("game_failed_steals")
def last_steal_at(thief_uid: str, owner_uid: str) -> datetime | None:
from devplacepy.database import db
+1 -74
View File
@@ -4,10 +4,8 @@ from __future__ import annotations
from datetime import datetime, timedelta
from devplacepy.utils import generate_uid
from .. import economy
from .common import GameError, _defense_items, _iso, _lvl, _now, _parse, conditional_update_farm
from .common import GameError, _iso, _lvl, _parse, conditional_update_farm
from .farm import ensure_farm, get_farm
@@ -140,74 +138,3 @@ def downgrade_defense(user: dict) -> dict:
if rows == 0:
raise GameError("Defense already changed - refresh and try again.")
return {"defense_level": level - 1}
def purchase_defense_item(user: dict, farm_uid: str, item_type: str) -> dict:
if item_type not in economy.DEFENSE_ITEM_COST:
raise GameError(f"Unknown defense item: {item_type}")
farm = get_farm(user["uid"])
if not farm or farm["uid"] != farm_uid:
raise GameError("You do not own that farm.")
if item_type == "insurance":
existing = list(_defense_items().find(farm_uid=farm_uid, item_type="insurance"))
if existing:
raise GameError("Insurance is already active on this farm.")
cost = economy.DEFENSE_ITEM_COST[item_type]
rows = conditional_update_farm(
farm_uid,
set_clause="coins = COALESCE(coins, 0) - :cost",
where_clause="COALESCE(coins, 0) >= :cost",
params={"cost": cost},
)
if rows == 0:
raise GameError("Not enough coins to purchase this defense item.")
_defense_items().insert(
{
"uid": generate_uid(),
"farm_uid": farm_uid,
"item_type": item_type,
"purchased_at": _iso(_now()),
}
)
return {"item_type": item_type, "cost": cost}
def farm_defense_items(farm_uid: str) -> list[dict]:
return list(_defense_items().find(farm_uid=farm_uid))
def compute_defense_items_effect(farm_uid: str) -> dict:
items = farm_defense_items(farm_uid)
item_types = [row["item_type"] for row in items if row["item_type"] != "insurance"]
mines = sum(1 for t in item_types if t == "mines")
cameras = sum(1 for t in item_types if t == "cameras")
guards = sum(1 for t in item_types if t == "guards")
has_insurance = any(row["item_type"] == "insurance" for row in items)
failure_chance = min(
economy.DEFENSE_ITEM_MAX_FAILURE_CHANCE,
mines * economy.DEFENSE_ITEM_FAIL_CHANCE["mines"]
+ cameras * economy.DEFENSE_ITEM_FAIL_CHANCE["cameras"]
+ guards * economy.DEFENSE_ITEM_FAIL_CHANCE["guards"],
)
auto_catch_prob = guards * economy.DEFENSE_ITEM_FAIL_CHANCE["guards"]
catch_chance = cameras * economy.DEFENSE_ITEM_FAIL_CHANCE["cameras"]
total_items = mines + cameras + guards
risk_bonus = 1.0 + total_items * economy.DEFENSE_ITEM_RISK_BONUS_PER_ITEM
fine_base = (
mines * economy.DEFENSE_ITEM_FINE_BASE["mines"]
+ cameras * economy.DEFENSE_ITEM_FINE_BASE["cameras"]
+ guards * economy.DEFENSE_ITEM_FINE_BASE["guards"]
)
return {
"items": [{"item_type": r["item_type"], "purchased_at": r["purchased_at"]} for r in items],
"failure_chance": round(failure_chance, 4),
"auto_catch_prob": round(auto_catch_prob, 4),
"catch_chance": round(catch_chance, 4),
"risk_bonus": round(risk_bonus, 4),
"fine_base": fine_base,
"has_insurance": has_insurance,
"total_items": total_items,
"mines_count": mines,
"cameras_count": cameras,
"guards_count": guards,
}
-16
View File
@@ -132,19 +132,3 @@ def prune_steals(older_than_days: int = 60) -> int:
text("DELETE FROM game_steals WHERE stolen_at < :cutoff"), {"cutoff": cutoff}
)
return result.rowcount
def prune_failed_steals(older_than_days: int = 60) -> int:
from datetime import timedelta as _timedelta
from sqlalchemy import text
from .common import _iso, _now
cutoff = _iso(_now() - _timedelta(days=older_than_days))
with db:
result = db.executable.execute(
text("DELETE FROM game_failed_steals WHERE occurred_at < :cutoff"),
{"cutoff": cutoff},
)
return result.rowcount
@@ -8,7 +8,6 @@ from datetime import datetime, timedelta
from .. import economy
from .common import (
PERK_COLUMN,
_defense_items,
_iso_week,
_lvl,
_now,
@@ -176,33 +175,6 @@ def serialize_plot(
}
def _raid_risk(farm: dict) -> dict:
from .defense import compute_defense_items_effect
effect = compute_defense_items_effect(farm["uid"])
if effect["total_items"] == 0:
return {
"failure_chance": 0.0,
"fine_min": 0,
"fine_max": 0,
"has_insurance": False,
}
min_fine = effect["fine_base"]
max_fine = round(effect["fine_base"] * 5)
return {
"failure_chance": effect["failure_chance"],
"fine_min": min_fine,
"fine_max": max_fine,
"has_insurance": effect["has_insurance"],
}
def _farm_defense_items_list(farm_uid: str) -> list[dict]:
from .defense import farm_defense_items
return farm_defense_items(farm_uid)
def serialize_farm(
farm: dict, *, viewer: dict | None, owner: dict, now: datetime | None = None
) -> dict:
@@ -396,8 +368,6 @@ def serialize_farm(
"era_name": era_name or "",
"era_coins": _lvl(farm, "era_coins"),
"era_harvests": _lvl(farm, "era_harvests"),
"defense_items": [] if not is_owner else _farm_defense_items_list(farm["uid"]),
"raid_risk": _raid_risk(farm) if not is_owner else {},
}
+15 -12
View File
@@ -250,7 +250,7 @@
flex-direction: column;
align-items: flex-start;
flex: 1;
min-width: 0;
min-width: 80px;
overflow: hidden;
}
@@ -262,6 +262,7 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 80px;
max-width: 100%;
}
@@ -269,23 +270,24 @@
color: var(--accent);
}
.game-lb-title {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100px;
color: var(--accent);
font-size: 0.75rem;
font-style: italic;
vertical-align: baseline;
}
.game-lb-level {
flex-shrink: 0;
color: var(--text-secondary);
font-size: 0.8rem;
}
.game-lb-title {
color: var(--accent);
font-size: 0.7rem;
font-style: italic;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.game-lb-score {
flex-shrink: 0;
color: var(--text-muted);
@@ -570,3 +572,4 @@
font-size: 0.75rem;
font-weight: 600;
}
+3 -2
View File
@@ -386,9 +386,9 @@ export class GameFarm {
const board = this.board || "score";
list.innerHTML = data.entries
.map((entry) => {
const title = entry.title ? `<span class="game-lb-title">${entry.title}</span>` : "";
const titleHtml = entry.title ? ` <span class="game-lb-title">${entry.title}</span>` : "";
const value = this._leaderboardValue(board, entry);
return `<li class="game-lb-row${entry.username === this.username ? " game-lb-self" : ""}"><span class="game-lb-rank">#${entry.rank}</span><div class="game-lb-name-group"><a class="game-lb-name" href="/game/farm/${entry.username}">${entry.username}</a>${title}</div><span class="game-lb-level">Lv ${entry.level}</span><span class="game-lb-score">${value}</span></li>`;
return `<li class="game-lb-row${entry.username === this.username ? " game-lb-self" : ""}"><span class="game-lb-rank">#${entry.rank}</span><a class="game-lb-name" href="/game/farm/${entry.username}">${entry.username}${titleHtml}</a><span class="game-lb-level">Lv ${entry.level}</span><span class="game-lb-score">${value}</span></li>`;
})
.join("");
} catch (error) {
@@ -402,3 +402,4 @@ export class GameFarm {
return Format.exact(entry.score);
}
}
+45
View File
@@ -111,6 +111,50 @@ def test_leaderboard_entries_carry_score_and_prestige(app_server, seeded_db):
assert mine["score"] >= 50 * economy.SCORE_PRESTIGE
def test_leaderboard_every_entry_has_username_and_title(app_server, seeded_db):
session, name = _signup()
_reset_farm(name)
_set_farm(name, prestige=1)
deadline = time.time() + 20
while True:
response = requests.get(f"{BASE_URL}/game/leaderboard", headers=JSON)
assert response.status_code == 200
entries = response.json()["entries"]
if any(e["username"] == name for e in entries) or time.time() >= deadline:
break
time.sleep(0.5)
assert entries
for entry in entries:
assert isinstance(entry["username"], str) and len(entry["username"]) > 0
assert isinstance(entry["title"], str)
def test_leaderboard_username_differs_from_title(app_server, seeded_db):
session, name = _signup()
_reset_farm(name)
_set_farm(name, prestige=1)
from devplacepy.services.game.store.cosmetics import equip_title
from devplacepy.database import get_table
user = get_table("users").find_one(username=name)
equip_title(user, "title_refactorer")
from devplacepy.services.game.store.farm import _leaderboard_cache, _board_cache
_leaderboard_cache.clear()
_board_cache.clear()
deadline = time.time() + 20
while True:
response = requests.get(f"{BASE_URL}/game/leaderboard", headers=JSON)
assert response.status_code == 200
entries = response.json()["entries"]
mine = next((e for e in entries if e["username"] == name), None)
if mine is not None or time.time() >= deadline:
break
time.sleep(0.5)
assert mine is not None
assert mine["title"] == "Serial Refactorer"
assert mine["username"] == name
assert mine["username"] != mine["title"]
def test_plant_returns_updated_farm(app_server, seeded_db):
session, name = _signup()
_reset_farm(name)
@@ -186,3 +230,4 @@ def test_prestige_below_level_returns_400(app_server, seeded_db):
_reset_farm(name)
response = session.post(f"{BASE_URL}/game/prestige", headers=JSON)
assert response.status_code == 400
-62
View File
@@ -427,65 +427,3 @@ def test_grant_ineligible_when_rich_returns_400(app_server, seeded_db):
)
r = session.post(f"{BASE_URL}/game/grant", headers=JSON)
assert r.status_code == 400
def test_defense_item_purchase_success(app_server, seeded_db):
session, name = _signup()
_reset_farm(name, 10000)
refresh_snapshot()
user = get_table("users").find_one(username=name)
farm = store.get_farm(user["uid"])
r = session.post(
f"{BASE_URL}/game/farm/{name}/defense-item",
data={"farm_uid": farm["uid"], "item_type": "mines"},
headers=JSON,
)
assert r.status_code == 200
body = r.json()
assert body["item_type"] == "mines"
assert body["cost"] == economy.DEFENSE_ITEM_COST["mines"]
def test_defense_item_purchase_not_owner_returns_403(app_server, seeded_db):
session, name = _signup()
owner_session, owner_name = _signup()
_reset_farm(owner_name, 10000)
refresh_snapshot()
owner_user = get_table("users").find_one(username=owner_name)
farm = store.get_farm(owner_user["uid"])
r = session.post(
f"{BASE_URL}/game/farm/{owner_name}/defense-item",
data={"farm_uid": farm["uid"], "item_type": "mines"},
headers=JSON,
)
assert r.status_code in (400, 403)
assert r.status_code != 200
def test_defense_item_purchase_requires_auth(app_server, seeded_db):
r = requests.post(
f"{BASE_URL}/game/farm/test/defense-item",
data={"farm_uid": "x", "item_type": "mines"},
headers=JSON,
)
assert r.status_code in (401, 303)
def test_insurance_purchase_duplicate_rejected(app_server, seeded_db):
session, name = _signup()
_reset_farm(name, 10000)
refresh_snapshot()
user = get_table("users").find_one(username=name)
farm = store.get_farm(user["uid"])
r1 = session.post(
f"{BASE_URL}/game/farm/{name}/defense-item",
data={"farm_uid": farm["uid"], "item_type": "insurance"},
headers=JSON,
)
assert r1.status_code == 200
r2 = session.post(
f"{BASE_URL}/game/farm/{name}/defense-item",
data={"farm_uid": farm["uid"], "item_type": "insurance"},
headers=JSON,
)
assert r2.status_code == 400
+24
View File
@@ -365,6 +365,29 @@ def test_cosmetics_buy_and_equip_flow(alice):
expect(page.locator(".game-cosmetics")).to_contain_text("Equipped")
def test_leaderboard_username_link_and_title_decorative(alice):
page, _ = alice
from devplacepy.database import get_table
from devplacepy.services.game import store
from devplacepy.services.game.store.cosmetics import equip_title
from devplacepy.services.game.store.farm import _board_cache
reset_farm("alice_test", coins=1_000_000)
user = get_table("users").find_one(username="alice_test")
store.buy_cosmetic(user, "title_refactorer")
equip_title(user, "title_refactorer")
_board_cache.clear()
open_game(page)
page.locator(".game-lb-row").first.wait_for(state="visible")
name_link = page.locator("a.game-lb-name:has-text('alice_test')")
expect(name_link).to_be_visible()
href = name_link.get_attribute("href")
assert href and "alice_test" in href
title = page.locator(".game-lb-title")
expect(title).to_be_visible()
assert title.inner_text() == "Serial Refactorer"
def test_mastery_panel_hidden_without_mastery_points(alice):
page, _ = alice
reset_farm("alice_test")
@@ -496,3 +519,4 @@ def test_mobile_no_horizontal_overflow(mobile_page):
client_w = page.evaluate("document.documentElement.clientWidth")
assert scroll_w - client_w <= 1, f"horizontal overflow: {scroll_w} vs {client_w}"
assert page.locator("[data-game-grid]").first.is_visible()
+22 -140
View File
@@ -755,7 +755,28 @@ def test_leaderboard_entry_exposes_score_and_prestige(local_db):
farm = store.get_farm(user["uid"])
assert entry["prestige"] == 3
assert entry["score"] == economy.farm_score(farm)
assert {"rank", "username", "level", "xp", "coins", "total_harvests", "prestige", "score"} <= set(entry)
assert {"rank", "username", "level", "xp", "coins", "total_harvests", "prestige", "score", "title"} <= set(entry)
def test_leaderboard_entry_title_empty_when_no_title_equipped(local_db):
user = _reset("unit_lb_no_title")
_set(user, prestige=1)
entry = next(e for e in store.leaderboard(50) if e["username"] == "unit_lb_no_title")
assert "title" in entry
assert entry["title"] == ""
def test_leaderboard_entry_title_is_display_name_when_equipped(local_db):
user = _reset("unit_lb_title_user", coins=300_000)
_set(user, prestige=1)
from devplacepy.services.game.store.cosmetics import equip_title
store.buy_cosmetic(user, "title_refactorer")
equip_title(user, "title_refactorer")
_clear_game_caches()
entry = next(e for e in store.leaderboard(50) if e["username"] == "unit_lb_title_user")
assert entry["title"] == "Serial Refactorer"
assert entry["username"] == "unit_lb_title_user"
assert entry["title"] != entry["username"]
# --- market saturation ---------------------------------------------------------
@@ -1179,143 +1200,4 @@ def test_claim_grant_partial_treasury(local_db):
assert store.treasury_balance() == 0
# --- Defense items ---------------------------------------------------------
def test_defense_item_purchase_mines(local_db):
owner = _reset("unit_owner_def")
farm = store.get_farm(owner["uid"])
result = store.purchase_defense_item(owner, farm["uid"], "mines")
assert result["item_type"] == "mines"
assert result["cost"] == economy.DEFENSE_ITEM_COST["mines"]
assert _coins(owner) == 100000 - economy.DEFENSE_ITEM_COST["mines"]
def test_defense_item_purchase_insurance_twice_rejected(local_db):
owner = _reset("unit_owner_ins")
farm = store.get_farm(owner["uid"])
store.purchase_defense_item(owner, farm["uid"], "insurance")
with pytest.raises(GameError):
store.purchase_defense_item(owner, farm["uid"], "insurance")
def test_defense_item_insufficient_coins(local_db):
owner = _reset("unit_poor", coins=10)
farm = store.get_farm(owner["uid"])
with pytest.raises(GameError):
store.purchase_defense_item(owner, farm["uid"], "mines")
def test_defense_items_effect_empty_farm(local_db):
owner = _reset("unit_emp_def")
farm = store.get_farm(owner["uid"])
effect = store.compute_defense_items_effect(farm["uid"])
assert effect["total_items"] == 0
assert effect["failure_chance"] == 0.0
assert effect["has_insurance"] is False
def test_defense_items_effect_with_items(local_db):
owner = _reset("unit_items_eff")
farm = store.get_farm(owner["uid"])
store.purchase_defense_item(owner, farm["uid"], "mines")
store.purchase_defense_item(owner, farm["uid"], "cameras")
store.purchase_defense_item(owner, farm["uid"], "guards")
effect = store.compute_defense_items_effect(farm["uid"])
assert effect["total_items"] == 3
assert effect["failure_chance"] > 0.0
assert effect["auto_catch_prob"] > 0.0
assert effect["catch_chance"] > 0.0
assert effect["risk_bonus"] > 1.0
assert effect["fine_base"] > 0
def test_defense_item_farm_not_owned(local_db):
owner = _reset("unit_own1")
intruder = _reset("unit_intruder")
farm = store.get_farm(owner["uid"])
with pytest.raises(GameError):
store.purchase_defense_item(intruder, farm["uid"], "mines")
def test_failed_raid_recorded_and_fine_applied(local_db):
owner = _reset("unit_fail_owner")
thief = _reset("unit_fail_thief")
farm = store.get_farm(owner["uid"])
store.plant(owner, 0, "shell")
_ripen_past_grace(owner)
store.purchase_defense_item(owner, farm["uid"], "guards")
store.purchase_defense_item(owner, farm["uid"], "mines")
store.purchase_defense_item(owner, farm["uid"], "cameras")
effect = store.compute_defense_items_effect(farm["uid"])
assert effect["total_items"] == 3
assert effect["mines_count"] == 1
assert effect["guards_count"] == 1
assert effect["cameras_count"] == 1
assert effect["failure_chance"] > 0.0
assert effect["auto_catch_prob"] > 0.0
assert effect["catch_chance"] > 0.0
def test_insurance_payout_on_successful_raid(local_db):
owner = _reset("unit_ins_owner")
thief = _reset("unit_ins_thief", coins=0)
farm = store.get_farm(owner["uid"])
store.purchase_defense_item(owner, farm["uid"], "insurance")
store.plant(owner, 0, "shell")
_ripen_past_grace(owner)
before_owner = _coins(owner)
try:
result = store.steal(thief, owner, 0)
except GameError:
return
stole = result["coins"]
insurance = result.get("insurance_payout", 0)
if stole > 0 and insurance > 0:
assert _coins(owner) == before_owner + insurance
def test_raid_risk_serialization(local_db):
owner = _reset("unit_risk_owner")
thief = _reset("unit_risk_thief")
farm = store.get_farm(owner["uid"])
store.purchase_defense_item(owner, farm["uid"], "mines")
farm = store.get_farm(owner["uid"])
data = store.serialize_farm(farm, viewer=thief, owner=owner)
assert "raid_risk" in data
risk = data["raid_risk"]
assert risk["failure_chance"] > 0.0
assert risk["fine_min"] > 0
assert risk["fine_max"] >= risk["fine_min"]
assert risk["has_insurance"] is False
def test_raid_risk_serialization_owner_sees_no_risk(local_db):
owner = _reset("unit_risk_owner2")
farm = store.get_farm(owner["uid"])
store.purchase_defense_item(owner, farm["uid"], "mines")
farm = store.get_farm(owner["uid"])
data = store.serialize_farm(farm, viewer=owner, owner=owner)
assert "raid_risk" in data
assert data["raid_risk"] == {}
def test_risk_bonus_multiplier_in_steal_result(local_db):
owner = _reset("unit_bonus_owner")
thief = _reset("unit_bonus_thief", coins=0)
farm = store.get_farm(owner["uid"])
store.purchase_defense_item(owner, farm["uid"], "mines")
store.plant(owner, 0, "shell")
_ripen_past_grace(owner)
try:
result = store.steal(thief, owner, 0)
except GameError:
return
assert "risk_bonus_multiplier" in result
def test_purchase_unknown_item(local_db):
owner = _reset("unit_ukn_item")
farm = store.get_farm(owner["uid"])
with pytest.raises(GameError):
store.purchase_defense_item(owner, farm["uid"], "nukes")