В прошлой статье мы описали создание интеллекта, который решает, какой предмет сейчас делать, опираясь на заданную технологическую схему.
Однако, технологические деревья могут состоять не только из крафта. Не во всех играх предметы можно получить лишь ручной сборкой из компонентов. Некоторые можно произвести только в специальных сооружениях. Соответственно, эти сооружения можно возводить тоже по каким-то рецептам.
Здесь мы объединяем в одно целое систему добычи, крафта, строительства и использования зданий.
Расширение крафтовой системы на постройку и производство
За основу взята крафтовая система, описанная в предыдущей статье и используемая в реальном проекте.
Предметы
ITEMS = {
'rock': {'stack': True, 'material': 'stone', 'nmat': 1, 'type': {'raw', 'hammer', 'pickaxe'}, 'cost': 0.0},
'copper coin': {'stack': True, 'material': 'copper', 'nmat': 1, 'type': {'valuables'}, 'cost': 0.1},
'bronze pickaxe': {'stack': False, 'material': 'wood', 'nmat': 1, 'type': {'pickaxe'}, 'cost': 1.0},
'wooden stick': {'stack': False, 'material': 'wood', 'nmat': 2, 'type': {'raw', 'handle', 'weapon', 'fuel'}, 'cost': 0.0},
'wooden club': {'stack': False, 'material': 'wood', 'nmat': 2, 'type': {'weapon', 'fuel'}, 'cost': 0.0},
'ruby': {'stack': True, 'material': 'gems', 'nmat': 1, 'type': {'valuables'}, 'cost': 10.0},
'bronze axe': {'stack': False, 'material': 'wood', 'nmat': 2, 'type': {'cutter', 'weapon'}, 'cost': 1.0},
'wooden chips': {'stack': True, 'material': 'wood', 'nmat': 1, 'type': {'flammable', 'fuel'}, 'cost': 0.0},
'coal': {'stack': True, 'material': 'coal', 'nmat': 1, 'type': {'raw', 'fuel'}, 'cost': 0.0},
'copper ore': {'stack': True, 'material': 'copper ore', 'nmat': 2, 'type': {'raw'}, 'cost': 0.0},
'tin ore': {'stack': True, 'material': 'tin ore', 'nmat': 2, 'type': {'raw'}, 'cost': 0.0},
'flint stone': {'stack': True, 'material': 'stone', 'nmat': 1, 'type': {'raw', 'cutter', 'flint'}, 'cost': 0.0},
'copper': {'stack': True, 'material': 'copper', 'nmat': 2, 'type': {'raw'}, 'cost': 0.2},
'tin': {'stack': True, 'material': 'tin', 'nmat': 2, 'type': {'raw'}, 'cost': 0.0},
'bronze': {'stack': True, 'material': 'bronze', 'nmat': 2, 'type': {'raw'}, 'cost': 0.0},
'clay': {'stack': True, 'material': 'clay', 'nmat': 2, 'type': {'raw'}, 'cost': 0.0},
'rope': {'stack': True, 'material': 'fabric', 'nmat': 1, 'type': {'rope'}, 'cost': 0.1},
'raw brick': {'stack': True, 'material': 'clay', 'nmat': 1, 'type': set(), 'cost': 0.0},
'brick': {'stack': True, 'material': 'stone', 'nmat': 2, 'type': set(), 'cost': 0.1},
'raw_bowl': {'stack': True, 'material': 'clay', 'nmat': 1, 'type': set(), 'cost': 0.0},
'bowl': {'stack': True, 'material': 'stone', 'nmat': 1, 'type': set(), 'cost': 0.1},
'stone hammer': {'stack': False, 'material': 'stone', 'nmat': 2, 'type': {'hammer', 'weapon'}, 'cost': 0.0},
'berry': {'stack': True, 'material': 'veg', 'nmat': 1, 'type': {'food'}, 'cost': 0.0},
'mushroom': {'stack': True, 'material': 'veg', 'nmat': 1, 'type': {'food'}, 'cost': 0.0},
'pickaxe mold': {'stack': True, 'material': 'clay', 'nmat': 1, 'type': {'casting mold'}, 'cost': 0.0},
'axe mold': {'stack': True, 'material': 'clay', 'nmat': 1, 'type': {'casting mold'}, 'cost': 0.0},
'bronze pickaxe head': {'stack': True, 'material': 'bronze', 'nmat': 2, 'type': {'tool head'}, 'cost': 0.5},
'bronze axe head': {'stack': True, 'material': 'bronze', 'nmat': 2, 'type': {'tool head'}, 'cost': 0.5},
'bones': {'stack': True, 'material': 'bone', 'nmat': 1, 'type': {'raw'}, 'cost': 0.0},
'small leather': {'stack': True, 'material': 'leather', 'nmat': 1, 'type': {'raw'}, 'cost': 0.0},
'big leather': {'stack': True, 'material': 'leather', 'nmat': 2, 'type': {'raw'}, 'cost': 0.0},
'vein': {'stack': True, 'material': 'leather', 'nmat': 1, 'type': {'rope'}, 'cost': 0.0},
'bone knife': {'stack': False, 'material': 'bone', 'nmat': 1, 'type': {'cutter', 'weapon'}, 'cost': 0.0},
'bone awl': {'stack': False, 'material': 'bone', 'nmat': 1, 'type': {'awl', 'weapon'}, 'cost': 0.0},
'bone spear': {'stack': False, 'material': 'bone', 'nmat': 1, 'type': {'weapon'}, 'cost': 0.0},
'leather cap': {'stack': False, 'material': 'leather', 'nmat': 2, 'type': {'clothes', 'armor'}, 'cost': 0.3},
'skin': {'stack': False, 'material': 'leather', 'nmat': 3, 'type': {'clothes', 'armor'}, 'cost': 0.5},
'stone axe': {'stack': False, 'material': 'stone', 'nmat': 2, 'type': {'weapon'}, 'cost': 0.0},
'silver': {'stack': True, 'material': 'silver', 'nmat': 1, 'type': {'raw', 'valuables'}, 'cost': 1.0},
'gold': {'stack': True, 'material': 'gold', 'nmat': 1, 'type': {'raw', 'valuables'}, 'cost': 10.0},
'iron ore': {'stack': True, 'material': 'iron ore', 'nmat': 2, 'type': {'raw'}, 'cost': 0.0},
}
Рецепты крафта (ручной сборки предметов из компонентов):
RECIPES = [
{'result': 'raw brick', 'tool1': set(), 'ingrs': [['id', 'clay', 1], ['material', 'stone', 1]]},
{'result': 'raw_bowl', 'tool1': set(), 'ingrs': [['id', 'clay', 3]]},
{'result': 'stone hammer', 'tool1': set(), 'ingrs': [['type', 'handle', 1], ['type', 'rope', 1], ['id', 'rock', 1]]},
{'result': 'pickaxe mold', 'tool1': set(), 'ingrs': [['id', 'clay', 2]]},
{'result': 'axe mold', 'tool1': set(), 'ingrs': [['id', 'clay', 2]]},
{'result': 'bronze pickaxe', 'tool1': set(), 'ingrs': [['type', 'handle', 1], ['id', 'bronze pickaxe head', 1]]},
{'result': 'bronze axe', 'tool1': set(), 'ingrs': [['type', 'handle', 1], ['id', 'bronze axe head', 1]]},
{'result': 'bone knife', 'tool1': {'cutter'}, 'ingrs': [['id', 'bones', 1]]},
{'result': 'bone awl', 'tool1': {'cutter'}, 'ingrs': [['id', 'bones', 1]]},
{'result': 'bone spear', 'tool1': set(), 'ingrs': [['type', 'handle', 1], ['type', 'rope', 1], ['id', 'bone awl', 1]]},
{'result': 'stone axe', 'tool1': set(), 'ingrs': [['type', 'handle', 1], ['type', 'rope', 1], ['id', 'flint stone', 1]]},
{'result': 'leather cap', 'tool1': {'awl'}, 'ingrs': [['type', 'rope', 1], ['id', 'small leather', 2]]},
{'result': 'skin', 'tool1': {'awl'}, 'ingrs': [['type', 'rope', 1], ['id', 'big leather', 2], ['id', 'small leather', 1]]},
]
Добавим производственные схемы зданий, это те же рецепты, но с добавочным свойством build, обозначающим, какое здание реализует данный рецепт:
BLD_RECIPES = [
{'result': 'copper', 'build': 'furnace', 'tool1': {'flint'}, 'ingrs': [['type', 'flammable', 2], ['id', 'coal', 1], ['id', 'copper ore', 2]]},
{'result': 'tin', 'build': 'furnace', 'tool1': {'flint'}, 'ingrs': [['type', 'flammable', 2], ['id', 'coal', 1], ['id', 'tin ore', 1]]},
{'result': 'brick', 'build': 'furnace', 'tool1': {'flint'},
'ingrs': [['type', 'flammable', 1], ['type', 'fuel', 1], ['id', 'raw brick', 1]]},
{'result': 'bowl', 'build': 'furnace', 'tool1': {'flint'},
'ingrs': [['type', 'flammable', 1], ['type', 'fuel', 1], ['id', 'raw_bowl', 1]]},
{'result': 'bronze pickaxe head', 'build': 'foundry', 'tool1': {'flint'},
'ingrs': [['type', 'fuel', 3], ['id', 'copper', 2], ['id', 'tin', 1], ['id', 'pickaxe mold', 1]]},
{'result': 'bronze axe head', 'build': 'foundry', 'tool1': {'flint'},
'ingrs': [['type', 'fuel', 3], ['id', 'copper', 2], ['id', 'tin', 1], ['id', 'axe mold', 1]]},
]
Появились печь и плавильня, для которых есть рецепты постройки:
CONSTR_RECIPES = [
{'result': 'furnace', 'tool1': set(), 'ingrs': [['material', 'stone', 10]]},
{'result': 'foundry', 'tool1': set(), 'ingrs': [['id', 'brick', 10], ['id', 'bowl', 1]]},
]
Поля аналогичны тем же крафтовым рецептам. А исходное сырьё теперь можно получать из … ну назовём это «месторождения» (deposites):
DEPOSITES = {
'copper wall': {'tools': set(), 'result': {'rock', 'flint stone', 'copper ore'}},
'tin wall': {'tools': set(), 'result': {'rock', 'flint stone', 'tin ore'}},
'clay wall': {'tools': set(), 'result': {'rock', 'coal', 'clay'}},
'bush': {'tools': {'hammer', 'cutter'}, 'result': {'wooden stick'}},
'small bush': {'tools': {'hammer'}, 'result': {'wooden chips'}},
'corpse': {'tools': {'cutter'}, 'result': {'vein', 'bones', 'small leather', 'big leather'}},
}
Каждое месторождение имеет свойство tools – инструменты, хотя бы один из которых нужен для добычи сырья и result – множество возможных предметов, которые можно получить. Здесь для простоты вероятность получения любого из предметов одинакова, но в реальном проекте это, разумеется, не так. Чтобы добывать из «стен» минералы нужна кирка, но по сеттингу NPC – это дварфы, которые всегда умеют рыть стены, поэтому считаем, что для этих месторождений инструмент не нужен.
Как и прежде код дан на питоне, но это не значит, что сама игра должна быть на нём. Также подчеркну, что все эти предметы и рецепты не обязательно писать кодом, их можно втянуть из какой-нибудь экселевской таблички.
Принцип работы и результат
Кратко опишу принцип работы: каждому предмету, рецепту, зданию и месторождению присваивается некий постоянный вектор (эмбеддинг), который предварительно оптимизируется в питоновском скрипте. У NPC есть динамический эмбеддинг инвентаря, который обновляется всякий раз, когда содержимое инвентаря меняется. Также, появляется эмбеддинг построенных зданий. Нейронная сеть, управляющая NPC, получает на вход эти эмбеддинги и вычисляет ценность каждого действия (добыча, крафт, строительство, использование постройки) и каждого варианта действия (что добывать, что строить, что производить и т.д.). Среди всех вариантов выбирается тот, который имеет наибольшую оценку. Ценность может быть динамической, предусмотрен режим «целевой предмет», который оптимизирует выбор, который в конечном итоге приведёт к созданию желаемого предмета. В редких случаях, когда предмет может выполнять несколько функций в одном рецепте, нейронка может выдать действие, на которое не хватает ингредиентов. Получив ошибку нехватки ингредиентов, создается «эмбеддинг дефицита», который блокирует проблемные рецепты. Дефицит снимется после добычи соответствующего предмета.
Для наглядности приведу упрощённый кусок производственной схемы, соответствующей созданию бронзовой кирки (bronze pickaxe) из полной схемы, данной рецептами выше.

