Zemeroth - двухмерная пошаговая игра

Шанс попасть, зоны расстановки и упрощение конфигов

Итак, вот запоздалое обновление за последние две недели.

  • Наконец-то реализован рассчет шансов попадания (взамен вечного 50% как было до этого).

    Пока остановился на таком подходе, вроде он не сильно сложный и довольно гибкий:

    • у бойцов появляются параметры “точность атаки”, “сила атаки”, “уклонение”;
    • эффективность_атаки = точность + сила - уклонение;
    • кидается куб с 10 гранями;
    • из эффективность_атаки вычитаем результат броска;
    • если получается <= 0 - значит это полный промах;
    • если получилось больше “силы атаки”, то срезаем до нее;
    • если посередине - значит такой урон и уходит на вражескую броню (типа, удар пришелся по касательной, но таки был ощутим);
    • значение брони просто вычитается из этого урона для подсчета итогового урона цели.

    В эту формулу интуитивно вставляются всякие коэффициенты, типа “если атакующий ранен, то вычесть силу его ран из эффективности атаки” (тоже реализовано).

    Пока я два недостатка описанной выше схемы знаю:

    1. Сходу в ней не показать оружие, у которого нет градации урона. Хз что это именно за оружие должно быть и нужно ли оно мне (вряд ли), но штуки вида “или попал и нанес 4 урона, или не попал совсем” непредставимы без дополнительных костылей.
    2. Отравляющий демон наносит 0 урона при атках - т.е. его шанс попасть ниже остальных демонов. Тут вбил костыль в виде повышения его точности атаки.

    Вживую выглядит так сейчас:

    Из визуала:

    • При выделении готового к атаке бойца поверх врагов показываются шансы попасть по ним;
    • Во время атаки под атакующим ненадолго появляется вероятность успеха атаки. Нужно, в первую очередь, что бы было понятнее насколько враги опасны.

    Какие изменния случились с балансом:

    • Теперь первоочередная цель это ранить врага, добивать уже может быть меньшим приоритетом - иногда удобно, что бы практически неспособный попасть по твоим бойцам враг занимал клетку и не давал его более здоровым друзьям подойти;
    • Важность способности лечения у алхимика возросла, потому что толку от своих раненных бойцов становится сильно меньше.
  • Добавлены зоны начального построения (lines) и генератор больше не создает агентов в упор к врагам.

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

    А насчет зон, добавлено перечисление pub enum Line { Any, Front, Middle, Back }, позволяющее указывать в сценарии где мы какие виды агентов хотим видить. Теперь демоны-вызываетли всегда сощдаются в дальнем конце карты за жвым щитом, т.е. застрахованы от быстрой расправы на первом ходу.

    Снимок тестовой карты, в которую специально нагнана прям куча демонов что бы четко были видны зоны и отступы:

    (Вот еще до кучи пример начальной расстановки на огромной карте).

  • Кстати об огромных картах, я тут из интереса провел небольшой стресс-тест с реальным боем на огромной карте:

    ИИ думает секунд по 10, а анимации его хода минуты две занимают. Играть в это, конечно, не реально, но хорошо что игра хоть как-то функционирует.

  • Убрал дублирование информации из конфигов.

      Strength((
    -     base_strength: 4,
          strength: 4,
      )),
    

    Поскольку начальное значение силы, очков здоровья/атаки, т.п. всегда равно их полному значению, нет никакого смысла в конфигах указывать и то, и то. Вставил всякой serde магии и функций-оберток, вроде стало почище.

Сейчас играюсь с настройкой винды в тревисе и пытаюсь закончить с базовым режимом компании.

(Извиняюсь, многовато получилось текста. Надо почаще писать.)

4 лайка

Выложили отчет о недавнем инди-стендапе в индиспейсе, куда я решлся заглянуть:

UPD: Просили описание проекта родить (не техническое), смог вот такое:

Описание: минималистичная фэнтезийная пошаговая тактика, механика которой вдохновлена Into the Breach, Banner Saga, Hoplite и Auro.

Процесс разработки разделен на два этапа:

В первом этапе будут реализованы: тактическое ядро, дерево улучшений бойцов, уникальные действия классов, короткая кампания на пару часов в виде линейной серии боев с вечной смертью (permadeath) и финальный босс - демон Земерот. Разные виды врагов будут иметь простые, но отличающиеся шаблоны поведения, которые дополняют друг друга и создают сложные тактические задачи.

Во втором этапе будет добавлен простой стратегический слой, рассказывающий предысторию придворного мага, который в тайне заключил демоническую сделку и постепенно стал Земеротом. Важной механикой стратегического режима будет сокрытие своих демонических умений и прислужников до момента открытого предательства.

5 лайков

@ozkriff Молодец! :+1:
А может быть нам еще чисто игровые растосходки наладить в Индиспейсе? Раз в месяц или хотя бы раз в два месяца.

1 лайк

Думаешь, имеет смысл? Ржавая игровая экосистема пока в таком состоянии, что только для готовых бороться с неудобствами годится. В целом, практичный игродельческий народ смеется даже когда говоришь на godot’е проект писать, а до такого уровня Аметисту (он же у нас самый зрелый и серьезный из движков все еще?) еще просто куча человеколет предстоит. Да и прям проектов-то игровых в питере на расте не то что бы много. Я боюсь что мы (пока?) из своих субботних сходок не выросли особо, что бы сильно уж народ зазывать.

раз в год самое то)))

Ладно, тогда сначала игрового опыта поднаберемся. Хотя мне бы хотелось как-то отдельно темы про игры обозначать, может быть и в рамках субботних сходок.

Базовый режим кампании

Нашел в себе силы влить PR c базовой кампанией.

Представляет из себя просто цепочку боев с заранее заданными сценариями. Если проигрываешь в бою - все, кампания для тебя закончилась, начинай сначала. Если выигрываешь, то тебе показывается переходный экран со списком погибших, текущим составом группы и вариантами кого ты можешь “докупить” в награду.

Файл описания демо-кампании выглядит примерно так:

initial_agents: ["swordsman", "alchemist"],
nodes: [
    (
        scenario: (
            map_radius: (4),
            rocky_tiles_count: 8,
            objects: [
                (owner: Some((1)), typename: "imp", line: Front, count: 3),
                (owner: Some((1)), typename: "imp_bomber", line: Middle, count: 2),
            ],
        ),
        award: (
            recruits: ["hammerman", "alchemist"],
        ),
    ),
    (
        scenario: (
            rocky_tiles_count: 10,
            objects: [
                (owner: None, typename: "boulder", line: Any, count: 3),
                (owner: None, typename: "spike_trap", line: Any, count: 3),
                (owner: Some((1)), typename: "imp", line: Front, count: 4),
                (owner: Some((1)), typename: "imp_toxic", line: Middle, count: 2),
                (owner: Some((1)), typename: "imp_bomber", line: Back, count: 1),
                (owner: Some((1)), typename: "imp_summoner", line: Back, count: 2),
            ],
        ),
        award: (
            recruits: ["swordsman", "spearman", "hammerman"],
        ),
    ),
]

^ Вначале идет список бойцов, с которым мы начинаем самый первый бой, потом перечисляется список сценариев боев, где задаются свойства карты, списки объектов (врагов и просто булыжников всяких) и варианты награды за победу.

Поскольку экран боя создается в экране главного меню или экране кампании, а затем складывается в виде типаж-объекта на стек экранов, возврат результата боя получилось организовать только через канал. Немного костыльно, но сойдет.

Тестов в коде игры пока крайне мало, но для разнообразия логическое ядро кампании я немного обмазал тестами в духе:

#[test]
fn short_happy_path() {
    let mut state = State::from_plan(campaign_plan_short());
    assert!(state.aviable_recruits().is_empty());
    assert_eq!(state.mode(), Mode::ReadyForBattle);
    let battle_result = BattleResult {
        winner_id: PlayerId(0),
        survivor_types: initial_agents(),
    };
    state.report_battle_results(&battle_result).unwrap();
    assert_eq!(state.mode(), Mode::Won);
}

Сейчас есть косяк с тем что если бой пошел неудачно, то можно в любой момент выйти из него в меню кампании и начать бой заново. Уже завел задачу на то что бы пресечь это безобразие - “вечная смерть” наше все.

Game Planet

Сходил на этих выходных на выставку Game Planet, никогда на такие штуки не ходил. И не зря, видимо - мероприятие в целом сильно не про меня оказалось. Куча школьников, фортнайта и странных косплееров. Убежал оттуда через пару часов, но хоть позалипал в секции с инди-стендами, посмотрел всякое про настолки и авторские комиксы. Сделал вывод: можно смело идти шоукейсить поделку - ничего дико страшного, в худшем случае просто все проигнорят. :slight_smile:

Твиторы

И да, если кому интересно, я тут психанул и завел две твитерных учетки дополнительных:

  • @ozkriff_ru - для всего, что будет интересно только русскоговорящим;
  • @rust_gamedev - для ретвитов всего подряд про ржавый игрострой.

Пыль

Запилил простенькую пыль при прыжках-бросках всяких:

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

Следующее на очереди: квадратные брызги крови при попаданиях.


UPD: еще внезапно решил заглянуть в /r/gamedev на субботний скриншотник, давно там уже ничего не писал.

Исчезающие брызги крови и улучшенные анимации атак

