Community milestones
This commit is contained in:
@@ -156,6 +156,40 @@ def write_state(connection, state):
|
||||
)
|
||||
|
||||
|
||||
def milestone_status(game_state, at):
|
||||
"""Eight 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"])
|
||||
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),
|
||||
("one_month", "Thirty days strong", "The game survived 30 days. What a community!", elapsed, 30 * 86400000),
|
||||
("ten_players", "A growing crowd", "Ten players have joined the experiment!", players, 10),
|
||||
("fifty_players", "Fifty strong", "Fifty players, one shared clock. Welcome aboard!", players, 50),
|
||||
("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),
|
||||
]
|
||||
return [{"id": key, "name": name, "message": message, "progress": min(progress, target), "target": target}
|
||||
for key, name, message, progress, target in goals]
|
||||
|
||||
|
||||
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):
|
||||
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"]),
|
||||
)
|
||||
|
||||
|
||||
def load_state():
|
||||
if DATABASE_FILE.resolve() == DATA_FILE.resolve():
|
||||
raise ValueError("BUTTON_DATABASE_FILE must differ from the legacy BUTTON_DATA_FILE")
|
||||
@@ -199,6 +233,12 @@ def load_state():
|
||||
message TEXT NOT NULL CHECK (length(message) BETWEEN 1 AND 200)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS chat_player_messages ON chat_messages (player_id, id);
|
||||
CREATE TABLE IF NOT EXISTS community_milestones (
|
||||
id TEXT PRIMARY KEY,
|
||||
reached_at INTEGER,
|
||||
announced_at INTEGER,
|
||||
message TEXT
|
||||
);
|
||||
""")
|
||||
columns = {row["name"] for row in connection.execute("PRAGMA table_info(chat_messages)")}
|
||||
if "kind" not in columns:
|
||||
@@ -219,6 +259,7 @@ def load_state():
|
||||
# Any read/validation/SQL failure aborts startup without consuming the
|
||||
# JSON source or committing a partial game. The next start can retry.
|
||||
write_state(connection, loaded)
|
||||
sync_milestones(connection, loaded)
|
||||
return loaded
|
||||
|
||||
loaded = dict(game)
|
||||
@@ -238,12 +279,14 @@ def load_state():
|
||||
press = dict(row)
|
||||
del press["position"]
|
||||
loaded["presses"].append(press)
|
||||
sync_milestones(connection, loaded)
|
||||
return loaded
|
||||
|
||||
|
||||
def save_state(state):
|
||||
with database() as connection:
|
||||
write_state(connection, state)
|
||||
sync_milestones(connection, state)
|
||||
|
||||
|
||||
state = load_state()
|
||||
@@ -274,7 +317,12 @@ def public_presence():
|
||||
def chat_messages():
|
||||
with database() as connection:
|
||||
rows = connection.execute(
|
||||
"SELECT id, name, at, message, kind FROM chat_messages ORDER BY id DESC LIMIT ?",
|
||||
"""SELECT id, name, at, message, kind FROM (
|
||||
SELECT id, name, at, message, kind, 0 AS source, id AS position FROM chat_messages
|
||||
UNION ALL
|
||||
SELECT 'milestone:' || id, 'The Button', announced_at, message, 'milestone', 1, rowid
|
||||
FROM community_milestones WHERE announced_at IS NOT NULL
|
||||
) ORDER BY at DESC, source DESC, position DESC LIMIT ?""",
|
||||
(CHAT_HISTORY_LIMIT,),
|
||||
).fetchall()
|
||||
return [dict(row) for row in reversed(rows)]
|
||||
@@ -319,6 +367,9 @@ def check_ended():
|
||||
state["ended"] = True
|
||||
state["ended_at"] = state["deadline"]
|
||||
save_state(state)
|
||||
else:
|
||||
with database() as connection:
|
||||
sync_milestones(connection, state)
|
||||
|
||||
|
||||
def empty_zone_counts():
|
||||
@@ -737,6 +788,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
"INSERT INTO chat_messages (player_id, name, at, message, kind) VALUES (?, ?, ?, ?, 'press')",
|
||||
(pid, "The Button", t, announcement),
|
||||
)
|
||||
sync_milestones(connection, state, t)
|
||||
except Exception:
|
||||
# Keep memory consistent with the rolled-back database snapshot.
|
||||
state["presses"].pop()
|
||||
|
||||
Reference in New Issue
Block a user