# теория · шаг 1 из 7
Игра как набор задач: от механики к спецификации
Коротко
Игру не пишут «целиком». Её раскладывают на механики, и каждую механику переводят в задачу из курса теми же вопросами, что в разделе 26: что на входе, какой вид ответа, что значит «лучше», какой размер, какие края. «Отряд идёт к цели» - это кратчайший путь по клеткам, и на одной карте 5 × 6 он даёт два разных маршрута: пять ходов за 11 единиц времени или девять ходов за 9, смотря что считать ценой. «Выгодный обмен на рынке» - круг, где произведение курсов больше единицы; логарифм превращает его в отрицательный цикл из раздела 11. Таблица модулей игры «Архипелаг» - карта всего раздела.
Если ты умеешь превращать «хочу, чтобы было умно» в вход, выход и критерий, листай до «Теперь сам».
Как думаешь, какая это задача
В диздоке игры - дизайн-документе, где описаны правила и механики, - написано: «отряд должен ходить к цели умно». Какой алгоритм курса это реализует - поиск в ширину, Дейкстра или что-то третье?
Ответь себе до чтения. Сама фраза ответа не содержит. «Умно» может значить «за меньшее число ходов» - тогда это поиск в ширину по клеткам. Может значить «быстрее по времени», если лес проходится втрое дольше луга, - тогда это Дейкстра с ценой клетки. Может значить «незаметно для врага» - и тогда ценой становится число клеток, которые видит противник, а это уже другая карта весов. Три толкования - три задачи, и выбирает между ними не программист, а тот, кто придумал игру. Работа программиста - задать вопрос.
На что это похоже
Смета на ремонт квартиры. «Сделать красиво» не превращается в работу, пока не разложено по комнатам: в ванной плитка, столько-то метров, такой-то клей; в кухне - розетки, столько-то точек. Каждая строка сметы - отдельная задача со своим мастером, сроком и ценой, и ошибка в одной не ломает остальные. Игра - та же квартира: механики - комнаты, и у каждой своя спецификация.
Механизм: мир «Архипелага» и его модули
Весь раздел строит одну игру - пошаговую песочницу «Архипелаг». Мир - карта 128 × 128 клеток: вода, луг (цена хода 1), лес (3), горы (непроходимы). До восьми игроков, у каждого до 25 отрядов. Отряды добывают сырьё, мастерские делают из него вещи по рецептам, на рынке товары меняют по курсам, между игроками есть чат, игру можно сохранить, а сообщество пишет моды - маленькие скрипты, которые сервер выполняет каждый ход. Сервер считает ход за 100 мс.
Каждая механика переводится в задачу курса:
| механика | задача | вид ответа | раздел |
|---|---|---|---|
| острова на карте связаны | связность, непересекающиеся множества | да / нет + мосты | 9, 10 |
| отряд идёт к цели | кратчайший путь на сетке | сам путь | 11 |
| что видит отряд | видимость по прямой | множество клеток | 23 |
| сколько сырья на вещь | обход дерева рецептов с запоминанием | число по каждому сырью | 10, 15 |
| что взять в трюм | рюкзак | лучший набор | 15, 22 |
| выгодный круг обмена | отрицательный цикл | сам круг или «нет» | 11 |
| место игрока в рейтинге | порядковая статистика | одно число | 6, 8 |
| подбор соперников | паросочетание | пары | 12, 14 |
| фильтр чата | поиск многих образцов | позиции | 17 |
| одинаковые ли сохранения | хеш состояния, при совпадении - сравнение целиком | да / нет | 7, 19 |
| завершится ли скрипт мода | проблема останова | нельзя решить - лимит | 21 |
Порядок работы над каждой строкой - из раздела 26: спецификация, бюджет, маленький пример с перебором, быстрое решение, проверка. Следующие уроки главы делают это для всей таблицы сразу: бюджет хода, структуры данных под частые операции, что нужно доказать, а что проверить.
Правило
Механика игры становится задачей, когда записаны вход, вид ответа, критерий «лучше», размер и края. Одна фраза диздока часто прячет несколько задач - выбери толкование явно и запиши его, прежде чем выбирать алгоритм.
Разбор: «отряд идёт к цели»
Спецификация. Вход - карта цен клеток, старт, цель. Вид ответа - сам путь, а не его длина: отряду нужно знать, куда шагнуть. «Лучше» - два толкования, их и сравним. Края: цель совпадает со стартом - путь из одной клетки, ход не нужен; цель в воде или недостижима - ответ пустой путь, а не ошибка. Размер: 16 384 клетки, до 200 отрядов.
# карта 5 × 6: цена входа в клетку (1 - луг, 3 - лес), 0 - вода
karta = [
[1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 3, 1],
[1, 3, 3, 1, 3, 1],
[1, 0, 1, 1, 0, 1],
[1, 1, 1, 0, 0, 1],
]
STROK = 5
STOLB = 6
def imya(k):
# клетка как на доске: столбец буквой, строка цифрой - "c2"
return "abcdef"[k[1]] + str(k[0])
def sosedi(k):
out = []
for d in [[-1, 0], [1, 0], [0, -1], [0, 1]]:
r = k[0] + d[0]
c = k[1] + d[1]
if 0 <= r < STROK and 0 <= c < STOLB and karta[r][c] > 0:
out.append([r, c])
return out
def put(start, cel, cena):
# Дейкстра за V²: карта маленькая. cena(k) - цена входа в клетку k
dist = {}
otkuda = {}
kletka = {}
gotovo = set()
dist[imya(start)] = 0
kletka[imya(start)] = start
while True:
v = ""
for k in dist:
if k not in gotovo and (v == "" or dist[k] < dist[v]):
v = k
if v == "" or v == imya(cel):
break
gotovo.add(v)
for s in sosedi(kletka[v]):
nd = dist[v] + cena(s)
if imya(s) not in dist or nd < dist[imya(s)]:
dist[imya(s)] = nd
otkuda[imya(s)] = v
kletka[imya(s)] = s
if imya(cel) not in dist:
return [] # цель недостижима: пустой путь
marshrut = [imya(cel)]
while marshrut[0] != imya(start):
marshrut = [otkuda[marshrut[0]]] + marshrut
return marshrut
def po_kletkam(k):
return 1
def po_vremeni(k):
return karta[k[0]][k[1]]
def vremya(marshrut):
t = 0
for i in range(1, len(marshrut)):
t = t + karta[int(marshrut[i][1])]["abcdef".find(marshrut[i][0])]
return t
for zadacha in [["меньше клеток: ", po_kletkam],
["меньше времени:", po_vremeni]]:
m = put([2, 0], [2, 5], zadacha[1])
print(zadacha[0], len(m) - 1, "ходов,", vremya(m), "времени |",
" ".join(m))
print("цель в воде: ", put([2, 0], [1, 2], po_vremeni))
меньше клеток: 5 ходов, 11 времени | a2 b2 c2 d2 e2 f2
меньше времени: 9 ходов, 9 времени | a2 a1 a0 b0 c0 d0 e0 f0 f1 f2
цель в воде: []
Один алгоритм, одна карта, два ответа. По клеткам выгоднее идти напрямик через лес - пять ходов, но 11 единиц времени. По времени выгоднее обойти лес по лугу - девять ходов, но 9 единиц. Ни один ответ не ошибка, ошибка - выбрать критерий молча. Цель в воде дала пустой путь: этот край записан в спецификации, и отряд, получив пустой путь, просто стоит, а не падает.
Разбор: «выгодный обмен на рынке»
Рынок меняет товары по курсам. Игроки ищут круг обмена, который возвращает больше, чем было: отдать рыбу, получить зерно, за зерно доски и так далее, пока не вернёшься к рыбе. Круг выгоден, если произведение курсов больше единицы. Переберём все простые круги на четырёх товарах.
import math
tovary = ["рыба", "зерно", "доски", "камень"]
# kurs[a][b] - сколько товара b дают за единицу товара a; 0 - не меняют
kurs = [
[0, 3, 0, 0.5],
[0.3, 0, 0.45, 0],
[0, 2, 0, 0.8],
[0.97, 0, 1.2, 0],
]
def krugi(put, out):
# все простые круги, начинающиеся в товаре put[0] с меньшим номером
a = put[len(put) - 1]
for b in range(len(tovary)):
if kurs[a][b] == 0:
continue
if b == put[0] and len(put) >= 2:
out.append(put + [b])
elif b > put[0] and b not in put: # круг - с меньшего номера
krugi(put + [b], out)
vse = []
for s in range(len(tovary)):
krugi([s], vse)
for k in vse:
proizv = 1
summa = 0
for i in range(len(k) - 1):
proizv = proizv * kurs[k[i]][k[i + 1]]
summa = summa - math.log(kurs[k[i]][k[i + 1]])
nazvaniya = []
for t in k:
nazvaniya.append(tovary[t])
print(" -> ".join(nazvaniya))
print(" произведение", round(proizv, 3), "| сумма -log",
round(summa, 3))
рыба -> зерно -> рыба
произведение 0.9 | сумма -log 0.105
рыба -> зерно -> доски -> камень -> рыба
произведение 1.048 | сумма -log -0.047
рыба -> камень -> рыба
произведение 0.485 | сумма -log 0.724
рыба -> камень -> доски -> зерно -> рыба
произведение 0.36 | сумма -log 1.022
зерно -> доски -> зерно
произведение 0.9 | сумма -log 0.105
доски -> камень -> доски
произведение 0.96 | сумма -log 0.041
Выгоден один круг: рыба - зерно - доски - камень - рыба, плюс 4,8% за оборот. Это дыра в экономике: игрок
крутит круг, пока не станет богаче всех. Перебор кругов растёт факториально, поэтому на сотне товаров он
не годится - и здесь работает переформулировка. math.log - натуральный логарифм; основание для нас
неважно, важны два его свойства. Логарифм произведения равен сумме логарифмов, и логарифм больше нуля ровно
у чисел больше единицы. Значит, «произведение курсов больше 1» - то же, что «сумма −log курса меньше 0».
Если весом ребра взять −log курса, выгодный круг - это отрицательный цикл, а его находит Беллман -
Форд из раздела 11 за V · E. Вывод это и показывает: у единственного выгодного круга сумма −0.047, у
остальных - больше нуля. Граница правила: переформулировка верна, только пока обмен идёт без комиссии и
округления. Комиссия в 1% за сделку - тоже множитель, её вносят в курс до логарифма.
Что ломается без этого
Код раньше спецификации. Поиск пути написан «по клеткам», а дизайнер ждал «по времени» - отряды лезут через лес, и это выглядит как ошибка ИИ, хотя алгоритм верен.
Пропущенный край. Цель в воде, а функция пути бросает исключение: сервер падает на ходу одного игрока.
Экономика с дырой. Курсы на рынке подобраны вручную, выгодный круг никто не искал - через неделю у одного игрока весь камень мира.
Зачем это в работе
Диздок игры, ТЗ заказчика, тикет «сделать подсказки умнее» - всё это просьбы, а не спецификации. Таблица «механика → задача → вид ответа → раздел» за час превращает документ в список тикетов, где у каждого понятно, как проверить готовность. А строка «нельзя решить - лимит» заранее снимает спор о невозможном.
Теперь сам
- Механика «туман войны»: отряд видит клетки, до которых прямая линия не пересекает гору. Какая это задача, какой вид ответа?
- Механика «игрок видит своё место в рейтинге среди 10 000 игроков после каждой партии». Что выбрать: сортировать всех после каждой партии или что-то другое?
- Курсы обмена: рыба → зерно 2, зерно → рыба 0,6. Выгоден ли круг? Чему равна сумма
−log?
Ответ. 1. Видимость из точки - геометрия (раздел 23): для каждой клетки - пересекает ли отрезок от отряда до неё непрозрачную клетку. Вид ответа - множество видимых клеток.
-
Порядковая статистика с обновлениями: дерево с размерами поддеревьев или дерево Фенвика по очкам - очки ограничены, например от 0 до 1000, иначе их сжимают - (раздел 8) отвечают «сколько игроков выше» за
log n; сортировка после каждой партии -n log nна каждый запрос. -
Произведение
2 · 0,6 = 1,2 > 1- выгоден. Сумма−log 2 − log 0,6 = −log 1,2 ≈ −0,18 < 0.
Главное
Механика игры - это просьба. Задачей её делают вход, вид ответа, критерий и края, и одна фраза диздока часто прячет несколько задач с разными алгоритмами.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий