
Игра “Космические рейнджеры” разработана в 2002 году компанией Elemental Games, под руководством Дмитрия Гусарова. Текстовые квесты, наряду с планетарными и аркадными боями, немало способствовали популярности игры, внося разнообразие в игровой процесс перелётов от планеты к планете, торговли и пошаговых боёв к космосе. Со временем, текстовые квесты выросли в самостоятельный жанр.
Необходимым, для развития этого направления, было наличие инструментов, при помощи которых фанаты могли создавать свои квесты. В самом начале, это был графический редактор TGE, разработанный Денисом Фёдоровым, Константином Савенковым и Алексеем Бондарчуком, совместно с Дмитрием Гусаровым. В настоящее время, существует более удобная альтернатива, поддержкой которой занимается Василий Рогин (там же квесты можно и запускать). Но графический редактор - это графический редактор.

Мой вклад во всё это - разработка текстового формата разметки QMS, позволяющего нормально работать с Git, повторно использовать код и вольготно обращаться с Copy-Paste (для интересующихся, код парсера QMS здесь, есть ещё декомпилятор, но он к теме сегодняшнего разговора отношения не имеет).
Первоначально, QMS задумывался как язык простой разметки, для разработки относительно небольших текстовых квестов. Аппетит, как это обычно бывает, пришёл во время еды. Дело в том, что некоторый опыт разработки квестов уже имелся.
В Perudo была реализована игра в кости из “Пиратов Карибского моря”
Охота на кнашей была основана на механике игры “Охота на лис”
Квест Орёл или решка предлагал поиграть в нетранзитивную орлянку.
Гонки получились не очень, но сами по себе представляли одну большую миниигру
Представление о том, как облегчить себе жизнь, было, но нельзя же просто взять и начать штамповать миниигры в промышленных масштабах. Требовалась некая объединяющая идея. И она нашлась. Было решено делать квест - космическую стратегию.
Синопсис следующий
В одной звёздной системе сосуществуют две враждующие расы (например, гаальцы и пеленги не сильно любят друг друга, поскольку последние, на постоянной основе, занимаются техническим шпионажем). Представители каждой из рас могут быть расселены по 2-3 планетам. Также, в системе могут присутствовать нейтральные или не заселённые планеты. Население разделено на следующие страты:
Мирное население
Военные
Учёные
Разведчики
Мирное население обеспечивает ежегодный прирост, а также снабжает остальные страты продуктами, обеспечивая их численность. Учёные занимаются исследованиями, разведчики - дипломатией, шпионажем и контрразведкой, а военные участвуют в боевых действиях. Вооружённые силы подразделяются на следующие рода войск:
Десант - пехота осуществляющая боевые действия "на земле"
ВКС - Военно-Космические силы (пилоты)
ПКО - Противо-Космическая оборона (артиллерия)
Поскольку планеты подчиняются законам орбитальной механики, перелёты между ними возможны не каждый год, а по расписанию. При этом, перелёт с внутренней планеты к внешней может быть доступен чаще чем, например, перелёт между двумя внешними планетами, а перелёты между планетами и их спутниками возможны всегда.
Задачей игрока (и противостоящего ему бота) является накопление оптимального количество войск и захват ими вражеских планет, по мере возможности. В таких межпланетных перелётах участвуют только десантники и ВКС. ПКО остаются дома, обеспечивая защиту родной планеты. Возможны два варианта боевых действий:
Планетарный - высадка войск на планету
Встречный - бой "на марше" с противником, выдвинувшимся навстречу
В ходе ведения боя, как игрок, так и противостоящий ему бот, могут предпринимать различные тактические действия, такие как:
Бой ВКС - размен с ВКС противника
Абордаж - атака ВКС силами десанта в космосе
Подавление ПКО - атака силами ВКС
Бомбардировка - Урон наносится, в том числе, мирному населению
Высадка десанта - с целью захвата планеты
Отступление - возможно только для атакующей стороны
Эффективность тех или иных действий определяется характеристиками игрока (подкрутить их можно будет в начале квеста):

Назначение характеристик должно быть понятно из названия. Производство определяет насколько эффективно мирное население обеспечивает военных, шпионаж влияет на успешность разведывательных и контрразведывательных мероприятий, хаотичность порождает случайных разброс характеристик, при выполнении тех или иных действий.
Начиная с 10000 населения, планета может быть объявлена столичной, что приведёт к приросту на ней населения. Столица может быть только одна и её потеря приведёт к немедленному поражению. В игре могут происходить случайные глобальные события, например нападения доминаторов, прореживающие состав ВКС на всех планетах системы.
Осталось рассказать про учёных и разведчиков. Исследования доступны при наличии учёных, также, от количества учёных зависит их продолжительность. Результатом успешного исследования является улучшение той или иной характеристики. На эффективность и продолжительность исследований влияют характеристики “Наука” и “Хаотичность”. Часть исследований доступна начиная с некоторого “технологического уровня”, повышающегося при успешном проведении других исследований.
Что касается разведки, то здесь возможно проведение самых разных операций: от получения сводок о составе населения на вражеских планетах и получении оповещений обо всех изменениях, до выкрадывания технологий противника и проведения диверсий. И здесь мы вплотную подошли к теме сегодняшней статьи.
Изобразительные возможности текстовых квестов скромны. Космические стратегии в них выглядят как бесконечные вереницы таблиц с числами. Возможность переключения на другой вид деятельности, при таких условиях, бесценна. Что нам могут предложить миниигры? Давайте посмотрим.
Манкала
Это семейство игр, распространённых в Африке и Юго-Восточной Азии, основанных на механике “посева”. Как таковые, игры с противником нам не очень интересны. Для встраивания в квест больше подходят головоломки. К счастью, в огромном семействе манкал, такие есть.

