Offer a Continue link for ongoing games in the lobby
A player who accidentally closes the game page can now get back to it from the lobby. Backend: - GameStore.list_ongoing() returns live unfinished sessions (newest first), skipping expired keys and finished matches; - GET /api/me/games lists the games the caller is seated in, with the game name/description, players, open seats and your seat (no join code); - store, platform route and app integration tests. Frontend: - MyGame/MyGamesPage model plus api::my_games(); - the lobby shows a "Your games" card with phase, seats, players and a Continue button linking to /game/<id>; hidden when empty or logged out.
This commit is contained in:
@@ -498,6 +498,66 @@ pub struct PreviewPlayer {
|
||||
pub team: Option<String>,
|
||||
}
|
||||
|
||||
/// One ongoing game the caller is seated in (GET /api/me/games): the
|
||||
/// lobby payload plus the game's display name, for the "Continue" list.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct MyGame {
|
||||
pub id: String,
|
||||
#[serde(default)]
|
||||
pub game_type: String,
|
||||
#[serde(default)]
|
||||
pub game_name: String,
|
||||
#[serde(default)]
|
||||
pub game_description: String,
|
||||
#[serde(default)]
|
||||
pub phase: String,
|
||||
#[serde(default)]
|
||||
pub players: Vec<PreviewPlayer>,
|
||||
#[serde(default)]
|
||||
pub seats_open: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub your_seat: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub created_at: Option<String>,
|
||||
}
|
||||
|
||||
impl MyGame {
|
||||
/// Human-readable phase for the lobby list.
|
||||
pub fn phase_label(&self) -> &'static str {
|
||||
match self.phase.as_str() {
|
||||
"lobby" => "Waiting for players",
|
||||
"hand_end" => "Between hands",
|
||||
_ => "In progress",
|
||||
}
|
||||
}
|
||||
|
||||
/// Seated/total note, e.g. `"2/4 seated"`.
|
||||
pub fn seats_text(&self) -> String {
|
||||
match self.seats_open {
|
||||
Some(open) => {
|
||||
format!("{}/{} seated", self.players.len(), self.players.len() + open)
|
||||
}
|
||||
None => format!("{} seated", self.players.len()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The seated players' names, for the list subtitle.
|
||||
pub fn players_text(&self) -> String {
|
||||
self.players
|
||||
.iter()
|
||||
.map(|p| p.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct MyGamesPage {
|
||||
#[serde(default)]
|
||||
pub results: Vec<MyGame>,
|
||||
}
|
||||
|
||||
/// One option to show in the join preview, rendered from the game's
|
||||
/// schema and the engine-echoed value.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -1218,6 +1278,41 @@ mod tests {
|
||||
assert_eq!("C", g.player_name_for_team("C"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn my_game_phase_and_seats_text() {
|
||||
let g: MyGame = serde_json::from_value(json!({
|
||||
"id": "g1",
|
||||
"game_type": "briscola_a_quattro",
|
||||
"game_name": "Briscola a quattro",
|
||||
"phase": "lobby",
|
||||
"players": [
|
||||
{"name": "Alice", "seat": 0, "team": "A"},
|
||||
{"name": "Bob", "seat": 1, "team": "B"},
|
||||
],
|
||||
"seats_open": 2,
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!("Waiting for players", g.phase_label());
|
||||
assert_eq!("2/4 seated", g.seats_text());
|
||||
assert_eq!("Alice, Bob", g.players_text());
|
||||
|
||||
let playing: MyGame = serde_json::from_value(json!({
|
||||
"id": "g2",
|
||||
"game_type": "briscola",
|
||||
"phase": "playing",
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!("In progress", playing.phase_label());
|
||||
assert_eq!("0 seated", playing.seats_text());
|
||||
|
||||
let between: MyGame = serde_json::from_value(json!({
|
||||
"id": "g3",
|
||||
"phase": "hand_end",
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!("Between hands", between.phase_label());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_game_types_cover_the_registered_games() {
|
||||
let types = fallback_game_types();
|
||||
|
||||
Reference in New Issue
Block a user