Модельный эксперимент все ещё далек от реальной ситуации: мы не учитываем перемещения NPC между месторождениями и постройками и время создания предметов. Каждый ход NPC просто выбирает между 4-мя действиями, описанными выше. В таблице ниже приведены результаты для режима «целевой предмет», усредненные по 10 прогонам. Число ходов для каждого предмета выбрано разным, поскольку изготовление кирки с нуля требует очень много подготовительной работы. Приведённые метрики:
-
доля добычи – количество действий «добыча» к общему числу ходов;
-
доля полезного крафта – отношение необходимого числа действий «крафт» для изготовления указанного количества целевого предмета к фактическому количеству действий «крафт»;
-
доля провалов – отношение количества проваленных действий, к общему количеству действий.
Целевой предмет |
Число ходов |
Кол-во произведенных целевых предметов |
Доля добычи (mining), % |
Доля полезного крафта, % |
Доля провалов, % |
Bronze pickaxe |
300 |
4.8 |
79 - 82 |
86 - 88 |
0 |
Brick |
100 |
10.5 |
69 - 77 |
89 - 100 |
0 - 9 |
Bone spear |
100 |
6.6 |
77 - 89 |
72 - 87 |
0 |
Видно, что NPC справляется даже с очень сложными технологическими схемами. В случае кирки такая схема включает постройку двух объектов, причем материалы для второго здания производятся с помощью первого. В то же время, агент не распыляется, почти все действия приводят в конечном итоге к созданию нужного предмета. Во всех эксперимента NPC не строил здания, если они не нужны. При этом, метод не требует программирования логики, достаточно лишь задать рецепты крафта/строительства/производства.
Дальнейшее развитие
Теперь можно думать о том, как сделать NPC существами социальными. Ведь в реальной жизни мы редко воспроизводим всю технологическую цепочку. Обычно происходит некоторое разделение по специальностям: одни делают одно – другие другое. Сейчас, когда у каждого предмета / здания / инвентаря появились эмбеддинги, можно попытаться организовать обмен этой информацией. Например, можно добавить ячейки памяти, где будут сохранятся эмбеддинги тех зданий, которые NPC когда-либо видел, чтобы он мог вернутся к нему и не строить новое. Также можно сделать какое-то подобие общения, сопоставив словам эмбеддинги и наоборот.
Автор: azTotMD