Головоломку придумал французский математик Henri-Auguste Delannoy в 1895 году. Правила просты: “семена” забираются из любой лунки кроме “амбара” (расположенного справа) и “засеваются” по одному в лунки слева-направо, не пропуская амбар и стартовую лунку. Если последнее семя падает в непустую лунку, следующий ход должен выполняться из неё. Если посев завершается в амбаре, следующий ход выполняется из любой лунки. Для победы, все семена надо собрать в амбар.
В плане реализации, всё просто
Прежде всего, нам потребуется игровое поле, которое удобно определить макросом, чтобы не повторять одно и то же в разных местах квеста:
#macro:maim #page:1 ^ ------------------------ ^ ^ @manc @ms| ^ ^ ($temp_1) ($temp_2) ($temp_3) ($temp_4) | ($temp_5) ^ ^ ------------------------ ^ ^ %A% %B% %C% %D% @ms| ^ $ms = ($temp_1 > 9) + ($temp_2 > 9) + ($temp_3 > 9) + ($temp_4 > 9) #end
На page пока внимания не обращаем. Символы ‘$’ и ‘@’ управляют отображением параметров в виде числа или строки, соответственно. Символ ‘^’ - это скобки, включающие моноширинный шрифт, а ‘%’ bold-разметку. Получается как-то так:

Звёздочки сверху показывают, из какой лунки разрешено выполнять ход (в начале игры, из любой кроме амбара). В целях экономии параметров (это лимитированный ресурс квеста), управляющую переменную можно определить как битовую маску:
#var:manc #range:0..15 #default:0 #mod:2 #hide #text:0..0 ' ' #text:1..1 '* ' #text:2..2 ' * ' #text:4..4 ' * ' #text:8..8 ' *' #text:15..15 '* * * *' #field:d // 8 #field:c // 4 #field:b // 2 #field:a // 1
Строки text определяют текстовое представление, отображаемое @ manc, а field позволяет обращаться к битам маски напрямую (это одно из нововведений очередной итерации QMS). В данном случае, важно чтобы text шли перед field, поскольку у полей могут быть свои правила для текстового представления. Есть еще одна, менее заметная, но важная переменная:
#var:ms #range:0..4 #default:0 #hide #text:0..0 ' ' #text:1..1 ' ' #text:2..2 ' ' #text:3..3 ' ' #text:4..4 ' '
Это просто цепочки из пробелов, корректирующие картинку в тех случаях, когда некоторые лунки содержат более 9 семян. Вычисляется переменная просто:
$ms = ($temp_1 > 9) + ($temp_2 > 9) + ($temp_3 > 9) + ($temp_4 > 9)
Здесь происходит неявное преобразование булевских значений в числовое (0 или 1), после чего все переполнения складываются. Поскольку присвоение определено внутри макроса, оно выполняется при каждом его использовании:
#site:mancala #maim #case:mancala_cont 'A' { $temp_1 = 1 and $manc.a > 0 } #order:1 #sound:seed.ogg $temp_1 = 0 $temp_2 = $temp_2 + 1 $manc = 2 #case:mancala_cont 'B' { $temp_2 = 1 and $manc.b > 0 } #order:2 #sound:seed.ogg $temp_2 = 0 $temp_3 = $temp_3 + 1 $manc = 4 #case:mancala_cont 'C' { $temp_3 = 1 and $manc.c > 0 } #order:3 #sound:seed.ogg $temp_3 = 0 $temp_4 = $temp_4 + 1 $manc = 8 #case:mancala_cont 'D' { $temp_4 = 1 and $manc.d > 0 } #order:4 #sound:seed.ogg $temp_4 = 0 $temp_5 = $temp_5 + 1 $manc = 15 ...
Переходов, из этой локации, приходится определять много, для всех лунок и каждого значения количества семян от 1 до 16. Сами же переходы просто изменяют значения переменных-лунок (в фигурных скобках записывается условие, при выполнении которого переход возможен. Получаем такое меню:

Осталось определить условия завершения игры:
#site:mancala_cont #case:game_win { $temp_1 + $temp_2 + $temp_3 + $temp_4 = 0 } #case:mancala { $temp_1 + $temp_2 + $temp_3 + $temp_4 > 0 and $manc = 15 } #case:mancala { $temp_1 > 1 and $manc.a > 0 } #case:mancala { $temp_2 > 1 and $manc.b > 0 } #case:mancala { $temp_3 > 1 and $manc.c > 0 } #case:mancala { $temp_4 > 1 and $manc.d > 0 } #case:mancala_lose { $manc = 1 and $temp_1 < 2 } #case:mancala_lose { $manc = 2 and $temp_2 < 2 } #case:mancala_lose { $manc = 4 and $temp_3 < 2 } #case:mancala_lose { $manc = 8 and $temp_4 < 2 }
Здесь как раз работа с битовой маской. Если ход из лунки разрешён, игра продолжается если посев закончился не на пустой лунке (больше одного семени). Ход разрешён из любой лунки если посев был завершён в амбаре.
Поиграть можно здесь или здесь (в меню выберите манкалу). Кстати, буквально на днях, Василий Рогин (разработчик Web-плейера квестов) предложил интересную доработку, делающую миниигры в квестах ещё более интерактивными. При этом, правда, ломается обратная совместимость, новые квесты не будут работать в “Космических рейнджерах”.
Суть в следующем
В описание игрового поля добавляются link-теги, создающие невидимые ссылки. При нажатии на ссылку, автоматически активируется ссылка в меню с совпадающим именем (если таковая имеется).
#var:manc #range:0..15 #default:0 #mod:2 #hide #text:0..0 ' ' #text:1..1 '<link=A>*</link> ' #text:2..2 ' <link=B>*</link> ' #text:4..4 ' <link=C>*</link> ' #text:8..8 ' <link=D>*</link>' #text:15..15 '<link=A>*</link> <link=B>*</link> <link=C>*</link> <link=D>*</link>' #field:d // 8 #field:c // 4 #field:b // 2 #field:a // 1 #macro:maim #page:1 ^ ------------------------ ^ ^ @manc @ms| ^ ^ <link=A>($temp_1)</link> <link=B>($temp_2)</link> <link=C>($temp_3)</link> <link=D>($temp_4)</link> | ($temp_5) ^ ^ ------------------------ ^ ^ <link=A>%A%</link> <link=B>%B%</link> <link=C>%C%</link> <link=D>%D%</link> @ms| ^ $ms = ($temp_1 > 9) + ($temp_2 > 9) + ($temp_3 > 9) + ($temp_4 > 9) #end
Теперь можно тыкать мышью не только в пункты меню снизу, но и в изображения лунок на странице, звёздочки над ними и метки под лунками. Если переход разрешён, он произойдёт. На мой взгляд, очень удобно.
Перестановка
Эту головоломку с шахматными слонами придумал знаменитый математик и популяризатор Генри Дьюдени в 1907 году.

Чёрные и белые фигуры над поменять местами, перемещая их ходом шахматного слона и не вставая под бой фигур противоположного цвета. Поскольку ход чёрными и белыми чередуется, существует вероятность того что решение зайдёт в тупик и придётся начинать с самого начала.
Как всё устроено
Эта игра сложнее, здесь есть движущиеся фигуры. Для начала, надо уметь отображать доску:
#macro:lcell #text:0..0 '.' #text:1..1 'x' #text:2..2 'X' #text:3..3 'x' #text:4..4 'X' #text:5..5 'x' #text:6..6 'X' #text:7..7 'x' #text:8..8 'X' #end #for:i:1..5 #var:A[$i] #range:0..8 #default:0 #hide #lcell #var:B[$i] #range:0..8 #default:0 #hide #lcell #var:C[$i] #range:0..8 #default:0 #hide #lcell #var:D[$i] #range:0..8 #default:0 #hide #lcell #end #macro:saim #var:LC #range:0..4 #hide #text:0..0 ' A B C D' #text:1..1 ' * B C D' #text:2..2 ' A * C D' #text:3..3 ' A B * D' #text:4..4 ' A B C *' ^ +---------+ ^ ^ %5% | <link=A5>@A5</link> <link=B5>@B5</link> <link=C5>@C5</link> <link=D5>@D5</link> | ^ ^ %4% | <link=A4>@A4</link> <link=B4>@B4</link> <link=C4>@C4</link> <link=D4>@D4</link> | ^ ^ %3% | <link=A3>@A3</link> <link=B3>@B3</link> <link=C3>@C3</link> <link=D3>@D3</link> | ^ ^ %2% | <link=A2>@A2</link> <link=B2>@B2</link> <link=C2>@C2</link> <link=D2>@D2</link> | ^ ^ %1% | <link=A1>@A1</link> <link=B1>@B1</link> <link=C1>@C1</link> <link=D1>@D1</link> | ^ ^ +---------+ ^ ^ @LC ^ #end
То, как работает оператор for, должно быть понятно из контекста. В плане реализации, это анонимный параметризованный макрос, автоматически вызываемый несколько раз, по месту определения. Помогает значительно сокращать объём исходного текста. Можно заметить, что каждое поле доски - целое от 0 до 8 (это для того, чтобы работать с каждой фигурой индивидуально). После правильного заполнения переменных, видим следующее:

Далее, надо уметь выбрать фигуру, после чего указать место, на которое она должна переместиться. Но дьявол кроется в деталях: во первых, фигуры должны перемещаться строго по диагоналям. Кроме того, нельзя перемещаться на клетки находящиеся под боем фигур другого цвета. Здесь понадобятся битовые маски:
#macro:wl:P:N #var:wline[$P][$N] #range:0..65535 #default:0 #mod:2 #hide #field:dd // 32768 #field:dc // 16384 #field:db // 8192 #field:da // 4096 #field:cd // 2048 #field:cc // 1024 #field:cb // 512 #field:ca // 256 #field:bd // 128 #field:bc // 64 #field:bb // 34 #field:ba // 16 #field:ad // 8 #field:ac // 4 #field:ab // 2 #field:aa // 1 #end #for:i:1..5 #wl:1:[$i] #wl:2:[$i] #end
Для каждой из пяти горизонталей, создаём маски достижимости клеток, индивидуально для каждой фигуры (wline1 для своего цвета и wline2 для цвета противника). Для вычисления достижимости полей используем процедуры (ещё одна возможность QMS):
#site:swap_calc_sub #for:Y:1..5 #case:swap_calc_ret { $temp_7 = 1 and $temp_8 = [$Y] and $A[$Y] > 0 } #case:swap_calc_sub_1 { $temp_7 = 1 and $temp_8 = [$Y] and $A[$Y] = 0 } #case:swap_calc_ret { $temp_7 = 2 and $temp_8 = [$Y] and $B[$Y] > 0 } #case:swap_calc_sub_1 { $temp_7 = 2 and $temp_8 = [$Y] and $B[$Y] = 0 } #case:swap_calc_ret { $temp_7 = 3 and $temp_8 = [$Y] and $C[$Y] > 0 } #case:swap_calc_sub_1 { $temp_7 = 3 and $temp_8 = [$Y] and $C[$Y] = 0 } #case:swap_calc_ret { $temp_7 = 4 and $temp_8 = [$Y] and $D[$Y] > 0 } #case:swap_calc_sub_1 { $temp_7 = 4 and $temp_8 = [$Y] and $D[$Y] = 0 } #end #site:swap_calc_sub_1 #for:Y:1..5 #case:swap_calc_ret { $temp_7 = 1 and $temp_8 = [$Y] and $temp_5 = 1 } $wline1[$Y].aa = 1 #case:swap_calc_ret { $temp_7 = 2 and $temp_8 = [$Y] and $temp_5 = 1 } $wline1[$Y].ab = 1 #case:swap_calc_ret { $temp_7 = 3 and $temp_8 = [$Y] and $temp_5 = 1 } $wline1[$Y].ac = 1 #case:swap_calc_ret { $temp_7 = 4 and $temp_8 = [$Y] and $temp_5 = 1 } $wline1[$Y].ad = 1 //... #case:swap_calc_ret { $temp_7 = 4 and $temp_8 = [$Y] and $temp_5 = 8 } $wline2[$Y].dd = 1 #end #site:swap_calc_ret #return #site:swap_calc $wline11 = 0 $wline12 = 0 $wline13 = 0 $wline14 = 0 $wline15 = 0 $wline21 = 0 $wline22 = 0 $wline23 = 0 $wline24 = 0 $wline25 = 0 $temp_3 = 1 // X $temp_4 = 1 // Y $temp_6 = 1 #case:swap_calc_1 #site:swap_calc_1 $temp_5 = 0 #for:Y:1..5 #case:swap_calc_3 { $temp_3 = 1 and $temp_4 = [$Y] and $A[$Y] = 0 } #case:swap_calc_3 { $temp_3 = 2 and $temp_4 = [$Y] and $B[$Y] = 0 } #case:swap_calc_3 { $temp_3 = 3 and $temp_4 = [$Y] and $C[$Y] = 0 } #case:swap_calc_3 { $temp_3 = 4 and $temp_4 = [$Y] and $D[$Y] = 0 } #case:swap_calc_2 { $temp_3 = 1 and $temp_4 = [$Y] and $A[$Y] > 0 } $temp_5 = $A[$Y] $temp_7 = $temp_3 $temp_8 = $temp_4 #case:swap_calc_2 { $temp_3 = 2 and $temp_4 = [$Y] and $B[$Y] > 0 } $temp_5 = $B[$Y] $temp_7 = $temp_3 $temp_8 = $temp_4 #case:swap_calc_2 { $temp_3 = 3 and $temp_4 = [$Y] and $C[$Y] > 0 } $temp_5 = $C[$Y] $temp_7 = $temp_3 $temp_8 = $temp_4 #case:swap_calc_2 { $temp_3 = 4 and $temp_4 = [$Y] and $D[$Y] > 0 } $temp_5 = $D[$Y] $temp_7 = $temp_3 $temp_8 = $temp_4 #end #site:swap_calc_2 #case:swap_calc_5 { $temp_7 <= 1 or $temp_8 >= 5 } $temp_7 = $temp_3 $temp_8 = $temp_4 #case:swap_calc_sub { $temp_7 > 1 and $temp_8 < 5 } #return:swap_calc_2 $temp_7 = $temp_7 - 1 $temp_8 = $temp_8 + 1 #site:swap_calc_5 #case:swap_calc_6 { $temp_7 >= 4 or $temp_8 >= 5 } $temp_7 = $temp_3 $temp_8 = $temp_4 #case:swap_calc_sub { $temp_7 < 4 and $temp_8 < 5 } #return:swap_calc_5 $temp_7 = $temp_7 + 1 $temp_8 = $temp_8 + 1 #site:swap_calc_6 #case:swap_calc_7 { $temp_7 <= 1 or $temp_8 <= 1 } $temp_7 = $temp_3 $temp_8 = $temp_4 #case:swap_calc_sub { $temp_7 > 1 and $temp_8 > 1 } #return:swap_calc_6 $temp_7 = $temp_7 - 1 $temp_8 = $temp_8 - 1 #site:swap_calc_7 #case:swap_calc_3 { $temp_7 >= 4 or $temp_8 <= 1 } #case:swap_calc_sub { $temp_7 < 4 and $temp_8 > 1 } #return:swap_calc_7 $temp_7 = $temp_7 + 1 $temp_8 = $temp_8 - 1 #site:swap_calc_3 #case:swap_cell { $temp_3 = 4 and $temp_4 = 5 } #case:swap_calc_1 { $temp_3 = 4 and $temp_4 < 5 } $temp_3 = 1 $temp_4 = $temp_4 + 1 $temp_6 = 1 #case:swap_calc_1 { $temp_3 < 4 } $temp_3 = $temp_3 + 1 $temp_6 = $temp_6 * 2
Много писанины, согласен, но зато теперь проверка достижимости какого либо поля выполняется тривиально. Осталось не забыть переключать очерёдность хода и подсвечивать выбранную фигуру.

#site:swap_cell { $temp_2 } #page:1 ^ +---------+ ^ ^ %5% | <link=A5>@A5</link> <link=B5>@B5</link> <link=C5>@C5</link> <link=D5>@D5</link> | ^ ^ %4% | <link=A4>@A4</link> <link=B4>@B4</link> <link=C4>@C4</link> <link=D4>@D4</link> | ^ ^ %3% | <link=A3>@A3</link> <link=B3>@B3</link> <link=C3>@C3</link> <link=D3>@D3</link> | ^ ^ %2% | <link=A2>@A2</link> <link=B2>@B2</link> <link=C2>@C2</link> <link=D2>@D2</link> | ^ ^ %*% | <link=A1>@A1</link> <link=B1>@B1</link> <link=C1>@C1</link> <link=D1>@D1</link> | ^ ^ +---------+ ^ ^ @LC ^ #page:2 ^ +---------+ ^ ^ %5% | <link=A5>@A5</link> <link=B5>@B5</link> <link=C5>@C5</link> <link=D5>@D5</link> | ^ ^ %4% | <link=A4>@A4</link> <link=B4>@B4</link> <link=C4>@C4</link> <link=D4>@D4</link> | ^ ^ %3% | <link=A3>@A3</link> <link=B3>@B3</link> <link=C3>@C3</link> <link=D3>@D3</link> | ^ ^ %*% | <link=A2>@A2</link> <link=B2>@B2</link> <link=C2>@C2</link> <link=D2>@D2</link> | ^ ^ %1% | <link=A1>@A1</link> <link=B1>@B1</link> <link=C1>@C1</link> <link=D1>@D1</link> | ^ ^ +---------+ ^ ^ @LC ^ #page:3 ^ +---------+ ^ ^ %5% | <link=A5>@A5</link> <link=B5>@B5</link> <link=C5>@C5</link> <link=D5>@D5</link> | ^ ^ %4% | <link=A4>@A4</link> <link=B4>@B4</link> <link=C4>@C4</link> <link=D4>@D4</link> | ^ ^ %*% | <link=A3>@A3</link> <link=B3>@B3</link> <link=C3>@C3</link> <link=D3>@D3</link> | ^ ^ %2% | <link=A2>@A2</link> <link=B2>@B2</link> <link=C2>@C2</link> <link=D2>@D2</link> | ^ ^ %1% | <link=A1>@A1</link> <link=B1>@B1</link> <link=C1>@C1</link> <link=D1>@D1</link> | ^ ^ +---------+ ^ ^ @LC ^ #page:4 ^ +---------+ ^ ^ %5% | <link=A5>@A5</link> <link=B5>@B5</link> <link=C5>@C5</link> <link=D5>@D5</link> | ^ ^ %*% | <link=A4>@A4</link> <link=B4>@B4</link> <link=C4>@C4</link> <link=D4>@D4</link> | ^ ^ %3% | <link=A3>@A3</link> <link=B3>@B3</link> <link=C3>@C3</link> <link=D3>@D3</link> | ^ ^ %2% | <link=A2>@A2</link> <link=B2>@B2</link> <link=C2>@C2</link> <link=D2>@D2</link> | ^ ^ %1% | <link=A1>@A1</link> <link=B1>@B1</link> <link=C1>@C1</link> <link=D1>@D1</link> | ^ ^ +---------+ ^ ^ @LC ^ #page:5 ^ +---------+ ^ ^ %*% | <link=A5>@A5</link> <link=B5>@B5</link> <link=C5>@C5</link> <link=D5>@D5</link> | ^ ^ %4% | <link=A4>@A4</link> <link=B4>@B4</link> <link=C4>@C4</link> <link=D4>@D4</link> | ^ ^ %3% | <link=A3>@A3</link> <link=B3>@B3</link> <link=C3>@C3</link> <link=D3>@D3</link> | ^ ^ %2% | <link=A2>@A2</link> <link=B2>@B2</link> <link=C2>@C2</link> <link=D2>@D2</link> | ^ ^ %1% | <link=A1>@A1</link> <link=B1>@B1</link> <link=C1>@C1</link> <link=D1>@D1</link> | ^ ^ +---------+ ^ ^ @LC ^
Здесь можно видеть ещё одну часто используемую идиому. Выбранный столбец отмечается звёздочкой текстовым представлением переменной LC. Поскольку подсветить строку таким образом нельзя, используются страницы, в каждой из которых звёздочкой отмечена одна из строк. Переключение страниц управлется конструкцией { $temp_2 } в определении локации. Завершение игры проверяется следующим образом:
#site:swap_check #case:game_win { $A1 in [1;3;5;7] and $B1 in [1;3;5;7] and $C1 in [1;3;5;7] and $C1 in [1;3;5;7] and $A5 in [2;4;6;8] and $B5 in [2;4;6;8] and $C5 in [2;4;6;8] and $D5 in [2;4;6;8] } #case:swap { 1 - ($A1 in [1;3;5;7] and $B1 in [1;3;5;7] and $C1 in [1;3;5;7] and $C1 in [1;3;5;7] and $A5 in [2;4;6;8] and $B5 in [2;4;6;8] and $C5 in [2;4;6;8] and $D5 in [2;4;6;8]) }
Здесь на выручку приходят операции со списками языка QM. Просто проверяем, что на целевых позициях находятся правильные фигуры.
Быки и коровы
Также известная как Mastermind. Ведущий загадывает цепочку цифр, а игрок пытается её угадать, предлагая различные варианты. Ответ ведущего содержит количество “быков” - цифр стоящих в правильном положении и “коров” - цифр не на своём месте. Сложность игры зависит от количества используемых цифр и от того можно ли повторять эти цифры в строке. Поиграть можно здесь.

Посмотрим код
Вновь начинаем с отображением доски. Игроку необходимо видеть несколько своих последовательных попыток с подсказками по "быкам" и "коровам". Здесь нам опять помогает многостраничное представление локации.
#macro:caim #page:1 ^ %????% | ^ ^ -------------- ^ ^ @temp_10@temp_11@temp_12@temp_13 | ^ #page:2 ^ %????% | ^ ^ -------------- ^ ^ $cline1 | @A1 @B1 @C1 @D1 ^ ^ @temp_10@temp_11@temp_12@temp_13 | ^ #page:3 ^ %????% | ^ ^ -------------- ^ ^ $cline1 | @A1 @B1 @C1 @D1 ^ ^ $cline2 | @A2 @B2 @C2 @D2 ^ ^ @temp_10@temp_11@temp_12@temp_13 | ^ #page:4 ^ %????% | ^ ^ -------------- ^ ^ $cline1 | @A1 @B1 @C1 @D1 ^ ^ $cline2 | @A2 @B2 @C2 @D2 ^ ^ $cline3 | @A3 @B3 @C3 @D3 ^ ^ @temp_10@temp_11@temp_12@temp_13 | ^ #page:5 ^ %????% | ^ ^ -------------- ^ ^ $cline1 | @A1 @B1 @C1 @D1 ^ ^ $cline2 | @A2 @B2 @C2 @D2 ^ ^ $cline3 | @A3 @B3 @C3 @D3 ^ ^ $cline4 | @A4 @B4 @C4 @D4 ^ ^ @temp_10@temp_11@temp_12@temp_13 | ^ #page:6 ^ %????% | ^ ^ -------------- ^ ^ $cline1 | @A1 @B1 @C1 @D1 ^ ^ $cline2 | @A2 @B2 @C2 @D2 ^ ^ $cline3 | @A3 @B3 @C3 @D3 ^ ^ $cline4 | @A4 @B4 @C4 @D4 ^ ^ $cline5 | @A5 @B5 @C5 @D5 ^ ^ @temp_10@temp_11@temp_12@temp_13 | ^ #end

Когда попыток станет больше 5, будем прокручивать список снизу-вверх. Цифры задумываем в переменных $temp_1,… $temp_4.
#site:cow_init $temp_9 = 0 $cline1 = 0 $cline2 = 0 $cline3 = 0 $cline4 = 0 $cline5 = 0 #for:i:1..5 $A[$i] = 0 $B[$i] = 0 $C[$i] = 0 $D[$i] = 0 #end #case:cow { $echars.spy = 0 } $temp_1 = <1..5> $temp_2 = <1..5> $temp_3 = <1..5> $temp_4 = <1..5> #site:cow { $temp_9 + 1 } #caim $temp_8 = 0 $temp_10 = 0 $temp_11 = 0 $temp_12 = 0 $temp_13 = 0 #for:i:1..5 #case:cow_1 '[$i]' #order:[$i - 1] $temp_8 = [$i] * 1000 $temp_10 = [$i] #end #case:game_lose 'Отказаться от выполнения' #order:9
Главная сложность с рассчётом количества быков и коров для подсказки игроку:
#site:cow_calc $temp_5 = ($temp_8 div 1000) $temp_6 = ($temp_8 div 100) mod 10 $temp_7 = ($temp_8 div 10) mod 10 #case:cow_calc_1 $temp_5 = ($temp_5 = $temp_1) + ($temp_6 = $temp_2) + ($temp_7 = $temp_3) + (($temp_8 mod 10) = $temp_4) $sline1 = ($temp_5 <> $temp_1) and ($temp_5 = $temp_2 or $temp_5 = $temp_3 or $temp_5 = $temp_4) $sline2 = ($temp_6 <> $temp_2) and ($temp_6 = $temp_1 or $temp_6 = $temp_3 or $temp_6 = $temp_4) $sline3 = ($temp_7 <> $temp_3) and ($temp_7 = $temp_1 or $temp_7 = $temp_2 or $temp_7 = $temp_4) $sline4 = (($temp_8 mod 10) <> $temp_4) and (($temp_8 mod 10) = $temp_1 or ($temp_8 mod 10) = $temp_2 or ($temp_8 mod 10) = $temp_3) #site:cow_calc_1 $temp_6 = $sline1 + $sline2 + $sline3 + $sline4 #for:i:1..5 #case:cow_end { $temp_9 = [$i - 1] and $temp_5 = 0 and $temp_6 = 0 } $A[$i] = 0 $B[$i] = 0 $C[$i] = 0 $D[$i] = 0 // ... #case:cow_end { $temp_9 = [$i - 1] and $temp_5 = 3 and $temp_6 = 1 } $A[$i] = 2 $B[$i] = 2 $C[$i] = 2 $D[$i] = 1 #case:cow_end { $temp_9 = [$i - 1] and $temp_5 = 4 and $temp_6 = 0 } $A[$i] = 2 $B[$i] = 2 $C[$i] = 2 $D[$i] = 2 #end
С этой игрой было забавное происшествие. В какой-то момент размер результрующего QMM-файла неожиданно вырос с 200 килобайт до 5 мегабайт.
Проблема заключалась в следующем
Поскольку хотелось сэкономить на количестве параметров, строки попыток в списке были представлены записями cline из 4 полей-десятичных цифр. Для большей красоты, цифры были разделены пробелами при отображении:
#macro:ct #text:0..0 ' ' #text:1..1 '1 ' #text:2..2 '2 ' #text:3..3 '3 ' #text:4..4 '4 ' #text:5..5 '5 ' #text:6..6 '6 ' #text:7..7 '7 ' #text:8..8 '8 ' #text:9..9 '9 ' #end #macro:cv #field:e #mod:10 // 10000 #text:0..9 '' #field:d #mod:10 // 1000 #ct #field:c #mod:10 // 100 #ct #field:b #mod:10 // 10 #ct #field:a #mod:10 // 1 #ct #end #var:temp_8 #range:0..99999 #default:0 #hide #cv #macro:cf #field:d // 1000 #ct #field:c // 100 #ct #field:b // 10 #ct #field:a // 1 #ct #end #for:i:1..5 #var:cline[$i] #range:0..9999 #default:0 #mod:10 #hide #cf #end #macro:caim #page:1 ^ $temp_1 $temp_2 $temp_3 $temp_4 | ^ ^ ----------------- ^ ^ @temp_8| ^ // ... #end
Всё прекрасно работало, но создавало по 9999 текстовых шаблонов для каждой из 5 переменных clineN и 99999 текстовых шаблонов для temp_8. Это отжирало больше 4 мегабайт в QMM-файле, а попытка просмотра описания параметра попросту вешала графический редактор. Разумеется, от разделяющих пробелов пришлось отказаться.
Сапёр
Всем известная офисная игра в которой требуется не подорваться на мине, используя подсказки - количество мин, расположенных на соседних, по отношению к открытой, клетках.

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

Кстати, похожая игра существует для MS DOS. Понятно, что задача становится сложнее чем в оригинальном сапёре. Кроме того, не исключена возможность того, что мины выстроятся в сплошную непреодолимую линию. Предоставим нашему “диверсанту” несколько зарядов для безопасного подрыва мин. Злоупотреблять этой возможностью не стоит, поскольку взрыв привлекает внимание охраны и запускает обратный отсчёт ходов до обнаружения.
Что у него внутри?
#var:charges #range:0..10 #default:0 #hide #order:2 #text:1..10 'Зарядов: $' #var:countdown #range:0..100 #default:0 #hide #order:3 #text:1..10 'До обнаружения: $' #var:col #range:-1..7 #default:-1 #hide #text:-1..-1 '^ A B C D E F G H ^' #text:0..0 '^ * B C D E F G H ^' #text:1..1 '^ A * C D E F G H ^' #text:2..2 '^ A B * D E F G H ^' #text:3..3 '^ A B C * E F G H ^' #text:4..4 '^ A B C D * F G H ^' #text:5..5 '^ A B C D E * G H ^' #text:6..6 '^ A B C D E F * H ^' #text:7..7 '^ A B C D E F G * ^' #var:row #range:-1..5 #default:-1 #hide #macro:mine_cell:A:B:R:X:Y #var:cl[$A][$B] #range:0..99 #default:99 #mod:10 #hide #field:H #text:0..0 ' ' #text:1..1 '1' #text:2..2 '2' #text:3..3 '3' #text:4..4 '4' #text:5..5 '5' #text:6..6 '6' #text:7..7 '7' #text:8..8 '8' #text:9..9 '<link=[$R];[$X];[7-$A]>.</link>' #field:L #text:0..0 ' ' #text:1..1 ' 1' #text:2..2 ' 2' #text:3..3 ' 3' #text:4..4 ' 4' #text:5..5 ' 5' #text:6..6 ' 6' #text:7..7 ' 7' #text:8..8 ' 8' #text:9..9 ' <link=[$R];[$Y];[7-$A]>.</link>' #end #for:y:0..6 #mine_cell:[$y]:0:A-D:A:B #mine_cell:[$y]:1:A-D:C:D #mine_cell:[$y]:2:E-H:E:F #mine_cell:[$y]:3:E-H:G:H #end #macro:aim #page:1 ^+-----------------+^ ^| @cl00 @cl01 @cl02 @cl03 |^ %7% ^| @cl10 @cl11 @cl12 @cl13 |^ %6% ^| @cl20 @cl21 @cl22 @cl23 |^ %5% ^| @cl30 @cl31 @cl32 @cl33 |^ %4% ^| @cl40 @cl41 @cl42 @cl43 |^ %3% ^| @cl50 @cl51 @cl52 @cl53 |^ %2% ^| @cl60 @cl61 @cl62 @cl63 |^ %1% ^+-----------------+^ @col #end
Можно заметить, что пара ячеек на “экране” представляется одной переменной. Такой подход помогает вдвое сократить количество параметров квеста (в QM количество параметров - лимитированный ресурс). Здесь легко впасть в другую крайность. Выше уже упоминалось об инциденте с 5-ти мегабайтным QMM-файлом. Всё дело в описаниях текстовых шаблонов. Их количество растёт как степенная функция от количества объединяемых переменных.

QMS позволяет лаконично описывать такие конструкции, но стоит соблюдать осторожность. Можно позволить себе параметры, объединяющие десятичные переменные в пары, но больше - скорее всего уже нет. Слишком большой расход памяти.
#site:mine_calc $temp_3 = (7 - $temp_1 + 1) * 10 + ($col + 1) $temp_4 = 0 $temp_5 = 0 #case:mine_calc_1 #site:mine_calc_1 $temp_5 = $temp_5 + 1 #case:mine_calc_ret { $temp_5 > 4 } #case:mine_calc_2 { $temp_5 = 1 } $temp_6 = $mn1 #case:mine_calc_2 { $temp_5 = 2 } $temp_6 = $mn2 #case:mine_calc_2 { $temp_5 = 3 } $temp_6 = $mn3 #case:mine_calc_2 { $temp_5 = 4 } $temp_6 = $mn4 #site:mine_calc_2 $temp_6 = $temp_6 div 100 $temp_7 = $temp_6 mod 100 #case:mine_calc_ret { $temp_7 = $temp_3 } $temp_4 = $temp_4 + 10 #case:mine_calc_1 { $temp_7 = 0 } #case:mine_calc_3 { $temp_7 = ($temp_3 - 1) } $temp_4 = $temp_4 + 1 #case:mine_calc_3 { $temp_7 <> ($temp_3 - 1) and $temp_7 <> 0 and $temp_7 <> $temp_3} #site:mine_calc_3 #case:mine_calc_4 { $temp_7 = ($temp_3 + 1) } $temp_4 = $temp_4 + 1 #case:mine_calc_4 { $temp_7 <> ($temp_3 + 1) } #site:mine_calc_4 #case:mine_calc_5 { $temp_7 = ($temp_3 - 10) } $temp_4 = $temp_4 + 1 #case:mine_calc_5 { $temp_7 <> ($temp_3 - 10) } #site:mine_calc_5 #case:mine_calc_6 { $temp_7 = ($temp_3 + 10) } $temp_4 = $temp_4 + 1 #case:mine_calc_6 { $temp_7 <> ($temp_3 + 10) } #site:mine_calc_6 #case:mine_calc_7 { $temp_7 = ($temp_3 - 9) } $temp_4 = $temp_4 + 1 #case:mine_calc_7 { $temp_7 <> ($temp_3 - 9) } #site:mine_calc_7 #case:mine_calc_8 { $temp_7 = ($temp_3 + 9) } $temp_4 = $temp_4 + 1 #case:mine_calc_8 { $temp_7 <> ($temp_3 + 9) } #site:mine_calc_8 #case:mine_calc_9 { $temp_7 = ($temp_3 - 11) } $temp_4 = $temp_4 + 1 #case:mine_calc_9 { $temp_7 <> ($temp_3 - 11) } #site:mine_calc_9 #case:mine_calc_2 { $temp_7 = ($temp_3 + 11) } $temp_4 = $temp_4 + 1 #case:mine_calc_2 { $temp_7 <> ($temp_3 + 11) } #site:mine_calc_ret #return #site:mine_show $temp_5 = $temp_3 mod 10 $temp_6 = $temp_3 div 10 #for:y:1..7 #case:mine_show_ret { $temp_6 = [$y] and $temp_5 = 1 } $cl[$y - 1]0.H = $temp_4 #case:mine_show_ret { $temp_6 = [$y] and $temp_5 = 2 } $cl[$y - 1]0.L = $temp_4 #case:mine_show_ret { $temp_6 = [$y] and $temp_5 = 3 } $cl[$y - 1]1.H = $temp_4 #case:mine_show_ret { $temp_6 = [$y] and $temp_5 = 4 } $cl[$y - 1]1.L = $temp_4 #case:mine_show_ret { $temp_6 = [$y] and $temp_5 = 5 } $cl[$y - 1]2.H = $temp_4 #case:mine_show_ret { $temp_6 = [$y] and $temp_5 = 6 } $cl[$y - 1]2.L = $temp_4 #case:mine_show_ret { $temp_6 = [$y] and $temp_5 = 7 } $cl[$y - 1]3.H = $temp_4 #case:mine_show_ret { $temp_6 = [$y] and $temp_5 = 8 } $cl[$y - 1]3.L = $temp_4 #end #site:mine_show_ret #return
Две подпрограммы: для подсчёта мин на соседних клетках и для отображения результатов на экране. Дальше всё просто: первую линию не заполняем и даём в ней информацию о соседних минах. Принимая ход от игрока, проверяем, что одна из четырёх соседних клеток открыта. Разумеется, проверяем подрыв на минах и достижение последней горизонтали:
#site:mine #image:siege_01 $col = -1 #aim #case:mine_ad 'A-D' #order:1 #case:mine_eh 'E-H' #order:2 #case:mine 'Подорвать' { $temp_9 = 0 and $charges > 0 } #order:3 $charges = $charges - 1 $temp_9 = 1 #case:mine_help 'Почитать правила' #order:8 #case:game_lose 'Отказаться от выполнения' #order:9 #site:mine_ad $col = -1 #aim #case:mine_col 'A' #order:1 $col = 0 #case:mine_col 'B' #order:2 $col = 1 #case:mine_col 'C' #order:3 $col = 2 #case:mine_col 'D' #order:4 $col = 3 #case:mine 'Другой столбец' #order:8 #case:game_lose 'Отказаться от выполнения' #order:9 #site:mine_eh $col = -1 #aim #case:mine_col 'E' #order:1 $col = 4 #case:mine_col 'F' #order:2 $col = 5 #case:mine_col 'G' #order:3 $col = 6 #case:mine_col 'H' #order:4 $col = 7 #case:mine 'Другой столбец' #order:8 #case:game_lose 'Отказаться от выполнения' #order:9 #site:mine_col #aim #case:mine_step '7' { $col = 0 and ($cl00.H = 9) and (($cl10.H < 9) or ($cl00.L < 9))} #order:7 $temp_1 = 7 $cl00.H = 0 #case:mine_step '6' { $col = 0 and ($cl10.H = 9) and (($cl00.H < 9) or ($cl20.H < 9) or ($cl10.L < 9))} #order:6 $temp_1 = 6 $cl10.H = 0 #case:mine_step '5' { $col = 0 and ($cl20.H = 9) and (($cl10.H < 9) or ($cl30.H < 9) or ($cl20.L < 9))} #order:5 $temp_1 = 5 $cl20.H = 0 // ... #case:mine_step '3' { $col = 7 and ($cl43.L = 9) and (($cl33.L < 9) or ($cl53.L < 9) or ($cl43.H < 9))} #order:3 $temp_1 = 3 $cl43.L = 0 #case:mine_step '2' { $col = 7 and ($cl53.L = 9) } #order:2 $temp_1 = 2 $cl53.L = 0 #case:mine 'Другая ячейка' #order:8 #case:game_lose 'Отказаться от выполнения' #order:9 #site:mine_step #case:mine_calc #return:mine_step_1 #site:mine_step_1 #case:mine_step_2 { $temp_9 = 1 and $temp_4 >= 9 } $temp_4 = $temp_4 - 10 $countdown = <5..8> #case:mine_step_2 { $temp_9 <> 1 or $temp_4 < 9 } #site:mine_step_2 $temp_9 = 0 #case:mine_lose { $temp_4 >= 9 } #sound:shoot #image:penetrator_03 $variant = 1 #case:mine_lose { $countdown = 1 } #image:borzukhan_08 $variant = 2 #case:game_win { $temp_4 < 9 and $temp_1 >= 7 and $countdown <> 1 } #case:mine_show { $temp_4 < 9 and $temp_1 < 7 and $countdown <> 1 } #return:mine $countdown = $countdown - 1 #site:mine_lose { $variant } #hide:charges:countdown #page:1 %!!! БА-БАХ !!!% Жизнь великого рейнджера закончилась... #page:2 %!!! Вас обнаружили !!!% #case:entry 'Сыграем ещё?'
Итог
В этой статье, мы разобрались, как новые возможности QMS помогают создавать логические игры в рамках квестого движка “Космических рейнджеров”. Разумеется, это лишь заготовка большой стратегической игры, находящейся в разработке. Головоломки будут использоваться в ней как средство ускорения научных исследований, а “Сапёр” и “Быки и коровы” будут использованы в шпионских операциях. Надеюсь, что игра получится. Пока же можно попробовать себя в минииграх.

