From 968eb0c155ffc01159011da294adadb419ab740a Mon Sep 17 00:00:00 2001 From: Acentech Dev Date: Mon, 20 Jul 2026 22:59:15 +0300 Subject: [PATCH] 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) --- .../MiniGames/HeykelSavasiContext.cs | 101 ++++++++++++++++++ src/GameLogic/MiniGames/HeykelSavasiTeam.cs | 26 +++++ .../HeykelSavasi/HeykelSavasiContextTests.cs | 37 +++++++ 3 files changed, 164 insertions(+) create mode 100644 src/GameLogic/MiniGames/HeykelSavasiContext.cs create mode 100644 src/GameLogic/MiniGames/HeykelSavasiTeam.cs create mode 100644 tests/MUnique.OpenMU.Tests/HeykelSavasi/HeykelSavasiContextTests.cs diff --git a/src/GameLogic/MiniGames/HeykelSavasiContext.cs b/src/GameLogic/MiniGames/HeykelSavasiContext.cs new file mode 100644 index 0000000..006944a --- /dev/null +++ b/src/GameLogic/MiniGames/HeykelSavasiContext.cs @@ -0,0 +1,101 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.MiniGames; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using MUnique.OpenMU.DataModel.Configuration; + +/// +/// The context of the Heykel Savasi (statue war) event. +/// +/// +/// 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. +/// +public class HeykelSavasiContext : MiniGameContext +{ + /// + /// The maximum allowed difference between the red and blue team sizes after a join. + /// + private const int MaxTeamDifference = 2; + + private readonly ConcurrentDictionary _teams = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The key of this context. + /// The definition of the mini game. + /// The game context, to which this game belongs. + /// The map initializer, which is used when the event starts. + public HeykelSavasiContext(MiniGameMapKey key, MiniGameDefinition definition, IGameContext gameContext, IMapInitializer mapInitializer) + : base(key, definition, gameContext, mapInitializer) + { + } + + /// + /// Gets the current number of players assigned to the given team. + /// + /// The team. + /// The number of players currently assigned to . + public int TeamCount(HeykelSavasiTeam team) => this._teams.Values.Count(t => t == team); + + /// + /// Determines whether a player may currently join the given team, based on the team-balance rule. + /// + /// The team which a player wants to join. + /// true if the join is allowed; otherwise, false. + public bool CanJoin(HeykelSavasiTeam team) => IsJoinAllowed(this.TeamCount(HeykelSavasiTeam.Red), this.TeamCount(HeykelSavasiTeam.Blue), team); + + /// + /// Assigns the given player to the given team. + /// + /// The player. + /// The team. + public void AssignTeam(Player player, HeykelSavasiTeam team) => this._teams[player] = team; + + /// + /// Gets the team of the given player. + /// + /// The player. + /// The team of the player, or if the player is not assigned to a team. + public HeykelSavasiTeam GetTeam(Player player) => this._teams.TryGetValue(player, out var team) ? team : HeykelSavasiTeam.None; + + /// + /// Gets all players which are currently assigned to the given team. + /// + /// The team. + /// All players assigned to . + public IEnumerable PlayersOf(HeykelSavasiTeam team) => this._teams.Where(kv => kv.Value == team).Select(kv => kv.Key); + + /// + /// Determines, in a pure way, whether joining 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 after the prospective join. + /// + /// The current number of players in the red team. + /// The current number of players in the blue team. + /// The team which a player wants to join. + /// true if the join is allowed; otherwise, false. + 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; + } +} diff --git a/src/GameLogic/MiniGames/HeykelSavasiTeam.cs b/src/GameLogic/MiniGames/HeykelSavasiTeam.cs new file mode 100644 index 0000000..f1b93c5 --- /dev/null +++ b/src/GameLogic/MiniGames/HeykelSavasiTeam.cs @@ -0,0 +1,26 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.MiniGames; + +/// +/// The team of a player participating in the Heykel Savasi event. +/// +public enum HeykelSavasiTeam +{ + /// + /// The player is not assigned to any team. + /// + None, + + /// + /// The red team. + /// + Red, + + /// + /// The blue team. + /// + Blue, +} diff --git a/tests/MUnique.OpenMU.Tests/HeykelSavasi/HeykelSavasiContextTests.cs b/tests/MUnique.OpenMU.Tests/HeykelSavasi/HeykelSavasiContextTests.cs new file mode 100644 index 0000000..c085724 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/HeykelSavasi/HeykelSavasiContextTests.cs @@ -0,0 +1,37 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests.HeykelSavasi; + +using MUnique.OpenMU.GameLogic.MiniGames; + +/// +/// Tests for the Heykel Savasi team-balance rule. +/// +/// +/// derives from , whose constructor +/// creates a map and launches a background game loop via Task.Run. Constructing a full context +/// is impractical in a fast unit test, so the balance decision is exposed as a pure static method, +/// , and tested directly against it. +/// +[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); + } +}