feat: add leaderboard and expand theme options
This commit is contained in:
1 parent
ae478755be
commit
e685fc0d65
9 files changed
+372
-8
No files matched your search
@@ -8,3 +8,6 @@ __pycache__/
|
||||
.DS_Store
|
||||
.claude/
|
||||
gitea.config
|
||||
CLAUDE.md
|
||||
memory/
|
||||
test/
|
||||
@@ -64,7 +64,7 @@ ERROR_TEXTS = {
|
||||
"spectator_readonly": "观战模式下不能执行该操作。",
|
||||
"ai_invalid_json": "AI 返回了无效 JSON,本轮未结算,请稍后重试。",
|
||||
}
|
||||
ALLOWED_THEMES = {"minimal", "dopamine", "midnight", "mint", "sunset"}
|
||||
ALLOWED_THEMES = {"minimal", "dopamine", "midnight", "mint", "sunset", "aurora", "graphite"}
|
||||
|
||||
|
||||
def readable_error(error_code: str) -> str:
|
||||
@@ -1610,6 +1610,16 @@ def create_app(config_object: type[Config] = Config) -> tuple[Flask, SocketIO]:
|
||||
row["card_title"] = card["title"] if card else "未知剧本"
|
||||
return jsonify({"ok": True, "rooms": rows})
|
||||
|
||||
@app.get("/api/leaderboard")
|
||||
def leaderboard_api() -> Any:
|
||||
limit_raw = request.args.get("limit", "20")
|
||||
try:
|
||||
limit = max(1, min(100, int(str(limit_raw))))
|
||||
except ValueError:
|
||||
limit = 20
|
||||
rows = persistence.list_player_leaderboard(limit=limit)
|
||||
return jsonify({"ok": True, "leaderboard": rows})
|
||||
|
||||
@app.get("/api/rooms/public")
|
||||
def public_rooms_api() -> Any:
|
||||
limit_raw = request.args.get("limit", "30")
|
||||
|
||||
@@ -876,6 +876,126 @@ class SQLitePersistence:
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
def _is_winning_ending(self, ending_state: Any) -> bool:
|
||||
ending = str(ending_state or "").strip().lower()
|
||||
if not ending:
|
||||
return False
|
||||
return ending in {
|
||||
"escaped",
|
||||
"sacrifice",
|
||||
"pact",
|
||||
"victory",
|
||||
"success",
|
||||
"win",
|
||||
"custom_end",
|
||||
}
|
||||
|
||||
def list_player_leaderboard(self, limit: int = 20) -> list[dict[str, Any]]:
|
||||
try:
|
||||
safe_limit = max(1, min(100, int(limit)))
|
||||
except (TypeError, ValueError):
|
||||
safe_limit = 20
|
||||
|
||||
with self._lock, self._connect() as conn:
|
||||
rows = conn.execute(
|
||||
"""
|
||||
SELECT
|
||||
u.user_id,
|
||||
u.username,
|
||||
u.display_name,
|
||||
u.avatar_color,
|
||||
u.avatar_url,
|
||||
u.theme,
|
||||
p.room_id,
|
||||
r.game_state,
|
||||
r.updated_at
|
||||
FROM users u
|
||||
JOIN players p ON p.user_id = u.user_id
|
||||
JOIN rooms r ON r.room_id = p.room_id
|
||||
WHERE r.status = 'ended'
|
||||
ORDER BY r.updated_at DESC
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
summary: dict[str, dict[str, Any]] = {}
|
||||
for row in rows:
|
||||
item = dict(row)
|
||||
user_id = str(item.get("user_id", "")).strip()
|
||||
room_id = str(item.get("room_id", "")).strip()
|
||||
if not user_id or not room_id:
|
||||
continue
|
||||
|
||||
entry = summary.get(user_id)
|
||||
if entry is None:
|
||||
entry = {
|
||||
"user_id": user_id,
|
||||
"username": str(item.get("username", "")).strip(),
|
||||
"display_name": str(item.get("display_name", "")).strip(),
|
||||
"avatar_color": str(item.get("avatar_color", "#0070f2")).strip() or "#0070f2",
|
||||
"avatar_url": str(item.get("avatar_url", "")).strip(),
|
||||
"theme": str(item.get("theme", "dopamine")).strip() or "dopamine",
|
||||
"played_games": 0,
|
||||
"wins": 0,
|
||||
"total_rounds": 0,
|
||||
"last_played_at": "",
|
||||
"_seen_rooms": set(),
|
||||
}
|
||||
summary[user_id] = entry
|
||||
|
||||
seen_rooms = entry["_seen_rooms"]
|
||||
if room_id in seen_rooms:
|
||||
continue
|
||||
seen_rooms.add(room_id)
|
||||
|
||||
game_state_raw = item.get("game_state")
|
||||
try:
|
||||
game_state = json.loads(game_state_raw) if isinstance(game_state_raw, str) else {}
|
||||
except json.JSONDecodeError:
|
||||
game_state = {}
|
||||
if not isinstance(game_state, dict):
|
||||
game_state = {}
|
||||
|
||||
entry["played_games"] += 1
|
||||
if self._is_winning_ending(game_state.get("ending_state")):
|
||||
entry["wins"] += 1
|
||||
|
||||
round_no_raw = game_state.get("round_no", 0)
|
||||
try:
|
||||
round_no = max(0, int(round_no_raw))
|
||||
except (TypeError, ValueError):
|
||||
round_no = 0
|
||||
entry["total_rounds"] += round_no
|
||||
|
||||
updated_at = str(item.get("updated_at", "")).strip()
|
||||
if updated_at and updated_at > entry["last_played_at"]:
|
||||
entry["last_played_at"] = updated_at
|
||||
|
||||
results: list[dict[str, Any]] = []
|
||||
for entry in summary.values():
|
||||
entry.pop("_seen_rooms", None)
|
||||
played = int(entry["played_games"])
|
||||
wins = int(entry["wins"])
|
||||
win_rate = round((wins / played) * 100, 1) if played else 0.0
|
||||
entry["win_rate"] = win_rate
|
||||
entry["score"] = wins * 100 + played * 12 + int(entry["total_rounds"])
|
||||
results.append(entry)
|
||||
|
||||
results.sort(
|
||||
key=lambda x: (
|
||||
int(x.get("wins", 0)),
|
||||
int(x.get("played_games", 0)),
|
||||
int(x.get("score", 0)),
|
||||
str(x.get("last_played_at", "")),
|
||||
str(x.get("username", "")),
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
ranked = results[:safe_limit]
|
||||
for idx, row in enumerate(ranked, start=1):
|
||||
row["rank"] = idx
|
||||
return ranked
|
||||
|
||||
def list_users(self, limit: int = 500) -> list[dict[str, Any]]:
|
||||
with self._lock, self._connect() as conn:
|
||||
rows = conn.execute(
|
||||
|
||||
+146
-1
@@ -122,6 +122,47 @@ body[data-theme="sunset"] {
|
||||
--ghost-bg: rgba(255, 248, 241, 0.93);
|
||||
}
|
||||
|
||||
body[data-theme="aurora"] {
|
||||
--bg: #eef7ff;
|
||||
--surface: #f8fcff;
|
||||
--surface-soft: #edf8ff;
|
||||
--text: #17263f;
|
||||
--muted: #4e6688;
|
||||
--border: #b6d7f7;
|
||||
--n-red: #ff5f7a;
|
||||
--n-blue: #2a8cff;
|
||||
--n-yellow: #ffcb5e;
|
||||
--n-green: #00c99a;
|
||||
--btn-primary-end: #3ea7ff;
|
||||
--btn-secondary-start: #42d5d0;
|
||||
--btn-secondary-end: #2a8cff;
|
||||
--link-btn-start: #2bb6ff;
|
||||
--link-btn-end: #2a8cff;
|
||||
--ghost-fg: #235a9d;
|
||||
--ghost-bg: rgba(245, 251, 255, 0.94);
|
||||
}
|
||||
|
||||
body[data-theme="graphite"] {
|
||||
--bg: #1b1f28;
|
||||
--surface: #242a34;
|
||||
--surface-soft: #2a313d;
|
||||
--text: #edf3ff;
|
||||
--muted: #a8b3c6;
|
||||
--border: #3c4759;
|
||||
--n-red: #ff6d6d;
|
||||
--n-blue: #74a6ff;
|
||||
--n-yellow: #f8c66d;
|
||||
--n-green: #4bd8a0;
|
||||
--btn-primary-end: #4c7bff;
|
||||
--btn-secondary-start: #56b3ff;
|
||||
--btn-secondary-end: #3f66db;
|
||||
--link-btn-start: #69a8ff;
|
||||
--link-btn-end: #3f66db;
|
||||
--ghost-fg: #d6e3ff;
|
||||
--ghost-bg: rgba(36, 42, 52, 0.92);
|
||||
--shadow: 0 16px 42px rgba(7, 11, 18, 0.5);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -170,6 +211,22 @@ body[data-theme="sunset"] {
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
body[data-theme="aurora"] {
|
||||
background:
|
||||
radial-gradient(circle at 10% -8%, rgba(0, 201, 154, 0.2) 0, transparent 34%),
|
||||
radial-gradient(circle at 92% 2%, rgba(42, 140, 255, 0.18) 0, transparent 30%),
|
||||
radial-gradient(circle at 50% 120%, rgba(255, 95, 122, 0.16) 0, transparent 34%),
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
body[data-theme="graphite"] {
|
||||
background:
|
||||
radial-gradient(circle at 12% -8%, rgba(116, 166, 255, 0.24) 0, transparent 32%),
|
||||
radial-gradient(circle at 88% 0%, rgba(255, 109, 109, 0.22) 0, transparent 30%),
|
||||
radial-gradient(circle at 50% 120%, rgba(75, 216, 160, 0.2) 0, transparent 36%),
|
||||
linear-gradient(180deg, #181c24 0%, #1f2530 52%, #171b23 100%);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
@@ -587,6 +644,11 @@ body[data-theme="midnight"] .card,
|
||||
body[data-theme="midnight"] .panel,
|
||||
body[data-theme="midnight"] .meta-block,
|
||||
body[data-theme="midnight"] .invite-panel,
|
||||
body[data-theme="graphite"] .card,
|
||||
body[data-theme="graphite"] .panel,
|
||||
body[data-theme="graphite"] .meta-block,
|
||||
body[data-theme="graphite"] .invite-panel,
|
||||
body[data-theme="graphite"] .auth-card,
|
||||
body[data-theme="midnight"] .auth-card {
|
||||
background: linear-gradient(155deg, rgba(21, 30, 58, 0.92), rgba(15, 22, 45, 0.9));
|
||||
border-color: rgba(82, 116, 208, 0.52);
|
||||
@@ -594,7 +656,10 @@ body[data-theme="midnight"] .auth-card {
|
||||
|
||||
body[data-theme="midnight"] input,
|
||||
body[data-theme="midnight"] textarea,
|
||||
body[data-theme="midnight"] select {
|
||||
body[data-theme="midnight"] select,
|
||||
body[data-theme="graphite"] input,
|
||||
body[data-theme="graphite"] textarea,
|
||||
body[data-theme="graphite"] select {
|
||||
background: rgba(17, 25, 49, 0.88);
|
||||
color: #ebf0ff;
|
||||
border-color: rgba(86, 119, 207, 0.65);
|
||||
@@ -602,6 +667,9 @@ body[data-theme="midnight"] select {
|
||||
|
||||
body[data-theme="midnight"] ul li,
|
||||
body[data-theme="midnight"] .compact-list li,
|
||||
body[data-theme="graphite"] ul li,
|
||||
body[data-theme="graphite"] .compact-list li,
|
||||
body[data-theme="graphite"] .msg,
|
||||
body[data-theme="midnight"] .msg {
|
||||
background: rgba(22, 33, 66, 0.72);
|
||||
border-color: rgba(94, 127, 218, 0.5);
|
||||
@@ -611,6 +679,10 @@ body[data-theme="midnight"] .msg {
|
||||
body[data-theme="midnight"] .helper-text,
|
||||
body[data-theme="midnight"] .brand-block p,
|
||||
body[data-theme="midnight"] .room-title p,
|
||||
body[data-theme="graphite"] .helper-text,
|
||||
body[data-theme="graphite"] .brand-block p,
|
||||
body[data-theme="graphite"] .room-title p,
|
||||
body[data-theme="graphite"] .header-note,
|
||||
body[data-theme="midnight"] .header-note {
|
||||
color: #b9c8f2;
|
||||
}
|
||||
@@ -1830,6 +1902,66 @@ body[data-theme="sunset"] .link-btn {
|
||||
background: linear-gradient(135deg, #4d7dff 0%, #6d90ff 100%) !important;
|
||||
}
|
||||
|
||||
/* Aurora: cool gradient, bright glass panels */
|
||||
body[data-theme="aurora"] {
|
||||
background:
|
||||
radial-gradient(circle at 14% -10%, rgba(0, 201, 154, 0.26), transparent 34%),
|
||||
radial-gradient(circle at 88% 2%, rgba(42, 140, 255, 0.25), transparent 30%),
|
||||
linear-gradient(180deg, #eef7ff 0%, #e5f2ff 58%, #edf7ff 100%) !important;
|
||||
}
|
||||
|
||||
body[data-theme="aurora"] .card,
|
||||
body[data-theme="aurora"] .panel,
|
||||
body[data-theme="aurora"] .meta-block,
|
||||
body[data-theme="aurora"] .auth-card,
|
||||
body[data-theme="aurora"] .invite-panel {
|
||||
background: rgba(250, 253, 255, 0.94) !important;
|
||||
border: 1px solid #9dc8ec !important;
|
||||
box-shadow: 0 12px 30px rgba(28, 110, 173, 0.14) !important;
|
||||
}
|
||||
|
||||
body[data-theme="aurora"] button {
|
||||
background: linear-gradient(135deg, #00c99a 0%, #2a8cff 100%) !important;
|
||||
}
|
||||
|
||||
body[data-theme="aurora"] .secondary-btn,
|
||||
body[data-theme="aurora"] .link-btn {
|
||||
background: linear-gradient(135deg, #2bb6ff 0%, #2a8cff 100%) !important;
|
||||
}
|
||||
|
||||
/* Graphite: muted industrial dark */
|
||||
body[data-theme="graphite"] {
|
||||
background:
|
||||
radial-gradient(circle at 12% -8%, rgba(116, 166, 255, 0.3), transparent 34%),
|
||||
radial-gradient(circle at 90% 0%, rgba(255, 109, 109, 0.24), transparent 30%),
|
||||
linear-gradient(180deg, #141820 0%, #1d232d 58%, #12161d 100%) !important;
|
||||
}
|
||||
|
||||
body[data-theme="graphite"] .card,
|
||||
body[data-theme="graphite"] .panel,
|
||||
body[data-theme="graphite"] .meta-block,
|
||||
body[data-theme="graphite"] .auth-card,
|
||||
body[data-theme="graphite"] .invite-panel {
|
||||
border: 1px solid rgba(129, 149, 181, 0.52) !important;
|
||||
box-shadow: 0 14px 38px rgba(0, 0, 0, 0.35) !important;
|
||||
}
|
||||
|
||||
body[data-theme="graphite"] button,
|
||||
body[data-theme="graphite"] .link-btn {
|
||||
background: linear-gradient(135deg, #5a87ff 0%, #3f66db 100%) !important;
|
||||
border: 1px solid rgba(163, 187, 236, 0.5) !important;
|
||||
}
|
||||
|
||||
body[data-theme="graphite"] .secondary-btn {
|
||||
background: linear-gradient(135deg, #56b3ff 0%, #3f66db 100%) !important;
|
||||
}
|
||||
|
||||
body[data-theme="graphite"] .ghost-btn {
|
||||
background: rgba(42, 49, 61, 0.9) !important;
|
||||
color: #d6e3ff !important;
|
||||
border-color: rgba(117, 139, 176, 0.6) !important;
|
||||
}
|
||||
|
||||
/* ===== Room Theme Contrast Guardrails ===== */
|
||||
/* Light-background themes in room view: force high-contrast titles/labels */
|
||||
body[data-theme="mint"].room-page .quick-panel h2,
|
||||
@@ -1837,6 +1969,11 @@ body[data-theme="mint"].room-page .chat-panel h3,
|
||||
body[data-theme="mint"].room-page .meta-panel h3,
|
||||
body[data-theme="mint"].room-page .meta-block h3,
|
||||
body[data-theme="mint"].room-page .story-major-block h3,
|
||||
body[data-theme="aurora"].room-page .quick-panel h2,
|
||||
body[data-theme="aurora"].room-page .chat-panel h3,
|
||||
body[data-theme="aurora"].room-page .meta-panel h3,
|
||||
body[data-theme="aurora"].room-page .meta-block h3,
|
||||
body[data-theme="aurora"].room-page .story-major-block h3,
|
||||
body[data-theme="sunset"].room-page .quick-panel h2,
|
||||
body[data-theme="sunset"].room-page .chat-panel h3,
|
||||
body[data-theme="sunset"].room-page .meta-panel h3,
|
||||
@@ -1848,12 +1985,15 @@ body[data-theme="sunset"].room-page .story-major-block h3 {
|
||||
|
||||
body[data-theme="mint"].room-page .quick-panel label,
|
||||
body[data-theme="mint"].room-page .chat-panel label,
|
||||
body[data-theme="aurora"].room-page .quick-panel label,
|
||||
body[data-theme="aurora"].room-page .chat-panel label,
|
||||
body[data-theme="sunset"].room-page .quick-panel label,
|
||||
body[data-theme="sunset"].room-page .chat-panel label {
|
||||
color: #334155 !important;
|
||||
}
|
||||
|
||||
body[data-theme="mint"].room-page .quick-hint,
|
||||
body[data-theme="aurora"].room-page .quick-hint,
|
||||
body[data-theme="sunset"].room-page .quick-hint {
|
||||
color: #475569 !important;
|
||||
}
|
||||
@@ -1861,6 +2001,9 @@ body[data-theme="sunset"].room-page .quick-hint {
|
||||
body[data-theme="mint"].room-page #choices-title,
|
||||
body[data-theme="mint"].room-page #clues-title,
|
||||
body[data-theme="mint"].room-page #inventory-title,
|
||||
body[data-theme="aurora"].room-page #choices-title,
|
||||
body[data-theme="aurora"].room-page #clues-title,
|
||||
body[data-theme="aurora"].room-page #inventory-title,
|
||||
body[data-theme="sunset"].room-page #choices-title,
|
||||
body[data-theme="sunset"].room-page #clues-title,
|
||||
body[data-theme="sunset"].room-page #inventory-title {
|
||||
@@ -1868,11 +2011,13 @@ body[data-theme="sunset"].room-page #inventory-title {
|
||||
}
|
||||
|
||||
body[data-theme="mint"].room-page .status-grid,
|
||||
body[data-theme="aurora"].room-page .status-grid,
|
||||
body[data-theme="sunset"].room-page .status-grid {
|
||||
color: #1f2937 !important;
|
||||
}
|
||||
|
||||
body[data-theme="mint"].room-page .meta-panel li,
|
||||
body[data-theme="aurora"].room-page .meta-panel li,
|
||||
body[data-theme="sunset"].room-page .meta-panel li {
|
||||
color: #1f2937 !important;
|
||||
}
|
||||
|
||||
+26
-2
@@ -16,6 +16,7 @@
|
||||
const createCardGallery = document.getElementById('create-card-gallery');
|
||||
const selectedCardDetail = document.getElementById('selected-card-detail');
|
||||
const rejoinList = document.getElementById('rejoin-list');
|
||||
const leaderboardList = document.getElementById('leaderboard-list');
|
||||
const statusEl = document.getElementById('status');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const topUserBadge = document.getElementById('top-user-badge');
|
||||
@@ -180,7 +181,7 @@
|
||||
|
||||
const normalizeTheme = (theme) => {
|
||||
const value = String(theme || '').trim().toLowerCase();
|
||||
if (['minimal', 'dopamine', 'midnight', 'mint', 'sunset'].includes(value)) return value;
|
||||
if (['minimal', 'dopamine', 'midnight', 'mint', 'sunset', 'aurora', 'graphite'].includes(value)) return value;
|
||||
return 'dopamine';
|
||||
};
|
||||
|
||||
@@ -257,11 +258,31 @@
|
||||
});
|
||||
};
|
||||
|
||||
const renderLeaderboard = (rows) => {
|
||||
if (!leaderboardList) return;
|
||||
leaderboardList.textContent = '';
|
||||
if (!rows.length) {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = '暂无已完成对局数据';
|
||||
leaderboardList.appendChild(li);
|
||||
return;
|
||||
}
|
||||
rows.forEach((row) => {
|
||||
const playedGames = Number(row.played_games || 0);
|
||||
const wins = Number(row.wins || 0);
|
||||
const winRate = Number(row.win_rate || 0).toFixed(1);
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `#${row.rank || '-'} ${row.username || '匿名玩家'} · 胜场 ${wins}/${playedGames} · 胜率 ${winRate}% · 回合 ${row.total_rounds || 0}`;
|
||||
leaderboardList.appendChild(li);
|
||||
});
|
||||
};
|
||||
|
||||
const loadBootstrap = async () => {
|
||||
try {
|
||||
const [cardsResp, roomsResp] = await Promise.all([
|
||||
const [cardsResp, roomsResp, leaderboardResp] = await Promise.all([
|
||||
getJson('/api/cards'),
|
||||
getJson('/api/me/rooms'),
|
||||
getJson('/api/leaderboard?limit=10'),
|
||||
]);
|
||||
if (cardsResp.ok) {
|
||||
renderCards(cardsResp.cards || []);
|
||||
@@ -269,6 +290,9 @@
|
||||
if (roomsResp.ok) {
|
||||
renderRejoinRooms(roomsResp.rooms || []);
|
||||
}
|
||||
if (leaderboardResp.ok) {
|
||||
renderLeaderboard(leaderboardResp.leaderboard || []);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(`加载大厅信息失败: ${error}`);
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
if (!body) return;
|
||||
|
||||
const THEME_KEY = 'echo-theme';
|
||||
const THEMES = new Set(['minimal', 'dopamine', 'midnight', 'mint', 'sunset']);
|
||||
const THEMES = new Set(['minimal', 'dopamine', 'midnight', 'mint', 'sunset', 'aurora', 'graphite']);
|
||||
|
||||
const normalizeTheme = (value) => {
|
||||
const theme = String(value || '').trim().toLowerCase();
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
<section class="card">
|
||||
<h2>快速重连</h2>
|
||||
<ul id="rejoin-list" class="compact-list"></ul>
|
||||
<h2 style="margin-top: 1rem;">玩家排行榜</h2>
|
||||
<p class="helper-text">按胜场、参与局数和回合数综合排序。</p>
|
||||
<ul id="leaderboard-list" class="compact-list"></ul>
|
||||
</section>
|
||||
|
||||
<section class="card" id="profile-card">
|
||||
@@ -83,6 +86,8 @@
|
||||
<option value="midnight">午夜霓虹</option>
|
||||
<option value="mint">薄荷雾光</option>
|
||||
<option value="sunset">落日胶片</option>
|
||||
<option value="aurora">极光谱系</option>
|
||||
<option value="graphite">石墨工业</option>
|
||||
</select>
|
||||
|
||||
<button type="submit">保存个性化</button>
|
||||
|
||||
@@ -91,13 +91,17 @@ def test_profile_invalid_theme_keeps_previous(client):
|
||||
'/api/auth/register',
|
||||
json={'username': 'theme_user_1', 'password': 'secret12', 'display_name': 'ThemeUser'},
|
||||
)
|
||||
first = client.patch('/api/me/profile', json={'theme': 'mint'}).get_json()
|
||||
first = client.patch('/api/me/profile', json={'theme': 'aurora'}).get_json()
|
||||
assert first['ok'] is True
|
||||
assert first['user']['theme'] == 'mint'
|
||||
assert first['user']['theme'] == 'aurora'
|
||||
|
||||
second_ok = client.patch('/api/me/profile', json={'theme': 'graphite'}).get_json()
|
||||
assert second_ok['ok'] is True
|
||||
assert second_ok['user']['theme'] == 'graphite'
|
||||
|
||||
second = client.patch('/api/me/profile', json={'theme': 'unknown_theme'}).get_json()
|
||||
assert second['ok'] is True
|
||||
assert second['user']['theme'] == 'mint'
|
||||
assert second['user']['theme'] == 'graphite'
|
||||
|
||||
|
||||
def test_user_can_rejoin_running_room_by_account(client, app_instance):
|
||||
|
||||
@@ -201,3 +201,56 @@ def test_non_host_cannot_set_room_public(client):
|
||||
payload = denied.get_json()
|
||||
assert payload['ok'] is False
|
||||
assert '只有房主可以设置房间公开状态' in payload['error']
|
||||
|
||||
|
||||
def test_leaderboard_api_returns_ranked_players(client, app_instance):
|
||||
app, _ = app_instance
|
||||
manager = app.extensions['room_manager']
|
||||
persistence = app.extensions['persistence']
|
||||
|
||||
_login(client, 'leader_alpha')
|
||||
alpha_1 = client.post('/api/rooms/create', json={}).get_json()
|
||||
manager.start_game(alpha_1['room_id'], alpha_1['player_id'])
|
||||
room = persistence.get_room(alpha_1['room_id'])
|
||||
state = dict(room['game_state'])
|
||||
state['game_phase'] = 'ended'
|
||||
state['ending_state'] = 'escaped'
|
||||
state['round_no'] = 6
|
||||
persistence.update_room_state(alpha_1['room_id'], 'ended', state)
|
||||
|
||||
alpha_2 = client.post('/api/rooms/create', json={}).get_json()
|
||||
manager.start_game(alpha_2['room_id'], alpha_2['player_id'])
|
||||
room = persistence.get_room(alpha_2['room_id'])
|
||||
state = dict(room['game_state'])
|
||||
state['game_phase'] = 'ended'
|
||||
state['ending_state'] = 'victory'
|
||||
state['round_no'] = 8
|
||||
persistence.update_room_state(alpha_2['room_id'], 'ended', state)
|
||||
|
||||
client.post('/api/auth/logout', json={})
|
||||
_login(client, 'leader_bravo')
|
||||
bravo_1 = client.post('/api/rooms/create', json={}).get_json()
|
||||
manager.start_game(bravo_1['room_id'], bravo_1['player_id'])
|
||||
room = persistence.get_room(bravo_1['room_id'])
|
||||
state = dict(room['game_state'])
|
||||
state['game_phase'] = 'ended'
|
||||
state['ending_state'] = 'trapped'
|
||||
state['round_no'] = 9
|
||||
persistence.update_room_state(bravo_1['room_id'], 'ended', state)
|
||||
|
||||
leaderboard_resp = client.get('/api/leaderboard?limit=10')
|
||||
assert leaderboard_resp.status_code == 200
|
||||
payload = leaderboard_resp.get_json()
|
||||
assert payload['ok'] is True
|
||||
|
||||
rows = payload['leaderboard']
|
||||
assert len(rows) >= 2
|
||||
assert rows[0]['username'] == 'leader_alpha'
|
||||
assert rows[0]['wins'] == 2
|
||||
assert rows[0]['played_games'] == 2
|
||||
assert rows[0]['rank'] == 1
|
||||
|
||||
bravo = next((row for row in rows if row['username'] == 'leader_bravo'), None)
|
||||
assert bravo is not None
|
||||
assert bravo['wins'] == 0
|
||||
assert bravo['played_games'] == 1
|
||||
Reference in new issue
Block a user