feat(hs): HeykelSavasiContext team tracking + balance rule
Adds the HeykelSavasiTeam enum (None/Red/Blue) and the HeykelSavasiContext skeleton (subclass of MiniGameContext) with team registration (AssignTeam/GetTeam/PlayersOf/TeamCount) and a join-balance rule (|Red-Blue| <= 2 after the prospective join), exposed as a pure static IsJoinAllowed for fast unit testing without constructing the full context. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
101
src/GameLogic/MiniGames/HeykelSavasiContext.cs
Normal file
101
src/GameLogic/MiniGames/HeykelSavasiContext.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// <copyright file="HeykelSavasiContext.cs" company="MUnique">
|
||||||
|
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||||
|
// </copyright>
|
||||||
|
|
||||||
|
namespace MUnique.OpenMU.GameLogic.MiniGames;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using MUnique.OpenMU.DataModel.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The context of the Heykel Savasi (statue war) event.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is the foundation class for the event: it tracks which team every participating player
|
||||||
|
/// belongs to, and enforces a team-balance rule when players attempt to join a team.
|
||||||
|
/// </remarks>
|
||||||
|
public class HeykelSavasiContext : MiniGameContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum allowed difference between the red and blue team sizes after a join.
|
||||||
|
/// </summary>
|
||||||
|
private const int MaxTeamDifference = 2;
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<Player, HeykelSavasiTeam> _teams = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HeykelSavasiContext"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key of this context.</param>
|
||||||
|
/// <param name="definition">The definition of the mini game.</param>
|
||||||
|
/// <param name="gameContext">The game context, to which this game belongs.</param>
|
||||||
|
/// <param name="mapInitializer">The map initializer, which is used when the event starts.</param>
|
||||||
|
public HeykelSavasiContext(MiniGameMapKey key, MiniGameDefinition definition, IGameContext gameContext, IMapInitializer mapInitializer)
|
||||||
|
: base(key, definition, gameContext, mapInitializer)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current number of players assigned to the given team.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="team">The team.</param>
|
||||||
|
/// <returns>The number of players currently assigned to <paramref name="team"/>.</returns>
|
||||||
|
public int TeamCount(HeykelSavasiTeam team) => this._teams.Values.Count(t => t == team);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether a player may currently join the given team, based on the team-balance rule.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="team">The team which a player wants to join.</param>
|
||||||
|
/// <returns><c>true</c> if the join is allowed; otherwise, <c>false</c>.</returns>
|
||||||
|
public bool CanJoin(HeykelSavasiTeam team) => IsJoinAllowed(this.TeamCount(HeykelSavasiTeam.Red), this.TeamCount(HeykelSavasiTeam.Blue), team);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Assigns the given player to the given team.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="player">The player.</param>
|
||||||
|
/// <param name="team">The team.</param>
|
||||||
|
public void AssignTeam(Player player, HeykelSavasiTeam team) => this._teams[player] = team;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the team of the given player.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="player">The player.</param>
|
||||||
|
/// <returns>The team of the player, or <see cref="HeykelSavasiTeam.None"/> if the player is not assigned to a team.</returns>
|
||||||
|
public HeykelSavasiTeam GetTeam(Player player) => this._teams.TryGetValue(player, out var team) ? team : HeykelSavasiTeam.None;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all players which are currently assigned to the given team.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="team">The team.</param>
|
||||||
|
/// <returns>All players assigned to <paramref name="team"/>.</returns>
|
||||||
|
public IEnumerable<Player> PlayersOf(HeykelSavasiTeam team) => this._teams.Where(kv => kv.Value == team).Select(kv => kv.Key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines, in a pure way, whether joining <paramref name="team"/> is allowed given the current
|
||||||
|
/// team counts, according to the balance rule: the absolute difference between the red and blue
|
||||||
|
/// team sizes must not exceed <see cref="MaxTeamDifference"/> after the prospective join.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="redCount">The current number of players in the red team.</param>
|
||||||
|
/// <param name="blueCount">The current number of players in the blue team.</param>
|
||||||
|
/// <param name="team">The team which a player wants to join.</param>
|
||||||
|
/// <returns><c>true</c> if the join is allowed; otherwise, <c>false</c>.</returns>
|
||||||
|
internal static bool IsJoinAllowed(int redCount, int blueCount, HeykelSavasiTeam team)
|
||||||
|
{
|
||||||
|
switch (team)
|
||||||
|
{
|
||||||
|
case HeykelSavasiTeam.Red:
|
||||||
|
redCount++;
|
||||||
|
break;
|
||||||
|
case HeykelSavasiTeam.Blue:
|
||||||
|
blueCount++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Abs(redCount - blueCount) <= MaxTeamDifference;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/GameLogic/MiniGames/HeykelSavasiTeam.cs
Normal file
26
src/GameLogic/MiniGames/HeykelSavasiTeam.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// <copyright file="HeykelSavasiTeam.cs" company="MUnique">
|
||||||
|
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||||
|
// </copyright>
|
||||||
|
|
||||||
|
namespace MUnique.OpenMU.GameLogic.MiniGames;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The team of a player participating in the Heykel Savasi event.
|
||||||
|
/// </summary>
|
||||||
|
public enum HeykelSavasiTeam
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The player is not assigned to any team.
|
||||||
|
/// </summary>
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The red team.
|
||||||
|
/// </summary>
|
||||||
|
Red,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The blue team.
|
||||||
|
/// </summary>
|
||||||
|
Blue,
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// <copyright file="HeykelSavasiContextTests.cs" company="MUnique">
|
||||||
|
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||||
|
// </copyright>
|
||||||
|
|
||||||
|
namespace MUnique.OpenMU.Tests.HeykelSavasi;
|
||||||
|
|
||||||
|
using MUnique.OpenMU.GameLogic.MiniGames;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for the Heykel Savasi team-balance rule.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="HeykelSavasiContext"/> derives from <see cref="MiniGameContext"/>, whose constructor
|
||||||
|
/// creates a map and launches a background game loop via <c>Task.Run</c>. Constructing a full context
|
||||||
|
/// is impractical in a fast unit test, so the balance decision is exposed as a pure static method,
|
||||||
|
/// <see cref="HeykelSavasiContext.IsJoinAllowed"/>, and tested directly against it.
|
||||||
|
/// </remarks>
|
||||||
|
[TestFixture]
|
||||||
|
public class HeykelSavasiContextTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void BalanceRule_BlocksJoinWhenDifferenceWouldExceedTwo()
|
||||||
|
{
|
||||||
|
// Red 0, Blue 0 -> either team may be joined.
|
||||||
|
Assert.That(HeykelSavasiContext.IsJoinAllowed(0, 0, HeykelSavasiTeam.Red), Is.True);
|
||||||
|
|
||||||
|
// Red already has 2, Blue has 0. Adding a 3rd to Red would make the difference 3.
|
||||||
|
Assert.That(HeykelSavasiContext.IsJoinAllowed(2, 0, HeykelSavasiTeam.Red), Is.False, "2-0 iken Red'e katilim farki 3 yapar");
|
||||||
|
Assert.That(HeykelSavasiContext.IsJoinAllowed(2, 0, HeykelSavasiTeam.Blue), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void BalanceRule_NoneTeamIsNeverJoinable()
|
||||||
|
{
|
||||||
|
Assert.That(HeykelSavasiContext.IsJoinAllowed(0, 0, HeykelSavasiTeam.None), Is.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user