Logic structure desdoc - Xrite/roguelike-haskell GitHub Wiki

images/logic_structure_task_1.png

Ссылка на диаграмму классов

Сериализация

К счастью в хаскеле отличная рефлексия, благодаря которой есть куча библиотек, сериализующих бесплатно.

Единственное что нужно сделать это почти к каждому типу в приложении 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. Видимо это можно считать функциональным аналогом паттерна "Команда".

На данный момент разрешено куча операций вида "получить какую-то информацию", операция "походить в ту сторону" и операции для работы с инвентарем игрока.