781 lines
26 KiB
C#
781 lines
26 KiB
C#
using System.Text.Json;
|
|
using Koogle.Domain.Enums;
|
|
|
|
namespace Koogle.Application.Games.ChristmasTree;
|
|
|
|
/// <summary>
|
|
/// Game logic service for the Christmas Tree (Tannenbaum) bowling game.
|
|
/// Teams take turns throwing. Numbers are crossed from own tree first,
|
|
/// then from all opponent trees if not available on own tree.
|
|
/// Game ends when the last 5 is crossed. Team with most crossed numbers wins.
|
|
/// </summary>
|
|
public class ChristmasTreeGameLogicService : IGameLogicService
|
|
{
|
|
/// <inheritdoc />
|
|
public object CreateInitialModel(Guid[] playerIds, object? setupOptions)
|
|
{
|
|
var options = ParseSetupOptions(setupOptions);
|
|
|
|
if (options.Teams == null || options.Teams.Count < 2)
|
|
{
|
|
throw new InvalidOperationException("Christmas Tree game requires at least 2 teams.");
|
|
}
|
|
|
|
var treeStructure = ChristmasTreeGameSetup.GetTreeStructure(options.Variant);
|
|
var teamTrees = new Dictionary<int, TeamTreeState>();
|
|
|
|
for (int i = 0; i < options.Teams.Count; i++)
|
|
{
|
|
teamTrees[i] = new TeamTreeState
|
|
{
|
|
RemainingNumbers = new Dictionary<int, int>(treeStructure),
|
|
CrossedOutCount = 0,
|
|
ThrowCount = 0,
|
|
TotalPins = 0
|
|
};
|
|
}
|
|
|
|
return new ChristmasTreeGameModel
|
|
{
|
|
Variant = options.Variant,
|
|
TeamTrees = teamTrees,
|
|
Teams = options.Teams.ToList(),
|
|
CurrentTeamIndex = 0,
|
|
CurrentPlayerIndexInTeam = 0,
|
|
EnableContinueOnMiss = options.EnableContinueOnMiss,
|
|
MaxContinueThrows = options.MaxContinueThrows,
|
|
ContinueThrowsRemaining = 0,
|
|
IsInContinueMode = false,
|
|
OpponentSelectingTeamIndex = null,
|
|
WinnerId = null,
|
|
WinnerTeamIndex = null,
|
|
IsGameOver = false,
|
|
LastThrow = null
|
|
};
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public (object UpdatedModel, ThrowResult Result) ProcessThrow(object gameModel, AfterThrowState afterThrow)
|
|
{
|
|
var model = this.CastModel<ChristmasTreeGameModel>(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
|
|
});
|
|
}
|
|
|
|
// Find player's team
|
|
int playerTeamIndex = FindPlayerTeamIndex(model, playerId);
|
|
if (playerTeamIndex < 0) playerTeamIndex = model.CurrentTeamIndex;
|
|
|
|
var teamTrees = new Dictionary<int, TeamTreeState>(model.TeamTrees);
|
|
var currentTeamState = teamTrees[playerTeamIndex] with
|
|
{
|
|
ThrowCount = teamTrees[playerTeamIndex].ThrowCount + 1,
|
|
TotalPins = teamTrees[playerTeamIndex].TotalPins + pinsKnocked
|
|
};
|
|
teamTrees[playerTeamIndex] = currentTeamState;
|
|
|
|
var triggers = new List<TriggerEvent>();
|
|
var lastThrow = new ChristmasTreeLastThrow
|
|
{
|
|
PlayerId = playerId,
|
|
TeamIndex = playerTeamIndex,
|
|
PinsKnocked = pinsKnocked,
|
|
WasGutter = afterThrow.IsGutter
|
|
};
|
|
|
|
// Gutter or 0 pins = miss, no crossing
|
|
if (pinsKnocked == 0)
|
|
{
|
|
lastThrow = lastThrow with { WasMiss = true };
|
|
|
|
if (model.IsInContinueMode)
|
|
{
|
|
return HandleContinueModeThrow(model, teamTrees, lastThrow, pinsKnocked, playerTeamIndex, false);
|
|
}
|
|
|
|
model = model with
|
|
{
|
|
TeamTrees = teamTrees,
|
|
LastThrow = lastThrow
|
|
};
|
|
|
|
return RotateToNextPlayer(model, triggers);
|
|
}
|
|
|
|
// Try to cross number from own tree first
|
|
bool crossedOwn = TryCrossNumber(teamTrees, playerTeamIndex, pinsKnocked);
|
|
|
|
if (crossedOwn)
|
|
{
|
|
lastThrow = lastThrow with { CrossedOwnTree = true };
|
|
teamTrees[playerTeamIndex] = teamTrees[playerTeamIndex] with
|
|
{
|
|
CrossedOutCount = teamTrees[playerTeamIndex].CrossedOutCount + 1
|
|
};
|
|
|
|
// Check if game ended (last 5 crossed)
|
|
bool gameEnded = CheckAllFivesGone(teamTrees);
|
|
if (gameEnded)
|
|
{
|
|
lastThrow = lastThrow with { TriggeredGameEnd = true };
|
|
return HandleGameEnd(model, teamTrees, lastThrow, triggers);
|
|
}
|
|
|
|
model = model with
|
|
{
|
|
TeamTrees = teamTrees,
|
|
LastThrow = lastThrow,
|
|
IsInContinueMode = false,
|
|
ContinueThrowsRemaining = 0
|
|
};
|
|
|
|
return RotateToNextPlayer(model, triggers);
|
|
}
|
|
|
|
// Try to cross from opponent trees
|
|
var crossedOpponents = TryCrossFromOpponents(teamTrees, playerTeamIndex, pinsKnocked);
|
|
|
|
if (crossedOpponents.Count > 0)
|
|
{
|
|
lastThrow = lastThrow with
|
|
{
|
|
CrossedOpponentTrees = true,
|
|
OpponentTeamsCrossed = crossedOpponents.ToArray()
|
|
};
|
|
|
|
// Check if game ended
|
|
bool gameEnded = CheckAllFivesGone(teamTrees);
|
|
if (gameEnded)
|
|
{
|
|
lastThrow = lastThrow with { TriggeredGameEnd = true };
|
|
return HandleGameEnd(model, teamTrees, lastThrow, triggers);
|
|
}
|
|
|
|
model = model with
|
|
{
|
|
TeamTrees = teamTrees,
|
|
LastThrow = lastThrow,
|
|
IsInContinueMode = false,
|
|
ContinueThrowsRemaining = 0
|
|
};
|
|
|
|
return RotateToNextPlayer(model, triggers);
|
|
}
|
|
|
|
// Number not available anywhere = miss
|
|
lastThrow = lastThrow with { WasMiss = true };
|
|
|
|
if (model.EnableContinueOnMiss)
|
|
{
|
|
return HandleContinueModeThrow(model, teamTrees, lastThrow, pinsKnocked, playerTeamIndex, true);
|
|
}
|
|
|
|
model = model with
|
|
{
|
|
TeamTrees = teamTrees,
|
|
LastThrow = lastThrow
|
|
};
|
|
|
|
return RotateToNextPlayer(model, triggers);
|
|
}
|
|
|
|
private (object UpdatedModel, ThrowResult Result) HandleContinueModeThrow(
|
|
ChristmasTreeGameModel model,
|
|
Dictionary<int, TeamTreeState> teamTrees,
|
|
ChristmasTreeLastThrow lastThrow,
|
|
int pinsKnocked,
|
|
int playerTeamIndex,
|
|
bool justEnteredContinueMode)
|
|
{
|
|
int throwsRemaining;
|
|
|
|
if (justEnteredContinueMode)
|
|
{
|
|
// Just entered continue mode
|
|
throwsRemaining = model.MaxContinueThrows - 1; // -1 because this throw counts
|
|
}
|
|
else
|
|
{
|
|
// Already in continue mode
|
|
throwsRemaining = model.ContinueThrowsRemaining - 1;
|
|
}
|
|
|
|
if (throwsRemaining > 0)
|
|
{
|
|
// Still has throws remaining
|
|
lastThrow = lastThrow with
|
|
{
|
|
StillInContinueMode = true,
|
|
ContinueThrowsRemaining = throwsRemaining
|
|
};
|
|
|
|
model = model with
|
|
{
|
|
TeamTrees = teamTrees,
|
|
LastThrow = lastThrow,
|
|
IsInContinueMode = true,
|
|
ContinueThrowsRemaining = throwsRemaining
|
|
};
|
|
|
|
// Don't rotate - same player continues
|
|
return (model, new ThrowResult
|
|
{
|
|
PointsScored = pinsKnocked,
|
|
ShouldRotatePlayer = false,
|
|
IsGameOver = false,
|
|
WinnerId = null,
|
|
Triggers = []
|
|
});
|
|
}
|
|
else
|
|
{
|
|
// Failed all continue throws - opponent can select a number
|
|
// Find next opponent team
|
|
int nextOpponentIndex = (playerTeamIndex + 1) % model.Teams.Count;
|
|
|
|
model = model with
|
|
{
|
|
TeamTrees = teamTrees,
|
|
LastThrow = lastThrow,
|
|
IsInContinueMode = false,
|
|
ContinueThrowsRemaining = 0,
|
|
OpponentSelectingTeamIndex = nextOpponentIndex
|
|
};
|
|
|
|
// Don't rotate yet - wait for opponent selection action
|
|
return (model, new ThrowResult
|
|
{
|
|
PointsScored = pinsKnocked,
|
|
ShouldRotatePlayer = false,
|
|
IsGameOver = false,
|
|
WinnerId = null,
|
|
Triggers = []
|
|
});
|
|
}
|
|
}
|
|
|
|
private (object UpdatedModel, ThrowResult Result) HandleGameEnd(
|
|
ChristmasTreeGameModel model,
|
|
Dictionary<int, TeamTreeState> teamTrees,
|
|
ChristmasTreeLastThrow lastThrow,
|
|
List<TriggerEvent> triggers)
|
|
{
|
|
// Find winner (team with most crossed numbers)
|
|
int winnerIndex = 0;
|
|
int maxCrossed = 0;
|
|
|
|
foreach (var (teamIndex, state) in teamTrees)
|
|
{
|
|
if (state.CrossedOutCount > maxCrossed)
|
|
{
|
|
maxCrossed = state.CrossedOutCount;
|
|
winnerIndex = teamIndex;
|
|
}
|
|
}
|
|
|
|
// Get winner ID (first player of winning team)
|
|
Guid? winnerId = model.Teams[winnerIndex].PlayerIds.FirstOrDefault();
|
|
|
|
// Calculate penalties for losing teams
|
|
foreach (var (teamIndex, state) in teamTrees)
|
|
{
|
|
if (teamIndex != winnerIndex)
|
|
{
|
|
int remainingNumbers = state.RemainingNumbers.Values.Sum();
|
|
if (remainingNumbers > 0)
|
|
{
|
|
// Each player in losing team pays per remaining number
|
|
foreach (var playerId in model.Teams[teamIndex].PlayerIds)
|
|
{
|
|
triggers.Add(new TriggerEvent
|
|
{
|
|
TriggerType = ExpenseTriggerType.ExpensePoint.ToString(),
|
|
PersonId = playerId,
|
|
Multiplier = remainingNumbers
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
model = model with
|
|
{
|
|
TeamTrees = teamTrees,
|
|
LastThrow = lastThrow,
|
|
IsGameOver = true,
|
|
WinnerId = winnerId,
|
|
WinnerTeamIndex = winnerIndex,
|
|
IsInContinueMode = false,
|
|
ContinueThrowsRemaining = 0,
|
|
OpponentSelectingTeamIndex = null
|
|
};
|
|
|
|
return (model, new ThrowResult
|
|
{
|
|
PointsScored = lastThrow.PinsKnocked,
|
|
ShouldRotatePlayer = false,
|
|
IsGameOver = true,
|
|
WinnerId = winnerId,
|
|
Triggers = triggers
|
|
});
|
|
}
|
|
|
|
private (object UpdatedModel, ThrowResult Result) RotateToNextPlayer(
|
|
ChristmasTreeGameModel model,
|
|
List<TriggerEvent> triggers)
|
|
{
|
|
// Rotate to next team and player
|
|
int nextTeamIndex = (model.CurrentTeamIndex + 1) % model.Teams.Count;
|
|
int nextPlayerIndex = 0;
|
|
|
|
if (nextTeamIndex == 0)
|
|
{
|
|
// Wrapped around - increment player index within teams
|
|
// This assumes all teams have same number of players
|
|
// For varying team sizes, we track per-team player index
|
|
nextPlayerIndex = (model.CurrentPlayerIndexInTeam + 1) %
|
|
Math.Max(1, model.Teams[nextTeamIndex].PlayerIds.Count);
|
|
}
|
|
else
|
|
{
|
|
nextPlayerIndex = model.CurrentPlayerIndexInTeam %
|
|
Math.Max(1, model.Teams[nextTeamIndex].PlayerIds.Count);
|
|
}
|
|
|
|
var nextPlayerId = model.Teams[nextTeamIndex].PlayerIds.Count > nextPlayerIndex
|
|
? model.Teams[nextTeamIndex].PlayerIds[nextPlayerIndex]
|
|
: model.Teams[nextTeamIndex].PlayerIds.FirstOrDefault();
|
|
|
|
model = model with
|
|
{
|
|
CurrentTeamIndex = nextTeamIndex,
|
|
CurrentPlayerIndexInTeam = nextPlayerIndex
|
|
};
|
|
|
|
return (model, new ThrowResult
|
|
{
|
|
PointsScored = model.LastThrow?.PinsKnocked ?? 0,
|
|
ShouldRotatePlayer = true,
|
|
IsGameOver = false,
|
|
WinnerId = null,
|
|
Triggers = triggers,
|
|
Overrides = new GameLogicOverrides
|
|
{
|
|
NextPlayerId = nextPlayerId
|
|
}
|
|
});
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public (bool IsGameOver, Guid? WinnerId) CheckGameEnd(object gameModel)
|
|
{
|
|
var model = this.CastModel<ChristmasTreeGameModel>(gameModel);
|
|
return (model.IsGameOver, model.WinnerId);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyDictionary<Guid, PlayerStatsSummary> GetPlayerStats(object gameModel)
|
|
{
|
|
var model = this.CastModel<ChristmasTreeGameModel>(gameModel);
|
|
var stats = new Dictionary<Guid, PlayerStatsSummary>();
|
|
|
|
foreach (var (teamIndex, team) in model.Teams.Select((t, i) => (i, t)))
|
|
{
|
|
var teamState = model.TeamTrees.GetValueOrDefault(teamIndex, new TeamTreeState());
|
|
|
|
foreach (var playerId in team.PlayerIds)
|
|
{
|
|
stats[playerId] = new PlayerStatsSummary
|
|
{
|
|
ThrowCount = teamState.ThrowCount,
|
|
PinCount = teamState.TotalPins,
|
|
CircleCount = 0,
|
|
StrikeCount = 0,
|
|
CurrentPoints = teamState.CrossedOutCount,
|
|
CustomStats = new Dictionary<string, object>
|
|
{
|
|
["TeamIndex"] = teamIndex,
|
|
["TeamName"] = team.Name,
|
|
["CrossedOut"] = teamState.CrossedOutCount,
|
|
["Remaining"] = teamState.RemainingNumbers.Values.Sum(),
|
|
["IsWinner"] = model.WinnerTeamIndex == teamIndex,
|
|
["IsLoser"] = model.IsGameOver && model.WinnerTeamIndex != teamIndex
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public IReadOnlyList<GameActionDescriptor> GetAvailableActions(object gameModel, Guid currentPlayerId)
|
|
{
|
|
var model = this.CastModel<ChristmasTreeGameModel>(gameModel);
|
|
|
|
if (model.IsGameOver)
|
|
return [];
|
|
|
|
// Check if opponent needs to select a number
|
|
if (model.OpponentSelectingTeamIndex.HasValue)
|
|
{
|
|
int selectingTeamIndex = model.OpponentSelectingTeamIndex.Value;
|
|
var selectingTeam = model.Teams[selectingTeamIndex];
|
|
|
|
// Only players of the selecting team can execute this action
|
|
if (selectingTeam.PlayerIds.Contains(currentPlayerId))
|
|
{
|
|
// Build list of available numbers to select
|
|
var availableNumbers = model.TeamTrees[selectingTeamIndex].RemainingNumbers
|
|
.Where(kvp => kvp.Value > 0)
|
|
.Select(kvp => kvp.Key)
|
|
.OrderBy(n => n)
|
|
.ToList();
|
|
|
|
if (availableNumbers.Count > 0)
|
|
{
|
|
return
|
|
[
|
|
new GameActionDescriptor
|
|
{
|
|
ActionId = "selectNumber",
|
|
Label = "Zahl wählen",
|
|
Icon = "TouchApp",
|
|
Color = "Primary",
|
|
Tooltip = $"Wähle eine Zahl zum Streichen: {string.Join(", ", availableNumbers)}",
|
|
Order = 1,
|
|
Parameters =
|
|
[
|
|
new ActionParameterDefinition
|
|
{
|
|
Key = "number",
|
|
Label = "Wähle eine Zahl",
|
|
Type = "select",
|
|
Options = availableNumbers.Select(n => new ActionParameterOption
|
|
{
|
|
Value = n,
|
|
Label = n.ToString()
|
|
}).ToList()
|
|
}
|
|
]
|
|
}
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public GameActionResult ExecuteAction(
|
|
object gameModel,
|
|
string actionId,
|
|
Guid currentPlayerId,
|
|
IReadOnlyDictionary<string, object>? parameters = null)
|
|
{
|
|
return actionId switch
|
|
{
|
|
"selectNumber" => ExecuteSelectNumber(gameModel, currentPlayerId, parameters),
|
|
_ => GameActionResult.Failure($"Unknown action: {actionId}")
|
|
};
|
|
}
|
|
|
|
private GameActionResult ExecuteSelectNumber(
|
|
object gameModel,
|
|
Guid currentPlayerId,
|
|
IReadOnlyDictionary<string, object>? parameters)
|
|
{
|
|
var model = this.CastModel<ChristmasTreeGameModel>(gameModel);
|
|
|
|
if (!model.OpponentSelectingTeamIndex.HasValue)
|
|
{
|
|
return GameActionResult.Failure("No number selection pending.");
|
|
}
|
|
|
|
int selectingTeamIndex = model.OpponentSelectingTeamIndex.Value;
|
|
var selectingTeam = model.Teams[selectingTeamIndex];
|
|
|
|
if (!selectingTeam.PlayerIds.Contains(currentPlayerId))
|
|
{
|
|
return GameActionResult.Failure("Only the opponent team can select a number.");
|
|
}
|
|
|
|
// Get selected number from parameters
|
|
if (parameters == null || !parameters.TryGetValue("number", out var numberObj))
|
|
{
|
|
return GameActionResult.Failure("No number specified.");
|
|
}
|
|
|
|
int selectedNumber;
|
|
if (numberObj is int num)
|
|
{
|
|
selectedNumber = num;
|
|
}
|
|
else if (numberObj is long longNum)
|
|
{
|
|
selectedNumber = (int)longNum;
|
|
}
|
|
else if (numberObj is JsonElement jsonElement && jsonElement.TryGetInt32(out var jsonNum))
|
|
{
|
|
selectedNumber = jsonNum;
|
|
}
|
|
else
|
|
{
|
|
return GameActionResult.Failure($"Invalid number format: {numberObj}");
|
|
}
|
|
|
|
// Validate number is available
|
|
var teamTrees = new Dictionary<int, TeamTreeState>(model.TeamTrees);
|
|
var teamState = teamTrees[selectingTeamIndex];
|
|
|
|
if (!teamState.RemainingNumbers.TryGetValue(selectedNumber, out var remaining) || remaining <= 0)
|
|
{
|
|
return GameActionResult.Failure($"Number {selectedNumber} is not available on your tree.");
|
|
}
|
|
|
|
// Cross the number
|
|
var updatedNumbers = new Dictionary<int, int>(teamState.RemainingNumbers)
|
|
{
|
|
[selectedNumber] = remaining - 1
|
|
};
|
|
|
|
teamTrees[selectingTeamIndex] = teamState with
|
|
{
|
|
RemainingNumbers = updatedNumbers,
|
|
CrossedOutCount = teamState.CrossedOutCount + 1
|
|
};
|
|
|
|
// Check for game end
|
|
bool gameEnded = CheckAllFivesGone(teamTrees);
|
|
var triggers = new List<TriggerEvent>();
|
|
|
|
if (gameEnded)
|
|
{
|
|
// Find winner
|
|
int winnerIndex = 0;
|
|
int maxCrossed = 0;
|
|
|
|
foreach (var (teamIndex, state) in teamTrees)
|
|
{
|
|
if (state.CrossedOutCount > maxCrossed)
|
|
{
|
|
maxCrossed = state.CrossedOutCount;
|
|
winnerIndex = teamIndex;
|
|
}
|
|
}
|
|
|
|
Guid? winnerId = model.Teams[winnerIndex].PlayerIds.FirstOrDefault();
|
|
|
|
// Calculate penalties
|
|
foreach (var (teamIndex, state) in teamTrees)
|
|
{
|
|
if (teamIndex != winnerIndex)
|
|
{
|
|
int remainingNumbers = state.RemainingNumbers.Values.Sum();
|
|
if (remainingNumbers > 0)
|
|
{
|
|
foreach (var playerId in model.Teams[teamIndex].PlayerIds)
|
|
{
|
|
triggers.Add(new TriggerEvent
|
|
{
|
|
TriggerType = ExpenseTriggerType.ExpensePoint.ToString(),
|
|
PersonId = playerId,
|
|
Multiplier = remainingNumbers
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
model = model with
|
|
{
|
|
TeamTrees = teamTrees,
|
|
OpponentSelectingTeamIndex = null,
|
|
IsGameOver = true,
|
|
WinnerId = winnerId,
|
|
WinnerTeamIndex = winnerIndex
|
|
};
|
|
|
|
return GameActionResult.SuccessResult(
|
|
model,
|
|
shouldRotate: false,
|
|
isGameOver: true,
|
|
winnerId: winnerId,
|
|
triggers: triggers);
|
|
}
|
|
|
|
// Clear selection state and rotate to next player
|
|
int nextTeamIndex = (model.CurrentTeamIndex + 1) % model.Teams.Count;
|
|
int nextPlayerIndex = model.CurrentPlayerIndexInTeam;
|
|
if (nextTeamIndex == 0)
|
|
{
|
|
nextPlayerIndex = (nextPlayerIndex + 1) % Math.Max(1, model.Teams[nextTeamIndex].PlayerIds.Count);
|
|
}
|
|
else
|
|
{
|
|
nextPlayerIndex = nextPlayerIndex % Math.Max(1, model.Teams[nextTeamIndex].PlayerIds.Count);
|
|
}
|
|
|
|
var nextPlayerId = model.Teams[nextTeamIndex].PlayerIds.Count > nextPlayerIndex
|
|
? model.Teams[nextTeamIndex].PlayerIds[nextPlayerIndex]
|
|
: model.Teams[nextTeamIndex].PlayerIds.FirstOrDefault();
|
|
|
|
model = model with
|
|
{
|
|
TeamTrees = teamTrees,
|
|
OpponentSelectingTeamIndex = null,
|
|
CurrentTeamIndex = nextTeamIndex,
|
|
CurrentPlayerIndexInTeam = nextPlayerIndex
|
|
};
|
|
|
|
return GameActionResult.SuccessResult(
|
|
model,
|
|
shouldRotate: true,
|
|
isGameOver: false,
|
|
winnerId: null,
|
|
triggers: triggers);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public GameSetupValidationResult ValidateSetup(object? setupOptions)
|
|
{
|
|
if (setupOptions is null)
|
|
{
|
|
return GameSetupValidationResult.Invalid(["Setup options required."]);
|
|
}
|
|
|
|
var options = ParseSetupOptions(setupOptions);
|
|
var errors = new List<string>();
|
|
|
|
if (options.Teams == null || options.Teams.Count < 2)
|
|
{
|
|
errors.Add("Mindestens 2 Mannschaften erforderlich.");
|
|
}
|
|
else
|
|
{
|
|
foreach (var team in options.Teams)
|
|
{
|
|
if (team.PlayerIds.Count == 0)
|
|
{
|
|
errors.Add($"Mannschaft '{team.Name}' hat keine Spieler.");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.MaxContinueThrows < 1 || options.MaxContinueThrows > 10)
|
|
{
|
|
errors.Add("Anzahl Weiterwürfe muss zwischen 1 und 10 liegen.");
|
|
}
|
|
|
|
return errors.Count > 0
|
|
? GameSetupValidationResult.Invalid(errors.ToArray())
|
|
: GameSetupValidationResult.Valid();
|
|
}
|
|
|
|
#region Helper Methods
|
|
|
|
private static int FindPlayerTeamIndex(ChristmasTreeGameModel model, Guid playerId)
|
|
{
|
|
for (int i = 0; i < model.Teams.Count; i++)
|
|
{
|
|
if (model.Teams[i].PlayerIds.Contains(playerId))
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
private static bool TryCrossNumber(Dictionary<int, TeamTreeState> teamTrees, int teamIndex, int number)
|
|
{
|
|
var state = teamTrees[teamIndex];
|
|
if (state.RemainingNumbers.TryGetValue(number, out var remaining) && remaining > 0)
|
|
{
|
|
var updatedNumbers = new Dictionary<int, int>(state.RemainingNumbers)
|
|
{
|
|
[number] = remaining - 1
|
|
};
|
|
teamTrees[teamIndex] = state with { RemainingNumbers = updatedNumbers };
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static List<int> TryCrossFromOpponents(
|
|
Dictionary<int, TeamTreeState> teamTrees,
|
|
int playerTeamIndex,
|
|
int number)
|
|
{
|
|
var crossedTeams = new List<int>();
|
|
|
|
foreach (var (teamIndex, state) in teamTrees)
|
|
{
|
|
if (teamIndex == playerTeamIndex) continue;
|
|
|
|
if (state.RemainingNumbers.TryGetValue(number, out var remaining) && remaining > 0)
|
|
{
|
|
var updatedNumbers = new Dictionary<int, int>(state.RemainingNumbers)
|
|
{
|
|
[number] = remaining - 1
|
|
};
|
|
teamTrees[teamIndex] = state with { RemainingNumbers = updatedNumbers };
|
|
crossedTeams.Add(teamIndex);
|
|
}
|
|
}
|
|
|
|
return crossedTeams;
|
|
}
|
|
|
|
private static bool CheckAllFivesGone(Dictionary<int, TeamTreeState> teamTrees)
|
|
{
|
|
foreach (var state in teamTrees.Values)
|
|
{
|
|
if (state.RemainingNumbers.TryGetValue(5, out var remaining) && remaining > 0)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static ChristmasTreeGameSetup ParseSetupOptions(object? setupOptions)
|
|
{
|
|
if (setupOptions is null)
|
|
{
|
|
return new ChristmasTreeGameSetup();
|
|
}
|
|
|
|
if (setupOptions is ChristmasTreeGameSetup typedSetup)
|
|
{
|
|
return typedSetup;
|
|
}
|
|
|
|
if (setupOptions is JsonElement jsonElement)
|
|
{
|
|
try
|
|
{
|
|
return JsonSerializer.Deserialize<ChristmasTreeGameSetup>(
|
|
jsonElement.GetRawText(),
|
|
GameModelFactory.JsonSerializerOptions) ?? new ChristmasTreeGameSetup();
|
|
}
|
|
catch
|
|
{
|
|
return new ChristmasTreeGameSetup();
|
|
}
|
|
}
|
|
|
|
return new ChristmasTreeGameSetup();
|
|
}
|
|
|
|
#endregion
|
|
}
|