Community milestones
This commit is contained in:
+124
-10
@@ -225,6 +225,120 @@ class PersistenceTests(unittest.TestCase):
|
||||
self.assertEqual(summary["personal_bests"]["highest_scoring_press"]["points"], 16)
|
||||
self.assertEqual(set(summary["ranking"]["rival"]), {"name", "total_points", "points_to_tie", "points_to_pass"})
|
||||
|
||||
def test_eight_milestones_match_achievement_count_and_start_unreached(self):
|
||||
self.game.state = self.game.load_state()
|
||||
goals = self.game.milestone_status(self.game.state, self.game.now_ms())
|
||||
self.assertEqual(len(goals), 8)
|
||||
self.assertEqual(len(goals), len(self.game.player_achievements("nobody")))
|
||||
self.assertTrue(all(goal["progress"] < goal["target"] for goal in goals))
|
||||
self.assertEqual(self.game.chat_messages(), [])
|
||||
|
||||
def test_milestone_thresholds_for_counts_and_precision(self):
|
||||
game = self.game.new_game()
|
||||
press = {"player_id": "p", "at": game["started_at"], "remaining_ms": 10000, "zone": "green", "points": 0}
|
||||
for count in (9, 10, 49, 50):
|
||||
game["players"] = {str(i): {} for i in range(count)}
|
||||
goals = {g["id"]: g for g in self.game.milestone_status(game, game["started_at"])}
|
||||
self.assertEqual(goals["ten_players"]["progress"] == goals["ten_players"]["target"], count >= 10)
|
||||
self.assertEqual(goals["fifty_players"]["progress"] == goals["fifty_players"]["target"], count >= 50)
|
||||
for count in (99, 100, 999, 1000):
|
||||
game["presses"] = [press] * count
|
||||
goals = {g["id"]: g for g in self.game.milestone_status(game, game["started_at"])}
|
||||
self.assertEqual(goals["hundred_presses"]["progress"] == 100, count >= 100)
|
||||
self.assertEqual(goals["thousand_presses"]["progress"] == 1000, count >= 1000)
|
||||
for remaining, qualifies in ((10000, False), (9999, True), (0, False), (-1, False)):
|
||||
game["presses"] = [dict(press, remaining_ms=remaining)]
|
||||
self.assertEqual(self.game.milestone_status(game, game["started_at"])[-1]["progress"], int(qualifies))
|
||||
|
||||
def test_survival_milestones_boundaries_and_game_end_cap(self):
|
||||
game = self.game.new_game()
|
||||
day = 86400000
|
||||
start = game["started_at"]
|
||||
game["deadline"] = start + 40 * day
|
||||
for days in (1, 7, 30):
|
||||
before = self.game.milestone_status(game, start + days * day - 1)
|
||||
after = self.game.milestone_status(game, start + days * day)
|
||||
index = (1, 7, 30).index(days)
|
||||
self.assertLess(before[index]["progress"], before[index]["target"])
|
||||
self.assertEqual(after[index]["progress"], after[index]["target"])
|
||||
game.update(ended=True, ended_at=start + day // 2)
|
||||
self.assertTrue(all(g["progress"] < g["target"] for g in self.game.milestone_status(game, start + 100 * day)[:3]))
|
||||
game.update(ended=False, ended_at=None, deadline=start + day // 2)
|
||||
self.assertTrue(all(g["progress"] < g["target"] for g in self.game.milestone_status(game, start + 100 * day)[:3]))
|
||||
|
||||
def test_historical_milestones_are_silent_on_upgrade(self):
|
||||
game = self.sample_state()
|
||||
game["players"].update({str(i): dict(game["players"]["bob"], token_hashes=[]) for i in range(50)})
|
||||
game["presses"] *= 334
|
||||
game["started_at"] = self.game.now_ms() - 31 * 86400000
|
||||
self.write_legacy(game)
|
||||
self.game.state = self.game.load_state()
|
||||
self.assertEqual(self.game.chat_messages(), [])
|
||||
with self.game.database() as connection:
|
||||
self.assertEqual(connection.execute("SELECT COUNT(*) FROM community_milestones WHERE reached_at IS NOT NULL").fetchone()[0], 8)
|
||||
self.game.load_state()
|
||||
self.assertEqual(self.game.chat_messages(), [])
|
||||
# Existing SQLite database without the new table follows the same baseline.
|
||||
with self.game.database() as connection:
|
||||
connection.execute("DROP TABLE community_milestones")
|
||||
self.game.load_state()
|
||||
self.assertEqual(self.game.chat_messages(), [])
|
||||
|
||||
def test_new_milestone_announces_once_survives_restart_and_has_no_player_fk(self):
|
||||
self.game.state = self.game.load_state()
|
||||
start = self.game.state["started_at"]
|
||||
self.game.state["deadline"] = start + 2 * 86400000
|
||||
with patch.object(self.game, "now_ms", return_value=start + 86400000):
|
||||
self.game.save_state(self.game.state)
|
||||
self.game.check_ended()
|
||||
self.game.state = self.game.load_state()
|
||||
messages = self.game.chat_messages()
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(messages[0]["id"], "milestone:day_one")
|
||||
self.assertEqual(messages[0]["kind"], "milestone")
|
||||
self.assertEqual(messages[0]["name"], "The Button")
|
||||
self.assertEqual(self.game.state["players"], {})
|
||||
with self.game.database() as connection:
|
||||
self.assertEqual(connection.execute("PRAGMA foreign_key_check").fetchall(), [])
|
||||
|
||||
def test_milestone_failure_rolls_back_game_and_unlock(self):
|
||||
self.game.state = self.game.load_state()
|
||||
before = copy.deepcopy(self.game.state)
|
||||
changed = copy.deepcopy(before)
|
||||
changed["started_at"] -= 86400000
|
||||
with self.game.database() as connection:
|
||||
connection.execute("""CREATE TRIGGER reject_milestone BEFORE UPDATE ON community_milestones
|
||||
BEGIN SELECT RAISE(ABORT, 'test rollback'); END""")
|
||||
with self.assertRaises(sqlite3.IntegrityError):
|
||||
self.game.save_state(changed)
|
||||
with self.game.database() as connection:
|
||||
connection.execute("DROP TRIGGER reject_milestone")
|
||||
self.assertEqual(self.game.load_state(), before)
|
||||
self.assertEqual(self.game.chat_messages(), [])
|
||||
|
||||
def test_milestones_share_chat_limit_and_do_not_affect_player_cooldown(self):
|
||||
self.write_legacy(self.sample_state())
|
||||
self.game.state = self.game.load_state()
|
||||
at = self.game.now_ms()
|
||||
with self.game.database() as connection:
|
||||
connection.executemany(
|
||||
"INSERT INTO chat_messages (player_id, name, at, message) VALUES (?, ?, ?, ?)",
|
||||
[("alice", "Alice", at - 1000 + i, str(i)) for i in range(105)],
|
||||
)
|
||||
connection.execute(
|
||||
"UPDATE community_milestones SET reached_at = ?, announced_at = ?, message = ? WHERE id = 'ten_players'",
|
||||
(at, at, "Milestone: Ten players!"),
|
||||
)
|
||||
messages = self.game.chat_messages()
|
||||
self.assertEqual(len(messages), 100)
|
||||
self.assertEqual(messages[-1]["id"], "milestone:ten_players")
|
||||
self.assertEqual(messages[0]["message"], "6")
|
||||
with self.game.database() as connection:
|
||||
latest_player = connection.execute(
|
||||
"SELECT at FROM chat_messages WHERE player_id = ? AND kind = 'message' ORDER BY id DESC LIMIT 1", ("alice",),
|
||||
).fetchone()
|
||||
self.assertEqual(latest_player["at"], at - 896)
|
||||
|
||||
def achievement_game(self):
|
||||
self.game.state = self.sample_state()
|
||||
self.game.state.update(duration_ms=120000, started_at=1000, presses=[])
|
||||
@@ -458,6 +572,7 @@ class PersistenceTests(unittest.TestCase):
|
||||
])
|
||||
|
||||
def test_leaderboard_http_is_public_before_and_after_game_ends(self):
|
||||
self.game.load_state()
|
||||
self.game.state = self.sample_state()
|
||||
httpd = ThreadingHTTPServer(("127.0.0.1", 0), self.game.Handler)
|
||||
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
||||
@@ -489,9 +604,8 @@ class PersistenceTests(unittest.TestCase):
|
||||
self.assertEqual(get("/api/leaderboard")[1], {"leaderboard": []})
|
||||
|
||||
def test_chat_auth_validation_cooldown_and_persistence(self):
|
||||
self.game.load_state()
|
||||
self.game.state = self.sample_state()
|
||||
self.game.save_state(self.game.state)
|
||||
self.write_legacy(self.sample_state())
|
||||
self.game.state = self.game.load_state()
|
||||
self.game.token_index = {self.game.hash_token("test-chat-token"): "alice"}
|
||||
httpd = ThreadingHTTPServer(("127.0.0.1", 0), self.game.Handler)
|
||||
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
||||
@@ -537,9 +651,8 @@ class PersistenceTests(unittest.TestCase):
|
||||
self.assertEqual(connection.execute("PRAGMA foreign_key_check").fetchall(), [])
|
||||
|
||||
def test_existing_chat_schema_upgrades_without_losing_messages(self):
|
||||
self.game.load_state()
|
||||
self.game.state = self.sample_state()
|
||||
self.game.save_state(self.game.state)
|
||||
self.write_legacy(self.sample_state())
|
||||
self.game.state = self.game.load_state()
|
||||
with self.game.database() as connection:
|
||||
connection.execute("DROP TABLE chat_messages")
|
||||
connection.execute("""CREATE TABLE chat_messages (
|
||||
@@ -557,9 +670,8 @@ class PersistenceTests(unittest.TestCase):
|
||||
])
|
||||
|
||||
def test_chat_returns_latest_hundred_in_chronological_order(self):
|
||||
self.game.load_state()
|
||||
self.game.state = self.sample_state()
|
||||
self.game.save_state(self.game.state)
|
||||
self.write_legacy(self.sample_state())
|
||||
self.game.state = self.game.load_state()
|
||||
with self.game.database() as connection:
|
||||
connection.executemany(
|
||||
"INSERT INTO chat_messages (player_id, name, at, message) VALUES (?, ?, ?, ?)",
|
||||
@@ -603,7 +715,9 @@ class PersistenceTests(unittest.TestCase):
|
||||
with patch.object(self.game, "now_ms", return_value=t):
|
||||
self.assertEqual(request("/api/press", {}, token)[0], 200)
|
||||
announcements = self.game.chat_messages()
|
||||
self.assertEqual(len(announcements), 1)
|
||||
self.assertEqual(len(announcements), 2)
|
||||
self.assertEqual(announcements[1]["kind"], "milestone")
|
||||
self.assertIn("Our first photo finish", announcements[1]["message"])
|
||||
self.assertEqual(announcements[0]["kind"], "press")
|
||||
self.assertEqual(announcements[0]["name"], "The Button")
|
||||
self.assertEqual(announcements[0]["message"], "Alice pushed the button with 00:00:01.234 remaining and received 32 points.")
|
||||
|
||||
Reference in New Issue
Block a user