KoogleApp/src/GoodWood.Application/Games/FoxHunt/FoxHuntGameLogicService.cs

326 lines
11 KiB
C#

using GoodWood.Application.Games.DeathBox;
using GoodWood.Application.Interfaces;
using GoodWood.Domain.Enums;
using Microsoft.Extensions.DependencyInjection;
using Org.BouncyCastle.Cms;
using System;
using System.Text.Json;
using GoodWood.Infrastructure.Data.Migrations;
namespace GoodWood.Application.Games.FoxHunt
{
/// <summary>
/// Game logic service for Fuchsjagd-Spiel (Fox Hunt) game type.
/// One Player starts as fox, and all others try to hunt him.
/// </summary>
public class FoxHuntGameLogicService : IGameLogicService
{
/// <inheritdoc />
public object CreateInitialModel(Guid[] playerIds, object? setupOptions)
{
var options = ParseSetupOptions(setupOptions);
var playerOrder = playerIds.ToArray();
var playerStates = playerIds.ToDictionary(
id => id,
_ => new FoxHuntPlayerState());
var model = new FoxHuntGameModel
{
FoxIndex = 0, // aktueller Fuchs (Index in PlayerOrder)
NonFoxIndex = 0, // Index für Nicht-Fuchs-Spieler
FoxTurnsRemaining = options.LeadingThrows - 1, // -1 weil erster Wurf bereits aus PlayerOrder[FoxIndex]
FoxTurn = true, // ein fuchs fängt an
LeadingThrows = options.LeadingThrows,
PinCountGoal = options.PinCountGoal,
PlayerStates = playerStates,
PlayerOrder = playerOrder,
WinnerId = null,
IsGameOver = false,
FoxCountLeft = playerStates.Count
};
return model;
}
/// <inheritdoc />
public (object UpdatedModel, ThrowResult Result) ProcessThrow(object gameModel, AfterThrowState afterThrow)
{
var model = this.CastModel<FoxHuntGameModel>(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 playerStates = new Dictionary<Guid, FoxHuntPlayerState>(model.PlayerStates);
var triggers = new List<TriggerEvent>();
var gameEvents = new List<GameEvent>();
var foxId = model.PlayerOrder[model.FoxIndex];
//var lastHunterId = model.GetPrev(model.PlayerOrder, model.FoxIndex);
//var isLastHunter = lastHunterId == playerId;
var chooseNextFox = false;
if (model.FoxTurn)
{
playerStates[foxId].PinCountFox+= afterThrow.PinsKnocked;
if (afterThrow.IsCircle)
{
playerStates[foxId].PinCountFox += 2;
}
if (playerStates[foxId].PinCountFox >= model.PinCountGoal)
{
playerStates[foxId].FoxEscaped = true;
chooseNextFox = true;
}
}
else
{
playerStates[foxId].PinCountHunters+= afterThrow.PinsKnocked;
if (afterThrow.IsCircle)
{
playerStates[foxId].PinCountHunters += 2;
}
if (playerStates[foxId].PinCountHunters >= playerStates[foxId].PinCountFox )
{
// fox has been caught
chooseNextFox = true; // will cause selection of the next fox
playerStates[foxId].FoxCaught = true;
// Calculate penalty (point difference the hunters achieved)
var pointDifference = playerStates[foxId].PinCountHunters - playerStates[foxId].PinCountFox;
if (pointDifference > 0)
{
triggers.Add(new TriggerEvent
{
TriggerType = ExpenseTriggerType.ExpensePoint.ToString(),
PersonId = foxId,
Multiplier = pointDifference
});
}
}
}
if (chooseNextFox)
{
model.FoxCountLeft--;
}
var nextPlayerId = GetNextId(model, chooseNextFox);
// Check GAME END
var isGameOver = model.FoxCountLeft == 0;
if (isGameOver)
{
var escapedFoxes = playerStates.Where(kv => !kv.Value.FoxCaught).ToList();
if (escapedFoxes.Count == 0)
{
// All foxes caught - no winner
gameEvents.Add(new GameEndedEvent
{
WinnerId = null,
IsDraw = false,
Message = "Kein Sieger - alle Füchse wurden gefangen!"
});
}
else
{
// looking for winner(s) among escaped foxes
var maxLeading = escapedFoxes
.Select(kv => kv.Value.PinCountFox - kv.Value.PinCountHunters).Max();
foreach (var kv in escapedFoxes)
{
if (kv.Value.PinCountFox - kv.Value.PinCountHunters == maxLeading)
{
playerStates[kv.Key].IsWinner = true;
gameEvents.Add(new PlayerWonEvent { PlayerId = kv.Key, Message = "Bester Fuchs!" });
}
}
}
}
// Update model
model = model with
{
PlayerStates = playerStates,
FoxIndex = model.FoxIndex,
FoxTurn = model.FoxTurn,
FoxTurnsRemaining = model.FoxTurnsRemaining,
NonFoxIndex = model.NonFoxIndex,
PendingGameEvents = gameEvents
};
var result = new ThrowResult
{
PointsScored = pinsKnocked,
ShouldRotatePlayer = true,
IsGameOver = isGameOver,
WinnerId = null,
Triggers = triggers,
Overrides = isGameOver ? null : new GameLogicOverrides
{
NextPlayerId = nextPlayerId
}
};
return (model, result);
}
/// <summary>
/// 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
/// </summary>
/// <returns></returns>
/// <exception cref="InvalidOperationException"></exception>
private Guid GetNextId(FoxHuntGameModel model, bool chooseNextFox)
{
// Abbruch: alle waren einmal Fuchs
if (model.FoxIndex >= model.PlayerOrder.Length)
throw new InvalidOperationException("Alle Spieler waren bereits Fuchs.");
var foxId = model.PlayerOrder[model.FoxIndex];
if (chooseNextFox)
{
model.FoxIndex = model.GetNextIndex(model.PlayerOrder, model.FoxIndex);
model.FoxTurnsRemaining = model.LeadingThrows - 1; // nächster Wurf zählt schon
model.FoxTurn = true;
model.NonFoxIndex = model.FoxIndex; // NonFoxIndex = FoxIndex -> Increment will choose next person after fox
return model.PlayerOrder[model.FoxIndex];
}
// Fuchs ist 2x hintereinander dran
if (model.FoxTurnsRemaining > 0)
{
model.FoxTurnsRemaining--;
model.FoxTurn = true;
return foxId;
}
model.FoxTurn = !model.FoxTurn; // danach abwechselnd
// Abwechselnd Nicht-Fuchs → Fuchs
if (!model.FoxTurn)
{
var nextNonFoxIndex = model.GetNextIndex(model.PlayerOrder, model.NonFoxIndex);
if (nextNonFoxIndex == model.FoxIndex)
{
throw new InvalidOperationException("this should never happen");
//nextNoneFoxIndex = GetNextIndex(model, model.NonFoxIndex);
}
model.NonFoxIndex = nextNonFoxIndex;
return model.PlayerOrder[nextNonFoxIndex];
}
// Fuchs-Zug im Wechsel
return foxId;
}
/// <inheritdoc />
public (bool IsGameOver, Guid? WinnerId) CheckGameEnd(object gameModel)
{
var model = this.CastModel<FoxHuntGameModel>(gameModel);
return (model.IsGameOver, model.WinnerId);
}
/// <inheritdoc />
public IReadOnlyDictionary<Guid, PlayerStatsSummary> GetPlayerStats(object gameModel)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public GameSetupValidationResult ValidateSetup(object? setupOptions)
{
if (setupOptions is null)
{
return GameSetupValidationResult.Valid();
}
var options = ParseSetupOptions(setupOptions);
var errors = new List<string>();
if (options.LeadingThrows < 2 || options.LeadingThrows > 4)
{
errors.Add("Vorsprung muss zwischen 2 und 4 liegen.");
}
if (options.PinCountGoal < 12 || options.PinCountGoal > 50)
{
errors.Add("Ziel-Punkte muss zwischen 12 und 50 liegen.");
}
//TODO: validate player-count, at lease 2 players needed
return errors.Count > 0
? GameSetupValidationResult.Invalid(errors.ToArray())
: GameSetupValidationResult.Valid();
}
/// <inheritdoc />
public IReadOnlyList<GameActionDescriptor> GetAvailableActions(object gameModel, Guid currentPlayerId)
{
// FoxHunt has no custom actions
return [];
}
/// <inheritdoc />
public GameActionResult ExecuteAction(object gameModel, string actionId, Guid currentPlayerId,
IReadOnlyDictionary<string, object>? 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<FoxHuntGameSetup>(
jsonElement.GetRawText(),
GameModelFactory.JsonSerializerOptions) ?? new FoxHuntGameSetup();
}
catch
{
return new FoxHuntGameSetup();
}
}
return new FoxHuntGameSetup();
}
}
}