refactorings

This commit is contained in:
beo3000 2026-01-05 19:49:41 +01:00
parent 4d584e98af
commit 3f62b3c0e0
4 changed files with 109 additions and 61 deletions

View File

@ -17,6 +17,8 @@ The name for the App is KOOGLE. Koogle is an app for club management.
- Automatic or rule-based cost allocation - Automatic or rule-based cost allocation
- Evaluation per person/day/game - Evaluation per person/day/game
- User-related actions (CurrentUserService) - User-related actions (CurrentUserService)
- cash book
- plugin system for different game logics
### Technical ### Technical
- ASP.NET Core backend - ASP.NET Core backend
@ -29,7 +31,7 @@ The name for the App is KOOGLE. Koogle is an app for club management.
- maybe in future also for darts, soccer betting pools, Leisure groups, Sport clubs, etc. - maybe in future also for darts, soccer betting pools, Leisure groups, Sport clubs, etc.
**Phase 1 MVP:** See [docs/IMPLEMENTATION_PLAN.md](./docs/IMPLEMENTATION_PLAN.md) **Phase 1 MVP:** See [docs/IMPLEMENTATION_PLAN.md](./docs/IMPLEMENTATION_PLAN.md)
- 23 granular phases (A1-F3) - granular phases (A1-K23)
- Foundation → Clubs → Users → Persons → Days → Dashboard - Foundation → Clubs → Users → Persons → Days → Dashboard
- Track progress via checkboxes in plan - Track progress via checkboxes in plan
- ~75 files total - ~75 files total

View File

