326 lines
11 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|