Продолжаю лениться выкладывать сюда частые обновления.

  • Добавил разлетающихся брызг крови и следов от оружия при атаках для оживления визуального ряда.

    Количество капель крови пропорционально нанесенному урону.

    Добавил бойцам параметр WeaponType. Пока есть четыре вида: smash, slash, pierce и claw и они чисто визуальные - для выбора подходящей текстурки. Некоторые спрайты атаки под углами смотрятся странно (копейщик, я на тебя смотрю), надо будет потом дополнительные варианты добавить и зеркалировать все это хозяйство по ситуации.

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

    Может потом еще для чего-то декоративного этот же механизм использую.

  • Первел все пакеты в репозитории на Rust 2018:

    • везде прописал edition = "2018"
    • обновил все импорты на новые пути (привет, вездесущий crate::);
    • удалил все extern crate;
    • заменил все #[macro_use] на use конкретных макросов;
    • перешел на стабильный rustfmt;
    • подчистил некоторые ставшие лишними явные ВЖ в паре мест;
    • выкинул все mod.rs.

    О переходе не жалею, в целом все неплохо, но некоторые вспомогательные штуки (например, cargo-outdated) пока со скрипом на Rust 2018 смотрят еще.

  • Выкинул прямую зависимость от serde_derive;

  • Отключил кэш тревиса, потому что Levans’ workshop: “Beware the rust cache on Travis”. А то замерил, что кэш после сборки иногда минуты по три собирается, а толку от него и правда не так что бы прям много.

  • Починил вертикальную позицию декоративной травы;

  • Обновил картинки в ридми. Изначально задумка была в том, что в ридми показываются картинки только с последней выпущенной версии, а не из мастера. Но 0.5 я выпущу только после перехода на ggez 0.5, который все маячит на горизонте, но не появляется. А картинки от 0.4 совсем уж сильно устарели, больше полугода уже прошло таки.

2 лайка

Затянувшийся переход на GGEZ 0.5, good-web-game/WASM и itch

Разработка за этот месяц продвинулась так себе.

Статус треклятого перехода на GGEZ 0.5

В начале года вышел ggez 0.5.0-rc.0 - первый релиз-кандидат 0.5 версии, который без сишного SDL2 и вообще весь классный и долгожданный. Я решил, что время настало, пора начинать портировать Земерота, потому как API там сильно переколбасили и лучше на ранних стадиях, до выхода настоящего стабильного 0.5 поправить обо что я там споткнусь.

Так и случилось, я неприятно споткнулся (пока что) о три момента:

  • Самое для меня неприятное: Text больше не реализуется простой картинкой, которую раньше можно было через into_inner() получить и не думать ни о чем.

    Какая абстракция мне нужна? Что бы был метод отрисовки этого дела + какой-то способ узнать размер примтива, как минимум что бы кнопки в UI расставить с правильными отступами.

    Первым пришедшим в голову решением было просто сделать перечисление с двумя вариантами (Text и Image) и прокинуть там эти две несчастных функции. Я это сделал и оно даже вполне работало. Но возникает проблема использования этого enum’а между разными пакетами. У меня же не только один пакет Zemeroth, у меня еще отдельные и независимые zgui и zscene пакеты, оба из которых так же должны уметь одинаково работать с текстом и картинками (zgui - показывать кнопки и с текстом, и с икноками, zscene - рисовать на сцене как всяких бойцов/объекты, так и двигать всплывающий текст). Так что пришлось бы делать из этого перечисления отдельный пакет, который потом везде подключать - уже сильно менее приятное решение, которое потом боком может выйти.


    Окей, какой тогда еще есть вариант? Взять родной ggez’овский типаж Drawable и научить его всему, что мне нужно. Перетер это дело с Ледолисом в дискорде, получил одобрение и пошел ковырять.

    Для начала убрал Into<DrawParams> из сигнатуры метода типажа, что бы его можно было использовать для динамического полиморфизма: #556: “The Drawable trait isn’t object safe” -> PR #559: “Remove the generic argument from Drawable::draw”.

    А потом на несколько вечеров провалился в попытки родить API получше для dimensions метода: #557: “Add a ‘dimensions’ method to the ‘Drawable’ trait”. Там пришлось докинуть Rect’у дополнительных методов для его вращения, вставить костылей для повершинного подсчета размеров Mesh’а и еще всякого наковырять. Плодом усилий стал среднего размера PR #567 “Drawable::dimensions()”, который Ледолис начисто две недели игнорирует без какой-либо обратной связи. :’-( (UPD: уже влито)

    Кроме игнора, эту беду я в целом считаю решенной, думаю, Ледолис в итоге найдет время отсмотреть и принять PR.

  • Другая беда с текстом - разная высота - пока не решена, надо будет еще в коде покопаться. Там суть в том, что у строк без символов с “хвостами” высота считается по базовой линии почему-то. Учитывая, что я все элементы UI масштабирую просто относительно высоты экрана, это приводик к неприятному эффекту (ggez v0.4 vs v0.5):

    Строки с хвостами выглядят как раньше, а вот штуки типа “moves: 1/1” в полтора раза больше нужного.

  • Ну и на десерт есть еще небольшая проблема с доступом к файловой системе без создания графического контекста. Тут, видимо, надо будет кидать PR, делающий некоторые pub(crate) методы просто pub'ами.

Так что вот, пока переход на ggez 0.5 буксует. Код WIP порта живет в wip_i409_ggez_0_5_alpha_0 ветке.

good-web-game и веб версия Земерота

Через какое-то время, после публикации ggez v0.5.0-rc.0, Ледолис еще выложил огромный пост “The State Of GGEZ 2019”. Среди прочего, он там говорит:

  • с веб версией много проблем и хз когда она будет;
  • он скоро перестанет уделять ggez много времени, потому что задолбался и хочет позаниматься другими проектами;
  • на последок он хочет выпустить ggez 0.6, в котором будет осуществлен переход на gfx-hal.

