Logic Structure - Xrite/roguelike-haskell GitHub Wiki
Переведено в UML согласно Metamodel and UML Profile for Functional Programming Languages.
В картинке наверняка не последняя версия, см. ссылку.
Сериализация
К счастью в хаскеле отличная рефлексия, благодаря которой есть куча библиотек, сериализующих бесплатно.
Единственное что нужно сделать это почти к каждому типу в приложении deriving (Generic)
и объявить его инстансом какого-то тайпкласса.
Была выбрана библиотека Binary
, потому что она относительно популярная. Но вообще это не особо важно, можно поменять на другую чуть ли не sed
-ом.
Unit
Есть дата-класс UnitData
, в нем содержатся данные юнита, необходимые для игровой системы. Он сериализуем.
Тайпкласс Unit
создан для объектов, желающих принимать участие в боевой системе игры. Он реализует функцию, приводящую объект к его UnitData
и функцию, применяющую к объекту UnitOp
.
UnitOp
(также известный как Modifier
) это монадический ДСЛ для взаимодействия с произвольными юнитами. Все взаимодействие с юнитами производится через него (кроме получения UnitData по причинам цикличности импортов). Не сериализуем.
Юниты сами определяют интерфейс Unit
, поэтому разные юниты могут по разному подвергаться воздействию UnitOp
.
Интерфейс Unit
реализуют Mob
и Player
. Еще AnyUnit
, который есть класс сумма Mob
и Player
, нужный чтобы писать общие функции для обоих типов юнита.
Эффекты
Учет пассивных эффектов
Эффекты реализованы как применение UnitOp
к юниту. Например колечко, дающее +1 силы будет хранить модификатор, увеличивающий силу юнита. Есть функция, которая берет юнита и применяет к нему эффекты надетых предметов и учитывает просто наложенные эффекты.
То есть можно взять юнита, учесть все эффекты, под которыми он находится и получить юнита. Можно дважды применить все эффекты.
Игровые механики в эффектах
Но UnitOp
это вообще-то про произвольные манипуляции с юнитом, это не совсем то. Поэтому был сделан EffectAtom
-- класс для эффектов игровой системы, вроде "нанести урон".
В UnitOp
есть опция "применить EffectAtom
".
Поскольку каждый тип юнитов по своему определяет воздействие UnitOp
, они могут определять и воздействие таких эффектов.
Хранение эффектов
EffectAtom
это один эффект, а хочется уметь применять несколько сразу. Например "кольцо +1 силы +2 урона". Можно было бы хранить в вещах UnitOp
с необходимыми эффектами, но тогда вещи были бы не сериализуемы.
Поэтому сделан EffectDesc
-- сериализуемая последовательность эффектов. В нем хранится последовательность EffectAtom
и его можно трансформировать в UnitOp
.
Но иногда может захотеться сделать хитроумный эффект, не описуемый в EffectAtom
. Поэтому также сделана возможность задать "стандартные UnitOp" и в EffectDesc
к ним отсылаться.
Стандартные операции хранятся в UnitOpFactory
, соответственно нужен её экземпляр для превращения EffectDesc
в UnitOp
.
Временные эффекты
В любой игре нужна система накладывания временных эффектов на юнитов. У нас для этого есть TimedUnitOps
, хранящаяся в UnitData
. На юнита можно наложить эффект, который будет висеть несколько ходов (и даже разный от хода к ходу).
Тоже сериализуем.
Environment
Environment
-- состояние игры. Внешние взаимодействия с ним производятся в MonadState Environment
под названием GameEnv
и FailableGameEnv
(второй вариант еще и MonadFail).
В нем хранятся все игровые уровни, все мобы и (отдельно) игрок.
Все юниты индексируются по UnitId
и все функции для работы с Environment
используют его как указатель на юнита.
Environment
содержит поля функции, а потому не сериализуем.
Action & ActionEvaluator
Действие юнита в игре выражается как Action
(в данный момент это "походить в том направлении). Чтобы перевести действие юнита на язык изменения Environment
(то есть GameEnv
) есть ActionEvaluator
.
На самом деле ActionEvaluator
это буквально алиас для функции, которая по UnitId
и Action
возвращает изменение Environment
.
У него есть декоратор confuseDecorator
, который с некоторой вероятностью меняет направление движения на соседнее.
И другой декоратор confuseAwareDecorator
, который применяет confuseDecorator
только если юнит действительно законфьюжен.
ActionEvaluator
для каждого моба хранится рядом с ним в Environment
.
Стратегии поведения
В каждом мобе хранится тег его стратегии поведения. По этому тегу можно получить его функцию поведения.
Генерация уровней
В языке с неизменяемыми переменными каждый вызов рандома создает новую копию генератора случайных чисел. Следить за ними
становится чрезвычайно неприятно, поэтому сделана монада RandomMonad, прячущая состояние генератора в себя.
Это тайпкласс, но написана реализация для (RandomGen g, MonadState g m) => RandomMonad g m
.
С использованием этой монады написан генератор уровней по алгоритмы с binary space tree (см. публикацию).
Save/Load
Environement
не сериализуем, зато все что в нем не сериализуемое пока что никогда не меняется. Поэтому сделан отдельный класс EnvMemento
, который можно преобразовывать в Environement
и наоборот. Его структура не экспортируется наружу, поэтому его можно считать что это паттерн "Хранитель".
Взаимодействие с пользователем и Команда
Все взаимодействия с Environment
извне происходит через монаду GameEnv.
Таким образом дозволенные действия определяются внутри Environment.hs
. Видимо это можно считать функциональным аналогом паттерна "Команда".
На данный момент разрешено куча операций вида "получить какую-то информацию", операция "походить в ту сторону" и операции для работы с инвентарем игрока.