feat(CS-P1): Castle Siege phase state machine (in-memory, time-injected) + tests

This commit is contained in:
Acentech Dev
2026-07-14 22:40:24 +03:00
parent c2b495c754
commit 73de807bef
4 changed files with 278 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
// <copyright file="CastleSiegeContext.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.CastleSiege;
/// <summary>
/// In-memory Castle Siege phase state machine (P1 skeleton: no battle/persistence).
/// Time is injected via method parameters so it can be tested deterministically.
/// </summary>
public class CastleSiegeContext
{
private readonly List<string> _registeredGuilds = new();
private DateTime _phaseStartedUtc;
/// <summary>Initializes a new instance of the <see cref="CastleSiegeContext"/> class.</summary>
/// <param name="configuration">The cycle timing configuration.</param>
public CastleSiegeContext(CastleSiegeConfiguration configuration)
{
this.Configuration = configuration;
this.Phase = CastleSiegePhase.Ownership;
}
/// <summary>Raised after the phase changes. Argument is the new phase.</summary>
public event Action<CastleSiegePhase>? PhaseChanged;
/// <summary>Gets the configuration.</summary>
public CastleSiegeConfiguration Configuration { get; }
/// <summary>Gets the current phase.</summary>
public CastleSiegePhase Phase { get; private set; }
/// <summary>Gets the current owner guild name, or null if unowned.</summary>
public string? OwnerGuildName { get; private set; }
/// <summary>Gets the guild names registered for the current cycle.</summary>
public IReadOnlyList<string> RegisteredGuilds => this._registeredGuilds;
/// <summary>Advances the state machine based on the current time.</summary>
/// <param name="now">The current UTC time.</param>
public ValueTask TickAsync(DateTime now)
{
switch (this.Phase)
{
case CastleSiegePhase.Ownership:
if (this.Configuration.IsRegistrationOpenTime(now))
{
return this.ForceStartRegistrationAsync(now);
}
break;
case CastleSiegePhase.Registration:
if (now >= this._phaseStartedUtc + this.Configuration.RegistrationDuration)
{
return this.TransitionAsync(CastleSiegePhase.Preparation, now);
}
break;
case CastleSiegePhase.Preparation:
if (now >= this._phaseStartedUtc + this.Configuration.PreparationDuration)
{
return this.TransitionAsync(CastleSiegePhase.Siege, now);
}
break;
case CastleSiegePhase.Siege:
if (now >= this._phaseStartedUtc + this.Configuration.SiegeDuration)
{
return this.TransitionAsync(CastleSiegePhase.Settlement, now);
}
break;
case CastleSiegePhase.Settlement:
// P1: no battle -> no winner determination yet. Settle immediately back to ownership.
return this.TransitionAsync(CastleSiegePhase.Ownership, now);
default:
break;
}
return ValueTask.CompletedTask;
}
/// <summary>Admin: forces the cycle into registration now (from any phase).</summary>
/// <param name="now">The current UTC time.</param>
public ValueTask ForceStartRegistrationAsync(DateTime now)
{
this._registeredGuilds.Clear();
return this.TransitionAsync(CastleSiegePhase.Registration, now);
}
/// <summary>Admin: forces a specific phase now.</summary>
/// <param name="phase">The target phase.</param>
/// <param name="now">The current UTC time.</param>
public ValueTask ForcePhaseAsync(CastleSiegePhase phase, DateTime now)
=> this.TransitionAsync(phase, now);
/// <summary>Admin: resets to the ownership (resting) phase and clears registrations.</summary>
/// <param name="now">The current UTC time.</param>
public ValueTask ResetAsync(DateTime now)
{
this._registeredGuilds.Clear();
return this.TransitionAsync(CastleSiegePhase.Ownership, now);
}
/// <summary>Registers a guild (by name) for the current cycle. No-op outside registration.</summary>
/// <param name="guildName">The guild name.</param>
public void RegisterGuild(string guildName)
{
if (this.Phase == CastleSiegePhase.Registration
&& !this._registeredGuilds.Contains(guildName))
{
this._registeredGuilds.Add(guildName);
}
}
/// <summary>Admin: sets (or clears) the current owner guild name.</summary>
/// <param name="guildName">The owner guild name, or null to clear.</param>
public void SetOwner(string? guildName) => this.OwnerGuildName = guildName;
/// <summary>Returns a human-readable status summary for admin display.</summary>
public string GetStatusText()
=> $"CS phase={this.Phase}, owner={this.OwnerGuildName ?? "(none)"}, "
+ $"registered={this._registeredGuilds.Count} [{string.Join(", ", this._registeredGuilds)}]";
private ValueTask TransitionAsync(CastleSiegePhase phase, DateTime now)
{
this.Phase = phase;
this._phaseStartedUtc = now;
this.PhaseChanged?.Invoke(phase);
return ValueTask.CompletedTask;
}
}