update planning
This commit is contained in:
parent
fab82fcde8
commit
7a96cf0cc4
|
|
@ -840,9 +840,12 @@ Policy: `@attribute [Authorize(Policy = "ClubViewer|ClubEditor|ClubAdmin")]`
|
||||||
|
|
||||||
## Übersicht
|
## Übersicht
|
||||||
|
|
||||||
Integration des Game-Management-Systems aus KoogleApp in Koogle.Web mit Clean Architecture.
|
Implementierung des Game-Management-Systems in Koogle.Web mit Clean Architecture.
|
||||||
|
Code wird komplett neu geschrieben, KoogleApp dient nur als Referenz.
|
||||||
Spiele laufen im Speicher auf der DayDetails-Seite ohne Browser-Reload.
|
Spiele laufen im Speicher auf der DayDetails-Seite ohne Browser-Reload.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Geklärte Anforderungen
|
## Geklärte Anforderungen
|
||||||
|
|
||||||
| Aspekt | Entscheidung |
|
| Aspekt | Entscheidung |
|
||||||
|
|
@ -853,7 +856,14 @@ Spiele laufen im Speicher auf der DayDetails-Seite ohne Browser-Reload.
|
||||||
| Persistenz | **Nach jedem Wurf** in DB speichern (Game.GameData) |
|
| Persistenz | **Nach jedem Wurf** in DB speichern (Game.GameData) |
|
||||||
| Multi-User | Spielfortschritt in anderen Sessions via **SignalR** sichtbar |
|
| Multi-User | Spielfortschritt in anderen Sessions via **SignalR** sichtbar |
|
||||||
| Scheiss-Spiel Ende | Erster mit 0 Punkten **gewinnt**, andere bekommen Strafen nach Restpunkten |
|
| Scheiss-Spiel Ende | Erster mit 0 Punkten **gewinnt**, andere bekommen Strafen nach Restpunkten |
|
||||||
| Strafen-Berechnung | Fixbetrag pro Restpunkt (z.B. 0.10€ × 30 Punkte = 3.00€) |
|
| Strafen-Berechnung | Trigger `ExpenseTriggerType.ExpensePoint` mit Multiplikator = Restpunkte |
|
||||||
|
| SignalR | Komplett neu aufsetzen + **Auto-Reconnect** |
|
||||||
|
| Port-Strategie | **Komplett neu schreiben**, KoogleApp nur als Referenz |
|
||||||
|
| Concurrency | **RowVersion** auf Game Entity für Optimistic Concurrency |
|
||||||
|
| Undo-Tiefe | **Unbegrenzt** (komplette Spiel-Historie) |
|
||||||
|
| Testing | Nur Kern-Logik (GameLogicService) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Design-Entscheidungen
|
## Design-Entscheidungen
|
||||||
|
|
||||||
|
|
@ -862,10 +872,13 @@ Spiele laufen im Speicher auf der DayDetails-Seite ohne Browser-Reload.
|
||||||
| State-Speicherung | Separater `GameState` (nicht in DayState) |
|
| State-Speicherung | Separater `GameState` (nicht in DayState) |
|
||||||
| View-Wechsel | ViewMode-Enum + MudTabs (kein URL-Routing) |
|
| View-Wechsel | ViewMode-Enum + MudTabs (kein URL-Routing) |
|
||||||
| Spieltyp-System | IGameDefinition in Domain, Registry in Application |
|
| Spieltyp-System | IGameDefinition in Domain, Registry in Application |
|
||||||
| Pin-Komponenten | Port aus KoogleApp mit neuem Namespace |
|
| Pin-Komponenten | **Neu schreiben** (nicht portieren) |
|
||||||
| Persistenz | **Nach jedem Wurf** → DB (Game.GameData JSON) |
|
| Persistenz | **Nach jedem Wurf** → DB (Game.GameData JSON) |
|
||||||
| Recovery | Bei Page-Load: aktives Spiel aus DB laden |
|
| Recovery | Bei Page-Load: aktives Spiel aus DB laden |
|
||||||
| Multi-User Sync | **SignalR** für Echtzeit-Updates (Live-Tafel) |
|
| Multi-User Sync | **SignalR** mit Auto-Reconnect für Echtzeit-Updates |
|
||||||
|
| Strafen-Engine | **ITriggerService** für automatische Strafen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Game Entity (erweitern)
|
## Game Entity (erweitern)
|
||||||
|
|
||||||
|
|
@ -885,11 +898,14 @@ public class Game : BaseEntity
|
||||||
public GameStatus Status { get; set; } // Active, Completed, Aborted
|
public GameStatus Status { get; set; } // Active, Completed, Aborted
|
||||||
public DateTime? StartedAt { get; set; }
|
public DateTime? StartedAt { get; set; }
|
||||||
public DateTime? CompletedAt { get; set; }
|
public DateTime? CompletedAt { get; set; }
|
||||||
|
public byte[] RowVersion { get; set; } // Optimistic Concurrency
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum GameStatus { Active, Completed, Aborted }
|
public enum GameStatus { Active, Completed, Aborted }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## GameData JSON Struktur (Beispiel Training)
|
## GameData JSON Struktur (Beispiel Training)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|
@ -910,40 +926,90 @@ public enum GameStatus { Active, Completed, Aborted }
|
||||||
"guid2": { "throwCount": 21, "pinCount": 138, "circleCount": 1 }
|
"guid2": { "throwCount": 21, "pinCount": 138, "circleCount": 1 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"undoStack": [ ... ]
|
"undoStack": [ /* Unbegrenzte Historie */ ]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Architektur
|
## Architektur
|
||||||
|
|
||||||
```
|
```
|
||||||
Domain: GameType Enum, IGameDefinition Interface
|
Domain: GameType Enum, GameStatus Enum, IGameDefinition Interface
|
||||||
Application: Games/ mit GameProgress, IGameLogicService, Training/, Shit/
|
Application: Games/ mit GameProgress, IGameLogicService, ITriggerService, Training/, Shit/
|
||||||
Web: Store/GameState/, Components/Game/
|
Infrastructure: TriggerRepository, GameRepository
|
||||||
|
Web: Store/GameState/, Components/Game/, Hubs/GameHub
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Umsetzungsreihenfolge Phase 2
|
## Umsetzungsreihenfolge Phase 2
|
||||||
|
|
||||||
| ☐ | Phase | Bereich | Beschreibung | Dateien |
|
| ☐ | Phase | Bereich | Beschreibung | Dateien |
|
||||||
|---|-------|---------|--------------|---------|
|
|---|-------|---------|--------------|---------|
|
||||||
| ☐ | H1 | Foundation | GameState Fluxor | 5 |
|
| ☐ | **H0** | **Trigger-Engine** | **ITriggerService + Repository** | **5** |
|
||||||
| ☐ | H2 | Components | Pin Input Components | 5 |
|
| ☐ | H1 | Foundation | GameState Fluxor + Enums | 6 |
|
||||||
|
| ☐ | H2 | Components | Pin Input Components (neu schreiben) | 5 |
|
||||||
| ☐ | H3 | Framework | Game Definition Framework | 5 |
|
| ☐ | H3 | Framework | Game Definition Framework | 5 |
|
||||||
| ☐ | H4 | Games | Training Game | 5 |
|
| ☐ | H4 | Games | Training Game | 5 |
|
||||||
| ☐ | H5 | Games | Scheiss-Spiel | 5 |
|
| ☐ | H5 | Games | Scheiss-Spiel + Trigger-Integration | 6 |
|
||||||
| ☐ | H6 | UI | Game Setup Dialog | 4 |
|
| ☐ | H6 | UI | Game Setup Dialog | 4 |
|
||||||
| ☐ | H7 | Integration | DayDetails Integration | 3 |
|
| ☐ | H7 | Integration | DayDetails Tabs | 3 |
|
||||||
| ☐ | H8 | Features | Undo/Redo | 3 |
|
| ☐ | H8 | Features | Undo (unbegrenzt, ohne Redo) | 2 |
|
||||||
| ☐ | H9 | Persistenz | DB Persistence & Recovery | 4 |
|
| ☐ | H9 | Persistenz | DB Persistence & Recovery | 4 |
|
||||||
| ☐ | H9b | Sync | SignalR Live-Updates | 4 |
|
| ☐ | H9b | Sync | SignalR komplett + Auto-Reconnect | 5 |
|
||||||
| ☐ | H10 | Testing | Game Tests | 3 |
|
| ☐ | H10 | Testing | Kern-Logik Tests | 2 |
|
||||||
|
|
||||||
**Geschätzte Dateien: ~48 neue/modifizierte**
|
**Geschätzte Dateien: ~52 neue/modifizierte**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Detaillierte Phasen
|
## Detaillierte Phasen
|
||||||
|
|
||||||
|
### **Phase H0: Trigger-Engine (NEU)**
|
||||||
|
|
||||||
|
Die Trigger-Engine wird für automatische Strafen benötigt (Scheiss-Spiel ist nur ein Beispiel, alle Spiele lösen Trigger für Strafen aus).
|
||||||
|
|
||||||
|
**Dateien:**
|
||||||
|
1. `src/Koogle.Domain/Interfaces/ITriggerRepository.cs`
|
||||||
|
2. `src/Koogle.Infrastructure/Repositories/TriggerRepository.cs`
|
||||||
|
3. `src/Koogle.Application/Interfaces/ITriggerService.cs`
|
||||||
|
4. `src/Koogle.Application/Services/TriggerService.cs`
|
||||||
|
5. `src/Koogle.Application/DTOs/TriggerDto.cs`
|
||||||
|
|
||||||
|
**ITriggerService:**
|
||||||
|
```csharp
|
||||||
|
public interface ITriggerService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all expenses linked to a trigger type for the current club.
|
||||||
|
/// </summary>
|
||||||
|
Task<IEnumerable<ExpenseDto>> GetExpensesForTriggerAsync(ExpenseTriggerType type);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fires a trigger, creating PersonExpenses for linked expenses.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The trigger type (e.g., ExpensePoint)</param>
|
||||||
|
/// <param name="personId">Person receiving the expense</param>
|
||||||
|
/// <param name="dayId">Day context</param>
|
||||||
|
/// <param name="gameId">Optional game context</param>
|
||||||
|
/// <param name="multiplier">Multiplier for expense price (e.g., remaining points)</param>
|
||||||
|
Task FireTriggerAsync(
|
||||||
|
ExpenseTriggerType type,
|
||||||
|
Guid personId,
|
||||||
|
Guid dayId,
|
||||||
|
Guid? gameId = null,
|
||||||
|
int multiplier = 1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logik FireTriggerAsync:**
|
||||||
|
1. Hole alle Expenses die mit Trigger-Typ verknüpft sind (via ExpenseTrigger Join-Table)
|
||||||
|
2. Für jede Expense: Erstelle PersonExpense mit `Price = Expense.Price × multiplier`
|
||||||
|
3. multiplier = Restpunkte, z.B. im Scheiss-Spiel
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### **Phase H1: GameState Foundation**
|
### **Phase H1: GameState Foundation**
|
||||||
|
|
||||||
**Dateien:**
|
**Dateien:**
|
||||||
|
|
@ -952,6 +1018,7 @@ Web: Store/GameState/, Components/Game/
|
||||||
3. `src/Koogle.Web/Store/GameState/GameReducers.cs`
|
3. `src/Koogle.Web/Store/GameState/GameReducers.cs`
|
||||||
4. `src/Koogle.Web/Store/GameState/GameEffects.cs`
|
4. `src/Koogle.Web/Store/GameState/GameEffects.cs`
|
||||||
5. `src/Koogle.Domain/Enums/GameType.cs`
|
5. `src/Koogle.Domain/Enums/GameType.cs`
|
||||||
|
6. `src/Koogle.Domain/Enums/PinStatus.cs`
|
||||||
|
|
||||||
**State-Struktur:**
|
**State-Struktur:**
|
||||||
```csharp
|
```csharp
|
||||||
|
|
@ -965,10 +1032,11 @@ public record GameState
|
||||||
public ThrowPanelState ThrowPanel { get; init; }
|
public ThrowPanelState ThrowPanel { get; init; }
|
||||||
public ParticipantsState Participants { get; init; }
|
public ParticipantsState Participants { get; init; }
|
||||||
public object? GameModel { get; init; } // JSON-serializable game-specific data
|
public object? GameModel { get; init; } // JSON-serializable game-specific data
|
||||||
public UndoRedoState UndoRedo { get; init; }
|
public ImmutableList<GameSnapshot> UndoStack { get; init; } // Unbegrenzt
|
||||||
public IReadOnlyList<GameSummaryDto> CompletedGames { get; init; }
|
public IReadOnlyList<GameSummaryDto> CompletedGames { get; init; }
|
||||||
public bool IsLoading { get; init; }
|
public bool IsLoading { get; init; }
|
||||||
public string? Error { get; init; }
|
public string? Error { get; init; }
|
||||||
|
public bool IsConcurrencyConflict { get; init; } // RowVersion conflict
|
||||||
}
|
}
|
||||||
|
|
||||||
public record ThrowPanelState(
|
public record ThrowPanelState(
|
||||||
|
|
@ -988,13 +1056,19 @@ public record ParticipantsState(
|
||||||
int CurrentPlayerIndex,
|
int CurrentPlayerIndex,
|
||||||
ParticipantsMode Mode
|
ParticipantsMode Mode
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public record GameSnapshot(
|
||||||
|
ThrowPanelState ThrowPanel,
|
||||||
|
ParticipantsState Participants,
|
||||||
|
object GameModel
|
||||||
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### **Phase H2: Pin Input Components**
|
### **Phase H2: Pin Input Components**
|
||||||
|
|
||||||
**Dateien (Port aus KoogleApp):**
|
**Dateien (NEU SCHREIBEN):**
|
||||||
1. `src/Koogle.Web/Components/Game/Pin.razor`
|
1. `src/Koogle.Web/Components/Game/Pin.razor`
|
||||||
2. `src/Koogle.Web/Components/Game/PinPanel.razor`
|
2. `src/Koogle.Web/Components/Game/PinPanel.razor`
|
||||||
3. `src/Koogle.Web/Components/Game/NumberPanel.razor`
|
3. `src/Koogle.Web/Components/Game/NumberPanel.razor`
|
||||||
|
|
@ -1041,11 +1115,27 @@ public interface IGameDefinition
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**GameProgress (Port):**
|
**GameProgress:**
|
||||||
- BeforeThrowState, AfterThrowState
|
- BeforeThrowState, AfterThrowState Records
|
||||||
- PinCount(), IsCircle() Extension Methods
|
- PinCount(), IsCircle() Extension Methods
|
||||||
- NextRound(), EndOfGame() Helpers
|
- NextRound(), EndOfGame() Helpers
|
||||||
|
|
||||||
|
**GameModelFactory (für Polymorphie):**
|
||||||
|
```csharp
|
||||||
|
public static class GameModelFactory
|
||||||
|
{
|
||||||
|
public static object Deserialize(string json, string gameType)
|
||||||
|
{
|
||||||
|
return gameType switch
|
||||||
|
{
|
||||||
|
"Training" => JsonSerializer.Deserialize<TrainingGameModel>(json),
|
||||||
|
"Shit" => JsonSerializer.Deserialize<ShitGameModel>(json),
|
||||||
|
_ => throw new NotSupportedException($"Unknown game type: {gameType}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### **Phase H4: Training Game**
|
### **Phase H4: Training Game**
|
||||||
|
|
@ -1085,7 +1175,7 @@ public record PlayerStats
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### **Phase H5: Scheiss-Spiel**
|
### **Phase H5: Scheiss-Spiel + Trigger-Integration**
|
||||||
|
|
||||||
**Dateien:**
|
**Dateien:**
|
||||||
1. `src/Koogle.Application/Games/Shit/ShitGameDefinition.cs`
|
1. `src/Koogle.Application/Games/Shit/ShitGameDefinition.cs`
|
||||||
|
|
@ -1093,6 +1183,7 @@ public record PlayerStats
|
||||||
3. `src/Koogle.Application/Games/Shit/ShitGameLogicService.cs`
|
3. `src/Koogle.Application/Games/Shit/ShitGameLogicService.cs`
|
||||||
4. `src/Koogle.Web/Components/Game/Shit/ShitSetup.razor`
|
4. `src/Koogle.Web/Components/Game/Shit/ShitSetup.razor`
|
||||||
5. `src/Koogle.Web/Components/Game/Shit/ShitBoard.razor`
|
5. `src/Koogle.Web/Components/Game/Shit/ShitBoard.razor`
|
||||||
|
6. Integration mit ITriggerService
|
||||||
|
|
||||||
**ShitGameModel:**
|
**ShitGameModel:**
|
||||||
```csharp
|
```csharp
|
||||||
|
|
@ -1100,7 +1191,6 @@ public record ShitGameModel
|
||||||
{
|
{
|
||||||
public int ShitNumber { get; init; } // 1-9 (Scheiss-Zahl)
|
public int ShitNumber { get; init; } // 1-9 (Scheiss-Zahl)
|
||||||
public int StartNumber { get; init; } // 10-1000 (Startzahl)
|
public int StartNumber { get; init; } // 10-1000 (Startzahl)
|
||||||
public decimal PricePerPoint { get; init; } // 0.01-1.00 (€ pro Restpunkt)
|
|
||||||
public Dictionary<Guid, int> PlayerPoints { get; init; }
|
public Dictionary<Guid, int> PlayerPoints { get; init; }
|
||||||
public int CollectedPoints { get; set; }
|
public int CollectedPoints { get; set; }
|
||||||
public Guid? WinnerId { get; set; }
|
public Guid? WinnerId { get; set; }
|
||||||
|
|
@ -1109,15 +1199,15 @@ public record ShitGameModel
|
||||||
|
|
||||||
**Spiellogik:**
|
**Spiellogik:**
|
||||||
1. Spieler wirft
|
1. Spieler wirft
|
||||||
2. Wenn Scheiss-Zahl oder Rinne → gesammelte Punkte werden vom Konto abgezogen, nächster Spieler
|
2. Wenn Scheiss-Zahl oder Rinne → bis dahin gesammelte Punkte werden zum Konto addiert, nächster Spieler
|
||||||
3. Sonst → Punkte werden gesammelt (addiert)
|
3. Sonst → Punkte werden gesammelt und subtrahiert, denn der Spieler entscheidet die Punkte mitzunehmenan den nächsten Spieler zu übergeben (subtrahieren). Spiel-Spezifischer "Weiter-Button" wird benötigt.
|
||||||
4. Erster mit 0 Punkten → **GEWINNER**
|
4. Erster mit 0 Punkten → **GEWINNER**
|
||||||
5. Spiel endet, alle anderen bekommen Strafe: `Restpunkte × PricePerPoint`
|
5. Spiel endet, für ein Spieler 0 erreicht hat. Alle danderen sind Verlierer: `ITriggerService.FireTriggerAsync(ExpensePoint, personId, dayId, gameId, restpunkte)`
|
||||||
|
|
||||||
**ShitSetup.razor:**
|
**ShitSetup.razor:**
|
||||||
- Scheiss-Zahl (1-9, Default: 5)
|
- Scheiss-Zahl (1-9, Default: 5)
|
||||||
- Start-Zahl (10-1000, Default: 50)
|
- Start-Zahl (10-1000, Default: 50)
|
||||||
- **Betrag pro Punkt** (0.01€ - 1.00€, Default: 0.10€)
|
- Warnung wenn kein ExpensePoint-Trigger konfiguriert
|
||||||
|
|
||||||
**ShitBoard.razor (Tafel):**
|
**ShitBoard.razor (Tafel):**
|
||||||
| Spieler | Punkte | Status |
|
| Spieler | Punkte | Status |
|
||||||
|
|
@ -1187,17 +1277,17 @@ public record ShitGameModel
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### **Phase H8: Undo/Redo**
|
### **Phase H8: Undo (Unbegrenzt)**
|
||||||
|
|
||||||
**Dateien:**
|
**Dateien:**
|
||||||
1. `src/Koogle.Web/Store/GameState/UndoRedoState.cs`
|
1. `src/Koogle.Web/Store/GameState/GameState.cs` (UndoStack bereits definiert)
|
||||||
2. `src/Koogle.Web/Store/GameState/UndoRedoActions.cs`
|
2. `src/Koogle.Web/Components/Game/UndoButton.razor`
|
||||||
3. `src/Koogle.Web/Components/Game/UndoRedoButtons.razor`
|
|
||||||
|
|
||||||
**Logic:**
|
**Logic:**
|
||||||
- Jeder Wurf erstellt Snapshot
|
- Jeder Wurf erstellt GameSnapshot → auf UndoStack pushen
|
||||||
- Stack für Undo (max 20 Einträge)
|
- Stack ist **unbegrenzt** (komplette Spiel-Historie)
|
||||||
- "Letzten Wurf rückgängig" Button
|
- "Letzten Wurf rückgängig" Button
|
||||||
|
- Bei Undo: Pop vom Stack, State wiederherstellen
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -1218,8 +1308,9 @@ public record ShitGameModel
|
||||||
```
|
```
|
||||||
1. Spiel starten → Game Entity mit Status=Active erstellen
|
1. Spiel starten → Game Entity mit Status=Active erstellen
|
||||||
2. Nach jedem Wurf:
|
2. Nach jedem Wurf:
|
||||||
- GameState → JSON serialisieren
|
- GameState → JSON serialisieren (inkl. UndoStack)
|
||||||
- Game.GameData updaten (Debounced, max 1x/500ms)
|
- Game.GameData updaten (Debounced, max 1x/500ms)
|
||||||
|
- RowVersion prüfen → bei Konflikt: IsConcurrencyConflict = true
|
||||||
3. Page Reload / Andere Session:
|
3. Page Reload / Andere Session:
|
||||||
- LoadActiveGameAsync(dayId)
|
- LoadActiveGameAsync(dayId)
|
||||||
- Wenn Active Game existiert → GameState wiederherstellen
|
- Wenn Active Game existiert → GameState wiederherstellen
|
||||||
|
|
@ -1228,15 +1319,39 @@ public record ShitGameModel
|
||||||
- Finales GameData speichern
|
- Finales GameData speichern
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Concurrency-Handling:**
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (DbUpdateConcurrencyException)
|
||||||
|
{
|
||||||
|
// Reload aktuellen State, UI informieren
|
||||||
|
dispatcher.Dispatch(new ConcurrencyConflictAction(gameId));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### **Phase H9b: SignalR Live-Updates**
|
### **Phase H9b: SignalR Live-Updates (Komplett)**
|
||||||
|
|
||||||
**Dateien:**
|
**Dateien:**
|
||||||
1. `src/Koogle.Web/Hubs/GameHub.cs`
|
1. `src/Koogle.Web/Hubs/GameHub.cs`
|
||||||
2. `src/Koogle.Web/Hubs/IGameHubClient.cs`
|
2. `src/Koogle.Web/Hubs/IGameHubClient.cs`
|
||||||
3. `src/Koogle.Web/Store/GameState/GameEffects.cs` (erweitern für Broadcast)
|
3. `src/Koogle.Web/Services/GameHubService.cs` (Wrapper)
|
||||||
4. `src/Koogle.Web/Program.cs` (AddSignalR, MapHub)
|
4. `src/Koogle.Web/Store/GameState/GameEffects.cs` (erweitern für Broadcast)
|
||||||
|
5. `src/Koogle.Web/Program.cs` (AddSignalR, MapHub)
|
||||||
|
|
||||||
|
**Program.cs Erweiterung:**
|
||||||
|
```csharp
|
||||||
|
// Services
|
||||||
|
builder.Services.AddSignalR();
|
||||||
|
builder.Services.AddScoped<GameHubService>();
|
||||||
|
|
||||||
|
// Endpoints
|
||||||
|
app.MapHub<GameHub>("/gamehub");
|
||||||
|
```
|
||||||
|
|
||||||
**SignalR Hub:**
|
**SignalR Hub:**
|
||||||
```csharp
|
```csharp
|
||||||
|
|
@ -1253,68 +1368,74 @@ public interface IGameHubClient
|
||||||
{
|
{
|
||||||
Task GameStateUpdated(GameStateDto state);
|
Task GameStateUpdated(GameStateDto state);
|
||||||
Task GameEnded(GameResultDto result);
|
Task GameEnded(GameResultDto result);
|
||||||
|
Task ConcurrencyConflict(Guid gameId);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Auto-Reconnect (Client-Side):**
|
||||||
|
```csharp
|
||||||
|
_hubConnection = new HubConnectionBuilder()
|
||||||
|
.WithUrl(Navigation.ToAbsoluteUri("/gamehub"))
|
||||||
|
.WithAutomaticReconnect(new[] {
|
||||||
|
TimeSpan.Zero,
|
||||||
|
TimeSpan.FromSeconds(2),
|
||||||
|
TimeSpan.FromSeconds(10),
|
||||||
|
TimeSpan.FromSeconds(30)
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_hubConnection.Reconnected += async (connectionId) =>
|
||||||
|
{
|
||||||
|
// Re-join active game
|
||||||
|
if (GameState.Value.ActiveGameId.HasValue)
|
||||||
|
{
|
||||||
|
await _hubConnection.InvokeAsync("JoinGame", GameState.Value.ActiveGameId.Value);
|
||||||
|
// Reload current state from DB
|
||||||
|
Dispatcher.Dispatch(new LoadActiveGameAction(GameState.Value.DayId!.Value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
**Flow:**
|
**Flow:**
|
||||||
1. Tafel-View öffnen → `JoinGame(gameId)`
|
1. Tafel-View öffnen → `JoinGame(gameId)`
|
||||||
2. Nach jedem Wurf: DB Save → `Clients.Group(gameId).GameStateUpdated(state)`
|
2. Nach jedem Wurf: DB Save → `Clients.Group(gameId).GameStateUpdated(state)`
|
||||||
3. Andere Sessions empfangen Update → Reducer aktualisiert State
|
3. Andere Sessions empfangen Update → Reducer aktualisiert State
|
||||||
4. Tafel-View schließen → `LeaveGame(gameId)`
|
4. Bei Reconnect → State aus DB laden
|
||||||
|
5. Tafel-View schließen → `LeaveGame(gameId)`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### **Phase H10: Testing**
|
### **Phase H10: Testing (Kern-Logik)**
|
||||||
|
|
||||||
**Dateien:**
|
**Dateien:**
|
||||||
1. `test/Koogle.Tests/Application/Games/TrainingGameLogicServiceTests.cs`
|
1. `test/Koogle.Tests/Application/Games/TrainingGameLogicServiceTests.cs`
|
||||||
2. `test/Koogle.Tests/Application/Games/ShitGameLogicServiceTests.cs`
|
2. `test/Koogle.Tests/Application/Games/ShitGameLogicServiceTests.cs`
|
||||||
3. `test/Koogle.Tests/Web/GameStateReducersTests.cs`
|
|
||||||
|
|
||||||
**Test-Cases:**
|
**Test-Cases:**
|
||||||
- Pin-Count Berechnung
|
- Pin-Count Berechnung (0-9 Kegel)
|
||||||
- Kranz-Erkennung (8 äußere + Mitte steht)
|
- Kranz-Erkennung (8 äußere + Mitte steht = 8 Punkte)
|
||||||
- Scheiss-Spiel: Gewinner bei 0
|
- Scheiss-Spiel: Gewinner bei 0 Punkten
|
||||||
- Scheiss-Spiel: Strafen-Berechnung
|
- Scheiss-Spiel: Korrekte Punkte-Abzüge
|
||||||
- Reducer: State-Transformationen
|
- Training: Statistik-Updates nach Wurf
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Scheiss-Spiel Spielende-Logik
|
|
||||||
|
|
||||||
```
|
|
||||||
1. Spieler erreicht 0 Punkte → GEWINNER (WinnerId setzen)
|
|
||||||
2. Spiel endet automatisch (IsGameActive = false)
|
|
||||||
3. Für jeden Verlierer:
|
|
||||||
- Strafe = Restpunkte × Betrag_pro_Punkt
|
|
||||||
- PersonExpense erstellen (ExpenseType: GamePenalty)
|
|
||||||
4. Game.GameData speichern (JSON mit Ergebnis)
|
|
||||||
5. CompletedGames-Liste aktualisieren
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Kritische Dateien (KoogleApp → Koogle.Web Port)
|
|
||||||
|
|
||||||
| Quelle (KoogleApp) | Ziel (Koogle.Web) |
|
|
||||||
|--------------------|-------------------|
|
|
||||||
| `src/KoogleApp/Games/GameProgress.cs` | `src/Koogle.Application/Games/GameProgress.cs` |
|
|
||||||
| `src/KoogleApp/Store/Game/ThrowPanel/State.cs` | In `GameState.cs` integriert |
|
|
||||||
| `src/KoogleApp/Games/Training/GameTrainingService.cs` | `src/Koogle.Application/Games/Training/TrainingGameLogicService.cs` |
|
|
||||||
| `src/KoogleApp/Components/Controls/PinPanel.razor` | `src/Koogle.Web/Components/Game/PinPanel.razor` |
|
|
||||||
| `src/KoogleApp/Components/Controls/NumberPanel.razor` | `src/Koogle.Web/Components/Game/NumberPanel.razor` |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Migration
|
## Migration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Game Entity erweitern: GameType, Status, StartedAt, CompletedAt
|
# Game Entity erweitern: GameType, Status, StartedAt, CompletedAt, RowVersion
|
||||||
dotnet ef migrations add ExtendGameEntity --project src/Koogle.Infrastructure --startup-project src/Koogle.Web --context AppDbContext --output-dir Data/Migrations
|
dotnet ef migrations add ExtendGameEntity --project src/Koogle.Infrastructure --startup-project src/Koogle.Web --context AppDbContext --output-dir Data/Migrations
|
||||||
|
|
||||||
dotnet ef database update -p src/Koogle.Infrastructure -s src/Koogle.Web --context AppDbContext
|
dotnet ef database update -p src/Koogle.Infrastructure -s src/Koogle.Web --context AppDbContext
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**GameConfiguration.cs erweitern:**
|
||||||
|
```csharp
|
||||||
|
builder.Property(g => g.RowVersion)
|
||||||
|
.IsRowVersion()
|
||||||
|
.IsConcurrencyToken();
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Berechtigungen Phase 2
|
## Berechtigungen Phase 2
|
||||||
|
|
@ -1325,15 +1446,32 @@ dotnet ef database update -p src/Koogle.Infrastructure -s src/Koogle.Web --conte
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Risiken & Mitigationen
|
||||||
|
|
||||||
|
| Risiko | Mitigation |
|
||||||
|
|--------|------------|
|
||||||
|
| Polymorphe GameModel-Serialisierung | GameModelFactory mit switch über GameType |
|
||||||
|
| SignalR + Blazor Server | Separater Hub `/gamehub`, keine Interferenz |
|
||||||
|
| Concurrent Updates | RowVersion + UI-Konfliktwarnung |
|
||||||
|
| Fehlende Trigger-Stammdaten | Warnung in ShitSetup wenn kein ExpensePoint-Trigger |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Zusammenfassung Phase 2
|
## Zusammenfassung Phase 2
|
||||||
|
|
||||||
**11 Phasen (H1-H10 + H9b)** → **~48 Dateien** → **Detaillierte Spielverwaltung**
|
**12 Phasen (H0-H10 inkl. H9b)** → **~52 Dateien** → **Detaillierte Spielverwaltung**
|
||||||
|
|
||||||
**Kernfeatures:**
|
**Kernfeatures:**
|
||||||
- Spiele mit Wurf-für-Wurf Eingabe
|
- Spiele mit Wurf-für-Wurf Eingabe
|
||||||
- DB-Persistenz nach jedem Wurf
|
- Trigger-Engine für automatische Strafen
|
||||||
- SignalR Live-Updates für Multi-User
|
- DB-Persistenz nach jedem Wurf mit RowVersion
|
||||||
|
- SignalR Live-Updates + Auto-Reconnect für Multi-User
|
||||||
- Recovery bei Page-Reload
|
- Recovery bei Page-Reload
|
||||||
- Undo/Redo für Wurf-Korrekturen
|
- Unbegrenzte Undo-Historie
|
||||||
|
|
||||||
Bereit für Implementierung Phase H1
|
**Abhängigkeiten zu bestehenden Entities:**
|
||||||
|
- ExpenseTrigger + Trigger (bereits vorhanden)
|
||||||
|
- ExpenseTriggerType.ExpensePoint (bereits vorhanden)
|
||||||
|
- PersonExpense mit GameId (bereits vorhanden)
|
||||||
|
|
||||||
|
Bereit für Implementierung Phase H0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue