diff --git a/src/Koogle.Application/Games/FoxHunt/FoxHuntGameDefinition.cs b/src/Koogle.Application/Games/FoxHunt/FoxHuntGameDefinition.cs
new file mode 100644
index 0000000..5f98eac
--- /dev/null
+++ b/src/Koogle.Application/Games/FoxHunt/FoxHuntGameDefinition.cs
@@ -0,0 +1,32 @@
+using Koogle.Application.Games.Shit;
+using Koogle.Domain.Interfaces;
+
+namespace Koogle.Application.Games.FoxHunt
+{
+ ///
+ /// Game definition for Fuchsjagd-Spiel (Fox Hunt) game type.
+ /// One Player starts as fox, and all others try to hunt him.
+ ///
+ public class FoxHuntGameDefinition : IGameDefinition
+ {
+ ///
+ public string Name => "FoxHunt";
+
+ ///
+ public string DisplayName => "Fuchsjagd";
+
+ ///
+ public Type SetupComponentType => Type.GetType(
+ "Koogle.Web.Components.Game.FoxHunt.FoxSetup, Koogle.Web")!;
+
+ ///
+ public Type BoardComponentType => Type.GetType(
+ "Koogle.Web.Components.Game.FoxHunt.FoxBoard, Koogle.Web")!;
+
+ ///
+ public Type GameLogicServiceType => typeof(FoxHuntGameLogicService);
+
+ ///
+ public Type GameModelType => typeof(FoxHuntGameModel);
+ }
+}
diff --git a/src/Koogle.Application/Games/FoxHunt/FoxHuntGameLogicService.cs b/src/Koogle.Application/Games/FoxHunt/FoxHuntGameLogicService.cs
new file mode 100644
index 0000000..1156dd9
--- /dev/null
+++ b/src/Koogle.Application/Games/FoxHunt/FoxHuntGameLogicService.cs
@@ -0,0 +1,272 @@
+using Koogle.Application.Games.DeathBox;
+using Koogle.Application.Interfaces;
+using Koogle.Domain.Enums;
+using Microsoft.Extensions.DependencyInjection;
+using Org.BouncyCastle.Cms;
+using System;
+using System.Text.Json;
+
+namespace Koogle.Application.Games.FoxHunt
+{
+ ///
+ /// Game logic service for Fuchsjagd-Spiel (Fox Hunt) game type.
+ /// One Player starts as fox, and all others try to hunt him.
+ ///
+ public class FoxHuntGameLogicService : IGameLogicService
+ {
+ private readonly IServiceProvider _serviceProvider;
+ public FoxHuntGameLogicService(IServiceProvider serviceProvider)
+ {
+ _serviceProvider = serviceProvider;
+ }
+
+ public object CreateInitialModel(Guid[] playerIds, object? setupOptions)
+ {
+ var options = ParseSetupOptions(setupOptions);
+
+ var playerOrder = playerIds.ToArray();
+
+ var playerStates = playerIds.ToDictionary(
+ id => id,
+ _ => new FoxHuntPlayerState());
+
+ return new FoxHuntGameModel
+ {
+ FoxIndex = 0, // aktueller Fuchs (Index in PlayerOrder)
+ NonFoxIndex = 0, // Index für Nicht-Fuchs-Spieler
+ FoxTurnsRemaining = 2, // erste 2 Fuchs-Züge
+ FoxTurn = false,
+
+ LeadingThrows = options.LeadingThrows,
+ PlayerStates = playerStates,
+ PlayerOrder = playerOrder,
+ //CurrentPlayerIndex = 0,
+ //FoxLeadingThrowCount = 0,
+ //CurrentFoxIndex = 0,
+ WinnerId = null,
+ IsGameOver = false,
+ };
+ }
+
+ public (object UpdatedModel, ThrowResult Result) ProcessThrow(object gameModel, AfterThrowState afterThrow)
+ {
+ var model = CastModel(gameModel);
+ var playerId = afterThrow.CurrentPlayerId;
+ var pinsKnocked = afterThrow.PinsKnocked;
+
+ if (model.IsGameOver)
+ {
+ return (model, new ThrowResult
+ {
+ PointsScored = 0,
+ ShouldRotatePlayer = false,
+ IsGameOver = true,
+ WinnerId = model.WinnerId
+ });
+ }
+
+ //var actualName = GetPlayerName(playerId).Result;
+ var playerStates = new Dictionary(model.PlayerStates);
+ //var eliminatedPlayers = new List(model.EliminatedPlayers);
+ var triggers = new List();
+
+ var foxId = model.PlayerOrder[model.FoxIndex];
+ var isFoxThrow = playerId == foxId;
+
+
+ // 5. Check GAME END
+ bool isGameOver = false;
+ Guid? winnerId = null;
+
+
+ var nextPlayerId = GetNextId(model);
+ //var nextName = GetPlayerName(nextPlayerId).Result;
+
+ // Update model
+ model = model with
+ {
+ FoxIndex = model.FoxIndex,
+ FoxTurn = model.FoxTurn,
+ FoxTurnsRemaining = model.FoxTurnsRemaining,
+ NonFoxIndex = model.NonFoxIndex
+ };
+
+ var result = new ThrowResult
+ {
+ PointsScored = pinsKnocked,
+ ShouldRotatePlayer = true,
+ IsGameOver = isGameOver,
+ WinnerId = winnerId,
+ Triggers = triggers,
+ Overrides = isGameOver ? null : new GameLogicOverrides
+ {
+ NextPlayerId = nextPlayerId
+ }
+ };
+
+ return (model, result);
+ }
+
+ ///
+ /// algorithm:
+ /// A, A, B, A, C, A, D,
+ /// B, B, A, B, C, B, D,
+ /// C, C, A, C, B, C, D,
+ /// D, D, A, D, B, D, C
+ ///
+ ///
+ ///
+ private Guid GetNextId(FoxHuntGameModel model)
+ {
+ // Abbruch: alle waren einmal Fuchs
+ if (model.FoxIndex >= model.PlayerOrder.Length)
+ throw new InvalidOperationException("Alle Spieler waren bereits Fuchs.");
+
+ Guid foxId = model.PlayerOrder[model.FoxIndex];
+
+ // 1️⃣ Fuchs ist 2x hintereinander dran
+ if (model.FoxTurnsRemaining > 0)
+ {
+ model.FoxTurnsRemaining--;
+ model.FoxTurn = false; // danach abwechselnd
+ return foxId;
+ }
+
+ // 2️⃣ Abwechselnd Nicht-Fuchs → Fuchs
+ if (!model.FoxTurn)
+ {
+ // nächsten Nicht-Fuchs suchen
+ while (model.NonFoxIndex == model.FoxIndex)
+ model.NonFoxIndex++;
+
+ if (model.NonFoxIndex < model.PlayerOrder.Length)
+ {
+ Guid next = model.PlayerOrder[model.NonFoxIndex];
+ model.NonFoxIndex++;
+ model.FoxTurn = true;
+ return next;
+ }
+ else
+ {
+ // Alle Nicht-Füchse durch → neuer Fuchs
+ model.FoxIndex++;
+ model.NonFoxIndex = 0;
+ model.FoxTurnsRemaining = 2;
+ model.FoxTurn = true;
+ return GetNextId(model);
+ }
+ }
+
+ // 3️⃣ Fuchs-Zug im Wechsel
+ model.FoxTurn = false;
+ return foxId;
+ }
+
+ private static int GetNextActivePlayerIndex(
+ Guid[] playerOrder,
+ int currentIndex,
+ Dictionary playerStates)
+ {
+ return 0;
+ }
+ private async Task GetPlayerName(Guid playerId)
+ {
+ using (var scope = _serviceProvider.CreateScope())
+ {
+ var _personService = scope.ServiceProvider.GetRequiredService();
+ var persons = await _personService.GetAllAsync();
+ var person = persons.FirstOrDefault(p => p.Id == playerId);
+ return person?.Name ?? "Unbekannt";
+ }
+ }
+ public (bool IsGameOver, Guid? WinnerId) CheckGameEnd(object gameModel)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IReadOnlyDictionary GetPlayerStats(object gameModel)
+ {
+ throw new NotImplementedException();
+ }
+
+ public GameSetupValidationResult ValidateSetup(object? setupOptions)
+ {
+ if (setupOptions is null)
+ {
+ return GameSetupValidationResult.Valid();
+ }
+
+ var options = ParseSetupOptions(setupOptions);
+ var errors = new List();
+
+ if (options.LeadingThrows < 2 || options.LeadingThrows > 4)
+ {
+ errors.Add("Vorsprung muss zwischen 6 und 12 liegen.");
+ }
+
+ //TODO: validate player-count, at lease 2 players needed
+
+ return errors.Count > 0
+ ? GameSetupValidationResult.Invalid(errors.ToArray())
+ : GameSetupValidationResult.Valid();
+ }
+
+ public IReadOnlyList GetAvailableActions(object gameModel, Guid currentPlayerId)
+ {
+ // FoxHunt has no custom actions
+ return [];
+ }
+
+ public GameActionResult ExecuteAction(object gameModel, string actionId, Guid currentPlayerId,
+ IReadOnlyDictionary? parameters = null)
+ {
+ return GameActionResult.Failure($"Unknown action: {actionId}");
+ }
+
+ private static FoxHuntGameSetup ParseSetupOptions(object? setupOptions)
+ {
+ if (setupOptions is null)
+ {
+ return new FoxHuntGameSetup();
+ }
+
+ if (setupOptions is FoxHuntGameSetup typedSetup)
+ {
+ return typedSetup;
+ }
+
+ if (setupOptions is JsonElement jsonElement)
+ {
+ try
+ {
+ return JsonSerializer.Deserialize(
+ jsonElement.GetRawText(),
+ GameModelFactory.JsonSerializerOptions) ?? new FoxHuntGameSetup();
+ }
+ catch
+ {
+ return new FoxHuntGameSetup();
+ }
+ }
+
+ return new FoxHuntGameSetup();
+ }
+
+ private static FoxHuntGameModel CastModel(object gameModel)
+ {
+ if (gameModel is FoxHuntGameModel model)
+ {
+ return model;
+ }
+
+ if (gameModel is JsonElement jsonElement)
+ {
+ return JsonSerializer.Deserialize(
+ jsonElement.GetRawText(),
+ GameModelFactory.JsonSerializerOptions)!;
+ }
+
+ throw new InvalidOperationException($"Expected FoxHuntGameModel but got {gameModel.GetType().Name}");
+ }
+ }
+}
diff --git a/src/Koogle.Application/Games/FoxHunt/FoxHuntGameModel.cs b/src/Koogle.Application/Games/FoxHunt/FoxHuntGameModel.cs
new file mode 100644
index 0000000..6b36ead
--- /dev/null
+++ b/src/Koogle.Application/Games/FoxHunt/FoxHuntGameModel.cs
@@ -0,0 +1,38 @@
+namespace Koogle.Application.Games.FoxHunt
+{
+ ///
+ /// Game model for Fuchsjagd-Spiel (Fox Hunt) game type.
+ /// One Player starts as fox, and all others try to hunt him.
+ ///
+ public record FoxHuntGameModel
+ {
+ ///
+ /// ID of the winner (first player to reach 0 points).
+ ///
+ public Guid? WinnerId { get; set; }
+
+ public bool IsGameOver { get; set; }
+ //public int LeadingThrows { get; set; }
+ //public int FoxLeadingThrowCount { get; set; }
+ public Dictionary PlayerStates { get; set; }
+ public Guid[] PlayerOrder { get; set; }
+
+ public int LeadingThrows { get; set; } // copy from setup model
+ //public int CurrentPlayerIndex { get; set; }
+ //public int CurrentFoxIndex { get; set; }
+
+
+
+ public int FoxIndex = 0; // aktueller Fuchs (Index in PlayerOrder)
+ public int NonFoxIndex = 0; // Index für Nicht-Fuchs-Spieler
+ public int FoxTurnsRemaining = 2; // erste 2 Fuchs-Züge
+ public bool FoxTurn = true; // wer ist dran
+
+ }
+
+ public record FoxHuntPlayerState
+ {
+
+ public int PinCount { get; set; }
+ }
+}
diff --git a/src/Koogle.Application/Games/FoxHunt/FoxHuntGameSetup.cs b/src/Koogle.Application/Games/FoxHunt/FoxHuntGameSetup.cs
new file mode 100644
index 0000000..c9643b2
--- /dev/null
+++ b/src/Koogle.Application/Games/FoxHunt/FoxHuntGameSetup.cs
@@ -0,0 +1,34 @@
+using Koogle.Domain.Enums;
+
+namespace Koogle.Application.Games.FoxHunt
+{
+ ///
+ /// Setup model for Fuchsjagd-Spiel (Fox Hunt) game type.
+ /// One Player starts as fox, and all others try to hunt him.
+ ///
+ public record FoxHuntGameSetup : GameSetupModelBase
+ {
+ public override string GameType => "FoxHunt";
+
+
+ ///
+ /// Leading throw the fox do, before the hunt begins
+ ///
+ public int LeadingThrows { get; init; } = 2;
+
+ ///
+ /// Creates a default ShitGameSetup with specified base parameters.
+ ///
+ public static FoxHuntGameSetup Create(
+ ThrowMode throwMode,
+ int throwsPerRound,
+ ParticipantsMode participantsMode,
+ int leadingThrows = 2) => new()
+ {
+ ThrowMode = throwMode,
+ ThrowsPerRound = throwsPerRound,
+ ParticipantsMode = participantsMode,
+ LeadingThrows = leadingThrows
+ };
+ }
+}
diff --git a/src/Koogle.Application/Games/IGameSetupModel.cs b/src/Koogle.Application/Games/IGameSetupModel.cs
index a048ee3..5d9a8f3 100644
--- a/src/Koogle.Application/Games/IGameSetupModel.cs
+++ b/src/Koogle.Application/Games/IGameSetupModel.cs
@@ -11,6 +11,7 @@ namespace Koogle.Application.Games;
[JsonDerivedType(typeof(Shit.ShitGameSetup), "Shit")]
[JsonDerivedType(typeof(Training.TrainingGameSetup), "Training")]
[JsonDerivedType(typeof(DeathBox.DeathBoxGameSetup), "DeathBox")]
+[JsonDerivedType(typeof(FoxHunt.FoxHuntGameSetup), "FoxHunt")]
public interface IGameSetupModel
{
///
diff --git a/src/Koogle.Web/Components/Game/FoxHunt/FoxBoard.razor b/src/Koogle.Web/Components/Game/FoxHunt/FoxBoard.razor
new file mode 100644
index 0000000..eb18945
--- /dev/null
+++ b/src/Koogle.Web/Components/Game/FoxHunt/FoxBoard.razor
@@ -0,0 +1,213 @@
+@inherits Fluxor.Blazor.Web.Components.FluxorComponent
+
+@using System.Text.Json
+@using Koogle.Application.Games.FoxHunt
+@using Koogle.Web.Store.DayState
+@using Koogle.Web.Store.GameState
+@using Koogle.Application.Games;
+
+@implements IDisposable
+@inject IState GameState
+@inject IState DayState
+
+
+ @_debug
+
+
+
+ @if (_model == null)
+ {
+
+ Spiel noch nicht gestartet.
+
+ }
+ else
+ {
+ @* Game info header *@
+
+
+
+ Vorsprung für den Fuchs:
+
+ @_model.LeadingThrows Vorsprung
+
+
+
+ Füchse übrig:
+
+ 0
+
+
+
+
+
+ @* Last throw info *@
+ @* @if (_model.LastThrow != null)
+ {
+
+ @GetLastThrowMessage()
+
+ } *@
+
+ @* Winner announcement *@
+ @if (_model.IsGameOver && _model.WinnerId.HasValue)
+ {
+ var winnerName = GetPlayerName(_model.WinnerId.Value);
+
+
+
+ @winnerName hat überlebt!
+
+
+ }
+
+ @* Players table *@
+
+
+ Spieler
+ Status
+ Xs
+ Eier
+ Status
+
+
+
+ @if (context.IsCurrentPlayer && !_model.IsGameOver)
+ {
+
+
+ @context.PlayerName
+
+
+ }
+ else
+ {
+
+ @context.PlayerName
+
+ }
+
+
+
+
+
+ @if (context.IsWinner)
+ {
+
+ SIEGER
+
+ }
+ else if (context.IsEliminated)
+ {
+
+ ☠️ Platz @context.FoxSurvivalOrder
+
+ }
+ else if (context.IsCurrentPlayer)
+ {
+
+ Am Zug
+
+ }
+
+
+
+
+ @* Info footer *@
+ @if (!_model.IsGameOver)
+ {
+
+
+ Überlebe als als Fuchs, mit dem größten Vorsprung!
+
+
+ }
+ }
+
+
+@code {
+ private FoxHuntGameModel? _model;
+ private List _playerStats = [];
+ private string _debug;
+
+ protected override void OnInitialized()
+ {
+ base.OnInitialized();
+ GameState.StateChanged += OnGameStateChanged;
+ UpdateStats();
+ }
+
+ private void OnGameStateChanged(object? sender, EventArgs e)
+ {
+ UpdateStats();
+ InvokeAsync(StateHasChanged);
+ }
+
+ private void UpdateStats()
+ {
+ _playerStats.Clear();
+
+ _model = null;
+ // _activePlayerCount = 0;
+
+ var gameState = GameState.Value;
+ if (gameState.GameModel is FoxHuntGameModel model)
+ {
+ _model = model;
+ }
+ else if (gameState.GameModel is JsonElement jsonElement)
+ {
+ try
+ {
+ _model = JsonSerializer.Deserialize(
+ jsonElement.GetRawText(),
+ GameModelFactory.JsonSerializerOptions);
+ }
+ catch
+ {
+ return;
+ }
+ }
+
+ _debug = JsonSerializer.Serialize(_model, new JsonSerializerOptions { WriteIndented = true });
+ InvokeAsync(StateHasChanged);
+
+
+ if (_model?.PlayerStates == null || _model.PlayerOrder == null)
+ {
+ return;
+ }
+
+ }
+
+ public void Dispose()
+ {
+ GameState.StateChanged -= OnGameStateChanged;
+ }
+
+ private string GetPlayerName(Guid playerId)
+ {
+ var person = DayState.Value.AvailablePersons.FirstOrDefault(p => p.Id == playerId);
+ return person?.Name ?? "Unbekannt";
+ }
+
+ private record PlayerStatsRow
+ {
+ public Guid PlayerId { get; init; }
+ public string PlayerName { get; init; } = "";
+ public string Status { get; init; } = "";
+ public bool IsWinner { get; init; }
+ public bool IsCurrentPlayer { get; init; }
+ public bool IsEliminated { get; init; }
+ public int FoxSurvivalOrder { get; init; }
+ }
+
+}
diff --git a/src/Koogle.Web/Components/Game/FoxHunt/FoxSetup.razor b/src/Koogle.Web/Components/Game/FoxHunt/FoxSetup.razor
new file mode 100644
index 0000000..ca4ca42
--- /dev/null
+++ b/src/Koogle.Web/Components/Game/FoxHunt/FoxSetup.razor
@@ -0,0 +1,121 @@
+@inject ITriggerService TriggerService
+@using Koogle.Application.Games
+@using Koogle.Application.Games.FoxHunt
+@using Koogle.Application.Interfaces
+@using Koogle.Domain.Enums
+@implements Koogle.Application.Interfaces.IGameSetupControl
+
+
+ Fuchsjagd Einstellungen
+
+
+
+
+
+
+ @if (!_hasExpensePointTrigger)
+ {
+
+
+ Hinweis: Es ist kein "Strafpunkt"-Trigger konfiguriert.
+ Die Verlierer-Strafen werden nicht automatisch berechnet.
+ Gehe zu Stammdaten → Trigger, um einen ExpensePoint-Trigger einzurichten.
+
+
+ }
+
+
+
+
+ Spielregeln:
+
+ - Jeder Spieler reihum ist einmal der Fuchs
+ - Der Fuchs startet mit 'Anzahl-Würfe-Vorsprung' in die Vollen
+ - Er sammelt möglichst viele Kegel und erarbeitet sich sich einen Vorsprung
+ - Anschließend hat jeder weitere Spieler einen Wurf, abwechselnd mit dem Fuchs
+ - Ziel der anderen Spieler ist es, den Fuchs zu fangen!
+ - Ziel des Fuchses ist es, am Ende der Runde mindestens einen Punkt Vorsprung zu haben
+ - Es gewinnt der Fuchs mit dem größten Vorsprung
+
+
+
+
+@code {
+ ///
+ /// Callback when setup options change.
+ ///
+ [Parameter]
+ public EventCallback