feat(engine): fetch daily-challenge seed from server on startup

- Add fetch_daily_challenge() to SyncProvider trait (default: Ok(None))
- SolitaireServerClient calls GET /api/daily-challenge (public endpoint)
  and returns the ChallengeGoal; non-2xx responses return Ok(None) so
  callers fall back to the local date-hash seed
- DailyChallengePlugin spawns an async task on Startup (only when
  SyncProviderResource is present) and polls it in Update; on success
  it overwrites DailyChallengeResource.seed with the server's seed,
  ensuring all players worldwide get the same deal on a given date

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-04-27 01:09:24 +00:00
parent 9a4071c74e
commit 9a38873891
3 changed files with 94 additions and 2 deletions
+26 -1
View File
@@ -12,7 +12,7 @@
//! without matching on [`SyncBackend`] anywhere else in the codebase.
use async_trait::async_trait;
use solitaire_sync::{LeaderboardEntry, SyncPayload, SyncResponse};
use solitaire_sync::{ChallengeGoal, LeaderboardEntry, SyncPayload, SyncResponse};
use crate::{
auth_tokens::{load_access_token, load_refresh_token, store_tokens},
@@ -200,6 +200,31 @@ impl SyncProvider for SolitaireServerClient {
load_access_token(&self.username).is_ok()
}
/// Fetch today's daily challenge from the server.
///
/// Does not require authentication — the endpoint is public. Returns `None`
/// on any non-success HTTP status so the caller falls back to the local seed.
async fn fetch_daily_challenge(&self) -> Result<Option<ChallengeGoal>, SyncError> {
let url = format!("{}/api/daily-challenge", self.base_url);
let resp = self
.client
.get(&url)
.send()
.await
.map_err(|e| SyncError::Network(e.to_string()))?;
if resp.status().is_success() {
let goal: ChallengeGoal = resp
.json()
.await
.map_err(|e| SyncError::Serialization(e.to_string()))?;
Ok(Some(goal))
} else {
// Non-fatal — caller will use the locally computed seed instead.
Ok(None)
}
}
async fn fetch_leaderboard(&self) -> Result<Vec<LeaderboardEntry>, SyncError> {
let token = self.access_token()?;
let url = format!("{}/api/leaderboard", self.base_url);