Files
OpenFUT-Core/src/routes/division.rs
T
funman300 a034e74c16
CI / Build, lint & test (push) Successful in 2m19s
style(core): apply cargo fmt across routes, services, models, tests
Pure rustfmt reflow (import grouping, array/match-arm/call-arg wrapping,
alphabetized module decls, comment realignment). No semantic change:
full-diff and `git diff -w` both confirm logic byte-identical to 271c363;
workspace builds and all 74 core tests pass. Retained pre-existing WIP
brought forward after verification.
2026-08-20 16:05:41 +00:00

159 lines
5.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::extractors::GameId;
use axum::{extract::State, Json};
use rand::{Rng, SeedableRng};
use serde_json::{json, Value};
use crate::{
app::AppState,
error::AppResult,
models::season::{PROMOTION_PTS, RELEGATION_PTS, SEASON_LENGTH},
services::{club as club_svc, profile as profile_svc, season as season_svc},
};
pub async fn get_division(State(state): State<AppState>, game: GameId) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let _club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let season = season_svc::get_or_create(&state.pool, &profile.id).await?;
Ok(Json(json!({
"division": season.division,
"season_number": season.season_number,
"season_points": season.season_points,
"matches_played": season.matches_played,
"matches_remaining": season.matches_remaining(),
"season_length": SEASON_LENGTH,
"promotion_pts": PROMOTION_PTS,
"relegation_pts": RELEGATION_PTS,
"pts_above_safe": season.pts_above_safe(),
"promotion_achievable": season.promotion_achievable(),
"can_be_relegated": season.can_be_relegated(),
"record": {
"wins": season.wins,
"draws": season.draws,
"losses": season.losses,
},
"pts_for_promotion": season.pts_for_promotion(),
"started_at": season.started_at,
})))
}
pub async fn get_division_history(
State(state): State<AppState>,
game: GameId,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let history = season_svc::get_history(&state.pool, &profile.id).await?;
Ok(Json(json!({ "history": history, "total": history.len() })))
}
pub async fn get_division_leaderboard(
State(state): State<AppState>,
game: GameId,
) -> AppResult<Json<Value>> {
let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?;
let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?;
let season = season_svc::get_or_create(&state.pool, &profile.id).await?;
// Seed from division + season_number so the NPC table is stable within a season
let seed = (season.division as u64) * 1000 + season.season_number as u64;
let mut rng = rand::rngs::SmallRng::seed_from_u64(seed);
const NPC_NAMES: &[&str] = &[
"Riverside FC",
"City Athletic",
"County United",
"Valley Rangers",
"Harbor Town FC",
"Mountside City",
"Lakewood Athletic",
"Eastbrook United",
"Westfield Rovers",
"Northgate FC",
"Southport Athletic",
"Ironbridge City",
"Milldale United",
"Hillcrest Rangers",
"Bayside FC",
"Thornfield Athletic",
"Greenhill United",
"Coldwater City",
"Redbury Rangers",
"Ashdown FC",
];
// Pick 9 NPC names without repetition using the seeded RNG
let mut name_indices: Vec<usize> = (0..NPC_NAMES.len()).collect();
name_indices.sort_by_key(|&_i| rng.gen::<u64>());
let npc_names: Vec<&str> = name_indices[..9].iter().map(|&i| NPC_NAMES[i]).collect();
// Generate NPC records: clubs in top of table have more wins, bottom have more losses
let matches_played = season.matches_played;
let npc_matches = matches_played.max(1); // NPC clubs play same number of matches as player
let mut table: Vec<serde_json::Value> = npc_names
.iter()
.enumerate()
.map(|(idx, name)| {
// Quality bias: index 0-2 = stronger, 6-8 = weaker
let quality: f64 = 1.0 - (idx as f64 / 8.0); // 1.0 → 0.0
let expected_win_rate = 0.2 + quality * 0.6; // 0.20.8
let wins =
(npc_matches as f64 * expected_win_rate * (0.8 + rng.gen::<f64>() * 0.4)) as i64;
let losses = (npc_matches as f64
* (1.0 - expected_win_rate)
* (0.8 + rng.gen::<f64>() * 0.4)) as i64;
let draws = (npc_matches - wins - losses).max(0);
let pts = wins * 3 + draws;
json!({
"club_name": name,
"wins": wins,
"draws": draws,
"losses": losses,
"pts": pts,
"matches_played": npc_matches,
"is_player": false,
})
})
.collect();
// Add player club row
let player_pts = season.wins * 3 + season.draws;
table.push(json!({
"club_name": club.name,
"wins": season.wins,
"draws": season.draws,
"losses": season.losses,
"pts": player_pts,
"matches_played": season.matches_played,
"is_player": true,
}));
// Sort by pts desc, then wins desc
table.sort_by(|a, b| {
let pts_a = a["pts"].as_i64().unwrap_or(0);
let pts_b = b["pts"].as_i64().unwrap_or(0);
pts_b.cmp(&pts_a).then_with(|| {
let w_b = b["wins"].as_i64().unwrap_or(0);
let w_a = a["wins"].as_i64().unwrap_or(0);
w_b.cmp(&w_a)
})
});
// Add position numbers
let table_with_pos: Vec<serde_json::Value> = table
.into_iter()
.enumerate()
.map(|(i, mut entry)| {
entry["position"] = json!(i as i64 + 1);
entry
})
.collect();
Ok(Json(json!({
"leaderboard": table_with_pos,
"division": season.division,
"season_number": season.season_number,
"promotion_threshold": 3, // top 3 promote
"relegation_threshold": 8, // bottom 2 relegate
})))
}