@ -14,6 +14,7 @@ namespace Koogle.Application.Games.FoxHunt
/// </summary> /// </summary>
public class FoxHuntGameLogicService : IGameLogicService public class FoxHuntGameLogicService : IGameLogicService
{ {
/// <inheritdoc />
public object CreateInitialModel(Guid[] playerIds, object? setupOptions) public object CreateInitialModel(Guid[] playerIds, object? setupOptions)
{ {
var options = ParseSetupOptions(setupOptions); var options = ParseSetupOptions(setupOptions);
@ -30,13 +31,9 @@ namespace Koogle.Application.Games.FoxHunt
NonFoxIndex = 0, // Index für Nicht-Fuchs-Spieler NonFoxIndex = 0, // Index für Nicht-Fuchs-Spieler
FoxTurnsRemaining = options.LeadingThrows - 1, // -1 weil erster Wurf bereits aus PlayerOrder[FoxIndex] FoxTurnsRemaining = options.LeadingThrows - 1, // -1 weil erster Wurf bereits aus PlayerOrder[FoxIndex]
FoxTurn = true, // ein fuchs fängt an FoxTurn = true, // ein fuchs fängt an
LeadingThrows = options.LeadingThrows, LeadingThrows = options.LeadingThrows,
PlayerStates = playerStates, PlayerStates = playerStates,
PlayerOrder = playerOrder, PlayerOrder = playerOrder,
//CurrentPlayerIndex = 0,
//FoxLeadingThrowCount = 0,
//CurrentFoxIndex = 0,
WinnerId = null, WinnerId = null,
IsGameOver = false, IsGameOver = false,
}; };
@ -44,6 +41,7 @@ namespace Koogle.Application.Games.FoxHunt
return model; return model;
} }
/// <inheritdoc />
public (object UpdatedModel, ThrowResult Result) ProcessThrow(object gameModel, AfterThrowState afterThrow) public (object UpdatedModel, ThrowResult Result) ProcessThrow(object gameModel, AfterThrowState afterThrow)
{ {
var model = this.CastModel<FoxHuntGameModel>(gameModel); var model = this.CastModel<FoxHuntGameModel>(gameModel);
@ -61,9 +59,7 @@ namespace Koogle.Application.Games.FoxHunt
}); });
} }
//var actualName = GetPlayerName(playerId).Result;
var playerStates = new Dictionary<Guid, FoxHuntPlayerState>(model.PlayerStates); var playerStates = new Dictionary<Guid, FoxHuntPlayerState>(model.PlayerStates);
//var eliminatedPlayers = new List<Guid>(model.EliminatedPlayers);
var triggers = new List<TriggerEvent>(); var triggers = new List<TriggerEvent>();
var foxId = model.PlayerOrder[model.FoxIndex]; var foxId = model.PlayerOrder[model.FoxIndex];
@ -73,19 +69,28 @@ namespace Koogle.Application.Games.FoxHunt
if (model.FoxTurn) if (model.FoxTurn)
{ {
playerStates[foxId].PinCountFox+= afterThrow.PinsKnocked; playerStates[foxId].PinCountFox+= afterThrow.PinsKnocked;
if (afterThrow.IsCircle)
{
playerStates[foxId].PinCountFox += 2;
}
} }
else else
{ {
playerStates[foxId].PinCountHunters+= afterThrow.PinsKnocked; playerStates[foxId].PinCountHunters+= afterThrow.PinsKnocked;
if (afterThrow.IsCircle)
{
playerStates[foxId].PinCountHunters += 2;
}
if (playerStates[foxId].PinCountHunters >= playerStates[foxId].PinCountFox ) if (playerStates[foxId].PinCountHunters >= playerStates[foxId].PinCountFox )
{ {
// fox has been caught // fox has been caught
isLastHunter = true; isLastHunter = true; // will cause selection of the next fox
playerStates[foxId].FoxCaught = true; playerStates[foxId].FoxCaught = true;
// Calculate penalty (point difference the hunters achieved) // Calculate penalty (point difference the hunters achieved)
int pointDifference = playerStates[foxId].PinCountHunters - playerStates[foxId].PinCountFox; var pointDifference = playerStates[foxId].PinCountHunters - playerStates[foxId].PinCountFox;
if (pointDifference > 0) if (pointDifference > 0)
{ {
triggers.Add(new TriggerEvent triggers.Add(new TriggerEvent
@ -109,7 +114,7 @@ namespace Koogle.Application.Games.FoxHunt
var nextPlayerId = GetNextId(model, isLastHunter); var nextPlayerId = GetNextId(model, isLastHunter);
// Check GAME END // Check GAME END
bool isGameOver = isLastHunter && model.FoxIndex == 0; var isGameOver = isLastHunter && model.FoxIndex == 0;
if (isGameOver) if (isGameOver)
{ {
@ -178,7 +183,7 @@ namespace Koogle.Application.Games.FoxHunt
return model.PlayerOrder[model.FoxIndex]; return model.PlayerOrder[model.FoxIndex];
} }
// 1Fuchs ist 2x hintereinander dran // Fuchs ist 2x hintereinander dran
if (model.FoxTurnsRemaining > 0) if (model.FoxTurnsRemaining > 0)
{ {
model.FoxTurnsRemaining--; model.FoxTurnsRemaining--;
@ -188,7 +193,7 @@ namespace Koogle.Application.Games.FoxHunt
model.FoxTurn = !model.FoxTurn; // danach abwechselnd model.FoxTurn = !model.FoxTurn; // danach abwechselnd
// 2Abwechselnd Nicht-Fuchs → Fuchs // Abwechselnd Nicht-Fuchs → Fuchs
if (!model.FoxTurn) if (!model.FoxTurn)
{ {
var nextNonFoxIndex = model.GetNextIndex(model.PlayerOrder, model.NonFoxIndex); var nextNonFoxIndex = model.GetNextIndex(model.PlayerOrder, model.NonFoxIndex);
@ -199,56 +204,26 @@ namespace Koogle.Application.Games.FoxHunt
} }
model.NonFoxIndex = nextNonFoxIndex; model.NonFoxIndex = nextNonFoxIndex;
return model.PlayerOrder[nextNonFoxIndex]; return model.PlayerOrder[nextNonFoxIndex];
// 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 = model.LeadingThrows - 1;
// model.FoxTurn = true;
// return GetNextId(model);
//}
} }
// Fuchs-Zug im Wechsel
// 3⃣ Fuchs-Zug im Wechsel
//model.FoxTurn = false;
return foxId; return foxId;
} }
private static int GetNextActivePlayerIndex( /// <inheritdoc />
Guid[] playerOrder,
int currentIndex,
Dictionary<Guid, FoxHuntPlayerState> playerStates)
{
return 0;
}
public (bool IsGameOver, Guid? WinnerId) CheckGameEnd(object gameModel) public (bool IsGameOver, Guid? WinnerId) CheckGameEnd(object gameModel)
{ {
var model = this.CastModel<FoxHuntGameModel>(gameModel); var model = this.CastModel<FoxHuntGameModel>(gameModel);
return (model.IsGameOver, model.WinnerId); return (model.IsGameOver, model.WinnerId);
} }
/// <inheritdoc />
public IReadOnlyDictionary<Guid, PlayerStatsSummary> GetPlayerStats(object gameModel) public IReadOnlyDictionary<Guid, PlayerStatsSummary> GetPlayerStats(object gameModel)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
/// <inheritdoc />
public GameSetupValidationResult ValidateSetup(object? setupOptions) public GameSetupValidationResult ValidateSetup(object? setupOptions)
{ {
if (setupOptions is null) if (setupOptions is null)
@ -261,7 +236,7 @@ namespace Koogle.Application.Games.FoxHunt
if (options.LeadingThrows < 2 || options.LeadingThrows > 4) if (options.LeadingThrows < 2 || options.LeadingThrows > 4)
{ {
errors.Add("Vorsprung muss zwischen 6 und 12 liegen."); errors.Add("Vorsprung muss zwischen 2 und 4 liegen.");
} }
//TODO: validate player-count, at lease 2 players needed //TODO: validate player-count, at lease 2 players needed
@ -271,12 +246,14 @@ namespace Koogle.Application.Games.FoxHunt
: GameSetupValidationResult.Valid(); : GameSetupValidationResult.Valid();
} }
/// <inheritdoc />
public IReadOnlyList<GameActionDescriptor> GetAvailableActions(object gameModel, Guid currentPlayerId) public IReadOnlyList<GameActionDescriptor> GetAvailableActions(object gameModel, Guid currentPlayerId)
{ {
// FoxHunt has no custom actions // FoxHunt has no custom actions
return []; return [];
} }
/// <inheritdoc />
public GameActionResult ExecuteAction(object gameModel, string actionId, Guid currentPlayerId, public GameActionResult ExecuteAction(object gameModel, string actionId, Guid currentPlayerId,
IReadOnlyDictionary<string, object>? parameters = null) IReadOnlyDictionary<string, object>? parameters = null)
{ {

View File

@ -1,18 +1,20 @@
namespace Koogle.Application.Games.FoxHunt namespace Koogle.Application.Games.FoxHunt
{ {
/// <summary> /// <summary>
/// Game model for Fuchsjagd-Spiel (Fox Hunt) game type. /// Game model for Fox Hunt (Fuchsjagd-Spiel) game type.
/// One Player starts as fox, and all others try to hunt him. /// One Player starts as fox, and all others try to hunt him.
/// </summary> /// </summary>
public record FoxHuntGameModel : IGameModel public record FoxHuntGameModel : IGameModel
{ {
/// <summary>
/// State for each player (details if fox survived or not).
/// </summary>
public Dictionary<Guid, FoxHuntPlayerState> PlayerStates { get; init; } = new();
/// <summary>
//public int LeadingThrows { get; set; } /// Fixed player order (default order at game start).
//public int FoxLeadingThrowCount { get; set; } /// </summary>
public Dictionary<Guid, FoxHuntPlayerState> PlayerStates { get; set; } public Guid[] PlayerOrder { get; set; } = [];
public Guid[] PlayerOrder { get; set; }
/// <summary> /// <summary>
/// count of throw the fox can start with /// count of throw the fox can start with
@ -20,15 +22,25 @@
/// </summary> /// </summary>
public int LeadingThrows { get; set; } public int LeadingThrows { get; set; }
//public int CurrentPlayerIndex { get; set; } /// <summary>
//public int CurrentFoxIndex { get; set; } /// index pointing to the current fox (Index in PlayerOrder)
/// </summary>
public int FoxIndex { get; set; }
/// <summary>
/// index pointing to the current hunter (Index in PlayerOrder)
/// </summary>
public int NonFoxIndex { get; set; }
public int FoxIndex { get; set; } // aktueller Fuchs (Index in PlayerOrder) /// <summary>
public int NonFoxIndex { get; set; } // Index für Nicht-Fuchs-Spieler /// remaining leading throws a fox has left
public int FoxTurnsRemaining { get; set; } // verbleibende Fuchs-Züge /// </summary>
public bool FoxTurn { get; set; } // true = Fuchs dran im Wechsel public int FoxTurnsRemaining { get; set; }
/// <summary>
/// true = a throw by fox, false = a throw by a hunter
/// </summary>
public bool FoxTurn { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public Guid? WinnerId { get; set; } public Guid? WinnerId { get; set; }
@ -37,12 +49,34 @@
public bool IsGameOver { get; set; } public bool IsGameOver { get; set; }
} }
/// <summary>
/// Info about the last throw for UI display.
/// </summary>
public record FoxHuntPlayerState public record FoxHuntPlayerState
{ {
/// <summary>
/// count of pins the fox has knocked.
/// </summary>
public int PinCountFox { get; set; } public int PinCountFox { get; set; }
/// <summary>
/// count of pins the hunters have knocked.
/// </summary>
public int PinCountHunters { get; set; } public int PinCountHunters { get; set; }
/// <summary>
/// Weather the hunters have caught the fox.
/// </summary>
public bool FoxCaught { get; set; } public bool FoxCaught { get; set; }
/// <summary>
/// Weather the fox survived with most point difference.
/// </summary>
public bool IsWinner { get; set; } public bool IsWinner { get; set; }
/// <summary>
/// Weather the fox could escape the hunters.
/// </summary>
public bool FoxEscaped { get; set; } public bool FoxEscaped { get; set; }
} }
} }

View File

@ -9,8 +9,19 @@ using System.Threading.Tasks;
namespace Koogle.Application.Games namespace Koogle.Application.Games
{ {
/// <summary>
/// Helper class for GameLogicServices
/// </summary>
public static class GameLogicServiceExtension public static class GameLogicServiceExtension
{ {
/// <summary>
/// casts an object to game model type
/// </summary>
/// <typeparam name="T">Type of the game model</typeparam>
/// <param name="logicService"></param>
/// <param name="gameModel"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
public static T CastModel<T>(this IGameLogicService logicService, object gameModel) where T : IGameModel public static T CastModel<T>(this IGameLogicService logicService, object gameModel) where T : IGameModel
{ {
if (gameModel is T model) if (gameModel is T model)
@ -29,8 +40,18 @@ namespace Koogle.Application.Games
} }
} }
/// <summary>
/// Helper methods for GameModel
/// </summary>
public static class GameModelExtensions public static class GameModelExtensions
{ {
/// <summary>
/// Get the previous Guid in array, considering loop of players.
/// </summary>
/// <param name="gameModel"></param>
/// <param name="guids"></param>
/// <param name="index"></param>
/// <returns></returns>
public static Guid GetPrev(this IGameModel gameModel, Guid[] guids, int index) public static Guid GetPrev(this IGameModel gameModel, Guid[] guids, int index)
{ {
if (index == 0) if (index == 0)
@ -40,6 +61,13 @@ namespace Koogle.Application.Games
return guids[index - 1]; return guids[index - 1];
} }
/// <summary>
/// Get the next Guid in array, considering loop of players.
/// </summary>
/// <param name="gameModel"></param>
/// <param name="guids"></param>
/// <param name="index"></param>
/// <returns></returns>
public static Guid GetNext(this IGameModel gameModel, Guid[] guids, int index) public static Guid GetNext(this IGameModel gameModel, Guid[] guids, int index)
{ {
if (index == guids.Length - 1) if (index == guids.Length - 1)
@ -49,6 +77,13 @@ namespace Koogle.Application.Games
return guids[index + 1]; return guids[index + 1];
} }
/// <summary>
/// Get the next index in array, considering loop of players.
/// </summary>
/// <param name="gameModel"></param>
/// <param name="guids"></param>
/// <param name="index"></param>
/// <returns></returns>
public static int GetNextIndex(this IGameModel gameModel,Guid[] guids, int index) public static int GetNextIndex(this IGameModel gameModel,Guid[] guids, int index)
{ {
if (index == guids.Length - 1) if (index == guids.Length - 1)