198 lines
12 KiB
Markdown
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
|
|
```
|