Если ggez 0.5 выйдет, то от десктопной версии мне особо ничего и не надо, вроде, а вот веб версию я давно и сильно хотел. Потому что небольшую 2д игру от ноунейма без возможности запуска в браузере в 2019м ооочень мало кто не поленится скачивать.

И тут, внезапно, Федя @not-fl3 замутил WASM/WebGL движок, частично апи-совместимый с GGEZ, так что у Земерота появилась полноценная веб версия!

https://github.com/not-fl3/good-web-game

На самом деле он не прям из воздуха его замутил, конечно: он давно экспериментирует с 2д протипами в вебе:

…и “просто” поверх своего движка накрутил GGEZ слой.

Работает это дело через замену обычного GGEZ’а в Cargo.toml проекта на good-web-game + там еще немного код инициализации надо поправить: за подробностями можно посмотреть пример из репы.

Я это рассматриваю как очень хороший костыль, который дает возможность иметь легкую и везде (десктопные и мобильные браузеры) работающую веб версию Земерота уже сейчас. Если когда-нибудь у GGEZ появится родное решение - можно будет на него перейти.

Кстати, в процессе синхронизации API Федя наткнулся на еще один косяк в черновике 0.5 GGEZ’а: #568: “There’re still some non-mint types in the API”.

Спасибо тебе, Федя, еще раз! :slight_smile:

itch.io и обратная связь

Выложил эту играбельную веб версию на itch.io:

и закинул клич в твитер с реддитом:

Твит и реддит пост отлично зашли, прям куча людей, которые до этого скорее пассивно поглядывали за проектом, реально сели и поиграли.

Обратной связи целый вагон, тут расписывать подробно поленюсь, но чаще всего повторялось, что нужен более человечный GUI, хоть какое-то руководство как в это играть и слишком сильный рандом - главное направление действий после окончания миграции на ggez 0.5 ясно. :slight_smile:

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


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

Еще думал (и мучал людей разговорами) о том, как можно (и нужно ли) тестировать игровую логику и что можно делать со выходными случайностями в игре, но про это я уже попозже постараюсь написать.


UPD: Кстати, гитхаб репа за 400 звезд перевалила уже (420 в момент написания) :slight_smile:

image

7 лайков

Продолжение возни с GGEZ (2019.02.12 - 2019.02.18)

  • Земерот попал в rustwasm рассылку: Feb 13, 2019 This Week in Rust and WebAssembly 10

  • PR в GGEZ про добавление Drawable::dimensions принят.

  • Покопался с Федей в #569: “Text::dimensions height behavior changed”.

    Меня немного загрузило, что может я не в ту сторону с текстом копаю, и не должен хотеть, что бы у него всегда одна высота была в dimensions, но поговорил с Icefoxen’ом в дискорде и он меня успокоил:

    icefox: ggez should handle this shit for you
    icefox: all yo ushould have to do is say “give me text where each line is X pixels tall”
    icefox: and so you can do for i in 0…lines.len() { draw(lines[i], Point2::new(0, i*x)); }
    icefox: and it doesn’t look like ass
    icefox: that’s the goal from ggez’s perspective

    В итоге в задаче висит патч от меня с Федей, который, вроде, чинит проблему, по крайней мере для моего кода. Жду реакции Ледолиса, готов по отмашке сделать PR.

  • На вот такое дело еще нарвался: #583: “(0.5 feedback) Text::dimensions should return Rect, not (u32, u32)” - может и мелочь, но выглядит криво.

  • В процессе всего выше копался еще и в https://docs.rs/glyph_brush - должен сказать, что хоть библиотека и толковая, но как же там много всяких типов и вспомогательных структур на каждый чих. Прям копаешься во всем этом и очень хочется на голый rusttype откатиться.

  • Icefox выпиливает остатки nalgebra типов из API (заменя все на mint), приходится чинить код, обмызвая все пачкой .into();

  • Добавил в zgui виджет “Spacer”, потому что использование Label с пустым текстом (приводящее к созданию текстуры 0 на 0) это такое себе дело;

  • PR #426 “GGEZ: v0.4 → 0.5.0-rc.0” - перетащил Земерот на git зависимость GGEZ’а (свой форк). И там еще пачка мелочевки осталась техдолга. Часть заблочена правками в самом GGEZ’е, а до штук вроде “свой тип ошибок” просто надо добраться отдельно.

    Так что вот, теперь мастер Земерота использует (временно) гитовый GGEZ.
    На этой неделе надеюсь втащить веб версию в мастер и вернуться наконец-то к полезной деятельность по улучшению интерфейса и функционала игры.

2 лайка

Ошибки и рефакторинги (2019.02.18 - 2019.02.24)

Я устанавливал какое-то время назад Земерот “на поиграть”, и понял, что это такой фановый чисто прогерский проект, потому что если почитать тебя, кажется, вау!, и каждому кодеру знакома эта эйфория от фанового проекта: какие-то идеи кипят, тут фичу новую добавил, там рефакторинг на ходу затеял — всё кипит, всё меняется, работа прёт, ты щастлив. Но со стороны юзера мне чего-то не хватило, хотя я вообще не геймер ни разу. Может лора, может дружелюбности: что делать?, куда бежать?, где враги? etc.
Игроку ведь не очень нужно, чтобы все мутации были засунуты в один модуль или надписи имели одинаковую высоту. Это мое мнение, не критика, просто здравый взгляд со стороны.

