More achievements

This commit is contained in:
2026-10-22 12:25:21 -04:00
parent 3b81cae024
commit 84b363b863
4 changed files with 115 additions and 16 deletions
+48 -4
View File
@@ -265,11 +265,16 @@ def write_state(connection, state):
def milestone_status(game_state, at):
"""Eight fixed community goals; elapsed survival stops at the deadline."""
"""Twelve fixed community goals; elapsed survival stops at the deadline."""
until = min(at, game_state["ended_at"] if game_state["ended_at"] is not None else game_state["deadline"])
elapsed = max(0, until - game_state["started_at"])
players = len(game_state["players"])
presses = len(game_state["presses"])
scores = {}
visited = set()
for press in game_state["presses"]:
scores[press["player_id"]] = scores.get(press["player_id"], 0) + press["points"]
visited.add(press["zone"])
goals = [
("day_one", "One day together", "The community kept the game alive for one day!", elapsed, 86400000),
("one_week", "A week of nerve", "Seven days together. The clock is still part of our story!", elapsed, 7 * 86400000),
@@ -279,6 +284,10 @@ def milestone_status(game_state, at):
("hundred_presses", "A hundred second chances", "Together we've pressed the button 100 times!", presses, 100),
("thousand_presses", "A thousand moments", "1,000 presses. A thousand decisions to keep us going!", presses, 1000),
("first_photo_finish", "Our first photo finish", "A community first: someone pressed with less than ten seconds left!", int(any(0 < p["remaining_ms"] < 10000 for p in game_state["presses"])), 1),
("shared_spectrum", "Every shade together", "The community has pressed in every scoring zone this season!", len(visited & {z["name"] for z in ZONES}), len(ZONES)),
("ten_contenders", "A race worth watching", "Ten players have earned points on this season's leaderboard!", sum(points > 0 for points in scores.values()), 10),
("thousand_points", "A thousand points together", "The seasonal leaderboard has reached 1,000 combined points!", sum(scores.values()), 1000),
("ten_thousand_points", "A season to remember", "The community has earned 10,000 points this season!", sum(scores.values()), 10000),
]
return [{"id": key, "name": name, "message": message, "progress": min(progress, target), "target": target}
for key, name, message, progress, target in goals]
@@ -287,14 +296,14 @@ def milestone_status(game_state, at):
def sync_milestones(connection, game_state, at=None):
"""Commit unlocks with game changes. Existing goals are silently baselined once."""
at = now_ms() if at is None else at
initialized = connection.execute("SELECT 1 FROM community_milestones LIMIT 1").fetchone() is not None
for goal in milestone_status(game_state, at):
existed = connection.execute("SELECT 1 FROM community_milestones WHERE id = ?", (goal["id"],)).fetchone() is not None
connection.execute("INSERT OR IGNORE INTO community_milestones (id) VALUES (?)", (goal["id"],))
if goal["progress"] >= goal["target"]:
connection.execute(
"""UPDATE community_milestones SET reached_at = ?, announced_at = ?, message = ?
WHERE id = ? AND reached_at IS NULL""",
(at, at if initialized else None, f"Milestone: {goal['name']}. {goal['message']}", goal["id"]),
(at, at if existed else None, f"Milestone: {goal['name']}. {goal['message']}", goal["id"]),
)
@@ -614,6 +623,41 @@ def empty_zone_counts():
return {z["name"]: 0 for z in ZONES}
def seasonal_achievements(pid):
"""Lifetime badges from immutable archived presses, keyed by player ID, not names."""
with database() as connection:
exists = connection.execute("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'season_results'").fetchone()
rows = connection.execute("SELECT finished_at, presses FROM season_results ORDER BY finished_at, starts_at").fetchall() if exists else []
finished = 0
finisher_at = veteran_at = podium_at = champion_at = None
for row in rows:
scores = {}
for press in json.loads(row["presses"]):
scores[press["player_id"]] = scores.get(press["player_id"], 0) + press["points"]
if pid not in scores:
continue
finished += 1
if finisher_at is None:
finisher_at = row["finished_at"]
if finished >= 2 and veteran_at is None:
veteran_at = row["finished_at"]
# Competition ranks: tied scores share rank; never use alphabetical order for awards.
rank = 1 + sum(points > scores[pid] for points in scores.values())
if rank <= 3 and podium_at is None:
podium_at = row["finished_at"]
if rank == 1 and champion_at is None:
champion_at = row["finished_at"]
rules = [
("season_finisher", "Part of the story", "Participate with a press in a completed season.", finisher_at, min(finished, 1), 1),
("season_veteran", "Back for another", "Participate in two completed seasons.", veteran_at, min(finished, 2), 2),
("season_podium", "Podium finish", "Finish a season in the top three by points. Ties share rank.", podium_at, int(podium_at is not None), 1),
("season_champion", "Season champion", "Finish a season with the highest score. Joint winners qualify.", champion_at, int(champion_at is not None), 1),
]
return [{"id": key, "name": name, "description": description, "unlocked": at is not None,
"unlocked_at": at, "progress": progress, "target": target, "scope": "lifetime"}
for key, name, description, at, progress, target in rules]
def player_achievements(pid):
"""Derive private achievements from recorded history, including legacy games."""
quiet_target = max(1, state["duration_ms"] // 2)
@@ -667,7 +711,7 @@ def player_achievements(pid):
"unlocked_at": extra_unlocks[achievement_id],
"progress": int(extra_unlocks[achievement_id] is not None), "target": 1}
for achievement_id, name, description, _, _ in extra_rules
]
] + seasonal_achievements(pid)
def player_summary(pid):