game planning

This commit is contained in:
beo3000 2025-12-26 10:43:14 +01:00
parent e2fe8b723a
commit d2ee1c5193
1 changed files with 415 additions and 3 deletions

View File

@ -828,8 +828,420 @@ Policy: `@attribute [Authorize(Policy = "ClubViewer|ClubEditor|ClubAdmin")]`
---
## Zusammenfassung
## Zusammenfassung Phase 1
**23 feine Phasen** → **~75 Dateien** → **MVP Phase 1 komplett**
**23 feine Phasen** → **~75 Dateien** → **MVP Phase 1 komplett**
Bereit für Implementierung Phase A1
---
---
# IMPLEMENTIERUNGSPLAN - Phase 2: Detaillierte Spielverwaltung
## Übersicht
Integration des Game-Management-Systems aus KoogleApp in Koogle.Web mit Clean Architecture.
Spiele laufen im Speicher auf der DayDetails-Seite ohne Browser-Reload.
## Geklärte Anforderungen
| Aspekt | Entscheidung |
|--------|--------------|
| Spiel-Ende | Auto-Ende + manuelle "Beenden" Option |
| Strafen-Schnellwahl | Nur im Details-Tab, nicht in Eingabe/Tafel |
| Multi-Game | Sequentiell - mehrere Spiele pro Tag, Liste sichtbar |
| Persistenz | JSON in `Game.GameData` nach Spielende speichern |
| 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€) |
## Design-Entscheidungen
| Entscheidung | Lösung |
|--------------|--------|
| State-Speicherung | Separater `GameState` (nicht in DayState) |
| View-Wechsel | ViewMode-Enum + MudTabs (kein URL-Routing) |
| Spieltyp-System | IGameDefinition in Domain, Registry in Application |
| Pin-Komponenten | Port aus KoogleApp mit neuem Namespace |
| Persistenz | In-Memory während Spiel → DB (Game.GameData) bei Ende |
## Existierende Entity
```csharp
// Koogle.Domain/Entities/Game.cs - bereits vorhanden
public class Game : BaseEntity
{
public string GameData { get; set; } = "{}"; // JSON für Spielergebnis
public Guid DayId { get; set; }
public Day Day { get; set; }
public ICollection<GamePerson> GamePersons { get; set; }
public Guid ClubId { get; set; }
public Club Club { get; set; }
}
// → GameType Property hinzufügen (string oder Enum)
```
## Architektur
```
Domain: GameType Enum, IGameDefinition Interface
Application: Games/ mit GameProgress, IGameLogicService, Training/, Shit/
Web: Store/GameState/, Components/Game/
```
## Umsetzungsreihenfolge Phase 2
| ☐ | Phase | Bereich | Beschreibung | Dateien |
|---|-------|---------|--------------|---------|
| ☐ | H1 | Foundation | GameState Fluxor | 5 |
| ☐ | H2 | Components | Pin Input Components | 5 |
| ☐ | H3 | Framework | Game Definition Framework | 5 |
| ☐ | H4 | Games | Training Game | 5 |
| ☐ | H5 | Games | Scheiss-Spiel | 5 |
| ☐ | H6 | UI | Game Setup Dialog | 4 |
| ☐ | H7 | Integration | DayDetails Integration | 3 |
| ☐ | H8 | Features | Undo/Redo | 3 |
| ☐ | H9 | Features | Session Persistence | 2 |
| ☐ | H10 | Testing | Game Tests | 3 |
**Geschätzte Dateien: ~42 neue/modifizierte**
---
## Detaillierte Phasen
### **Phase H1: GameState Foundation**
**Dateien:**
1. `src/Koogle.Web/Store/GameState/GameState.cs`
2. `src/Koogle.Web/Store/GameState/GameActions.cs`
3. `src/Koogle.Web/Store/GameState/GameReducers.cs`
4. `src/Koogle.Web/Store/GameState/GameEffects.cs`
5. `src/Koogle.Domain/Enums/GameType.cs`
**State-Struktur:**
```csharp
[FeatureState]
public record GameState
{
public bool IsGameActive { get; init; }
public string? GameTypeName { get; init; } // "Training", "Shit"
public Guid? DayId { get; init; }
public Guid? ActiveGameId { get; init; }
public ThrowPanelState ThrowPanel { get; init; }
public ParticipantsState Participants { get; init; }
public object? GameModel { get; init; } // JSON-serializable game-specific data
public UndoRedoState UndoRedo { get; init; }
public IReadOnlyList<GameSummaryDto> CompletedGames { get; init; }
public bool IsLoading { get; init; }
public string? Error { get; init; }
}
public record ThrowPanelState(
bool IsStarted,
PinStatus Pin1, PinStatus Pin2, PinStatus Pin3,
PinStatus Pin4, PinStatus Pin5, PinStatus Pin6,
PinStatus Pin7, PinStatus Pin8, PinStatus Pin9,
int ThrowsPerRound,
int ThrowCounterPerRound,
ThrowMode ThrowMode,
int TotalThrowCounter,
bool BellValue
);
public record ParticipantsState(
Guid[] PlayerIds,
int CurrentPlayerIndex,
ParticipantsMode Mode
);
```
---
### **Phase H2: Pin Input Components**
**Dateien (Port aus KoogleApp):**
1. `src/Koogle.Web/Components/Game/Pin.razor`
2. `src/Koogle.Web/Components/Game/PinPanel.razor`
3. `src/Koogle.Web/Components/Game/NumberPanel.razor`
4. `src/Koogle.Web/Components/Game/ThrowPanel.razor`
5. `src/Koogle.Web/Components/Game/GameInputPanel.razor`
**Pin-Layout (9 Kegel):**
```
② ③
④ ⑤ ⑥
⑦ ⑧
```
**Features:**
- Pin.razor: Einzelner Kegel, klickbar, Status (Standing/Fallen/Disabled)
- PinPanel.razor: 9-Pin-Layout, Touch-optimiert
- NumberPanel.razor: Schnelleingabe 1-9, Wurf-Button, Glocke
- ThrowPanel.razor: Wurf-Bestätigung, Rinnen links/rechts
- GameInputPanel.razor: Orchestriert alles, zeigt aktuellen Spieler
---
### **Phase H3: Game Definition Framework**
**Dateien:**
1. `src/Koogle.Domain/Interfaces/IGameDefinition.cs`
2. `src/Koogle.Application/Games/GameProgress.cs`
3. `src/Koogle.Application/Games/IGameLogicService.cs`
4. `src/Koogle.Application/Games/GameDefinitionRegistry.cs`
5. `src/Koogle.Application/DependencyInjection.cs` (erweitern)
**IGameDefinition:**
```csharp
public interface IGameDefinition
{
string Name { get; } // "Training"
string DisplayName { get; } // "Kegel-Training"
Type SetupComponentType { get; }
Type BoardComponentType { get; }
Type GameLogicServiceType { get; }
Type GameModelType { get; }
}
```
**GameProgress (Port):**
- BeforeThrowState, AfterThrowState
- PinCount(), IsCircle() Extension Methods
- NextRound(), EndOfGame() Helpers
---
### **Phase H4: Training Game**
**Dateien:**
1. `src/Koogle.Application/Games/Training/TrainingGameDefinition.cs`
2. `src/Koogle.Application/Games/Training/TrainingGameModel.cs`
3. `src/Koogle.Application/Games/Training/TrainingGameLogicService.cs`
4. `src/Koogle.Web/Components/Game/Training/TrainingSetup.razor`
5. `src/Koogle.Web/Components/Game/Training/TrainingBoard.razor`
**TrainingGameModel:**
```csharp
public record TrainingGameModel
{
public Dictionary<Guid, PlayerStats> PlayerStatistics { get; init; }
}
public record PlayerStats
{
public int ThrowCount { get; set; }
public int PinCount { get; set; }
public int CircleCount { get; set; } // Kränze (8+1)
public int StrikeCount { get; set; } // Alle 9
}
```
**TrainingSetup.razor:**
- ThrowMode: Reposition (in die Vollen) / Decrease (Abräumen)
- ThrowsPerRound: 1-5
- ParticipantsMode: GameLogic (Rotation) / FreeToChoose
**TrainingBoard.razor (Tafel):**
| Spieler | Würfe | Kegel | Kränze | ⌀ |
|---------|-------|-------|--------|---|
| Max | 24 | 156 | 3 | 6.5 |
---
### **Phase H5: Scheiss-Spiel**
**Dateien:**
1. `src/Koogle.Application/Games/Shit/ShitGameDefinition.cs`
2. `src/Koogle.Application/Games/Shit/ShitGameModel.cs`
3. `src/Koogle.Application/Games/Shit/ShitGameLogicService.cs`
4. `src/Koogle.Web/Components/Game/Shit/ShitSetup.razor`
5. `src/Koogle.Web/Components/Game/Shit/ShitBoard.razor`
**ShitGameModel:**
```csharp
public record ShitGameModel
{
public int ShitNumber { get; init; } // 1-9 (Scheiss-Zahl)
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 int CollectedPoints { get; set; }
public Guid? WinnerId { get; set; }
}
```
**Spiellogik:**
1. Spieler wirft
2. Wenn Scheiss-Zahl oder Rinne → gesammelte Punkte werden vom Konto abgezogen, nächster Spieler
3. Sonst → Punkte werden gesammelt (addiert)
4. Erster mit 0 Punkten → **GEWINNER**
5. Spiel endet, alle anderen bekommen Strafe: `Restpunkte × PricePerPoint`
**ShitSetup.razor:**
- Scheiss-Zahl (1-9, Default: 5)
- Start-Zahl (10-1000, Default: 50)
- **Betrag pro Punkt** (0.01€ - 1.00€, Default: 0.10€)
**ShitBoard.razor (Tafel):**
| Spieler | Punkte | Status |
|---------|--------|--------|
| Max | 23 | 🎯 |
| Anna | 0 | 🏆 GEWINNER |
---
### **Phase H6: Game Setup Dialog**
**Dateien:**
1. `src/Koogle.Web/Components/Game/GameSetupDialog.razor`
2. `src/Koogle.Web/Components/Game/GameTypeSelector.razor`
3. `src/Koogle.Web/Components/Game/ParticipantSelector.razor`
4. `src/Koogle.Web/Components/Game/CommonSetupOptions.razor`
**Flow:**
1. Dialog öffnen
2. Spieltyp auswählen (Training, Scheiss-Spiel)
3. DynamicComponent rendert spieltyp-spezifisches Setup
4. Teilnehmer aus Day-Participants auswählen
5. "Spiel starten" → `StartGameAction` dispatch
---
### **Phase H7: DayDetails Integration**
**Dateien ändern:**
1. `src/Koogle.Web/Components/Pages/Days/DayDetails.razor`
2. `src/Koogle.Web/Components/Game/GameBoardPanel.razor`
3. `src/Koogle.Web/Components/Game/CompletedGamesList.razor`
**DayDetails Erweiterung:**
```razor
@code {
private int _activeTabIndex = 0;
}
<MudTabs @bind-ActivePanelIndex="_activeTabIndex">
<MudTabPanel Text="Details" Icon="@Icons.Material.Filled.Info">
<!-- Bestehend: Participants + Expenses + Quick-Assign -->
</MudTabPanel>
<MudTabPanel Text="Eingabe" Icon="@Icons.Material.Filled.SportsScore"
Disabled="@(!GameState.Value.IsGameActive)">
<GameInputPanel />
</MudTabPanel>
<MudTabPanel Text="Tafel" Icon="@Icons.Material.Filled.TableChart"
Disabled="@(!GameState.Value.IsGameActive)">
<GameBoardPanel />
</MudTabPanel>
</MudTabs>
<!-- Spiel starten Button (nur wenn Day.Status == Started und kein aktives Spiel) -->
@if (SelectedDay?.Status == DayStatus.Started && !GameState.Value.IsGameActive)
{
<MudButton OnClick="OpenGameSetupDialog" Color="Color.Primary">
Neues Spiel starten
</MudButton>
}
<!-- Liste abgeschlossener Spiele -->
<CompletedGamesList DayId="DayId" />
```
---
### **Phase H8: Undo/Redo**
**Dateien:**
1. `src/Koogle.Web/Store/GameState/UndoRedoState.cs`
2. `src/Koogle.Web/Store/GameState/UndoRedoActions.cs`
3. `src/Koogle.Web/Components/Game/UndoRedoButtons.razor`
**Logic:**
- Jeder Wurf erstellt Snapshot
- Stack für Undo (max 20 Einträge)
- "Letzten Wurf rückgängig" Button
---
### **Phase H9: Session Persistence**
**Dateien:**
1. `src/Koogle.Web/wwwroot/js/gameSession.js`
2. `src/Koogle.Web/Store/GameState/GameEffects.cs` (erweitern)
**Mechanismus:**
- Bei State-Änderung: SessionStorage speichern
- Bei Page-Load: SessionStorage prüfen
- Wenn Session für aktuellen DayId existiert: Restore anbieten
---
### **Phase H10: Testing**
**Dateien:**
1. `test/Koogle.Tests/Application/Games/TrainingGameLogicServiceTests.cs`
2. `test/Koogle.Tests/Application/Games/ShitGameLogicServiceTests.cs`
3. `test/Koogle.Tests/Web/GameStateReducersTests.cs`
**Test-Cases:**
- Pin-Count Berechnung
- Kranz-Erkennung (8 äußere + Mitte steht)
- Scheiss-Spiel: Gewinner bei 0
- Scheiss-Spiel: Strafen-Berechnung
- Reducer: State-Transformationen
---
## 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
```bash
# GameType Property zu Game Entity hinzufügen
dotnet ef migrations add AddGameType --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
```
---
## Berechtigungen Phase 2
- **Spiel starten/beenden**: ClubEditor+
- **Würfe eingeben**: ClubEditor+
- **Tafel ansehen**: ClubViewer+
---
## Zusammenfassung Phase 2
**10 Phasen (H1-H10)** → **~42 Dateien** → **Detaillierte Spielverwaltung**
Bereit für Implementierung Phase H1