1 лайк

Я в целом согласен, что проект для пользователя еще очень сильно сырой и надо бы заняться “выплатитой пользовательского долга” проекта, а не в техдолге копошиться. Вон, пару недель назад писал о базовых выводах из собранной обратной связи по веб версии:

Только дело упирается в то, что работа у меня по ощущениям еле ползет (то ли времени мало, то ли прокрастинирую много, хз), а без выплаты минимального техдолга я рискую скоро оказаться посреди совсем жуткого неструктурированного кодомесива, с которым сделать уже ничего нельзя будет, кроме как выбросить.

image

Так что я пытаюсь приоритизировать задачи по критичности. Очень короткая версия краткосрочной дорожной карты проекта выглядит как-то так:

  1. выплатить минимальный техдолг (наладить тестирование логики, дорефакторить мутации состояния, влить веб версию в мастер);
  2. выплатить минимальный “пользовательский долг” (гуй, руководство, переработка механик случайностей);
  3. заняться реализацией прям нового пользовательского функционала;

(Надо бы, кстати, расширенную дорожную карту проекта в ридми затащить, что бы читающие сразу понимали мое видение степени готовности проекта.)


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

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


Ты так от слова “критика” открещиваешься, будто что-то плохое) Не, я люблю, когда конструктивно тыкают - значит есть какое-то дело до поделки. :slight_smile:

1 лайк

Потому что не считаю свои замечания критикой, критика это либо конкретика, либо заурядный хэйт. А я обращаю внимание на абстрактные, метафизические вопросы, на то, что в игру прежде всего должно быть приятно играть, если попросту говорить, это не тот тип ПО, где функционал решает всё.
Вот ты говоришь, что устал. Это нормально, когда упираешься в оверинжиниринг — беду всех прогеров и их пет-проектов. Потому что хочется добиться совершенства, а не получается, не получается. Потому что совершенство обычно конечный итог длительной эволюции, а кто способен вывезти долгую эволюцию проекта?)
Я бы сосредоточился на веб-версии, эта вещь может дать проекту жизнь. И обрезай фичи, не распыляйся, иначе не вывезешь. Простынешь там, выпадешь на время головой из проекта и не вернешься. Вполне реальный сценарий, сколько таких я реализовывал на своем веку.

Вот тут не соглашусь. Несмотря на то, что Земерот “сыроват”, таки очень много сил вложено в его визуальную составляющую и он смотрится мало сказать “приятно” - он офигенен. Единственное слабое место в визуальной составляющей - это отсутствие GUI для управления. Все эти менюшки действий и их статусов, которые достаточно тяжело парсить глазами с непривычки. И разнобой со шрифтами их окончательно добивал. В общем, забота о визуальной части - это важно.

Ну а то, что видение разработчика не совпадает с видением игрока… Ну, блин, да - Земерот отличается тем, что разработчик пустил всех желающих “в мастерскую” и подробно всё объясняет по пути. В этом имеется своя прелесть и своя доля восторга, наблюдать как из практически “ничего” рождается и растёт что-то волшебное. Но это точно не про то, что “вжух и у нас готовый играбельный продукт, (заранее кладём на верстак, чтобы желающие посмотреть на разработку изнутри, сразу же и играли в готовое)” :slight_smile:

Я мимокрокодил, можешь ещё попробовать quick_error. Он даёт макросы для тривиальных реализаций description, display, from, into, с возможностью перегрузки. При этом это обычные enum, всё очень примитивно.

1 лайк

Тесты, рефакторинг и влитие WASM

Темпом разработки по прежнему не могу похвастаться, но некоторые обновления за месяц все-таки случились.

Тесты.

Как писал в прошлой заметке, проект дорос до размера, когда польза от тестов перевешивает геморрой их написания-поддержки.

Немного цитат старых обсуждений про тесты из чатов для контекста

@ozkriff:
тем временем, я ломаю себе и другим голову вопросом “как писать тесты логики для пошаговой игры со случайностями?” - https://twitter.com/ozkriff/status/1085488884799160320 (и там ниже обсуждение небольшое)

Пока выходит, что самый адекватный способ, если без наворачивания очень сложной инфраструктуры с отдельными мокаемыми правилами, это просто создать специальные тестовые объекты с нереалистичными характеристиками, так что бы они всегда какой-то определенный тест проходили (например, боец с 999 точностью, который абсолютно всегда попадает). Грязноватое решение, но альтернативы как-то еще хуже.

Есть бесящее ощущение, что я что-то очевидное из вида упускаю, только вот без понятия что.

@Nick Linker
Чтобы сразу откинуть банальщину: у тебя все рандомы с явными сидами, и эти сиды ты записываешь в файл сохранения и они являются частью game state, правильно я понимаю?

У меня (пока) вообще сохранений нет. Я человек простой, рандом сейчас из thread_rng() беру.

@ozkriff:
Если ты о варианте “сохранять зерно” в тестах, то мне он не нравится тем, что вытсавляет наружу часть кишок логики и потом, например, насмерть затруднит добавление какого-то нового правила с рандомом, потому что все тесты разом сломает непредсказуемым образом из-за сдвига последовательности псевдослучайных чисел.
Или ты о чем-то другом?
Чел (и до этого другие) предлагает вероятностные тесты (если результат работы сценария хотя бы столько-то раз приведет к конечным состояниям. удовлетворяющим критериям, то все ок), но я такое могу стерпеть только в очень вспомогательных тестах, слишком уж грязное какое-то решения для основного покрытия логики.

@Nick Linker:
я о недетерминизме, который возникает из-за рандомов, мультипоточности и неявно при IO. Если тебе удалось эффективно и полностью избавиться от недетерминизма в юнит тестах, то ты делаешь всё правильно и беспокоиться не о чем.
Моя игруля конечно хелловорлд по сравнению с Zemeroth, но там тоже рандомы и чтобы сделать воспроизводимые юнит тесты нужно эти рандомы делать явными. Пример куда можно посмотреть, это скажем сюда:
https://github.com/nlinker/xcg/blob/master/tests/model.rs
Тебя может заинтересовать test_run_match_with_reordering

@Alex Zhukovsky: А что конкретно ты тестировать хочешь?

@ozkriff: какие-нибудь сценарии, типа “стоит отравленный демон с единицей здоровья и броней, мы кидаем рядом с ним бомбу, которая взрывом его в огонь толкнет. ожидается, что яд перед смертью у него пройдет, а умрет он от огня в начале своего хода”
даже в таком не слишком сложном сценарии можно ненароком сломать какую-то логику во время рефакторингов (или добавления новых правил-механик) и без автотестов совсем не факт что во время быстрых плейтестов это заметишь

@XX: Почему же? По-моему это нормальное решение: вероятностная логика покрывается статистическими тестами )

@Nlinker: Это имело бы смысл, когда у тебя хардварный RNG, и всё, что ты можешь - это тягать из него значения. Но здесь ведь есть seed!

@ozkriff: Ну, неряшливое какое-то решение, хочу что б все точно было несмотря на случайности.
Плюс, у меня глаз подергивается и флешбеки нехорошие начинаются, когда я представляю себе “а чего это тревис красный статус показывает? ааа, там тест тот проблемный грохнулся, где я коэффеициент не могу подобрать, надо просто перезапустить тесты.”

Mike Lubinets:
А если замокать генератор случайных чисел, добавив сигнатуры сущностей в метод получения числа?
Можно будет его перегружать в тестах и описывать правилами какой именно output должен следовать из конкретной сигнатуры.
Типа вектор (signature, i32), соответствующий кодичеству запросов рандома их тех сущностей, рандом которых ты хочешь контролировать.
Если сигнатура следующего запроса не совпала с [0], пишем ворнинг и возвращаем обычный thread_rng().
Так можно будет и стейт в тестах контролировать и добавлять новые вызовы без заморочек с подбором сида после каждого коммита

@nlinker: тогда проще сделать n генераторов, каждый для отдельной сущности.

@mike:
Либо так, да, но будет жирно по генерикам, если генератор в каждую сущность пихать типом-параметром
Можно в принципе матчиться по get_random(self as &Any) в качестве альтернативы, и иметь возможность зардкодить последовательность генерируемых чисел или сид, если такой вариант подходит, если не глобально, а для отдельных сущностей.
Кстати, а как можно узнать сид для заданной последовательности?)

@nlinker: Если это не криптостойкий rng, то брутфорсом каким-нибудь. Но это сложновато.

Главная беда с тестами Земерота - что делать со случайностью? Выше накидано немного мыслей, но в целом подходы сводятся к:

  1. запоминать глобальное зерно. просто, но тесты все время будут ломаться от малейшего рефакторинга;
  2. сильно переработать архитектуру, что бы можно было подменять реализации игровых правил на неслучайные. требует очень сильного усложнения архитектуры;
  3. хранить отдельные зерна для разных подсистем/типов/правил;
  4. использовать специальновые тестовые объекты;

Третий вариант, возможно, не так и плох, но мне все еще немного натянуть его на реальный код. Зато последний вариант выглядит просто и, в целом, всем хорош: тестируется именно реальный код, изменений-дополнений минимум нужно, рефакторинги неплохо должен переживать.

Пока выходит, что самый адекватный способ … это просто создать специальные тестовые объекты с нереалистичными характеристиками, так что бы они всегда какой-то определенный тест проходили (например, боец с 999 точностью, который абсолютно всегда попадает).

Получившиеся тесты устроены просто:

  • объявляем прототипы;
  • объявляем сценарий при помощи этих прототипов;
  • создаем отладочное игровое состояние;
  • подаем на вход команды;
  • на выходе сверяем с ожидаемыми события (+ мгновенные/длительные эффекты и отложенные вызовы способностей).

Простейший тест выглядит как-то так:

