Phase 23: season history + division zone data + new route
CI / Build, lint & test (push) Failing after 1m19s
CI / Build, lint & test (push) Failing after 1m19s
- migrations/0011_season_history.sql: persist one row per completed season - models/season.rs: SeasonHistoryEntry struct; public SEASON_LENGTH / PROMOTION_PTS / RELEGATION_PTS consts; pts_above_safe, can_be_relegated, promotion_achievable helpers - services/season.rs: write history entry on season rollover; get_history() returns last 20 seasons newest-first - routes/division.rs: GET /division now includes promotion_pts, relegation_pts, season_length, pts_above_safe, promotion_achievable, can_be_relegated; new GET /division/history endpoint - 3 new integration tests: history empty, history records after promotion, division response has zone fields Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct Season {
|
||||
pub profile_id: String,
|
||||
pub division: i64,
|
||||
pub season_number: i64,
|
||||
pub season_points: i64,
|
||||
pub matches_played: i64,
|
||||
pub wins: i64,
|
||||
pub draws: i64,
|
||||
pub losses: i64,
|
||||
pub started_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, FromRow)]
|
||||
pub struct SeasonHistoryEntry {
|
||||
pub id: String,
|
||||
pub profile_id: String,
|
||||
pub season_number: i64,
|
||||
pub division: i64,
|
||||
pub season_points: i64,
|
||||
pub wins: i64,
|
||||
pub draws: i64,
|
||||
pub losses: i64,
|
||||
pub result: String,
|
||||
pub new_division: i64,
|
||||
pub coins_awarded: i64,
|
||||
pub pack_awarded: Option<String>,
|
||||
pub ended_at: String,
|
||||
}
|
||||
|
||||
pub const SEASON_LENGTH: i64 = 10;
|
||||
pub const PROMOTION_PTS: i64 = 21;
|
||||
pub const RELEGATION_PTS: i64 = 3;
|
||||
const MIN_DIVISION: i64 = 1;
|
||||
const MAX_DIVISION: i64 = 10;
|
||||
|
||||
impl Season {
|
||||
/// Whether this season's match quota is complete.
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.matches_played >= SEASON_LENGTH
|
||||
}
|
||||
|
||||
/// Season end result: promote / maintain / relegate.
|
||||
pub fn end_result(&self) -> SeasonResult {
|
||||
if self.season_points >= PROMOTION_PTS && self.division > MIN_DIVISION {
|
||||
SeasonResult::Promoted
|
||||
} else if self.season_points <= RELEGATION_PTS && self.division < MAX_DIVISION {
|
||||
SeasonResult::Relegated
|
||||
} else {
|
||||
SeasonResult::Maintained
|
||||
}
|
||||
}
|
||||
|
||||
/// Coin reward for ending the season in this division.
|
||||
pub fn season_reward_coins(&self) -> i64 {
|
||||
match self.division {
|
||||
1 => 5000,
|
||||
2 => 4000,
|
||||
3 => 3000,
|
||||
4 => 2000,
|
||||
5 => 1500,
|
||||
6 | 7 => 1000,
|
||||
_ => 500,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pack reward (if any) for ending the season in this division.
|
||||
pub fn season_reward_pack(&self) -> Option<&'static str> {
|
||||
match self.division {
|
||||
1 | 2 => Some("gold_pack"),
|
||||
3 | 4 => Some("silver_pack"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches left in the current season.
|
||||
pub fn matches_remaining(&self) -> i64 {
|
||||
(SEASON_LENGTH - self.matches_played).max(0)
|
||||
}
|
||||
|
||||
/// Points needed for promotion (None if already at div 1).
|
||||
pub fn pts_for_promotion(&self) -> Option<i64> {
|
||||
if self.division <= MIN_DIVISION {
|
||||
None
|
||||
} else {
|
||||
Some((PROMOTION_PTS - self.season_points).max(0))
|
||||
}
|
||||
}
|
||||
|
||||
/// Points above the relegation threshold (always ≥ 0 means safe).
|
||||
pub fn pts_above_safe(&self) -> i64 {
|
||||
self.season_points - RELEGATION_PTS
|
||||
}
|
||||
|
||||
/// Whether relegation is still possible (false if at max division already).
|
||||
pub fn can_be_relegated(&self) -> bool {
|
||||
self.division < MAX_DIVISION
|
||||
}
|
||||
|
||||
/// Whether promotion is still achievable given matches remaining.
|
||||
pub fn promotion_achievable(&self) -> bool {
|
||||
self.division > MIN_DIVISION
|
||||
&& self.season_points + self.matches_remaining() * 3 >= PROMOTION_PTS
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SeasonResult {
|
||||
Promoted,
|
||||
Maintained,
|
||||
Relegated,
|
||||
}
|
||||
|
||||
/// Returned in match result when a season ends.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SeasonEndSummary {
|
||||
pub result: SeasonResult,
|
||||
pub old_division: i64,
|
||||
pub new_division: i64,
|
||||
pub new_season_number: i64,
|
||||
pub coins_awarded: i64,
|
||||
pub pack_awarded: Option<String>,
|
||||
}
|
||||
Reference in New Issue
Block a user