Полный интерактивный путеводитель по Лабораторной работе №3: изоляция состояний через Prototype (Clone), «матрёшка» модификаторов на Decorator без отдельного интерфейса, отложенная сборка через Builder + Applier, симулятор боя и шпаргалка для защиты.
Мы проектируем ядро карточного автобаттлера (в духе Hearthstone Battlegrounds). Главная архитектурная задача — соединить порождающие и структурные паттерны так, чтобы выполнить три ключевых требования ТЗ:
Баффы заклинаний на столе не меняют шаблон в каталоге, а ранения, баффы и смерти в бою не портят существ на столе игрока. Решается паттерном Prototype (Clone).
По ТЗ у модификаторов не должно быть отдельного интерфейса, а существа не должны знать о модификаторах. Поэтому каждый модификатор сам является ICreature и оборачивает другое существо.
На этапе настройки в билдере самого существа ещё нет — мы копим статы и фабрики-апплайеры ICreatureModifierApplier, а в момент Build() создаём ядро и надеваем на него слои-декораторы.
AttackPoints, HealthPoints, контракт ICreature и базовый класс Creature.
CombatAnalyst, SpitefulFighter, MimicChest, UndyingHorror, AmuletMaster.
CreatureDecorator, Щит, Мастерство, Вампиризм и ICreatureModifierApplier.
ICreatureBuilder, CreatureBuilderBase, AmuletMasterBuilder и CreatureCatalog.
ISpell (4 заклинания), PlayerBoard (лимит 7 существ, DU-результаты, копия для боя).
ITargetSelector, BattleResult (DU), пошаговый цикл BattleSimulation и 29 тестов.
Способность: Перед каждой атакой увеличивает свою атаку на +3 до конца боя.
public override void AttackTarget(ICreature target)
{
if (!CanAttack || !target.IsAlive) return;
SetAttack(Attack.Increase(AttackBonus)); // +3
base.AttackTarget(target);
}
Способность: Когда получает несмертельный урон, удваивает показатель атаки (×2).
public override void TakeDamage(int damage)
{
if (damage <= 0 || !IsAlive) return;
base.TakeDamage(damage);
if (IsAlive)
SetAttack(Attack.Multiply(AttackMultiplier));
}
Способность: Перед атакой берёт максимум атаки и здоровья между собой и целью: max(self, target).
int newAtk = Math.Max(Attack.Value, target.Attack.Value);
int newHp = Math.Max(Health.Value, target.Health.Value);
SetAttack(new AttackPoints(newAtk));
SetHealth(new HealthPoints(newHp));
base.AttackTarget(target);
Способность: Один раз за бой при смертельном уроне перерождается с 1 HP и прежней атакой.
if (damage >= Health.Value && !HasRevived)
{
HasRevived = true;
SetHealth(new HealthPoints(RevivedHealth)); // 1 HP
return;
}
По ТЗ Мастер амулетов изначально обладает Магическим щитом и Мастерством атаки, но «реализации существ, подразумевающих наличие модификаторов, не должны знать о наличии модификаторов». Поэтому сам класс AmuletMaster — просто существо 5/2, а его врождённые модификаторы инкапсулированы внутри конструктора AmuletMasterBuilder!
public AmuletMasterBuilder()
: base(new AttackPoints(AmuletMaster.DefaultAttack), new HealthPoints(AmuletMaster.DefaultHealth))
{
WithModifier(new MagicShieldApplier());
WithModifier(new AttackMasteryApplier());
}
Перехватывает TakeDamage(damage). Если HasShield == true и damage > 0, сбрасывает флаг в false и делает return, не пропуская урон внутрь. Два щита подряд заблокируют два удара по очереди.
Перехватывает AttackTarget(target). Вызывает base.AttackTarget(target) первый раз, а затем проверяет if (IsAlive && target.IsAlive) — и если оба живы, проводит вторую атаку!
Перехватывает AttackTarget(target). После вызова base.AttackTarget(target) лечит атакующего на +1 HP через SetHealth(Health.Restore(1)).
Где в коде: CreatureDecorator : ICreature и его наследники (MagicShieldDecorator, AttackMasteryDecorator, VampirismDecorator).
Зачем нужен: Позволяет динамически навешивать на любое существо новые способности в рантайме (и через билдер, и через заклинания на столе), комбинировать их в любом количестве и не создавать отдельный интерфейс IModifier (которого нет по ТЗ).
Где в коде: метод ICreature.Clone() во всех существах и декораторах + PlayerBoard.CreateCombatCopy().
Зачем нужен: Обеспечивает глубокое копирование всей цепочки декораторов и состояния существа (включая приватные флаги HasRevived и HasShield), гарантируя полную изоляцию между Каталогом, Столом и Боем.
Где в коде: ICreatureBuilder, CreatureBuilderBase и 5 конкретных билдеров.
Зачем нужен: Позволяет пошагово конфигурировать характеристики (WithAttack, WithHealth) и список апплайеров (WithModifier) до создания самого экземпляра существа, а также инкапсулирует стартовые модификаторы Мастера амулетов.
Где в коде: ICreatureModifierApplier (фабрика оборачивания существа в декоратор) и ITargetSelector (стратегия выбора цели в бою).
Зачем нужен: Апплайер отвязывает билдер от конкретных классов декораторов (OCP), а ITargetSelector позволяет подменять RandomTargetSelector на детерминированный селектор в модульных тестах (DIP).
Проверь, как работают способности и цепочка декораторов при ударе друг по другу (после боя столы остаются нетронутыми!)
Чтобы защитить инвариант типа! Снаружи нельзя создать новый щит, который уже сломан (hasShield: false), или Бессмертного ужаса, который уже потратил возрождение (hasRevived: true). Этот конструктор нужен исключительно внутри метода Clone(), чтобы скопировать текущее состояние объекта во время боя.
На этапе работы с ICreatureBuilder самого объекта ICreature ещё не существует (пользователь может менять атаку/здоровье в любом порядке). А конструктору декоратора нужен готовый ICreature inner. Поэтому билдер сохраняет список легковесных фабрик ICreatureModifierApplier и применяет их по цепочке только в момент вызова Build().
В ТЗ есть прямое нефункциональное требование: «Конкретные существа не должны знать о модификаторах. Реализации существ, подразумевающих наличие модификаторов, также не должны знать о наличии модификаторов. Конфигурация существ, подразумевающих модификаторы, должна инкапсулировать их применение». Поэтому AmuletMaster — чистое существо 5/2, а модификаторы добавляет AmuletMasterBuilder.
Потому что заклинания «Амулет защиты» и «Яд Каленов» накладывают на существо модификатор-декоратор (new MagicShieldDecorator(target)), создавая новую внешнюю обёртку вокруг существа. Стол игрока PlayerBoard сохраняет возвращённый ICreature в слот _creatures[index].
PlayerBoard.AddCreature и ApplySpell возвращают Discriminated Unions (AddCreatureResult, ApplySpellResult).BattleResult реализован как иерархия abstract record вместо enum BattleOutcome..AsReadOnly(), а магические числа вынесены в const.