#[test]
fn basic_move() {
    let prototypes = prototypes(&[
        ("mover", [component_agent_move_basic()].to_vec()),
        ("dull", [component_agent_dull()].to_vec()),
    ]);
    let scenario = scenario::default()
        .object(P0, "mover", PosHex { q: 0, r: 0 })
        .object(P1, "dull", PosHex { q: 0, r: 2 });
    let mut state = debug_state(prototypes, scenario);
    let path = Path::new(vec![PosHex { q: 0, r: 0 }, PosHex { q: 0, r: 1 }]);
    exec_and_check(
        &mut state,
        command::MoveTo {
            id: ObjId(0),
            path: path.clone(),
        },
        &[Event {
            active_event: event::MoveTo {
                id: ObjId(0),
                path,
                cost: Moves(1),
            }
            .into(),
            actor_ids: vec![ObjId(0)],
            instant_effects: Vec::new(),
            timed_effects: Vec::new(),
            scheduled_abilities: Vec::new(),
        }],
    );
}

Список прототипов и сценарий для каждого теста создаются заново. Это занимает немного больше места, чем переиспользование уже готовых прототипов и сценариев, зато так (по задумке) за раз тестируется минимальное количество движущихся частей, за счет чего тесты становятся более точечными (если тест падает, проще понять что пошло не так) и надежными (меньше вероятность, что левое изменение поломает тест).

Вражеский “тупой” агент в этом сценарии нужен просто что бы игра тут же не завершилась.

Методы для декларативного построения сценария из кода подцепляются через дубовый расширяющий типаж:

trait ScenarioConstructor {
    fn object(self, player_id: PlayerId, object_name: &str, pos: PosHex) -> Self;
    fn object_without_owner(self, object_name: &str, pos: PosHex) -> Self;
}

Саму структуру сценария пришлось переработать, потому что раньше сценарии были обязательно случайными (например, нельзя было точно указать позицию объекта, только примерную область), а в тестах как раз нужна определенность. Добавил полей и всяких проверок.

Кстати, в первом PR’е накосячил с переработанными сценариями - #447 scenario::check: Fix invalid Error::NoPlayerAgents - тесты работали, а вот настоящий бой из интерфейса запустить не выходило, протупил. Лишнее напоминание, что полностью на тесты полагаться не стоит. :slight_smile:

Вызов debug_state прячет внутри взведение страховочного set_deterministic_mode флажка, задача которого проста: паниковать при попадании потока управления в неконтролируемый рандом, например когда шанс попадания не меньше нуля и не выше максимума:

