ai generated solutions to our ai generated problems
This commit is contained in:
@@ -15,11 +15,13 @@ It currently exposes:
|
||||
|
||||
- `GET /health`
|
||||
- `GET /api/tss/leaderboard/teams?limit=100`
|
||||
- `GET /api/tss/leaderboard/players?limit=100`
|
||||
- `GET /api/tss/teams/resolve?name=...`
|
||||
- `GET /api/tss/teams/search?q=...&limit=10`
|
||||
- `GET /api/tss/teams/:team`
|
||||
- `GET /api/tss/teams/:team/history`
|
||||
- `GET /api/tss/teams/:team/games`
|
||||
- `GET /api/tss/player/:uid`
|
||||
|
||||
## Local development
|
||||
|
||||
|
||||
@@ -103,6 +103,11 @@ struct LeaderboardResponse {
|
||||
teams: Vec<TeamLeaderboardRow>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PlayerLeaderboardResponse {
|
||||
players: Vec<PlayerLeaderboardRow>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct RecentGamesResponse {
|
||||
matches: Vec<GameRow>,
|
||||
@@ -138,6 +143,26 @@ struct TeamLeaderboardRow {
|
||||
total_kills: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PlayerLeaderboardRow {
|
||||
uid: String,
|
||||
nick: Option<String>,
|
||||
total_battles: i64,
|
||||
wins: i64,
|
||||
losses: i64,
|
||||
win_rate: f64,
|
||||
ground_kills: i64,
|
||||
air_kills: i64,
|
||||
total_kills: i64,
|
||||
assists: i64,
|
||||
captures: i64,
|
||||
deaths: i64,
|
||||
score: i64,
|
||||
kdr: f64,
|
||||
teams_seen: i64,
|
||||
last_seen: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TeamDetail {
|
||||
team_id: i64,
|
||||
@@ -323,6 +348,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let app = Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/api/tss/leaderboard/teams", get(leaderboard))
|
||||
.route("/api/tss/leaderboard/players", get(player_leaderboard))
|
||||
.route("/api/tss/games/recent", get(recent_games))
|
||||
.route("/api/tss/games/{session_id}", get(game_detail))
|
||||
.route("/api/tss/teams/resolve", get(resolve_team))
|
||||
@@ -413,6 +439,16 @@ async fn leaderboard(
|
||||
Ok(Json(LeaderboardResponse { teams }))
|
||||
}
|
||||
|
||||
async fn player_leaderboard(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<LimitQuery>,
|
||||
) -> ApiResult<PlayerLeaderboardResponse> {
|
||||
let limit = i64::from(query.limit.unwrap_or(100).clamp(1, 100));
|
||||
let battles_conn = open_db(&state.battles_db)?;
|
||||
let players = player_leaderboard_rows(&battles_conn, limit)?;
|
||||
Ok(Json(PlayerLeaderboardResponse { players }))
|
||||
}
|
||||
|
||||
fn leaderboard_teams(state: &AppState, limit: usize) -> Result<Vec<TeamRecord>, ApiError> {
|
||||
let teams_conn = open_db(&state.teams_db)?;
|
||||
// Deduplicate teams by name across tournaments — pick the highest team_id
|
||||
@@ -951,6 +987,100 @@ fn player_summaries_for(
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn player_leaderboard_rows(
|
||||
conn: &Connection,
|
||||
limit: i64,
|
||||
) -> Result<Vec<PlayerLeaderboardRow>, ApiError> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"WITH per_session AS (
|
||||
SELECT
|
||||
UID,
|
||||
session_id,
|
||||
MAX(victor_bool) AS victor_bool,
|
||||
MAX(ground_kills) AS ground_kills,
|
||||
MAX(air_kills) AS air_kills,
|
||||
MAX(assists) AS assists,
|
||||
MAX(captures) AS captures,
|
||||
MAX(deaths) AS deaths,
|
||||
MAX(score) AS score,
|
||||
MAX(endtime_unix) AS endtime_unix,
|
||||
MAX(team_name) AS team_name
|
||||
FROM player_games_hist
|
||||
WHERE UID IS NOT NULL AND UID != ''
|
||||
GROUP BY UID, session_id
|
||||
),
|
||||
latest_names AS (
|
||||
SELECT UID, nick
|
||||
FROM (
|
||||
SELECT
|
||||
UID,
|
||||
nick,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY UID
|
||||
ORDER BY endtime_unix DESC, nick COLLATE NOCASE ASC
|
||||
) AS rn
|
||||
FROM player_games_hist
|
||||
WHERE nick IS NOT NULL AND nick != ''
|
||||
)
|
||||
WHERE rn = 1
|
||||
)
|
||||
SELECT
|
||||
p.UID,
|
||||
n.nick,
|
||||
COUNT(*) AS battles,
|
||||
COALESCE(SUM(CASE WHEN UPPER(p.victor_bool) = 'WIN' THEN 1 ELSE 0 END), 0) AS wins,
|
||||
COALESCE(SUM(CASE WHEN UPPER(p.victor_bool) = 'LOSS' THEN 1 ELSE 0 END), 0) AS losses,
|
||||
COALESCE(SUM(p.ground_kills), 0) AS ground_kills,
|
||||
COALESCE(SUM(p.air_kills), 0) AS air_kills,
|
||||
COALESCE(SUM(p.assists), 0) AS assists,
|
||||
COALESCE(SUM(p.captures), 0) AS captures,
|
||||
COALESCE(SUM(p.deaths), 0) AS deaths,
|
||||
COALESCE(SUM(p.score), 0) AS score,
|
||||
COUNT(DISTINCT p.team_name) AS teams_seen,
|
||||
MAX(p.endtime_unix) AS last_seen
|
||||
FROM per_session p
|
||||
LEFT JOIN latest_names n ON n.UID = p.UID
|
||||
GROUP BY p.UID
|
||||
ORDER BY score DESC, (ground_kills + air_kills) DESC, battles DESC, p.UID ASC
|
||||
LIMIT ?1",
|
||||
)
|
||||
.map_err(db_error)?;
|
||||
|
||||
let players = stmt
|
||||
.query_map(params![limit], |row| {
|
||||
let ground: i64 = row.get(5)?;
|
||||
let air: i64 = row.get(6)?;
|
||||
let battles: i64 = row.get(2)?;
|
||||
let wins: i64 = row.get(3)?;
|
||||
let deaths: i64 = row.get(9)?;
|
||||
let total_kills = ground + air;
|
||||
Ok(PlayerLeaderboardRow {
|
||||
uid: row.get(0)?,
|
||||
nick: row.get(1)?,
|
||||
total_battles: battles,
|
||||
wins,
|
||||
losses: row.get(4)?,
|
||||
win_rate: percent(wins, battles),
|
||||
ground_kills: ground,
|
||||
air_kills: air,
|
||||
total_kills,
|
||||
assists: row.get(7)?,
|
||||
captures: row.get(8)?,
|
||||
deaths,
|
||||
score: row.get(10)?,
|
||||
kdr: ratio(total_kills, deaths),
|
||||
teams_seen: row.get(11)?,
|
||||
last_seen: row.get(12)?,
|
||||
})
|
||||
})
|
||||
.map_err(db_error)?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(db_error)?;
|
||||
|
||||
Ok(players)
|
||||
}
|
||||
|
||||
fn player_search(conn: &Connection, query: &str, limit: i64) -> Result<Vec<PlayerRef>, ApiError> {
|
||||
let like = format!("%{}%", escape_like(query));
|
||||
let mut stmt = conn
|
||||
|
||||
Reference in New Issue
Block a user