From 6d621a393c815e59555d629189154c4cbf733ca4 Mon Sep 17 00:00:00 2001 From: Sidney Date: Sat, 1 Aug 2026 21:19:44 +0200 Subject: [PATCH] Running games --- client/src/api/client.ts | 1 + client/src/api/generated/index.ts | 4 +- client/src/api/generated/sdk.gen.ts | 14 ++++- client/src/api/generated/types.gen.ts | 30 ++++++++++ client/src/components/lobby/daily-section.tsx | 26 ++++++--- client/src/components/lobby/join-tab.tsx | 11 ++++ client/src/components/lobby/running-games.tsx | 56 +++++++++++++++++++ client/src/components/main-screen.tsx | 11 +++- client/src/i18n/de.json | 11 ++++ client/src/i18n/en.json | 11 ++++ client/src/store/game-info.ts | 31 ++++++++++ .../kotlin/ch/yass/game/GameController.kt | 8 +++ .../kotlin/ch/yass/game/GameRepository.kt | 13 +++++ .../main/kotlin/ch/yass/game/GameService.kt | 14 +++++ .../ch/yass/game/api/GameInfoResponse.kt | 8 +++ .../kotlin/ch/yass/game/dto/RunningGame.kt | 18 ++++++ server/main/resources/openapi-spec.yaml | 50 +++++++++++++++++ 17 files changed, 303 insertions(+), 14 deletions(-) create mode 100644 client/src/components/lobby/join-tab.tsx create mode 100644 client/src/components/lobby/running-games.tsx create mode 100644 client/src/store/game-info.ts create mode 100644 server/main/kotlin/ch/yass/game/api/GameInfoResponse.kt create mode 100644 server/main/kotlin/ch/yass/game/dto/RunningGame.kt diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 0ee2452..41eb1de 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -44,6 +44,7 @@ export const api = { createGame: (data: CreateCustomGameRequest) => generatedApi.postGameCreate({client: customClient, body: data}), dailyChallenge: () => generatedApi.postGameDaily({client: customClient}), dailyLeaderboard: () => generatedApi.getGameDailyLeaderboard({client: customClient}), + gameInfo: () => generatedApi.getGameInfo({client: customClient}), joinGame: (data: JoinGameRequest) => generatedApi.postGameJoin({client: customClient, body: data}), playCard: (data: PlayCardRequest) => generatedApi.postGamePlay({client: customClient, body: data}), chooseTrump: (data: ChooseTrumpRequest) => generatedApi.postGameTrump({client: customClient, body: data}), diff --git a/client/src/api/generated/index.ts b/client/src/api/generated/index.ts index 7d3ff64..2dc43ec 100644 --- a/client/src/api/generated/index.ts +++ b/client/src/api/generated/index.ts @@ -1,4 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts -export { anonLink, anonLogout, anonSignup, connect, getAdminAnalyzeGameByCode, getGameDailyLeaderboard, type Options, postAdminMessage, postGameCreate, postGameDaily, postGameJoin, postGamePing, postGamePlay, postGameSchiebe, postGameTrump, postGameWeisen, subscribe, whoami } from './sdk.gen'; -export type { AnalyzeGameStateResponse, AnalyzeHand, AnonLinkData, AnonLinkErrors, AnonLinkResponse, AnonLinkResponses, AnonLogoutData, AnonLogoutResponse, AnonLogoutResponses, AnonSignupData, AnonSignupErrors, AnonSignupRequest, AnonSignupResponse, AnonSignupResponse2, AnonSignupResponses, Card, CardInHand, CardOnTable, ChooseTrumpRequest, ClientOptions, ConnectData, ConnectErrors, ConnectResponse, ConnectResponses, CreateCustomGameRequest, CreateCustomGameResponse, CreateDailyChallengeResponse, DailyLeaderboardEntry, DailyLeaderboardResponse, GameFinished, GetAdminAnalyzeGameByCodeData, GetAdminAnalyzeGameByCodeResponse, GetAdminAnalyzeGameByCodeResponses, GetGameDailyLeaderboardData, GetGameDailyLeaderboardResponse, GetGameDailyLeaderboardResponses, JoinGameRequest, JoinGameResponse, MessageRequest, PingSeatRequest, PlayCardRequest, PlayedCard, PlayedCardWithPlayer, Player, PlayerAtTable, PlayerWithCards, PlayerWithWeise, Position, PostAdminMessageData, PostAdminMessageResponse, PostAdminMessageResponses, PostGameCreateData, PostGameCreateResponse, PostGameCreateResponses, PostGameDailyData, PostGameDailyResponse, PostGameDailyResponses, PostGameJoinData, PostGameJoinResponse, PostGameJoinResponses, PostGamePingData, PostGamePingResponse, PostGamePingResponses, PostGamePlayData, PostGamePlayResponse, PostGamePlayResponses, PostGameSchiebeData, PostGameSchiebeResponse, PostGameSchiebeResponses, PostGameTrumpData, PostGameTrumpResponse, PostGameTrumpResponses, PostGameWeisenData, PostGameWeisenResponse, PostGameWeisenResponses, Rank, SchiebeRequest, SeatState, State, SubscribeData, SubscribeErrors, SubscribeRequest, SubscribeResponse, SubscribeResponses, SuccessfulActionResponse, Suit, TeamWithPoints, TotalPoints, TrickWithCards, Trump, Weis, WeisenRequest, WeisType, WeisWithPoints, WhoamiData, WhoamiErrors, WhoamiResponse, WhoAmIResponse, WhoamiResponses } from './types.gen'; +export { anonLink, anonLogout, anonSignup, connect, getAdminAnalyzeGameByCode, getGameDailyLeaderboard, getGameInfo, type Options, postAdminMessage, postGameCreate, postGameDaily, postGameJoin, postGamePing, postGamePlay, postGameSchiebe, postGameTrump, postGameWeisen, subscribe, whoami } from './sdk.gen'; +export type { AnalyzeGameStateResponse, AnalyzeHand, AnonLinkData, AnonLinkErrors, AnonLinkResponse, AnonLinkResponses, AnonLogoutData, AnonLogoutResponse, AnonLogoutResponses, AnonSignupData, AnonSignupErrors, AnonSignupRequest, AnonSignupResponse, AnonSignupResponse2, AnonSignupResponses, Card, CardInHand, CardOnTable, ChooseTrumpRequest, ClientOptions, ConnectData, ConnectErrors, ConnectResponse, ConnectResponses, CreateCustomGameRequest, CreateCustomGameResponse, CreateDailyChallengeResponse, DailyLeaderboardEntry, DailyLeaderboardResponse, GameFinished, GameInfoResponse, GameKind, GetAdminAnalyzeGameByCodeData, GetAdminAnalyzeGameByCodeResponse, GetAdminAnalyzeGameByCodeResponses, GetGameDailyLeaderboardData, GetGameDailyLeaderboardResponse, GetGameDailyLeaderboardResponses, GetGameInfoData, GetGameInfoResponse, GetGameInfoResponses, JoinGameRequest, JoinGameResponse, MessageRequest, PingSeatRequest, PlayCardRequest, PlayedCard, PlayedCardWithPlayer, Player, PlayerAtTable, PlayerWithCards, PlayerWithWeise, Position, PostAdminMessageData, PostAdminMessageResponse, PostAdminMessageResponses, PostGameCreateData, PostGameCreateResponse, PostGameCreateResponses, PostGameDailyData, PostGameDailyResponse, PostGameDailyResponses, PostGameJoinData, PostGameJoinResponse, PostGameJoinResponses, PostGamePingData, PostGamePingResponse, PostGamePingResponses, PostGamePlayData, PostGamePlayResponse, PostGamePlayResponses, PostGameSchiebeData, PostGameSchiebeResponse, PostGameSchiebeResponses, PostGameTrumpData, PostGameTrumpResponse, PostGameTrumpResponses, PostGameWeisenData, PostGameWeisenResponse, PostGameWeisenResponses, Rank, RunningGame, SchiebeRequest, SeatState, State, SubscribeData, SubscribeErrors, SubscribeRequest, SubscribeResponse, SubscribeResponses, SuccessfulActionResponse, Suit, TeamWithPoints, TotalPoints, TrickWithCards, Trump, Weis, WeisenRequest, WeisType, WeisWithPoints, WhoamiData, WhoamiErrors, WhoamiResponse, WhoAmIResponse, WhoamiResponses } from './types.gen'; diff --git a/client/src/api/generated/sdk.gen.ts b/client/src/api/generated/sdk.gen.ts index e9d6239..b8ae0c4 100644 --- a/client/src/api/generated/sdk.gen.ts +++ b/client/src/api/generated/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { AnonLinkData, AnonLinkErrors, AnonLinkResponses, AnonLogoutData, AnonLogoutResponses, AnonSignupData, AnonSignupErrors, AnonSignupResponses, ConnectData, ConnectErrors, ConnectResponses, GetAdminAnalyzeGameByCodeData, GetAdminAnalyzeGameByCodeResponses, GetGameDailyLeaderboardData, GetGameDailyLeaderboardResponses, PostAdminMessageData, PostAdminMessageResponses, PostGameCreateData, PostGameCreateResponses, PostGameDailyData, PostGameDailyResponses, PostGameJoinData, PostGameJoinResponses, PostGamePingData, PostGamePingResponses, PostGamePlayData, PostGamePlayResponses, PostGameSchiebeData, PostGameSchiebeResponses, PostGameTrumpData, PostGameTrumpResponses, PostGameWeisenData, PostGameWeisenResponses, SubscribeData, SubscribeErrors, SubscribeResponses, WhoamiData, WhoamiErrors, WhoamiResponses } from './types.gen'; +import type { AnonLinkData, AnonLinkErrors, AnonLinkResponses, AnonLogoutData, AnonLogoutResponses, AnonSignupData, AnonSignupErrors, AnonSignupResponses, ConnectData, ConnectErrors, ConnectResponses, GetAdminAnalyzeGameByCodeData, GetAdminAnalyzeGameByCodeResponses, GetGameDailyLeaderboardData, GetGameDailyLeaderboardResponses, GetGameInfoData, GetGameInfoResponses, PostAdminMessageData, PostAdminMessageResponses, PostGameCreateData, PostGameCreateResponses, PostGameDailyData, PostGameDailyResponses, PostGameJoinData, PostGameJoinResponses, PostGamePingData, PostGamePingResponses, PostGamePlayData, PostGamePlayResponses, PostGameSchiebeData, PostGameSchiebeResponses, PostGameTrumpData, PostGameTrumpResponses, PostGameWeisenData, PostGameWeisenResponses, SubscribeData, SubscribeErrors, SubscribeResponses, WhoamiData, WhoamiErrors, WhoamiResponses } from './types.gen'; export type Options = Options2 & { /** @@ -155,6 +155,18 @@ export const getGameDailyLeaderboard = (op ...options }); +/** + * Game info for the current player + * + * Everything the lobby needs about the current player: the games they are seated at and could rejoin, and whether they already played today's daily challenge to the end. + * + */ +export const getGameInfo = (options?: Options) => (options?.client ?? client).get({ + responseType: 'json', + url: '/game/info', + ...options +}); + /** * Get current user information */ diff --git a/client/src/api/generated/types.gen.ts b/client/src/api/generated/types.gen.ts index 15d7f52..4c3e54e 100644 --- a/client/src/api/generated/types.gen.ts +++ b/client/src/api/generated/types.gen.ts @@ -73,6 +73,20 @@ export type DailyLeaderboardEntry = { points: number; }; +export type GameInfoResponse = { + runningGames: Array; + dailyChallengePlayed: boolean; +}; + +export type RunningGame = { + uuid: string; + code: string; + kind: GameKind; + startedAt: string; +}; + +export type GameKind = 'QUEUE' | 'CUSTOM' | 'DAILY'; + export type JoinGameRequest = { code: string; }; @@ -446,6 +460,22 @@ export type GetGameDailyLeaderboardResponses = { export type GetGameDailyLeaderboardResponse = GetGameDailyLeaderboardResponses[keyof GetGameDailyLeaderboardResponses]; +export type GetGameInfoData = { + body?: never; + path?: never; + query?: never; + url: '/game/info'; +}; + +export type GetGameInfoResponses = { + /** + * Info retrieved successfully + */ + 200: GameInfoResponse; +}; + +export type GetGameInfoResponse = GetGameInfoResponses[keyof GetGameInfoResponses]; + export type WhoamiData = { body?: never; path?: never; diff --git a/client/src/components/lobby/daily-section.tsx b/client/src/components/lobby/daily-section.tsx index 20f7cdd..a42b0c0 100644 --- a/client/src/components/lobby/daily-section.tsx +++ b/client/src/components/lobby/daily-section.tsx @@ -12,6 +12,7 @@ import {useAsyncAction} from "@/hooks/use-async-action" import {useAxiosErrorHandler} from "@/hooks/use-axios-error-handler" import {useMidnightCountdown} from "@/hooks/use-midnight-countdown" import {usePlayerStore} from "@/store/player.ts" +import {useGameInfoStore} from "@/store/game-info.ts" const rules = ["sameCards", "oncePerDay", "opponents"] @@ -21,6 +22,8 @@ export function DailySection() { const handleAxiosError = useAxiosErrorHandler() const isAnon = usePlayerStore(state => state.isAnon) const remaining = useMidnightCountdown() + const played = useGameInfoStore(state => state.dailyChallengePlayed) + const runningDaily = useGameInfoStore(state => state.runningGames.find(game => game.kind === "DAILY")) const [error, setError] = useState(null) const {execute: executeDaily, isLoading, hasError, reset} = useAsyncAction(async () => { @@ -73,15 +76,20 @@ export function DailySection() { ) : ( - +
+ + {played && !runningDaily && ( +

{t("lobby.daily.alreadyPlayed")}

+ )} +
)} diff --git a/client/src/components/lobby/join-tab.tsx b/client/src/components/lobby/join-tab.tsx new file mode 100644 index 0000000..ade1ea6 --- /dev/null +++ b/client/src/components/lobby/join-tab.tsx @@ -0,0 +1,11 @@ +import {RunningGames} from "@/components/lobby/running-games.tsx" +import {JoinSection} from "@/components/lobby/join-section.tsx" + +export function JoinTab() { + return ( +
+ + +
+ ) +} diff --git a/client/src/components/lobby/running-games.tsx b/client/src/components/lobby/running-games.tsx new file mode 100644 index 0000000..c6d23c0 --- /dev/null +++ b/client/src/components/lobby/running-games.tsx @@ -0,0 +1,56 @@ +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" +import {Button} from "@/components/ui/button" +import {History, Play} from "lucide-react" +import {useTranslation} from "react-i18next" +import {useNavigate} from "react-router-dom" +import {useGameInfoStore} from "@/store/game-info.ts" + +const units: [Intl.RelativeTimeFormatUnit, number][] = [["day", 86400], ["hour", 3600], ["minute", 60]] + +const startedAgo = (startedAt: string, language: string) => { + const seconds = (Date.now() - new Date(startedAt).getTime()) / 1000 + const [unit, size] = units.find(([, size]) => seconds >= size) ?? units[units.length - 1] + return new Intl.RelativeTimeFormat(language, {numeric: "always"}) + .format(-Math.max(1, Math.floor(seconds / size)), unit) +} + +export function RunningGames() { + const {t, i18n} = useTranslation() + const navigate = useNavigate() + const runningGames = useGameInfoStore(state => state.runningGames) + + if (runningGames.length === 0) return null + + return ( + + +
+ + {t("lobby.running.title")} +
+ {t("lobby.running.description")} +
+ + {runningGames.map(game => ( + + ))} + +
+ ) +} diff --git a/client/src/components/main-screen.tsx b/client/src/components/main-screen.tsx index 1c3556a..b7ef200 100644 --- a/client/src/components/main-screen.tsx +++ b/client/src/components/main-screen.tsx @@ -5,12 +5,13 @@ import {motion} from "motion/react"; import {CalendarDays, LogIn, Plus} from "lucide-react"; import {cn} from "@/lib/utils"; import {DailyTab} from "@/components/lobby/daily-tab.tsx"; -import {JoinSection} from "@/components/lobby/join-section.tsx"; +import {JoinTab} from "@/components/lobby/join-tab.tsx"; import {CreateSection} from "@/components/lobby/create-section.tsx"; +import {useGameInfoStore} from "@/store/game-info.ts"; const sections = [ {id: "daily" as const, icon: CalendarDays, Section: DailyTab}, - {id: "join" as const, icon: LogIn, Section: JoinSection}, + {id: "join" as const, icon: LogIn, Section: JoinTab}, {id: "create" as const, icon: Plus, Section: CreateSection}, ] @@ -22,9 +23,15 @@ export function MainScreen() { const alignedRef = useRef(false) const fromScrollRef = useRef(false) const targetRef = useRef(null) + const refreshGameInfo = useGameInfoStore(state => state.refresh) const active = sections.find(s => s.id === section)?.id + // Fetched once per lobby mount, so coming back from a game picks up the game that just finished. + useEffect(() => { + refreshGameInfo() + }, [refreshGameInfo]) + // Aligns the carousel whenever the section changes through the URL, be it a tab click, a browser // back or a deep link. The first alignment jumps so a deep link does not animate on load. useEffect(() => { diff --git a/client/src/i18n/de.json b/client/src/i18n/de.json index 0f8e178..f5fc2f4 100644 --- a/client/src/i18n/de.json +++ b/client/src/i18n/de.json @@ -136,6 +136,8 @@ "title": "Tagestrumpf", "countdown": "bis zum nächsten Tagestrumpf", "play": "Jetzt spielen", + "finish": "Spiel abschliessen", + "alreadyPlayed": "Du hast den Tagestrumpf von heute schon gespielt.", "explanation": "Was ist das?", "rules": { "sameCards": "Alle spielen mit den exakt gleich gemischten Karten", @@ -152,6 +154,15 @@ "join": { "title": "Spiel beitreten", "description": "Falls du einen Code erhalten hast kannst du ihn hier eingeben." + }, + "running": { + "title": "Laufende Spiele", + "description": "Spiele, an denen du noch am Tisch sitzt. Tippe drauf, um weiterzuspielen.", + "kind": { + "DAILY": "Tagestrumpf", + "CUSTOM": "Eigenes Spiel", + "QUEUE": "Schnelles Spiel" + } } }, "logout": { diff --git a/client/src/i18n/en.json b/client/src/i18n/en.json index 171d2a7..c1d00bc 100644 --- a/client/src/i18n/en.json +++ b/client/src/i18n/en.json @@ -109,6 +109,8 @@ "title": "Daily challenge", "countdown": "until the next challenge", "play": "Play challenge", + "finish": "Finish challenge", + "alreadyPlayed": "You already played today's challenge.", "explanation": "What is this?", "rules": { "sameCards": "Everyone plays the exact same shuffle", @@ -125,6 +127,15 @@ "join": { "title": "Join a game", "description": "Got a code from a friend? Enter it here to take a seat." + }, + "running": { + "title": "Running games", + "description": "Games you still have a seat at. Tap one to keep playing.", + "kind": { + "DAILY": "Daily challenge", + "CUSTOM": "Custom game", + "QUEUE": "Quick game" + } } }, "create": { diff --git a/client/src/store/game-info.ts b/client/src/store/game-info.ts new file mode 100644 index 0000000..0cf2105 --- /dev/null +++ b/client/src/store/game-info.ts @@ -0,0 +1,31 @@ +import {create} from 'zustand' +import {api} from '@/api/client' +import type {RunningGame} from '@/api/generated' + +interface GameInfoState { + runningGames: RunningGame[] + dailyChallengePlayed: boolean + isLoading: boolean + refresh: () => Promise +} + +export const useGameInfoStore = create((set) => ({ + runningGames: [], + dailyChallengePlayed: false, + isLoading: true, + // The lobby stays usable if this fails, the sections just fall back to their default state. + refresh: async () => { + set({isLoading: true}) + try { + const {data} = await api.gameInfo() + set({ + runningGames: data?.runningGames ?? [], + dailyChallengePlayed: data?.dailyChallengePlayed ?? false + }) + } catch { + set({runningGames: [], dailyChallengePlayed: false}) + } finally { + set({isLoading: false}) + } + } +})) diff --git a/server/main/kotlin/ch/yass/game/GameController.kt b/server/main/kotlin/ch/yass/game/GameController.kt index b2167fa..d92dcfd 100644 --- a/server/main/kotlin/ch/yass/game/GameController.kt +++ b/server/main/kotlin/ch/yass/game/GameController.kt @@ -38,6 +38,7 @@ class GameController(private val service: GameService, private val repo: GameRep post("/ping", ::ping) post("/daily", ::daily) get("/daily-leaderboard", ::dailyLeaderboard) + get("/info", ::info) } private fun ping(ctx: Context) = either { @@ -175,4 +176,11 @@ class GameController(private val service: GameService, private val repo: GameRep { errorResponse(ctx, it) }, { successResponse(ctx, it) } ) + + private fun info(ctx: Context) = either { + service.info(player(ctx), LocalDateTime.now(ZoneOffset.UTC)) + }.fold( + { errorResponse(ctx, it) }, + { successResponse(ctx, it) } + ) } diff --git a/server/main/kotlin/ch/yass/game/GameRepository.kt b/server/main/kotlin/ch/yass/game/GameRepository.kt index d1e46b8..a3cbbae 100644 --- a/server/main/kotlin/ch/yass/game/GameRepository.kt +++ b/server/main/kotlin/ch/yass/game/GameRepository.kt @@ -120,6 +120,19 @@ class GameRepository(private val db: DSLContext) { return game ?: r.raise(GameNotFound(uuid)) } + /** + * Games the player is seated at and could still rejoin. STALE ones count as well, they are only + * waiting for someone to come back (see [rejoinSeat]). + */ + fun getRunningGamesForPlayer(player: InternalPlayer): List = + db.select(GAME) + .from(SEAT) + .join(GAME).on(SEAT.GAME_ID.eq(GAME.ID)) + .where(SEAT.PLAYER_ID.eq(player.id)) + .and(GAME.STATUS.`in`(GameStatus.RUNNING.name, GameStatus.STALE.name)) + .orderBy(GAME.CREATED_AT.desc()) + .fetch(mapping(Game::fromRecord)) + fun getDailyGameForPlayer(player: InternalPlayer, start: LocalDateTime, end: LocalDateTime): Game? = db.select(GAME) .from(SEAT) diff --git a/server/main/kotlin/ch/yass/game/GameService.kt b/server/main/kotlin/ch/yass/game/GameService.kt index ab35d27..25ce978 100644 --- a/server/main/kotlin/ch/yass/game/GameService.kt +++ b/server/main/kotlin/ch/yass/game/GameService.kt @@ -167,6 +167,20 @@ class GameService( return DailyLeaderboardResponse(day, entries) } + /** + * What the lobby needs to know about the player: the games they can rejoin and whether today's + * daily challenge is already behind them. + */ + fun info(player: InternalPlayer, now: LocalDateTime): GameInfoResponse { + val (start, end) = swissDayWindowUTC(swissDay(now)) + val daily = repo.getDailyGameForPlayer(player, start, end) + + return GameInfoResponse( + repo.getRunningGamesForPlayer(player).map(RunningGame::from), + daily?.status == GameStatus.FINISHED + ) + } + context(_: Raise) fun getStateByCode(code: String): GameState { val game = repo.getByCode(code) diff --git a/server/main/kotlin/ch/yass/game/api/GameInfoResponse.kt b/server/main/kotlin/ch/yass/game/api/GameInfoResponse.kt new file mode 100644 index 0000000..82fa8bb --- /dev/null +++ b/server/main/kotlin/ch/yass/game/api/GameInfoResponse.kt @@ -0,0 +1,8 @@ +package ch.yass.game.api + +import ch.yass.game.dto.RunningGame + +data class GameInfoResponse( + val runningGames: List, + val dailyChallengePlayed: Boolean +) diff --git a/server/main/kotlin/ch/yass/game/dto/RunningGame.kt b/server/main/kotlin/ch/yass/game/dto/RunningGame.kt new file mode 100644 index 0000000..a01531f --- /dev/null +++ b/server/main/kotlin/ch/yass/game/dto/RunningGame.kt @@ -0,0 +1,18 @@ +package ch.yass.game.dto + +import ch.yass.game.dto.db.Game +import java.time.Instant +import java.time.ZoneOffset +import java.util.* + +data class RunningGame( + val uuid: UUID, + val code: String, + val kind: GameKind, + val startedAt: Instant +) { + companion object { + fun from(game: Game): RunningGame = + RunningGame(game.uuid, game.code, game.kind, game.createdAt.toInstant(ZoneOffset.UTC)) + } +} diff --git a/server/main/resources/openapi-spec.yaml b/server/main/resources/openapi-spec.yaml index 6dda164..0f82923 100644 --- a/server/main/resources/openapi-spec.yaml +++ b/server/main/resources/openapi-spec.yaml @@ -184,6 +184,20 @@ paths: schema: $ref: '#/components/schemas/DailyLeaderboardResponse' + /game/info: + get: + summary: Game info for the current player + description: > + Everything the lobby needs about the current player: the games they are seated at and could + rejoin, and whether they already played today's daily challenge to the end. + responses: + '200': + description: Info retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GameInfoResponse' + /auth/whoami: get: summary: Get current user information @@ -510,6 +524,42 @@ components: points: type: integer + GameInfoResponse: + type: object + required: + - runningGames + - dailyChallengePlayed + properties: + runningGames: + type: array + items: + $ref: '#/components/schemas/RunningGame' + dailyChallengePlayed: + type: boolean + + RunningGame: + type: object + required: + - uuid + - code + - kind + - startedAt + properties: + uuid: + type: string + format: uuid + code: + type: string + kind: + $ref: '#/components/schemas/GameKind' + startedAt: + type: string + format: date-time + + GameKind: + type: string + enum: [ QUEUE, CUSTOM, DAILY ] + JoinGameRequest: type: object required: