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);
+ }
+}