fn try_attack(state: &State, attacker_id: ObjId, target_id: ObjId) -> Option<Effect> {
    ...
    if state.deterministic_mode() {
        // I want to be sure that I either will totally miss
        // or that I'll surely hit the target at a full force.
        let sure_miss = k_min < 0;
        let sure_hit = k_min > 10;
        assert!(
            sure_miss || sure_hit,
            "Hit isn't determined: {:?}",
            (k_min, k_max)
        );
    }

Пришлось заменить все HashMap<ObjId, Vec<X>> в событиях на Vec<(ObjId, Vec<X>)> ради сохранения порядка событий и эффектов, иначе тесты мгновенно взрывались.

В процессе выбора формата описания тестов споткнулся о внезапные кривости/проблемы стандартного форматирования. Отчасти из-за этого бага форматирования я не стал, например, использовать билдер для структур Event + тестам размазано много лишних полей с пустыми векторами (а Default’ом их не убрать, потому что там первое поле с ActiveEvent должно быть осмысленное).

И да, реальные тесты можно посмотреть тут: src/core/battle/tests.rs.


мысль на будущее: В текущей реализации тестов мне не очень нравится, что я завязываюсь на конкретные захардкоженный ID объектов, потому что это добавляет тестам хрупкости - я ведь могу во вемя переработки кода взять и во время выполнения какого-то действия вставить код создания нового вспомогательного объекта и тогда вся последующая цепочка айдишников навернется. Думаю, надо относиться к конкретной цифре айдишника как к особенности реализации и получать ее динамически через метод в духе:

fn get_id(state: &GameState, pos: PosHex, metatype: &ObjType) -> ObjId { ... }

Рефакторинг

После написания вышеописанных базовых тестов я наконец решился на рефакторинг. В чем его суть?

По задумке, игрок отправляет команды, которые проверяются на адекватность и “выполняются”, генерируя поток событий и вот уже события изменяют игровое состояние. Проблема была в том, что я из-за лени и невнимательности в нескольких местах обработки команд менял игровое состояние напрямую, в обход системы событий. Это приводило к невозможности выразить некоторые новые запланированные способности, так что рефакторинг был неизбежен.

Несанкционированные места прямого изменения состояния были найдены и переработаны:

  • В структуру Event было добавлено поле scheduled_abilities для передачи отложенных вызовов способностей (например, что бомба должна взорваться через ход).
  • Для “перезапуска” длительных эффектов и отложенных вызовов способностей теперь просто используется их добавление в событие: если такого, например, длительного эффекта на объекте не было, то он появится, а если бы - он перезапишется. Немного костыльный способ, но я не вижу нужды иметь несколько одинаковых эффектов на одном объекте, так что пойдет.

В целом рефакторинг прошел вполне гладко, спасибо тестам. Правда, немного я все равно умудрился накосячить - #449 “regression: bomb_push is broken” - у меня не было теста на поведение мгновенно взрывающихся отталкивающих бомб, так что их надо будет сейчас починить.

WebAssembly порт влит в мастер

Веб версия игры наконец влита в мастер! До эттого руки наконец-то тоже дошли. :slight_smile:

Не обошлось без нескольких #[cfg(wasm)] и крошечного шелл скрипта, прячущего запихивание ресурсов игры в static, но в остальном все довольно гладко.

#[cfg(not(target_arch = "wasm32"))]
extern crate ggez;

#[cfg(target_arch = "wasm32")]
extern crate good_web_game as ggez;

main у wasm’а тоже отдельный:

#[cfg(target_arch = "wasm32")]
fn main() -> GameResult {
    ggez::start(
        conf::Conf {
            cache: conf::Cache::Index,
            loading: conf::Loading::Embedded,
            ..Default::default()
        },
        |mut context| {
            let state = MainState::new(&mut context).unwrap();
            event::run(context, state)
        },
    )
}

Шелл скриптик для складывания ресурсов в static и генерации их списка выглядит вот так:

$ cat utils/wasm/build.sh
#!/bin/sh
cp -r assets static
cp utils/wasm/index.html static
ls static > static/index.txt
cargo web build

И еще пришлось обновить good-web-game под новым GGEZ, но там просто надо было заменить cgmath типы на mint.

Amit

Внезапно увидел, что Земерот попал к Амиту на страничку с реализациями гексо-математики, миленько: https://www.redblobgames.com/grids/hexagons/implementation.html

Что дальше?

Дальнеший план действий: перевожу журнал на Zola, срезаю 0.5 версию Земерота и пишу заметку в журнал об этом. Надо когда-то выпустить уже новую версию, а блокеров особых уже все равно не осталось. :slight_smile:


Я мимокрокодил, можешь ещё попробовать quick_error. Он даёт макросы для тривиальных реализаций description, display, from, into, с возможностью перегрузки. При этом это обычные enum, всё очень примитивно.

Не, quick_error не хочу, у меня сложные отношения с макросами и для такого кода я сильно за процедурные атрибуты.

4 лайка

Zemeroth v0.5

Таки срезал 0.5 версию:

https://github.com/ozkriff/zemeroth/releases/tag/v0.5.0

И написал ооогромную новость об этом:

"Zemeroth v0.5: ggez, WASM, itch.io, visuals, AI, campaign, tests" (reddit, twitter)


Так же, в README были добавлены секции Roadmap и Inspiration.

2 лайка

Первое видео разработки, визуальные улучшения, ozkriff.games, социалки и патреон

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

Первый раз что-то пытался так говорить в камеру, тем более на английском, химичил в audacity/kdenlive, так что вышло не особо круто. Надеюсь, практика сделает свое дело и постепенно станет лучше получаться. :slight_smile:

/r/rust_gamedev обсуждение

Визуальные улучшения

В основном, я успел поковырять всякие мелкие визуальные штуки со спрайтами.

Для начала, небольшое интерфейсное обновление: добавил подсветку клетки под курсором. Уже несколько людей просили. На тач устройствах подсветка отключена, а то она там после нажатия так и оставалась странно висеть на клетке до следующего тычка.

Добавил горизонтальное отражение спрайтов при движении/действиях в обратном направлении. Теперь больше не должно быть странно выглядящих атак спиной вперед.

Добавил небольшие анимации уклонения при промахах.

Добавил всплывающее информационное сообщение о прерывании попытки движения вражеской реакционной атакой.

Добавил агентам переключаемые кадры при использовании некоторых способностей - компромисс между полноценной покадровой анимацией и полностью статическими спрайтами.

Сначала хотел еще всем атакам специальные кадры сделать, но эксперименты показали, что во время атак и так достаточно всего происходит. Разве что копейщику надо будет сделать отдельные кадры атаки в разных направлениях - там еще в тему будет, потому что он через клетку часто тыкает врагов.

Лого

Небольшое обновление: Земерот обзавелся текстовым лого.

Чтобы немного стыковалось со стилем игровых спрайтов, сделана ручная “низкополигональная векторизация” (т.е. оно теперь угловато-рубленное, как и графика игры) в Inkscape текста, написанного Old London шрифтом. Хз, насколько оно подходит игре (много у кого такой шрифт в первую очередь внезапно с блэк металом ассоциируется, а не средневековьем), но пока сойдет.

Так теперь выглядит itch.io страничка игры:

Информирование населения

Прикупил домен ozkriff.games (только пока не запили в него отдельную страничку, так что пока он просто перенаправляет на журнал разработки).

Оживил свой патреон:

Распылил по интернету группы и темы о Земероте:

Посмотрим, как у меня получится во всю эту армаду своевременно публиковать обновления)


Тем временм, звезды на гитхабе перевалили за пять сотен, приятная мелочь.

5 лайков