Move: rear-guard moves within its column and swaps with an occupied circle
MoveCommand(from, to) in the Main phase. Only 0-3 (left) and 2-4 (right) are valid pairs; the center back row has no partner. Rested state stays with the card. Five tests.
This commit is contained in:
parent
4dd96e6f02
commit
71db856480
|
|
@ -110,7 +110,8 @@
|
|||
- [x] Поддержка (Boost) как отдельная способность: по умолчанию у 0–1 ранга, передаёт силу карте впереди в той же колонне; `BoostOverride` позволяет выдать или снять её эффектом.
|
||||
- [x] Идеальная защита (Sentinel): щит 0; оплата сбросом карты того же клана полностью отменяет попадание; без оплаты не даёт ничего.
|
||||
- [x] Перехват: карта 2 ранга из переднего ряда выходит в круг защиты, сила атаки сравнивается с ней; `InterceptOverride` и `InterceptFromBackRow` позволяют выдать способность эффектом.
|
||||
- [ ] Move, Discard, Shuffle как отдельные интенты.
|
||||
- [x] Перемещение Арьергарда в пределах колонки; если круг занят — карты меняются местами; у центрального заднего ряда пары нет.
|
||||
- [ ] Discard, Shuffle как отдельные интенты.
|
||||
- [ ] `driveCheckCount` как модифицируемое свойство (сейчас константа 2).
|
||||
- [x] Результат боя: `Hit` / `Guarded` как событие.
|
||||
- [ ] Событийная шина и три обработчика: Continuous / Activated / Triggered.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ public sealed record FinishMulliganCommand : GameCommand;
|
|||
public sealed record RideCommand(string PrintingId) : GameCommand;
|
||||
public sealed record SkipRideCommand : GameCommand;
|
||||
public sealed record CallCommand(string PrintingId, int Slot) : GameCommand;
|
||||
public sealed record MoveCommand(int FromSlot, int ToSlot) : GameCommand;
|
||||
public sealed record EnterBattleCommand : GameCommand;
|
||||
public sealed record AttackCommand(int AttackerSlot, int TargetSlot, int BoosterSlot = -1) : GameCommand;
|
||||
public sealed record GuardCommand(string PrintingId, string? DiscardPrintingId = null) : GameCommand;
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ public sealed class GameEngine
|
|||
RideCommand c => Ride(c.PrintingId),
|
||||
SkipRideCommand => SkipRide(),
|
||||
CallCommand c => Call(c.PrintingId, c.Slot),
|
||||
MoveCommand c => Move(c.FromSlot, c.ToSlot),
|
||||
EnterBattleCommand => EnterBattle(),
|
||||
AttackCommand c => Attack(c.AttackerSlot, c.TargetSlot, c.BoosterSlot),
|
||||
GuardCommand c => Guard(c.PrintingId, c.DiscardPrintingId),
|
||||
|
|
@ -225,6 +226,35 @@ public sealed class GameEngine
|
|||
return CommandResult.Ok();
|
||||
}
|
||||
|
||||
private CommandResult Move(int fromSlot, int toSlot)
|
||||
{
|
||||
if (State.Phase != Phase.Main)
|
||||
return CommandResult.Reject("Перемещение доступно только в Основной фазе.");
|
||||
|
||||
if (fromSlot is < 0 or >= PlayerState.RearGuardSlots || toSlot is < 0 or >= PlayerState.RearGuardSlots)
|
||||
return CommandResult.Reject("Некорректный круг Арьергарда.");
|
||||
|
||||
var player = State.Players[State.ActivePlayer];
|
||||
var unit = player.RearGuards[fromSlot];
|
||||
if (unit is null)
|
||||
return CommandResult.Reject("В исходном кругу нет карты.");
|
||||
|
||||
if (!SameColumn(fromSlot, toSlot))
|
||||
return CommandResult.Reject("Двигать можно только в пределах своей колонки.");
|
||||
|
||||
// целевой круг может быть занят — тогда карты меняются местами
|
||||
var other = player.RearGuards[toSlot];
|
||||
player.RearGuards[toSlot] = unit;
|
||||
player.RearGuards[fromSlot] = other;
|
||||
|
||||
_events.Add(new UnitMovedEvent(State.ActivePlayer, fromSlot, toSlot));
|
||||
return CommandResult.Ok();
|
||||
}
|
||||
|
||||
/// <summary>Колонки: 0↔3 (левая), 2↔4 (правая). У центрального заднего ряда пары нет.</summary>
|
||||
private static bool SameColumn(int fromSlot, int toSlot) =>
|
||||
(fromSlot, toSlot) is (0, 3) or (3, 0) or (2, 4) or (4, 2);
|
||||
|
||||
private CommandResult EnterBattle()
|
||||
{
|
||||
if (State.Phase != Phase.Main)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ public sealed record MulliganEvent(int Player, int Count) : GameEvent;
|
|||
public sealed record CardDrawnEvent(int Player) : GameEvent;
|
||||
public sealed record UnitRodeEvent(int Player, string PrintingId) : GameEvent;
|
||||
public sealed record UnitCalledEvent(int Player, int Slot, string PrintingId) : GameEvent;
|
||||
public sealed record UnitMovedEvent(int Player, int FromSlot, int ToSlot) : GameEvent;
|
||||
public sealed record AttackDeclaredEvent(int Player, int AttackerSlot, int TargetSlot) : GameEvent;
|
||||
public sealed record BoostedEvent(int Player, int BoosterSlot, int AttackerSlot) : GameEvent;
|
||||
public sealed record GuardAddedEvent(int Player, string PrintingId, int Shield) : GameEvent;
|
||||
|
|
|
|||
|
|
@ -414,6 +414,64 @@ public class GameEngineTests
|
|||
Assert.False(engine.Execute(new InterceptCommand(0)).Accepted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Move_To_Empty_Circle_In_Same_Column()
|
||||
{
|
||||
var engine = InMainPhase();
|
||||
Give(engine, 0, Marron);
|
||||
engine.Execute(new CallCommand(Marron, 0));
|
||||
|
||||
Assert.True(engine.Execute(new MoveCommand(0, 3)).Accepted);
|
||||
|
||||
Assert.Null(engine.State.Players[0].RearGuards[0]);
|
||||
Assert.NotNull(engine.State.Players[0].RearGuards[3]);
|
||||
Assert.Contains(engine.Events, e => e is UnitMovedEvent { FromSlot: 0, ToSlot: 3 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Move_Swaps_Occupied_Circles()
|
||||
{
|
||||
var engine = InMainPhase();
|
||||
Give(engine, 0, Marron);
|
||||
engine.Execute(new CallCommand(Marron, 0));
|
||||
Give(engine, 0, Gallatin);
|
||||
engine.Execute(new CallCommand(Gallatin, 3));
|
||||
|
||||
Assert.True(engine.Execute(new MoveCommand(0, 3)).Accepted);
|
||||
|
||||
Assert.Equal(Marron, engine.State.Players[0].RearGuards[3]!.PrintingId);
|
||||
Assert.Equal(Gallatin, engine.State.Players[0].RearGuards[0]!.PrintingId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Move_Rejects_Other_Column()
|
||||
{
|
||||
var engine = InMainPhase();
|
||||
Give(engine, 0, Marron);
|
||||
engine.Execute(new CallCommand(Marron, 0));
|
||||
|
||||
Assert.False(engine.Execute(new MoveCommand(0, 4)).Accepted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Center_Back_Row_Has_No_Pair()
|
||||
{
|
||||
var engine = InMainPhase();
|
||||
Give(engine, 0, Marron);
|
||||
engine.Execute(new CallCommand(Marron, 1));
|
||||
|
||||
Assert.False(engine.Execute(new MoveCommand(1, 3)).Accepted);
|
||||
Assert.False(engine.Execute(new MoveCommand(1, 0)).Accepted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Move_Is_Available_Only_In_Main_Phase()
|
||||
{
|
||||
var engine = StartedEngine(); // фаза Смены
|
||||
|
||||
Assert.False(engine.Execute(new MoveCommand(0, 3)).Accepted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue