Files
SlipItIn/openwiki/workflows/slip-and-challenge.md
2026-08-08 18:09:52 +00:00

198 lines
12 KiB
Markdown

---
type: Workflow
title: Slip & Challenge Workflows
description: Real-time game loops covering authentication, lobby creation, card dealing, phrase slipping, slip challenging, challenge resolution, and offline action queuing.
tags: [workflow, game-loop, signalr, slip-mechanic, real-time, offline-resync]
openwiki:
roles: [workflow]
change_kinds: [public-api, lifecycle]
source_paths: [SlipItIn/ViewModels/GameBoardViewModel.cs, SlipItIn/Services/GameStateService.cs, SlipItIn/Services/SignalRService.cs, SlipItIn.Server/Hubs/GameHub.cs]
symbols: [LobbyViewModel, GameBoardViewModel, SignalRService, GameStateService, GameHub]
validation_commands: ["dotnet build SlipItIn.slnx"]
---
# Slip & Challenge Workflows
This document outlines the core real-time game workflows in SlipItIn, detailing how client MVVM ViewModels, `WeakReferenceMessenger`, backend SignalR hubs, database services, and offline action queues interact.
These workflows [execute state transitions on](/openwiki/domain/game-mechanics.md) domain entities, [invoke real-time methods in](/openwiki/architecture/overview.md) the ASP.NET Core `GameHub` and client `SignalRService`, [are implemented across backend and client services mapped in](/openwiki/source-map.md) the source map, and [are verified using tests described in](/openwiki/operations/runbook.md) the operations runbook.
---
## 1. Game Setup & Lobby Workflow
```
[Client App] LoginVM.LoginAsync() ──> REST Auth ──> Save Token in SecureStorage ──> SignalR.ConnectAsync()
[Host] LobbyVM.CreateLobbyAsync() ──> SignalR.CreateLobbyAsync() ──> Server Generates Code ──┤
[Player] LobbyVM.JoinLobbyAsync() ──> SignalR.JoinLobbyAsync(code) ──────────────────────────┤
[Player] LobbyVM.SetReadyAsync() ──> SignalR.PlayerReadyAsync() ─────────────────────────────┼──> Broadcast GameStateUpdated
[Host] LobbyVM.StartGameAsync() ──> SignalR.StartGameAsync() ──> Deal 5 Cards / Player ──────┴──> Unicast PlayerHandUpdated
```
1. **Authentication & Session Setup**: Player logs in via `LoginPage` (`LoginViewModel.LoginAsync()`). `ApiService` posts credentials to `/api/auth/login`. `AuthSessionService` persists the token to `SecureStorage`. `SignalRService.ConnectAsync(token)` opens a WebSocket connection to `/hubs/game`.
2. **Lobby Creation**: Host invokes `LobbyViewModel.CreateLobbyAsync()`. `SignalRService.CreateLobbyAsync()` triggers `GameHub.CreateLobby()`. `GameService.CreateGameAsync()` creates a `Game` record (`Status = Lobby`), generates a random 6-character `LobbyCode`, adds host to `game.Players`, and registers host's `ConnectionId`. Caller receives `LobbyCreated` message via `WeakReferenceMessenger`.
3. **Joining Lobby**: Opponents call `LobbyViewModel.JoinLobbyAsync()`. `SignalRService.JoinLobbyAsync(code)` validates game status and capacity. `GameService.JoinGameAsync()` assigns player to game, registers `ConnectionId`, adds player to SignalR group `lobbyCode`, and broadcasts `PlayerJoined` (`GameStateDto`) to all participants.
4. **Player Ready**: Players invoke `LobbyViewModel.SetReadyAsync()`. `GameHub.PlayerReady()` verifies access and broadcasts `GameStateUpdated`.
5. **Game Start & Card Dealing**: The host invokes `LobbyViewModel.StartGameAsync()`.
- `GameHub.StartGame()` verifies host identity (`ValidateHostAccessAsync`).
- `GameService.StartGameAsync()` transitions `GameStatus` to `InProgress` and creates `GameRound` 1 (`RoundStatus.Active`).
- `GameService.DealCardsAsync()` selects 5 random phrases per player, creates `PlayerCard` records, and saves them to PostgreSQL.
- SignalR broadcasts `GameStarted` to the group (triggering client navigation to `GameBoardPage`).
- SignalR sends private `PlayerHandUpdated` notifications (`PlayerHandDto`) individually to each player's `ConnectionId`.
---
## 2. Phrase Slipping & Offline Queueing Workflow
During real-life conversation or chat, a player speaks or types one of their secret phrases and marks it as used in the app:
1. **User Action**: Player clicks **Submit Slip** on `GameBoardPage` (`GameBoardViewModel.SubmitSlipAsync(card)`).
2. **Online / Offline Branching**:
- **If Connected (`_signalR.IsConnected`)**: Calls `SignalRService.SubmitSlipAsync(gameId, playerId, cardId)` directly. `GameHub.SubmitSlip()` validates ownership and updates `PlayerCard.IsUsed = true`. `GameHub` broadcasts `SlipSubmitted` to the lobby group.
- **If Offline (`!_signalR.IsConnected`)**: Calls `GameStateService.EnqueueActionAsync("SubmitSlip", gameId, playerId, cardId)`. Action is serialized into `QueuedGameAction` and saved in `Preferences`. UI updates status to `"Offline: Aktion wurde zwischengespeichert."`
---
## 3. Slip Challenge & Resolution Workflow
If an opponent suspects that a submitted phrase or recent conversation statement was an invalid slip, they can challenge the slipper.
### Step 1: Challenge Initiation
1. Challenger clicks **Challenge** on `GameBoardPage` (`GameBoardViewModel.ChallengeCardAsync(card)`).
2. If online, calls `SignalRService.ChallengeSlipAsync(gameId, playerId, cardId)`.
3. `GameHub.ChallengeSlip()` verifies challenger identity (`ValidatePlayerAccessAsync`).
4. `GameService.CreateChallengeAsync()` creates a `SlipChallenge` entity with `ChallengeStatus.Pending`.
5. `GameHub` broadcasts `SlipChallenged` (`SlipChallengeDto`) to the group and unicasts `ChallengeReceived` directly to target player's `ConnectionId`.
6. Client target player sees `ChallengeNotificationOverlay` on `GameBoardPage`.
### Step 2: Challenge Resolution
The accused player responds via `ChallengeNotificationOverlay`:
1. Accused player calls `GameBoardViewModel.ResolveChallengeAsync(challengeId, approved)`.
2. `SignalRService.ResolveChallengeAsync(challengeId, approved)` calls `GameHub.ResolveChallenge()`.
3. `GameHub` executes `ValidateChallengeTargetAccessAsync(challengeId, authUserId)` to guarantee *only* the accused player can resolve the challenge.
4. `GameService.ResolveChallengeAsync()` executes penalty logic:
- **`approved = true` (Justified Accusation / Legitimate Catch)**: The accused admits the phrase was a fake slip. `SlipChallenge.Status` is set to `Approved`. The card remains with accused player.
- **`approved = false` (False Accusation / Wrong Penalty)**: The accused denies the charge. `SlipChallenge.Status` is set to `Rejected`. As a penalty for making a false accusation, the phrase card is reassigned to the challenger: `targetCard.PlayerId = challenge.ChallengingPlayerId`.
5. `GameHub` broadcasts `ChallengeResolved` to the group.
```mermaid
sequenceDiagram
autonumber
actor Slipper as Accused Player A
actor Challenger as Challenger Player B
participant GameBoardVM as GameBoardViewModel
participant Hub as GameHub
participant Service as GameService
participant DB as SlipItInDbContext
Note over Slipper, Challenger: Active Gameplay
Slipper->>GameBoardVM: SubmitSlipAsync(card)
GameBoardVM->>Hub: SubmitSlip(gameId, playerAId, cardId)
Hub->>Service: SubmitSlipAsync()
Service->>DB: Set PlayerCard.IsUsed = true
Hub-->>Challenger: Broadcast SlipSubmitted
Note over Challenger: Suspects invalid phrase
Challenger->>GameBoardVM: ChallengeCardAsync(card)
GameBoardVM->>Hub: ChallengeSlip(gameId, playerBId, cardId)
Hub->>Service: CreateChallengeAsync()
Service->>DB: Insert SlipChallenge (Status = Pending)
Hub-->>Challenger: Broadcast SlipChallenged
Hub-->>Slipper: Unicast ChallengeReceived
Note over Slipper: Notification Overlay Displays
Slipper->>GameBoardVM: ResolveChallengeAsync(challengeId, approved)
GameBoardVM->>Hub: ResolveChallenge(challengeId, approved)
Hub->>Hub: ValidateChallengeTargetAccessAsync()
Hub->>Service: ResolveChallengeAsync()
alt approved == false (False Accusation)
Service->>DB: Reassign card.PlayerId = PlayerBId
Service->>DB: Set Challenge.Status = Rejected
else approved == true (Justified Catch)
Service->>DB: Set Challenge.Status = Approved
end
Hub-->>Challenger: Broadcast ChallengeResolved
```
*Sequence diagram showing phrase submission, challenge initiation, target notification, and penalty resolution.*
---
## 4. Offline Action Queueing & Reconnection Resync Workflow
When network connection is temporarily interrupted during gameplay, SlipItIn ensures action durability and smooth state synchronization:
```mermaid
sequenceDiagram
autonumber
actor User as Player
participant GameBoardVM as GameBoardViewModel
participant GameStateSvc as GameStateService
participant LocalStorage as LocalStorageService
participant SignalRSvc as SignalRService
participant Hub as GameHub
Note over User, SignalRSvc: Network Disconnected (Offline)
User->>GameBoardVM: SubmitSlipAsync(card)
GameBoardVM->>GameStateSvc: EnqueueActionAsync("SubmitSlip", args)
GameStateSvc->>LocalStorage: SaveQueuedActionsAsync(queuedList)
LocalStorage-->>GameBoardVM: Saved to Preferences
Note over SignalRSvc: Network Restored
SignalRSvc->>Hub: Automatic Reconnect (Exponential Backoff)
SignalRSvc->>GameStateSvc: ConnectionStateChanged(true)
GameStateSvc->>GameStateSvc: ResyncAsync()
GameStateSvc->>SignalRSvc: RequestGameStateAsync(gameId)
SignalRSvc->>Hub: RequestGameState(gameId)
Hub-->>GameStateSvc: GameStateUpdated (Fresh DTO)
GameStateSvc->>GameStateSvc: FlushQueuedActionsAsync()
loop For each QueuedGameAction
GameStateSvc->>SignalRSvc: SendQueuedActionAsync(action)
SignalRSvc->>Hub: InvokeCoreAsync(action.Method, action.Args)
end
GameStateSvc->>LocalStorage: ClearQueuedActionsAsync()
```
*Sequence diagram showing offline action queueing in local preferences, automatic reconnection, state request, and queued action replay.*
---
## 5. SignalR Hub Event Reference
Summary of server-to-client events emitted by `GameHub.cs` and handled by `SignalRService`:
| Event Name | Scope | Payload | Trigger |
|---|---|---|---|
| `LobbyCreated` | Caller | `{ GameId, LobbyCode }` | Host calls `CreateLobby()` |
| `PlayerJoined` | Group | `GameStateDto` | Player calls `JoinLobby()` |
| `GameStateUpdated` | Group | `GameStateDto` | Player calls `PlayerReady()` or `RequestGameState()` |
| `GameStarted` | Group | `{ GameId }` | Host calls `StartGame()` |
| `PlayerHandUpdated` | Unicast | `PlayerHandDto` | Hand dealt or updated |
| `SlipSubmitted` | Group | `{ PlayerId, CardId, Success }` | Player calls `SubmitSlip()` |
| `SlipChallenged` | Group | `SlipChallengeDto` | Opponent calls `ChallengeSlip()` |
| `ChallengeReceived` | Unicast | `SlipChallengeDto` | Direct alert to target player |
| `ChallengeResolved` | Group | `SlipChallengeDto` | Target calls `ResolveChallenge()` |
| `Error` | Caller | `{ Message }` | Any authorization or engine exception |
---
## 6. Guidance for Future Agents & Developers
- **When to Consult**: Refer to this document when adding new game interactions, modifying UI challenge flows, changing reconnection strategies, or replaying offline actions.
- **Invariants**:
- `FlushQueuedActionsAsync()` MUST execute in strictly sequential order based on `CreatedAtUtc`.
- `ResolveChallenge` MUST throw `UnauthorizedAccessException` if invoked by anyone other than the target player.
- **Primary Source Files**:
- `SlipItIn/ViewModels/GameBoardViewModel.cs`
- `SlipItIn/Services/GameStateService.cs`
- `SlipItIn/Services/SignalRService.cs`
- `SlipItIn.Server/Hubs/GameHub.cs`
- **Minimal Validation Command**:
```bash
dotnet build SlipItIn.slnx
```