Игра “Космические рейнджеры” разработана в 2002 году компанией Elemental Games, под руководством Дмитрия Гусарова. Текстовые квесты, наряду с планетарными и аркадными боями, немало способствовали популярности игры, внося разнообразие в игровой процесс перелётов от планеты к планете, торговли и пошаговых боёв к космосе. Со временем, текстовые квесты выросли в самостоятельный жанр.

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

Более-менее сложный квест в нём выглядит примерно так
Более-менее сложный квест в нём выглядит примерно так

Мой вклад во всё это - разработка текстового формата разметки QMS, позволяющего нормально работать с Git, повторно использовать код и вольготно обращаться с Copy-Paste (для интересующихся, код парсера QMS здесь, есть ещё декомпилятор, но он к теме сегодняшнего разговора отношения не имеет).

Первоначально, QMS задумывался как язык простой разметки, для разработки относительно небольших текстовых квестов. Аппетит, как это обычно бывает, пришёл во время еды. Дело в том, что некоторый опыт разработки квестов уже имелся.

Представление о том, как облегчить себе жизнь, было, но нельзя же просто взять и начать штамповать миниигры в промышленных масштабах. Требовалась некая объединяющая идея. И она нашлась. Было решено делать квест - космическую стратегию.

Синопсис следующий

В одной звёздной системе сосуществуют две враждующие расы (например, гаальцы и пеленги не сильно любят друг друга, поскольку последние, на постоянной основе, занимаются техническим шпионажем). Представители каждой из рас могут быть расселены по 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 помогают создавать логические игры в рамках квестого движка “Космических рейнджеров”. Разумеется, это лишь заготовка большой стратегической игры, находящейся в разработке. Головоломки будут использоваться в ней как средство ускорения научных исследований, а “Сапёр” и “Быки и коровы” будут использованы в шпионских операциях. Надеюсь, что игра получится. Пока же можно попробовать себя в минииграх.