Введение
Программирование просто и вполне доступно здравому уму человека. Но программировать сложно.
Дональд Кнут
Морской бой — простая игра, в которую можно научить играть даже первоклассника. Тридцать лет назад реализацию этой игры на компьютере можно было продавать. Десять лет назад написание морского боя на любимом языке было хорошим упражнением для будущего программиста. Сегодня, когда с написанием такой игры справится и продвинутая нейронка, сложно кого-то удивить очередной ее реализацией. Но я все же попробую.
Я решил написать морской бой на sed — потоковом текстовом редакторе из набора стандартных юниксовых утилит. Обычно его применяют для того, чтобы заменить в потоке или файле одну регулярку на другую. Но дополнительные директивы, которые есть в sed, формируют Тьюринг-полный язык, на котором теоретически можно написать что угодно.
Так, энтузиасты писали на sed мастермайнд (на наши деньги — «Быки и коровы»), сокобан, сапер и даже шахматы. Я упоролся несколько сильнее и написал игру с неполной информацией, псевдослучайной генерацией расстановок и ходов и достаточно сильным противником. Причем реализованный алгоритм позволяет усилить его еще больше, изменив буквально пару строк. Насколько я могу видеть по гитхабу, у меня получился один из самых масштабных на сегодняшний день проектов (если не самый масштабный) среди всей этой адской эзотерики.
В процессе работы я никак не использовал нейронки — ни для генераций, ни для консультаций. С одной стороны, это было правильно, ведь проекты, которые были написаны раньше, люди писали без нейронок. Так что их использование было бы просто неспортивным. С другой стороны, нейронки могли бы помочь в плане критики тех или иных подходов, а потому некоторые мои архитектурные решения наверняка весьма дремучи. С третьей стороны, именно в личном почерке ведь и заключается искусство, а ради него все и затевалось.
Сперва я просто хотел показать, какие клевые вещи можно реализовать самыми минимальными средствами, но в итоге получился большой пошаговый гайд. Для тех, кому результат интереснее процесса, в конце статьи есть ссылка на гитхаб, чтобы сразу посмотреть, что получилось. Остальные — наливайте чайку и поехали :)
⚠️ Я предполагаю, что ты, читатель, на базовом уровне знаком с регулярными выражениями и представляешь, зачем они нужны. Остальную хтонь, специфичную именно для sed, я постараюсь объяснить по ходу дела максимально человеческим языком.
Итак, обозрим для начала наш скудный инструментарий. Что же есть в sed? В нем есть два текстовых буфера — основной (pattern space) и вспомогательный (hold space). Содержимое первого мы можем всячески менять, а во второй можем только что-нибудь дописать, переписать его полностью или обменять его содержимое с содержимым основного буфера. Еще можно расставлять метки и переходить к ним: проще говоря, у нас есть оператор goto. Также есть команды для запроса ввода. И, наконец, в нашем распоряжении вся мощь регулярных выражений.
Расклад, честно говоря, не очень воодушевляющий, но все же приступим.
Базовая арифметика: сложение и вычитание
Дашь взаймы сто долларов? Ну или, для ровного счета, сто двадцать восемь.
Клод Шеннон
Первое, что я решил соорудить — это генератор случайных чисел: не будет же компьютер постоянно ставить свои корабли на одних и тех же клетках. При реализации сапера автор изящно обошел этот шаг: он генерирует поля через вызовы утилит seq и shuf. Но с точки зрения исходной задумки такой подход отдает жульничеством. Я хотел обойтись без любых посторонних программ, пуризм есть пуризм!
Тут же возникла заминка. В sed нет никакой арифметики. Вообще. Максимум, что можно делать с числами — задавать количество вхождений паттерна в регулярке, но этого явно недостаточно даже для элементарного сложения и вычитания. Без арифметики же генератор случайных чисел писать трудно. Придется предварительно научиться считать. Но как же это делать, если мы умеем лишь манипулировать строками? Что ж, когда мы в школе считали в столбик, мы ведь тоже манипулировали строками. Значит, и текстовый редактор справится! Но обрабатывать комбинации десяти символов — это очень муторное занятие, поэтому я решил остановиться на двоичной арифметике: в конце концов, мыжпрограммисты, нам такое привычно. Для еще большего упрощения допустим, что наши строки из нулей и единиц будут одинаковой длины.
Для реализации простого генератора случайных чисел нам понадобится умение складывать, умножать и находить остаток от деления. Ну а если уж мы находим остаток от деления, почему бы не посчитать и частное? Кроме того, для реализации алгоритма деления нам потребуется и вычитание. Таким образом, нам предстоит научиться всем четырем базовым арифметическим операциям. Но обо всем по порядку.
Начнем со сложения. Прежде всего научимся прибавлять к числу единицу. Итак, у нас есть N-символьная строка из нулей и единиц. Чтобы прибавить к ней единицу, нам нужно:
Найти в числе финализирующую подстроку из единиц:
1*$Заменить в ней все единицы на нули
Ноль перед этой подстрокой, если он там есть, заменить на единицу.
Напишем программу инкрементирования, а заодно научимся работать с циклами и ветвлениями. Выглядит программа с комментариями так (сорян, подсветки синтаксиса для sed на Хабр не завезли):
# Ставим при помощи двоеточия метку loop, чтобы прыгать к ней через goto. :loop # Меняем единицу, стоящую перед хвостом из скольки угодно подчеркиваний, на подчеркивание. s/1\(_*\)$/_\1/ # Если перед хвостом подчеркиваний стоит единица, прыгаем к метке loop. /1_*$/ b loop # Заменяем ноль, стоящий перед хвостом подчеркиваний, на единицу. s/0\(_*\)$/1\1/ # Ставим метку reverseloop, чтобы прыгать к ней через goto. :reverseloop # Заменяем первое подчеркивание в хвосте нулем. s/_\(_*\)$/0\1/ # Если последний символ — подчеркивание, прыгаем к метке reverseloop. /_$/ b reverseloop
Алгоритм несложный, но нужно подробнее разобрать несколько нюансов.
Прежде всего, откуда вообще берутся данные, с которыми мы работаем? Они берутся из основного буфера (pattern space). Так, когда мы указываем в регулярке символ окончания строки ($), мы имеем в виду именно окончание этого буфера. Все изменения, которые мы проделываем, происходят в нем же.
Далее, разберем логику ветвлений и переходов (строки 6 и 16). Она основана на возможности адресации, встроенной в sed. Когда мы используем sed для замены текста в потоке, она позволяет нам редактировать не все строки, а только некоторые. В частности, можно настроить работу только с теми строками, которые соответствуют определенному регулярному выражению. Именно это мы и делаем, разве что работаем не со строками потока а с одной «строкой» основного буфера. То есть, если содержимое основного буфера соответствует определенному регулярному выражению (/1_*$/), мы переходим (b) к определенной метке (loop).
Может возникнуть вопрос: зачем нужно было заменять символы по одному в цикле? Дело в том, что мы не сможем средствами регулярных выражений заменить на подчеркивания ровно столько символов, сколько нужно. Считать мы пока не научились :)
В принципе, этот алгоритм можно написать и проще, вместо второго цикла вызвав команду s/_/0/g. Тем самым мы бы сразу заменили все подчеркивания в pattern space на нули. Но это испортит нам данные, если в других строчках основного буфера будут символы подчеркивания. Так или иначе, мы научились прибавлять единицу к двоичному числу средствами sed!

Программу можно сохранить в файл inc.sed и побаловаться:
$ echo 00000000 | sed -f inc.sed 00000001 $ echo 001101 | sed -f inc.sed 001110 $ echo 00111 | sed -f inc.sed 01000
Вообще инкремент — это едва ли не самая важная процедура, которой мы будем пользоваться еще много раз. Она позволит нам не только выполнять более продвинутые арифметические действия, но и считать нужные нам объекты: достаточно завести дополнительный счетчик и в цикле пройтись по строке, которую мы хотим обработать.
Но пока что нас интересует арифметика. Чтобы перейти к сложению произвольных чисел, вспомним, как вообще складываются числа в столбик. Мы двигаемся с конца, берем два последних разряда и складываем тамошние цифры. Если при сложении происходит переполнение, то при сложении следующего разряда мы добавим «один в уме» и сложим уже не две, а три цифры. Так доходим до конца, на каждом шаге работая с числом меньшей разрядности. Получается, уже во втором классе мы имели дело с рекурсией, хотя еще и слов-то таких не знали :)
На компьютере мы будем проделывать примерно то же самое, но с некоторыми особенностями. Во-первых, прибавлять очередной бит второго слагаемого мы будем непосредственно к первому слагаемому (точнее, к тому, что от него останется на очередном шаге), а не к его биту. Это даст нам возможность в качестве очередной цифры промежуточного результата тупо брать последний бит первого слагаемого. Кроме того, так мы сможем не задумываться об «одном в уме».
Пример
Сложим 1011 и 0010 Возьмем последний бит второго слагаемого (0) Прибавим его к 1011, получим 1011, сохраним последний бит результата (1) Осталось сложить 101 и 001 Возьмем последний бит второго слагаемого (1) Прибавим его к 101, получим 110, сохраним последний бит результата (0) Осталось сложить 11 и 00 ... Склеим все сохраненные биты: 1,1,0,1 В результате получаем 1101
Во-вторых, мы не будем увеличивать битность при переполнении: если сумма оказалась большей разрядности, чем слагаемые, то старшие разряды будут отброшены. Собственно, так и работает сложение типов ограниченной битности вроде uint32 в других языках. Теперь мы можем состряпать такую программу:
Сложение
# Сохраненные биты будем хранить через пробел от первого слагаемого. s/\([01]*\)+\([01]*\)$/\1 \n\2/ :add::loop # если во втором слагаемом последний бит — 0, то пропускаем сложение. /0$/ b add::continueloop # если там единица, то прибавим ее к первому слагаемому, это мы умеем! :add::_replaceloop s/\([01]*\)1\(_*\) \([01]*\)\n\([01]*\)$/\1_\2 \3\n\4/ /[01]*1_* [01]*\n[01]*$/ b add::_replaceloop s/\([01]*\)0\(_*\) \([01]*\)\n\([01]*\)$/\11\2 \3\n\4/ :add::_reversereplaceloop s/\([01]*\)_\([^\n ]*\) \([01]*\)\n\([01]*\)$/\10\2 \3\n\4/ /[01]*_[01 _]*\n[01]*$/ b add::_reversereplaceloop :add::continueloop # сохраним очередной бит, удалим последний обработанный бит s/\([01]*\)\([01]\) \([01]*\)\n\([01]*\).$/\1 \2\3\n\4/ # продолжаем, пока во втором слагаемом есть биты /[01]$/ b add::loop # чистим артефакты — лишний пробел в начале и лишний перенос в конце s/ \([^\n]*\)\n$/\1/
Сохраним скрипт в файл add.sed и проверим:
echo "10101011+01010011" | sed -f add.sed 11111110 echo "0111+1001" | sed -f add.sed 0000
Последний пример особенно поучителен. Мы сложили два числа и получили ноль из-за переполнения. Свойством переполнения можно воспользоваться, чтобы реализовать вычитание. Для этого нам понадобятся вот какие дополнительные соображения.
Во-первых, вычесть из числа x число y — это все равно, что прибавить к числу x число -y: . В свою очередь, число
-y — это такое число, которое при сложении с y дает ноль: .
Далее, если мы инвертируем все биты числа y, то получим число ~y, которое при сложении с y даст число из одних единиц. А если прибавить к такому числу единицу, то из-за переполнения как раз и получится 0. Значит, . Так мы получаем итоговую формулу вычитания:
А программа для вычитания чисел выглядит так:
Вычитание
# Инвертируем биты вычитаемого: меняем 1 на _, потом 0 на 1, потом _ на 0 sub::1_loop s/1\([0_]*\)$/_\1/ /1[0_]*$/ b sub::1_loop :sub::01loop s/0\([_1]*\)$/1\1/ /0[_1]*$/ b sub::01loop :sub::_0loop s/_\([10]*\)$/0\1/ /_[10]*$/ b sub::_0loop # Прибавим к инвертированному числу единицу :sub::_replaceloop s/\([01]*\)1\(_*\)$/\1_\2/ /[01]*1_*$/ b sub::_replaceloop s/0\(_*\)$/1\1/ :sub::_reversereplaceloop s/\([01]*\)_\([^\n ]*\)$/\10\2/ /[01]*_[01_]*$/ b sub::_reversereplaceloop # Меняем минус на плюс в выражении s/-\([01]*\)$/+\1/ # А дальше — просто складываем, это мы уже умеем! s/\([01]*\)+\([01]*\)$/\1 \n\2/ :add::loop /0$/ b add::continueloop :add::_replaceloop s/\([01]*\)1\(_*\) \([01]*\)\n\([01]*\)$/\1_\2 \3\n\4/ /[01]*1_* [01]*\n[01]*/ b add::_replaceloop s/\([01]*\)0\(_*\) \([01]*\)\n\([01]*\)$/\11\2 \3\n\4/ :add::_reversereplaceloop s/\([01]*\)_\([^\n ]*\) \([01]*\)\n\([01]*\)$/\10\2 \3\n\4/ /[01]*_[01 _]*\n[01]*$/ b add::_reversereplaceloop :add::continueloop s/\([01]*\)\([01]\) \([01]*\)\n\([01]*\).$/\1 \2\3\n\4/ /[01]$/ b add::loop s/ \([^\n]*\)\n$/\1/
Теперь мы умеем складывать и вычитать. Отлично! Можно было бы сразу двигаться к умножению, но в нем есть неприятная особенность, к которой мы подошли вплотную. Дело в том, что умножение в столбик потребует целой серии сложений, и писать этакую портянку каждый раз не хотелось бы. А ведь для ГСЧ нам потребуется последовательно умножить, сложить и взять остаток. Получится совершенно кошмарная простыня, на которую и смотреть-то страшно, а уж об отладке не может быть и речи. Было бы неплохо научиться переиспользовать код не только копипастой. Так что сейчас мы ненадолго отвлечемся от арифметики и реализуем хоть какой-то способ писать подпрограммы, а заодно изобретем некое подобие переменных.
Функции и переменные
Больше всего на свете я ненавижу спагетти.
Эдсгер Дейкстра
С подобием условного оператора мы уже познакомились, с goto — тоже. Мы можем расставлять по коду метки и прыгать к ним, если наш pattern space удовлетворяет заданной регулярке. И да, обратно прыгать тоже можно. Но вот переиспользование блоков кода становится затруднительным: оператор goto не позволяет прыгать к различным меткам в зависимости от условий. А значит, если мы обозначим меткой :func некий блок, то дважды подряд вызвать b func не получится: после исполнения кода внутри блока мы не вернемся куда следует:
$ cat func.sed b func b func :func { s/^.*$/Function called/ ; p } $ sed -nf func.sed Function called # Всего один раз :(
Нам потребуется придумать что-то более хитрое. На помощь придет hold space — дополнительный текстовый буфер, встроенный в sed. Мы будем использовать его (помимо прочего) для хранения стека вызовов. Это не единственный способ реализации подпрограмм, но я придумал именно такой :) Итак, переключимся в hold space и инициализируем стек вызовов:
# Переключимся в hold space. С точки зрения sed директива 'x' меняет местами содержимое hold space и pattern space. x # Допишем в hold space еще одну строку путем замены символа конца буфера s/$/\ncall_stack = ,/ # Переключимся обратно. x
Перед вызовом очередного goto, требующего возврата (то есть, перед вызовом функции), мы будем дописывать в стек вызовов метку, к которой надо будет перейти при выходе из функции. Кроме того, нам потребуется дополнительный обработчик возвратов, который будет убирать из стека эту метку и осуществлять собственно возврат. Выглядит это так:
Работа с функциями
# Инициализация стека x ; s/$/\ncall_stack = ,/ ; x # Первый вызов функции x ; s/\(call_stack = [^\n]*\)/\1,first_label/ ; x b func :first_label # Второй вызов функции x ; s/\(call_stack = [^\n]*\)/\1,second_label/ ; x b func :second_label p ; q # Распечатаем pattern space и выйдем из скрипта # Функция добавляет в pattern space строчку 'function called' # В итоге в буфере будет столько строчек, сколько раз мы ее вызвали :func { s/$/\nfunction called/ b return } # Обработчик возвратов. :return { # Переходим в hold space x # Код перехода к метке first_label. Удаляем из стека последнюю метку, переключаемся в pattern space и переходим на метку first_label /call_stack = [^\n]*,first_label\n/ { s/\(call_stack = [^\n]*\),[^\n,]*/\1/ ; x ; b first_label ; } # Код перехода к метке second_label. Фактически копипаста /call_stack = [^\n]*,second_label\n/ { s/\(call_stack = [^\n]*\),[^\n,]*/\1/ ; x ; b second_label ; } }
Понятен смысл, да? Мы держим в обработчике возвратов своего рода таблицу соответствий текстовых записей в call_stack и команд перехода к меткам. Перед вызовом функции мы добавляем в call_stack очередную запись, а потом, при помощи команды b return, переходим к этой таблице и прыгаем на соответствующую записи метку, которая стоит сразу после вызова функции.
⚠️ Да, строго говоря, это не стек вызовов, а стек возвратов. Но не буду же я вводить новый термин, если уже есть похожий привычный :)
К сожалению, передать в goto строку из pattern space напрямую нельзя, так что выглядит код обработчика возвратов при большом количестве меток весьма трешово: у меня в финальной версии игры получилось больше трехсот записей. С другой стороны, для очередного вызова любой, сколь угодно сложной, функции нам теперь достаточно добавить очередную метку возврата в обработчик и в стек вызовов. Код вызова получается несколько сложнее, чем мы привыкли в нормальных языках программирования, но теперь мы куда успешнее можем избегать копипасты и переиспользовать код (копипаста в таблице обработки возвратов не в счет ;)).
⚠️ В последующих листингах кода я не буду писать, какие нужно вносить изменения в обработчик возвратов. Так что если будете запускать код по частям, проще всего будет внести в таблицу обработчика вообще все метки, которые есть в скрипте. Это, кстати, тоже можно сделать sed-скриптом ;)
Посмотрим теперь повнимательнее на строку, которую мы сохранили в hold space и используем как стек вызовов. Когда мы с ней работаем, при добавлении или удалении очередного элемента мы фактически меняем некий кусочек памяти, снабженный именем. Перед нами не что иное, как глобальная переменная! Но с ней есть один подвох. Метки, которые мы записываем в эту переменную, хранятся не в pattern space, а лишь у нас в голове. Именно поэтому у нас получилось дописать в стек метку, просто переключившись на hold space и обратно. Но когда мы будем иметь дело с неизвестными на этапе написания кода данными, хранящимися в pattern space, такой простой трюк уже не сработает: при переключении на вспомогательный буфер мы не сможем обратиться к данным из основного.
Будем считать, что команда на чтение или запись переменной хранится в последней строке pattern space. Теперь мы можем завести две функции — сеттер и геттер. Работают они так:
Последнюю строчку-команду мы переносим в начало и ставим перед ней уникальный маркер, которого в hold space нет. Уникальность маркера — на совести программиста.
Дальше встроенной командой H мы дописываем содержимое основного буфера в hold space и переключаемся на него.
После этого творится магия регулярных выражений: поскольку мы дописали в hold space не только последнюю строку, но вообще весь основной буфер, нам надо как-то стереть все лишнее, не испортив при этом другие данные в hold space. Тут-то нам и поможет уникальная метка.
Наконец, через уникальную метку и таблицу возврата значение переменной попадает в основной буфер. API у сеттера и геттера получается таким:
variable_name # Для чтения. После исполнения функции строка заменяется значением переменной variable_name. variable_name = variable_value # Для записи. После исполнения функции строка удаляется из pattern space.
Сеттер и геттер
:set { # Перекинем содержимое последней строки вверх, поставив перед ним строчку ':set' s/\([^\n]*\)$/:set\n\1/ s/^\(.*\)\(:set\n[^\n]*\)$/\2\n\1/ s/\n$// # Допишем содержимое основного буфера во вспомогательный H # Удалим артефакты в начале основного буфера s/^:set\n[^\n]*\n*// # Переключимся на hold space x # Сотрем лишние (вторая и далее) строки после уникальной метки ':set' (и ее саму тоже) # Содержательную строчку перенесем в начало s/^\(.*\)\n:set\n\([^\n]*\).*$/\2\n\1/ # Отыщем строчку со старым значением переменной и удалим ее s/^\([^\n= ]*\) = \([^\n]*\)\n\(.*\)\1 = [^\n]*\(.*\)$/\3\1 = \2\4/ # Переключимся обратно в основной буфер x b return } :get { # Перекинем содержимое последней строки вверх, поставив перед ним строчку ':get' s/\([^\n]*\)$/:get\n\1/ s/^\(.*\)\(:get\n[^\n]*\)$/\2\n\1/ s/\n$// # Допишем содержимое основного буфера во вспомогательный H # Удалим артефакты в начале основного буфера s/^:get\n[^\n]*\n*// # Переключимся на hold space x # Сотрем лишние (вторая и далее) строки после уникальной метки ':get' (и ее саму тоже) # Содержательную строчку перенесем в начало s/^\(.*\)\n:get\n\([^\n]*\).*$/\2\n\1/ # Отыщем нужную переменную и запишем ее значение в первую строку. В начале буфера снова поставим метку ':get' s/^\([^\n]*\)\n\(.*\)\1 = \([^\n]*\)/:get\n\3\n\2\1 = \3/ # Допишем содержимое hold space в pattern space H # Удалим артефакты (две верхние строки) из hold space s/^:get\n[^\n]*\n\(.*\)$/\1/ # Переключимся обратно в основной буфер x # Пользуясь уникальной меткой, удалим лишнее, оставив только старое содержимое буфера и прочитанное значение переменной s/^\n*\(.*\)\n:get\n\([^\n]*\).*$/\1\n\2/ b return }
По схожему принципу напишем функцию copy, чтобы присваивать одной переменной значение другой. Это можно делать и через последовательный вызов get и set, но получается очень громоздко и вдобавок требует лишней работы с pattern space, а мы и так не шибко быстро работаем:
Копирование значения переменной
:copy { s/\([^\n]*\)$/:copy\n\1/ s/^\(.*\)\(:copy\n[^\n]*\)$/\2\n\1/ s/\n$// H s/^:copy\n[^\n]*\n*// x s/^\(.*\)\n:copy\n\([^\n]*\).*$/\2\n\1/ s/^\([^ ]*\) = \([^\n]*\)\n\(.*\)\2 = \([^\n]*\)/\1 = \4\n\3\2 = \4/ s/^\([^ ]*\) = \([^\n]*\)\n\(.*\)\1 = [^\n]*/\3\1 = \2/ x b return }
Вот теперь совсем славно. Теперь мы можем записывать артефакты, порождаемые функциями, в переменные, равно как и брать из переменных их аргументы, проводя в pattern space только сами вычисления. Перепишем в соответствии с этим подходом наши функции сложения и вычитания:
Сложение и вычитание: продвинутая версия
:add { # Чтение слагаемых s/$/\nadd::first_operand/ x ; s/\(call_stack = [^\n]*\)/\1,add::get1/ ; x b get :add::get1 s/$/\nadd::second_operand/ x ; s/\(call_stack = [^\n]*\)/\1,add::get2/ ; x b get :add::get2 # Подготовка к сложению s/\([01]*\)\n\([01]*\)$/\1 \n\2/ # Цикл сложения, тут ничего нового :add::loop /0$/ b add::continueloop :add::_replaceloop s/\([01]*\)1\(_*\) \([01]*\)\n\([01]*\)$/\1_\2 \3\n\4/ /[01]*1_* [01]*\n[01]*$/ b add::_replaceloop s/\([01]*\)0\(_*\) \([01]*\)\n\([01]*\)$/\11\2 \3\n\4/ :add::_reversereplaceloop s/\([01]*\)_\([^\n ]*\) \([01]*\)\n\([01]*\)$/\10\2 \3\n\4/ /[01]*_[01 _]*\n[01]*$/ b add::_reversereplaceloop :add::continueloop s/\([01]*\)\([01]\) \([01]*\)\n\([01]*\).$/\1 \2\3\n\4/ /[01]$/ b add::loop # Запись результата s/ \([^\n]*\)\n$/add::result = \1/ x ; s/\(call_stack = [^\n]*\)/\1,add::result/ ; x b set :add::result b return } :sub { # Чтение операндов (из тех же переменных) s/$/\nadd::second_operand/ x ; s/\(call_stack = [^\n]*\)/\1,sub::get1/ ; x b get :sub::get1 # Инвертируем биты :sub::1_loop s/1\([0_]*\)$/_\1/ /1[0_]*$/ b sub::1_loop :sub::01loop s/0\([_1]*\)$/1\1/ /0[_1]*$/ b sub::01loop :sub::_0loop s/_\([10]*\)$/0\1/ /_[10]*$/ b sub::_0loop # Инкремент :sub::_replaceloop s/\([01]*\)1\(_*\)$/\1_\2/ /[01]*1_*$/ b sub::_replaceloop s/0\(_*\)$/1\1/ :sub::_reversereplaceloop s/\([01]*\)_\([^\n ]*\)$/\10\2/ /[01]*_[01_]*$/ b sub::_reversereplaceloop # Перезаписываем переменную s/\([^\n]*\)$/add::second_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,sub::set1/ ; x b set :sub::set1 # Переходим к сложению. b add }
Обратите внимание: перед вызовом add мы не проделываем никаких манипуляций с call_stack, потому что это последняя операция, до которой мы никак метку возврата не модифицировали. Это сделано просто для краткости. Элегантнее, конечно, было бы использовать отдельные переменные для вычитания и отдельный внутренний вызов сложения со своей меткой возврата.

Здесь можно сказать, что я занимаюсь херней: всю работу можно проводить непосредственно в pattern space, записав в последней строчке все необходимое для вызова функции, а потом переключившись на нее. И безо всяких там first_operand, second_operand и прочих result. Пока что это правда. Однако такой подход позволяет нам легко переиспользовать одно и то же значение несколько раз в качестве аргумента. Кроме того, теперь мы можем задавать переменные не скопом, а по отдельности, по мере вычисления. Это будет удобнее (насколько вообще можно так выразиться) при реализации более сложных алгоритмов, когда нам нужно будет как-то изолировать вызовы разных функций. Поэтому я решил пользоваться именно таким подходом на протяжении всего процесса разработки.
Продвинутая арифметика: умножение и деление
Да что такого страшного в этом вашем делении?
Роберт Оппенгеймер
Мы изрядно продвинулись, начав с замены одной регулярки другой! Сложение и вычитание для текстового редактора, который ничего не знает о числах — не жук накакал. Но к написанию игры это, будем честными, приблизило нас не сильно. Даже до простого генератора случайных чисел пока далеко. Но не будем отчаиваться. Время идти дальше, к умножению и делению.
Алгоритм Карацубы и прочие продвинутые штуки мы программировать не будем, обойдемся «столбиком»: на числах той битности, которую мы будем использовать, выигрыш от продвинутых алгоритмов получается небольшим, а программировать на sed, как мы уже убедились — то еще удовольствие. Умножение в столбик для наших нужд подойдет как нельзя лучше.
Итак, умножение двоичных чисел в столбик устроено очень просто:
Инициализируем текущий результат нулем.
Если последний бит второго множителя равен нулю, goto 4.
Прибавим к текущему результату первый множитель.
Сдвинем первый множитель влево на 1 бит.
Удалим последний бит второго множителя.
Если во втором множителе не осталось битов (строка пустая), алгоритм завершен, вернем результат.
goto 2.
Любопытно, что аналог инкремента для умножения (битовый сдвиг на 1) делается на регулярках значительно проще: достаточно удалить первый символ числа и приписать в конце ноль. Таким образом, имеем одну команду вместо семи: s/.\([^\n]\)/\10/.
Единственным трудным этапом остается сложение, но его делать мы уже научились. Поэтому не будем мешкать и приступим:
Умножение
:mul { # Читаем множители зи переменных s/$/\nmul::first_operand/ x ; s/\(call_stack = [^\n]*\)/\1,mul::get1/ ; x b get :mul::get1 s/$/\nmul::second_operand/ x ; s/\(call_stack = [^\n]*\)/\1,mul::get2/ ; x b get :mul::get2 # Инициализируем результат нулем нужной битности s/\([01]*\)$/\1\n\1/ :mul::init_loop s/1\(0*\)$/0\1/ /10*$/ b mul::init_loop # Будем умножать в цикле бит за битом :mul::loop /0\n[01]*$/ b mul::shift # Подготовимся к сложению. Первое слагаемое — это текущий результат s/\([01]*\)$/\1\nadd::first_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,mul::prepare_intermediate_result_1/ ; x b set :mul::prepare_intermediate_result_1 # Второе слагаемое — это текущее значение первого множителя s/\([01]*\)\n\([01]*\)\n\([01]*\)$/\1\n\2\n\3\nadd::second_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,mul::prepare_intermediate_result_2/ ; x b set :mul::prepare_intermediate_result_2 # Выполним сложение x ; s/\(call_stack = [^\n]*\)/\1,mul::calculate_intermediate_result/ ; x b add :mul::calculate_intermediate_result # Обновим текущий результат s/[01]*$/add::result/ x ; s/\(call_stack = [^\n]*\)/\1,mul::get_intermediate_result/ ; x b get :mul::get_intermediate_result :mul::shift # Умножим первый множитель на 2 (через битовый сдвигом на 1) s/[01]\([01]*\)\n\([01]*\)\n\([01]*\)$/\10\n\2\n\3/ # Удалим последний бит второго множителя s/\([01]*\)[01]\n\([01]*\)$/\1\n\2/ /\n\n[01]*$/ b mul::end_loop b mul::loop :mul::end_loop # Запишем финальный результат в переменную mul::result s/\([^\n]*\)$/mul::result = \1/ x ; s/\(call_stack = [^\n]*\)/\1,mul::result/ ; x b set :mul::result # Чистим артефакты. Можно было и выше сделать, это чисто для ясности s/\n*[01]*\n$// b return }
Уфф. Непростое же это дело — умножать! Еще раз порадуемся, что остановились на двоичной системе счисления: без этого потребовалось бы в рамках этой функции реализовать полноценную таблицу умножения на десять… Может показаться также, что вся возня с переменными для проведения сложений того не стоила: по крайней мере, сократился код не сильно. Но насколько же он стал понятнее! Большая часть кода — это блоки манипулирования переменными и вызовов функций.
Для проверки работоспособности этой хтони пришлось написать отдельный скрипт (уже на питоне), который проверил корректность перемножения всех пар восьмибитных чисел.
Настало время реализовать целочисленное деление. Его алгоритм с точки зрения обработки строк несколько хитрее алгоритма умножения, но в двоичном виде ситуация, как обычно, упрощается. Итак, как же происходит деление в столбик в двоичном виде?
Прежде всего стоит проверить, не делим ли мы на ноль, но этот шаг можно и пропустить: в рамках исходной задачи мы будем контролировать операнды и никогда не столкнемся с такой ситуацией.
Инициализируем нулями результаты деления — неполное частное и остаток.
Возьмем старший бит делимого,
Умножим остаток на 2.
Младший бит остатка заменим на старший бит делимого.
Если остаток меньше делителя, goto 8
Вычтем делитель из остатка и положим текущий бит частного равным единице
Удалим старший бит делимого.
goto 3
На псевдокоде этот алгоритм можно глянуть в Википедии. Как видно, мы не зря учились вычитать, но нам еще нужно научиться сравнивать числа между собой. По счастью, это очень просто: достаточно последовательно идти по битам сравниваемых чисел и остановиться на первом расхождении:
Сравнение чисел
# Если строки одинаковы, то и числа одинаковы /\([01]*\)\n\1$/ b equal # Поставим пробелы перед обоими числами s/\([01]*\)\n\([01]*\)$/ \1\n \2/ :compare_loop # Выделим первый бит после пробела, поставим его перед пробелом s/[01]* \([01]\)\([01]*\)\n[01]* \([01]\)\([01]*\)$/\1 \2\n\3 \4/ /0 [01]*\n1 [01]*$/ b less /1 [01]*\n0 [01]*$/ b greater b compare_loop
Еще одна сложность — понять, как задавать текущий бит частного, ведь обращаться к энному символу мы не умеем. На помощь придет уже известный по алгоритму сложения трюк с пробелом: мы просто будем разделять им обработанные и необработанные части, шаг за шагом продвигаясь вместе с текущим битом делимого.
Остальное мы делать уже умеем. Теперь у нас есть все необходимое, чтобы реализовать деление:
Деление
:div { # Читаем делимое и делитель s/$/\ndiv::first_operand/ x ; s/\(call_stack = [^\n]*\)/\1,div::get1/ ; x b get :div::get1 s/$/\ndiv::second_operand/ x ; s/\(call_stack = [^\n]*\)/\1,div::get2/ ; x b get :div::get2 # Определим также старший бит делимого: поставим пробел после ведущих нулей s/\(0*\)\([01]*\)\n\([01]*\)$/\1 \2\n\3\n\1 \2\n\1\2/ # Инициализируем частное и остаток нулями нужной битности. :div::init_loop # Заменим очередной символ "1" на "0" в частном s/\([0 ]*\)1\([01]*\)\n\([01]*\)$/\10\2\n\3/ # ... а потом — в остатке. s/\(0*\)1\([01]*\)$/\10\2/ # Продолжаем, пока остаются символы "1" /0*1[01]*\n[01]*$/ b div::init_loop /0*1[01]*$/ b div::init_loop # Основной цикл деления :div::loop # Умножим остаток на 2, присвоим его младшему биту значение старшего бита делимого s/\([01]*\) \([01]\)\([01]*\)\n\([01]*\)\n\([01 ]*\)\n[01]\([01]*\)$/\1 \2\3\n\4\n\5\n\6\2/ # Процедура сравнения остатка и делителя. # Скопируем оба числа s/\([01]*\)\n\([01 ]*\)\n\([01]*\)$/\1\n\2\n\3\n \1\n \3/ # При равенстве чистим артефакты и переходим к вычитанию /\([01 ]*\)\n\1$/ { s/\n[01 ]*\n[01 ]*$// ; b div::sub ; } # То же самое, если остаток больше делителя. # В противном случае просто очистим артефакты и пропустим шаг вычитания :div::compare_loop s/[01]* \([01]\)\([01]*\)\n[01]* \([01]\)\([01]*\)$/\1 \2\n\3 \4/ /0 [01]*\n1 [01]*$/ { s/\n[01 ]*\n[01 ]*$// ; b div::sub ; } /1 [01]*\n0 [01]*$/ { s/\n[01 ]*\n[01 ]*$// ; b div::subend ; } b div::compare_loop # Цикл вычитания :div::sub # Задаем операнды # Уменьшаемое := остаток s/\([^\n]*\)$/\1\nadd::first_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,div::prepare_sub_1/ ; x b set :div::prepare_sub_1 # Вычитаемое := делитель s/\([01]*\)\n\([01 ]*\)\n\([01]*\)$/\1\n\2\n\3\nadd::second_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,div::prepare_sub_2/ ; x b set :div::prepare_sub_2 # Выполняем вычитание x ; s/\(call_stack = [^\n]*\)/\1,div::execute_sub/ ; x b sub :div::execute_sub # Обновляем значение остатка s/[01]*$/add::result/ x ; s/\(call_stack = [^\n]*\)/\1,div::set_remainder/ ; x b get :div::set_remainder # Выставляем в единицу соответствующий бит частного s/ 0\([01]*\)\n\([01]*\)$/ 1\1\n\2/ :div::subend # Сохраняем очередной бит делимого и частного, продолжаем, пока остаются биты s/\([01]*\) \([01]\)\([01]*\)\n\([01]*\)\n\([01]*\) \([01]\)\([01]*\)\n\([01]*\)$/\1\2 \3\n\4\n\5\6 \7\n\8/ /[01]\n[01]*$/ b div::loop # Удаляем из частного пробел s/ \n\([01]*\)$/\n\1/ # Чистим артефакты s/[01 ]*\n[01]*\n\([01]*\)\n\([01]*\)$/\1\n\2/ # Запишем результат (остаток и частное) в переменные s/\([01]*\)$/div::mod_result = \1/ x ; s/\(call_stack = [^\n]*\)/\1,div::result_1/ ; x b set :div::result_1 s/\([^\n]*\)$/div::div_result = \1/ x ; s/\(call_stack = [^\n]*\)/\1,div::result_2/ ; x b set :div::result_2 b return }
Отлично! Теперь мы умеем складывать, вычитать, умножать и делить. Фактически, мы запрограммировали настоящий калькулятор: приделать к нему интерфейс и обработать деление на ноль — и можно пользоваться. Так я и сделал, к слову: когда мои первые эксперименты по воссозданию арифметики увенчались успехом, я опубликовал это как отдельный проект. Очень неплохое достижение для текстового редактора, который не умеет считать! Теперь можно программировать и более сложные алгоритмы. Вспомним, с чего мы начинали и соорудим, наконец…
Генератор случайных чисел
Игральные кости в тюрьме приходилось делать из хлеба.
Владимир Ленин
Реализовывать мы будем один из самых простых алгоритмов — линейный конгруэнтный метод. Метод Фибоначчи с запаздыванием, в принципе, тоже можно написать с тем, что мы умеем сейчас. Но у него уж больно громоздкая процедура сидирования, поэтому я просто поленился :)
К слову о сидировании. Вы наверняка уже задаетесь вопросом, где я собрался брать начальное значение ГСЧ, если в самом по себе sed никаких методов для получения хоть какого-то случайного числа не предусмотрено. Когда я писал рогалик на jq, я сгенерировал сид при помощи встроенной функции получения текущего времени, но в суровом мире sed так сделать не выйдет. Можно, конечно, передавать какое-нибудь случайное число из оболочки при запуске, но это будет отход от чистого sed, как и вызов внешней программы. Поэтому такой вариант нам не подходит, придется что-то (в который раз) изобретать. Но о получении сида я расскажу ближе к финалу: для процесса разработки и отладки он не требуется. Пока что реализуем сам алгоритм, предполагая, что честный сид мы откуда-то получить все же смогли. А до той поры сидируем его 32-битным нулем.

Линейный конгруэнтный генератор устроен достаточно просто. У нас есть три константы — . Кроме того, есть текущее состояние генератора
. Каждое последующее число получается по формуле
. Вся арифметика нам уже знакома, осталось лишь задать константы и текущее состояние генератора в качестве переменных.
Инициализацию мы проведем в начале скрипта, чтобы не запариваться с лишними вызовами сеттера. Что касается функции генерации случайного числа, она получается довольно длинная, но весьма простая для понимания, поскольку состоит из вызовов copy и выполнения арифметических операций. Кстати, если бы мы не реализовали в свое время copy, она была бы еще раза в два длиннее :)
Генератор случайных чисел
# Инициализируем стек вызовов s/^.*$/call_stack = / # Инициализируем константы ГСЧ s/$/\nrng::current_state = 00000000000000000000000000000000/ s/$/\nrng::A = 00000000000000000000010101010110/ # A = 1366 s/$/\nrng::C = 00000000000000100100110101101001/ # C = 150889 s/$/\nrng::M = 00000000000010101110010100101001/ # M = 714025 # Запишем все в hold space, очистим pattern space h ; s/^.*$// :rng { # Зададим операнды умножения s/$/\nmul::first_operand = rng::A/ x ; s/\(call_stack = [^\n]*\)/\1,rng::mul_set_1/ ; x b copy :rng::mul_set_1 s/$/\nmul::second_operand = rng::current_state/ x ; s/\(call_stack = [^\n]*\)/\1,rng::mul_set_2/ ; x b copy :rng::mul_set_2 # Выполним умножение x ; s/\(call_stack = [^\n]*\)/\1,rng::execute_mul/ ; x b mul :rng::execute_mul # Зададим операнды сложения s/$/\nadd::first_operand = mul::result/ x ; s/\(call_stack = [^\n]*\)/\1,rng::add_set_1/ ; x b copy :rng::add_set_1 s/$/\nadd::second_operand = rng::C/ x ; s/\(call_stack = [^\n]*\)/\1,rng::add_set_2/ ; x b copy :rng::add_set_2 # Выполним сложение x ; s/\(call_stack = [^\n]*\)/\1,rng::execute_add/ ; x b add :rng::execute_add # Зададим операнды деления s/$/\ndiv::first_operand = add::result/ x ; s/\(call_stack = [^\n]*\)/\1,rng::div_set_1/ ; x b copy :rng::div_set_1 s/$/\ndiv::second_operand = rng::M/ x ; s/\(call_stack = [^\n]*\)/\1,rng::div_set_2/ ; x b copy :rng::div_set_2 # Выполним деление и получим в mod_result новое число x ; s/\(call_stack = [^\n]*\)/\1,rng::execute_div/ ; x b div :rng::execute_div # Скопируем новое число в текущее состояние генератора s/$/\nrng::current_state = div::mod_result/ x ; s/\(call_stack = [^\n]*\)/\1,rng::result/ ; x b copy :rng::result b return }
По сути, больше сказать про ГСЧ нечего. Арифметику было реализовывать значительно сложнее :)
Игровые поля
Возьмем не более чем счетное множество точек пространства…
Макс Планк
Мы уже проделали непростой и увлекательный путь, но пока вся работа была, все-таки, подготовительной. Теперь, наконец, можно приступить к программированию самой игры.
Начнем с игровых полей. Каждое игровое поле представляет собой двумерный массив 10х10 клеток. Вполне естественно было бы именно так его и хранить, но выбранная архитектура работы с hold space не позволяет нам так просто вписывать туда значения переменных с переносами. Поэтому хранить мы там будем стасимвольные сосиски.

Основная задача на этом этапе — научиться сохранять и отрисовывать поля. Их у нас будет 3 штуки: одно для кораблей игрока, второе — для его предположений, а третье — для кораблей компьютерного оппонента. Оппонент обойдется и без поля предположений: расчеты, куда стрелять, он будет проводить в pattern space, а результаты его ходов и так будут отображаться на поле игрока.
Каждая клетка может находиться в следующих состояниях:
Непроверенная клетка;
Пустая клетка;
Клетка с целым кораблем;
Клетка с подбитым кораблем;
Клетка с затопленным кораблем.
Состояния 4 и 5 отличаются друг от друга важной деталью: когда клетка в состоянии 4, мы можем гарантировать лишь то, что соседние по диагонали клетки — пустые. Если же наша клетка в состоянии 5, то можно делать выводы и о соседях по горизонтали и вертикали.
Я остановился на символах ·, ~, H, D и X для отражения этих состояний. Сгенерируем какие-нибудь стасимвольные «поля» для пробы:
s/$/\nfield::player = ~·X·HHHX··D~X·HD~~D~DD~D·XDH~·~~~XX~~HD·HDDXXHXHHH·H·XDX·D~H~DH~HD~·HH·D~DDD·H·HX~D·~DH·H·H··DX~XDHD/ s/$/\nfield::suggestions = D·H·~~XDXH~HDHDH~~·~··H~XD·DXH··D~~··D·D·D·DXHX··X··H~·X~··D··~D·X·X··X~DX~·~~D~XDDHX·D~X~~X·XHH·DDD/ s/$/\nfield::computer = ~XXD~XD~XDH·~·XXDXH·HXHXD~D~HXDD·DXD~XX~X~~HXD~D~·D·~XD·HH··DD~X··XXDD·~H·DX·~H··~DDDXXD··HH~DDHHHXH/
Теперь попробуем их нарисовать. Точнее, нарисовать надо лишь два из них: поле компьютерного игрока отображать не требуется:
Отрисовка поля
:draw { # Очищаем pattern space s/^.*$// # Получаем поля s/$/\nfield::player/ x ; s/\(call_stack = [^\n]*\)/\1,draw::get_field1/ ; x b get :draw::get_field1 s/$/\nfield::suggestions/ x ; s/\(call_stack = [^\n]*\)/\1,draw::get_field2/ ; x b get :draw::get_field2 # Расставляем пробелы после каждого символа: так в терминале выглядит красивее :) s/\([·~HDX]\)/\1 /g # Не слишком изящно, но весьма наглядно: берем первые 20 символов поля из каждой строки (10 значащих и 10 пробелов), # удаляем их оттуда и добавляем в конец, снабжая буквенной координатой. Так делаем 10 раз. s/$/\n | 0 1 2 3 4 5 6 7 8 9\t | 0 1 2 3 4 5 6 7 8 9/ s/$/\n---+--------------------\t---+--------------------/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ A | \1\t A | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ B | \1\t B | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ C | \1\t C | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ D | \1\t D | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ E | \1\t E | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ F | \1\t F | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ G | \1\t G | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ H | \1\t H | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ I | \1\t I | \3/ s/^\([·~HDX ]\{20\}\)\n\([·~HDX ]\{20\}\)\n\(.*\)$/\3\ J | \1\t J | \2/ # Выводим поля p # Еще раз очищаем pattern space s/^.*$// b return }

Сохранить новое состояние поля после выстрела можно будет просто при помощи сеттера. Но вот получать это самое новое состояние поля мы пока что не умеем. Самое время этому научиться.
Ходы игрока
Они и выстрела в ответ сделать не успеют!
Горацио Нельсон
Сделаем для целей отладки фиксированную расстановку кораблей противника и инициализируем поле саджестов. Заодно сделаем такую же расстановку и для кораблей игрока: чтобы не надо было запоминать, куда мы там что расставили:
s/$/\nfield::player = HHHH~~~~~~~~~~~HHH~~H~H~~~~~~HH~H~HH~~~~H~~~~~~~~H~~~~~~~~~~HH~H~H~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/ s/$/\nfield::suggestions = ····································································································/ s/$/\nfield::computer= HHHH~~~~~~~~~~~HHH~~H~H~~~~~~HH~H~HH~~~~H~~~~~~~~H~~~~~~~~~~HH~H~H~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~/
Итак, что собой представляет ход игрока? Всю процедуру можно разложить на следующие шаги:
Конвертируем ввод игрока в клетку поля саджестов.
Если клетка уже проверена, возвращаем ошибку и завершаем работу функции хода. Уровнем выше мы обработаем такую ситуацию и предложим игроку сделать ход еще раз.
Если клетка не проверена, то сравниваем ее с соответствующей клеткой поля оппонента. При промахе просто выставляем в поле саджестов нужный символ и завершаем работу.
При попадании заменяем в поле саджестов и в поле оппонента клетку на символ подбитого корабля.
Анализируем поле оппонента — остались ли у соответствующего корабля здоровые клетки. Если нет, то заменяем все подбитые клетки корабля на символы затопленного корабля и окружаем его ореолом в поле саджестов. Если да, то расставляем пустоты в соседние по диагонали клетки.
Звучит алгоритм достаточно просто, но теперь нам необходимо научиться конвертировать ввод игрока в соответствующую клетку, а еще разработать алгоритм для поиска соседних клеток и определения, ранен корабль или убит. Это резко осложняет задачу, но к этому моменту мы уже не новички в sed, так что справимся!
Начнем с конвертации. Естественное желание — конвертировать ввод в номер клетки. Здесь придется захардкодить конвертацию букв и цифр: цифры у нас, к сожалению, десятичные (что уж говорить о буквах), а мы умеем работать лишь в двоичной системе. Сама функция преобразования, однако, достаточно проста: мы конвертируем букву и цифру в двоичные числа от 0000000 до 0001001 (лишние биты нам нужны, чтобы правильно умножать без переполнения). После этого номер клетки определяется по формуле , а арифметика для этого у нас уже реализована:
Конвертация ввода
:convert_coordinates { # Прочитаем переменную с координатой (она будет задана из инпута) s/$/\nconvert_coordinates::coordinates/ x ; s/\(call_stack = [^\n]*\)/\1,convert_coordinates::get_coordinates/ ; x b get :convert_coordinates::get_coordinates s/\([A-Ja-j]\) *\([0-9]\)$/\1\n\2/ # Конвертируем цифру s/0$/0000000/ ; s/1$/0000001/ ; s/2$/0000010/ ; s/3$/0000011/ ; s/4$/0000100/ ; s/5$/0000101/ ; s/6$/0000110/ ; s/7$/0000111/ ; s/8$/0001000/ ; s/9$/0001001/ s/\([^\n]*\)$/convert_coordinates::digit = \1/ x ; s/\(call_stack = [^\n]*\)/\1,convert_coordinates::set_digit/ ; x b set :convert_coordinates::set_digit # Конвертируем букву s/[Aa]$/0000000/ ; s/[Bb]$/0000001/ ; s/[Cc]$/0000010/ ; s/[Dd]$/0000011/ ; s/[Ee]$/0000100/ ; s/[Ff]$/0000101/ ; s/[Gg]$/0000110/ ; s/[Hh]$/0000111/ ; s/[Ii]$/0001000/ ; s/[Jj]$/0001001/ s/\([^\n]*\)$/mul::first_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,convert_coordinates::set_letter/ ; x b set :convert_coordinates::set_letter # Подготовим умножение «буквы» на 10. s/$/\n0001010/ s/\([^\n]*\)$/mul::second_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,convert_coordinates::set_ten/ ; x b set :convert_coordinates::set_ten # Выполним умножение x ; s/\(call_stack = [^\n]*\)/\1,convert_coordinates::execute_mul/ ; x b mul :convert_coordinates::execute_mul # Подготовим сложение (letter*10 + digit) s/$/\nadd::first_operand = convert_coordinates::digit/ x ; s/\(call_stack = [^\n]*\)/\1,convert_coordinates::set_add_1/ ; x b copy :convert_coordinates::set_add_1 s/$/\nadd::second_operand = mul::result/ x ; s/\(call_stack = [^\n]*\)/\1,convert_coordinates::set_add_2/ ; x b copy :convert_coordinates::set_add_2 # Выполним сложение x ; s/\(call_stack = [^\n]*\)/\1,convert_coordinates::execute_add/ ; x b add :convert_coordinates::execute_add # Запишем результат s/$/\nconvert_coordinates::result = add::result/ x ; s/\(call_stack = [^\n]*\)/\1,convert_coordinates::result/ ; x b copy :convert_coordinates::result b return }
Дальше интереснее. Само по себе полученное число, к сожалению, подставить в квантификатор регулярного выражения нельзя. Поэтому считать символы в строке нам придется вручную. Впрочем, похожие действия мы уже проделывали при реализации арифметики, так что ничего сложного в этом нет:
Получение N-го символа строки
:get_symbol { # Получаем строку s/$/\nget_symbol::string/ x ; s/\(call_stack = [^\n]*\)/\1,get_symbol::get_string/ ; x b get :get_symbol::get_string # Получаем число в двоичном виде s/$/\nget_symbol::number/ x ; s/\(call_stack = [^\n]*\)/\1,get_symbol::get_number/ ; x b get :get_symbol::get_number # Инициализируем счетчик нулем нужной битности s/\([01]*\)$/\1\n\1/ :get_symbol::init_loop s/1\(0*\)$/0\1/ /10*$/ b get_symbol::init_loop # Считаем символы, пока counter < number :get_symbol::count_loop /\([01]*\)\n\1$/ b get_symbol::end_loop # Удаляем первый символ строки s/[^\n]\([^\n]*\)\n\([01]*\)\n\([01]*\)$/\1\n\2\n\3/ # Инлайновый инкремент счетчика: так короче, чем использовать add /\n0*$/ {s/\(0*\)0$/\11/ ; b get_symbol::count_loop; } :get_symbol::inc_loop ; s/1\(_*\)$/_\1/ ; /1_*$/ b get_symbol::inc_loop s/0\(_*\)$/1\1/ :get_symbol::inc_reverseloop ; s/_\(_*\)$/0\1/ ; /_$/ b get_symbol::inc_reverseloop; b get_symbol::count_loop :get_symbol::end_loop # Чистим артефакты, записываем результат s/\([^\n]\)[^\n]*\n[01]*\n[01]*$/get_symbol::result = \1/ x ; s/\(call_stack = [^\n]*\)/\1,get_symbol::result/ ; x b set :get_symbol::result b return }
По такому же принципу работает и функция set_symbol: она нам понадобится, чтобы заменить нужный символ в поле противника и поле саджестов, когда мы делаем выстрел:
Замена N-го символа в строке
:set_symbol { # Получим строку, поставим перед ней пробел s/$/\nset_symbol::string/ x ; s/\(call_stack = [^\n]*\)/\1,set_symbol::get_string/ ; x b get :set_symbol::get_string s/\([^\n]*\)$/ \1/ # Получим символ s/$/\nset_symbol::symbol/ x ; s/\(call_stack = [^\n]*\)/\1,set_symbol::get_symbol/ ; x b get :set_symbol::get_symbol # Получчим число в двоичном виде s/$/\nset_symbol::number/ x ; s/\(call_stack = [^\n]*\)/\1,set_symbol::get_number/ ; x b get :set_symbol::get_number # Инициализируем счетчик нулем нужной битности s/\([01]*\)$/\1\n\1/ :set_symbol::init_loop s/1\(0*\)$/0\1/ /10*$/ b set_symbol::init_loop # Считаем символы, пока counter < number :set_symbol::count_loop /\([01]*\)\n\1$/ b set_symbol::end_loop # Перемещаем очередной символ за пробел s/\([^\n ]*\) \([^\n]\)\([^\n]*\)\n\([^\n]\)\n\([01]*\)\n\([01]*\)$/\1\2 \3\n\4\n\5\n\6/ # Инлайновый инкремент /\n0*$/ {s/\(0*\)0$/\11/ ; b set_symbol::count_loop; } :set_symbol::inc_loop ; s/1\(_*\)$/_\1/ ; /1_*$/ b set_symbol::inc_loop s/0\(_*\)$/1\1/ :set_symbol::inc_reverseloop ; s/_\(_*\)$/0\1/ ; /_$/ b set_symbol::inc_reverseloop; b set_symbol::count_loop :set_symbol::end_loop # Заменим символ и почистим артефакты s/\([^\n ]*\) [^\n]\([^\n]*\)\n\([^\n]\)\n[01]*\n[01]*$/\1\3\2/ # Запишем в результат новую строку s/\([^\n]*\)$/set_symbol::result = \1/ x ; s/\(call_stack = [^\n]*\)/\1,set_symbol::result/ ; x b set :set_symbol::result b return }
Теперь самое сложное. Если мы выбрали клетку, в которой есть корабль оппонента, нам нужно понимать, подбит корабль или затоплен. Но чтобы проверить, затоплен ли корабль, надо перебрать все его клетки. Где их взять? Теоретически можно было бы пройтись по полю противника в четырех направлениях от подбитой клетки и непосредственно вычислить все клетки корабля. Но это очень плохая идея с точки зрения производительности, поэтому мы сделаем иначе. Мы запомним номера клеток всех кораблей на этапе расстановки в отдельной переменной, а потом, зная номер одной из них, получим все нужные номера просто парой замен. Выглядеть это для текущей расстановки будет так:
field::computer_ships = 0000000,0000001,0000010,0000011 0001111,0010000,0010001 0010100,0011110,0101000 0010110,0100000 0100010,0100011 0111100,0111101 0011101 0110001 0111111 1000001
Как видно, координаты кораблей отделены пробелами, а координаты клеток одного корабля — запятыми. Разумеется, когда мы будем генерировать расстановку для компьютера, нам придется предпринять дополнительные действия, чтобы транслировать ее в такой вид. Но это сделать легче, чем анализировать уже имеющуюся расстановку, да и выполнить эту операцию придется всего один раз. Теперь, зная, что координаты кораблей всегда хранятся в переменной в определенном формате, мы можем написать функцию затопления вражеского корабля, если анализ покажет, что мы попали по нему:
Затопление корабля оппонента
:sink_computer_ship { # Получим подбитую клетку s/$/\nconvert_coordinates::result/ x ; s/\(call_stack = [^\n]*\)/\1,sink_computer_ship::get_ship_cell/ ; x b get :sink_computer_ship::get_ship_cell # Получим все корабли противника s/$/\nfield::computer_ships/ x ; s/\(call_stack = [^\n]*\)/\1,sink_computer_ship::get_all_ships/ ; x b get :sink_computer_ship::get_all_ships # Оставим координаты только нужного корабля s/\([^\n]*\)$/ \1/ s/\([01]*\)\n[^\n]* \([01,]*\)\1\([01,]*\)[^\n]*$/\2\1\3/ # Выставим символ затопленного корабля s/\([^\n]*\)$/\1\nset_symbol::symbol = X/ x ; s/\(call_stack = [^\n]*\)/\1,sink_computer_ship::set_sink_symbol/ ; x b set :sink_computer_ship::set_sink_symbol :sink_computer_ship::sink_loop /[01]$/ { # Выставим номер клетки для затопления s/\([01]*\)$/\nset_symbol::number = \1/ x ; s/\(call_stack = [^\n]*\)/\1,sink_computer_ship::set_cell_number/ ; x b set :sink_computer_ship::set_cell_number # Подготовим поле противника для изменения s/$/\nset_symbol::string = field::computer/ x ; s/\(call_stack = [^\n]*\)/\1,sink_computer_ship::set_enemy_string/ ; x b copy :sink_computer_ship::set_enemy_string # Заменим символ на поле противника x ; s/\(call_stack = [^\n]*\)/\1,sink_computer_ship::change_enemy_field/ ; x b set_symbol :sink_computer_ship::change_enemy_field s/$/\nfield::computer = set_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,sink_computer_ship::save_enemy_field/ ; x b copy :sink_computer_ship::save_enemy_field # Подготовим поле саджестов для изменения s/$/\nset_symbol::string = field::suggestions/ x ; s/\(call_stack = [^\n]*\)/\1,sink_computer_ship::set_suggestions_string/ ; x b copy :sink_computer_ship::set_suggestions_string # Заменим символ на поле саджестов x ; s/\(call_stack = [^\n]*\)/\1,sink_computer_ship::change_suggestions_field/ ; x b set_symbol :sink_computer_ship::change_suggestions_field s/$/\nfield::suggestions = set_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,sink_computer_ship::save_suggestions_field/ ; x b copy :sink_computer_ship::save_suggestions_field s/[01]*\n[^\n]$// s/,$// b sink_computer_ship::sink_loop } b return }
А вот так выглядит анализ состояния подбитого корабля. Ранее написанные примитивы используются на всю катушку! По результату выполнения этой функции мы сможем судить, нужно ли топить корабль функцией sink_computer_ship.
Анализ состояния подбитого корабля
:analyze_ship_state { # Инициаллизируем результат s/$/\nanalyze_ship_state::result = / x ; s/\(call_stack = [^\n]*\)/\1,analyze_ship_state::init/ ; x b set :analyze_ship_state::init s/$/\nanalyze_ship_state::coordinate/ x ; s/\(call_stack = [^\n]*\)/\1,analyze_ship_state::get_ship_cell/ ; x b get :analyze_ship_state::get_ship_cell s/$/\nanalyze_ship_state::ships/ x ; s/\(call_stack = [^\n]*\)/\1,analyze_ship_state::get_all_ships/ ; x b get :analyze_ship_state::get_all_ships s/$/\nget_symbol::string = analyze_ship_state::field/ x ; s/\(call_stack = [^\n]*\)/\1,analyze_ship_state::set_enemy_string/ ; x b copy :analyze_ship_state::set_enemy_string # Оставим только нужный корабль s/\([^\n]*\)$/ \1/ s/\([01]*\)\n[^\n]* \([01,]*\)\1\([01,]*\)[^\n]*$/\2\1\3/ :analyze_ship_state::cell_loop /[01]$/ { # Зададим номер клетки s/\([01]*\)$/\nget_symbol::number = \1/ x ; s/\(call_stack = [^\n]*\)/\1,analyze_ship_state::set_enemy_cell/ ; x b set :analyze_ship_state::set_enemy_cell x ; s/\(call_stack = [^\n]*\)/\1,analyze_ship_state::calculate_enemy_cell/ ; x b get_symbol :analyze_ship_state::calculate_enemy_cell s/$/\nget_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,analyze_ship_state::get_enemy_cell/ ; x b get :analyze_ship_state::get_enemy_cell # Если наткнулись на здоровую клетку, то корабль просто подбит /H$/ b analyze_ship_state::not_sunk # Удалим проанализированную клетку s/[01]*\n[^\n]$// s/,$// b analyze_ship_state::cell_loop } s/D*\n*$/analyze_ship_state::result = sunk/ x ; s/\(call_stack = [^\n]*\)/\1,analyze_ship_state::sunk/ ; x b set :analyze_ship_state::sunk :analyze_ship_state::not_sunk b return }
Напишем сразу и функцию определения выигрыша. Она совсем простая: если на поле противника все корабли затоплены (то есть, на поле есть только символы воды и затопленных кораблей), то мы победили:
Проверка выигрыша
:check_win { s/$/\nfield::computer/ x ; s/\(call_stack = [^\n]*\)/\1,check_win::get_enemy_field/ ; x b get :check_win::get_enemy_field # Если на поле только вода и затопленные корабли, игрок побеждает /\n[~X]*$/ { x ; s/\(call_stack = [^\n]*\)/\1,check_win::draw/ ; x b draw :check_win::draw s/^.*$/\nCongratulations, you've won!/ p q } s/\n*[~HDX]*// b return }
Теперь у нас есть все, чтобы стрелять по любым клеткам и обрабатывать затопление корабля. Неплохо! Но нужно добавить еще одну деталь: когда мы попадаем по кораблю, мы сразу можем сделать вывод, что на соседних по диагонали клетках корабля точно нет. А при затоплении можно заключить, что кораблей нет и на его соседних по вертикали и горизонтали клетках. Давайте это тоже обработаем и отразим на поле саджестов. Для этого заведем три функции: mark_diagonal_cross, mark_common_cross и mark_circle. Первую будем вызывать при любом попадании, последнюю — при затоплении. Она, в свою очередь, будет проходиться по всем клеткам затопленного корабля и вызывать вторую функцию для каждой из них.
Продвинутая обработка попаданий: маркируем гарантированно пустые клетки
:mark_diagonal_cross { # Выставим символ воды s/\([^\n]*\)$/\1\nset_symbol::symbol = ~/ x ; s/\(call_stack = [^\n]*\)/\1,mark_diagonal_cross::set_water_symbol/ ; x b set :mark_diagonal_cross::set_water_symbol # Надо убедиться, что мы не выходим за пределы поля. # Это проще делать, выполнив деление на 10 и получив отдельные координаты по X и Y s/$/\ndiv::first_operand = mark::coordinates/ x ; s/\(call_stack = [^\n]*\)/\1,mark_diagonal_cross::div_set_1/ ; x b copy :mark_diagonal_cross::div_set_1 s/$/\ndiv::second_operand = 0001010/ x ; s/\(call_stack = [^\n]*\)/\1,mark_diagonal_cross::div_set_2/ ; x b set :mark_diagonal_cross::div_set_2 # Выполним деление x ; s/\(call_stack = [^\n]*\)/\1,mark_diagonal_cross::execute_div/ ; x b div :mark_diagonal_cross::execute_div # Получим результаты деления (координаты) s/$/\ndiv::div_result/ x ; s/\(call_stack = [^\n]*\)/\1,mark_diagonal_cross::get_y_coordinate/ ; x b get :mark_diagonal_cross::get_y_coordinate s/$/\ndiv::mod_result/ x ; s/\(call_stack = [^\n]*\)/\1,mark_diagonal_cross::get_x_coordinate/ ; x b get :mark_diagonal_cross::get_x_coordinate # Заведем слагаемые для перехода к соседям по диагонали: 9, 11, -9, -11 s/$/\n,1110101,1110111,0001001,0001011/ :mark_diagonal_cross::loop /[01]$/ { # Проверка северной границы: если y-координата равна 0, пропускаем прибавление -9 и -11 /0000000\n[01]*\n[01,]*,1[01]*$/ b mark_diagonal_cross::cross_cell_handled # Проверка южной границы: если y-координата равна 9, пропускаем прибавление 9 и 11 /0001001\n[01]*\n[01,]*,0[01]*$/ b mark_diagonal_cross::cross_cell_handled # Проверка западной границы: если x-координата равна 0, пропускаем прибавление 9 и -11 /0000000\n[01,]*,[01]*01$/ b mark_diagonal_cross::cross_cell_handled # Проверка восточной границы: если x-координата равна 9, пропускаем прибавление 11 и -9 /0001001\n[01,]*,[01]*11$/ b mark_diagonal_cross::cross_cell_handled # Задаем первое слагаемое — подбитая клетка s/$/\nadd::first_operand = mark::coordinates/ x ; s/\(call_stack = [^\n]*\)/\1,mark_diagonal_cross::set_add_1/ ; x b copy :mark_diagonal_cross::set_add_1 # Задаем второе слагаемое: 9, 11, -9 или -11 s/\([01]*\)$/\1\nadd::second_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,mark_diagonal_cross::set_add_2/ ; x b set :mark_diagonal_cross::set_add_2 # Выполним сложение x ; s/\(call_stack = [^\n]*\)/\1,mark_diagonal_cross::calculate_cross_cell/ ; x b add :mark_diagonal_cross::calculate_cross_cell # Зададим номер клетки s/\([^\n]*\)$/\1\nset_symbol::number = add::result/ x ; s/\(call_stack = [^\n]*\)/\1,mark_diagonal_cross::set_mark_cell/ ; x b copy :mark_diagonal_cross::set_mark_cell # Подготовим поле саджестов для изменений s/$/\nset_symbol::string = mark::field/ x ; s/\(call_stack = [^\n]*\)/\1,mark_diagonal_cross::set_suggestions_string/ ; x b copy :mark_diagonal_cross::set_suggestions_string # Обновим поле саджестов x ; s/\(call_stack = [^\n]*\)/\1,mark_diagonal_cross::perform_field_change/ ; x b set_symbol :mark_diagonal_cross::perform_field_change s/$/\nmark::field = set_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,mark_diagonal_cross::change_suggestions_field/ ; x b copy :mark_diagonal_cross::change_suggestions_field :mark_diagonal_cross::cross_cell_handled # Clean the handled cell s/,[01]*$// b mark_diagonal_cross::loop } # Чистим артефакты s/\n[01]*\n[01]*\n$// b return } :mark_circle { # Получаем клетки корабля компьютера s/$/\nmark::ship/ x ; s/\(call_stack = [^\n]*\)/\1,mark_circle::get_ship_cells/ ; x b get :mark_circle::get_ship_cells :mark_circle::loop /[01]$/ { s/\([01]*\)$/\1\nmark_common_cross::cell = \1/ x ; s/\(call_stack = [^\n]*\)/\1,mark_circle::set_ship_cell/ ; x b set :mark_circle::set_ship_cell x ; s/\(call_stack = [^\n]*\)/\1,mark_circle::mark_common_cross/ ; x b mark_common_cross :mark_circle::mark_common_cross s/,[01]*$// b mark_circle::loop } b return } :mark_common_cross { # Выставим символ воды s/\([^\n]*\)$/\1\nset_symbol::symbol = ~/ x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::set_water_symbol/ ; x b set :mark_common_cross::set_water_symbol # Надо убедиться, что мы не выходим за пределы поля. # Это проще делать, выполнив деление на 10 и получив отдельные координаты по X и Y s/$/\ndiv::first_operand = mark_common_cross::cell/ x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::div_set_1/ ; x b copy :mark_common_cross::div_set_1 s/$/\ndiv::second_operand = 0001010/ x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::div_set_2/ ; x b set :mark_common_cross::div_set_2 # Выполняем деление и получаем результаты x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::execute_div/ ; x b div :mark_common_cross::execute_div s/$/\ndiv::div_result/ x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::get_y_coordinate/ ; x b get :mark_common_cross::get_y_coordinate s/$/\ndiv::mod_result/ x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::get_x_coordinate/ ; x b get :mark_common_cross::get_x_coordinate # Получаем клетки корабля компьютера s/$/\nmark::ship/ x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::get_ship_cells/ ; x b get :mark_common_cross::get_ship_cells # Заведем слагаемые для доступа к соседям по ребру: 1, -1, 10, -10 s/$/\n,0001010,1110110,0000001,1111111/ :mark_common_cross::loop /[01]$/ { # Север: не вычитаем 10, если y-координата равна 0 /0000000\n[01]*\n[01,]*\n[01,]*,1110110$/ b mark_common_cross::cross_cell_handled # Юг: не прибавляем 10, если y-координата равна 9 /0001001\n[01]*\n[01,]*\n[01,]*,0001010$/ b mark_common_cross::cross_cell_handled # Запад: не вычитаем 1, если x-координата равна 0 /0000000\n[01,]*\n[01,]*,1111111$/ b mark_common_cross::cross_cell_handled # Восток: не прибавляем 1, если x-координата равна 9 /0001001\n[01,]*\n[01,]*,0000001$/ b mark_common_cross::cross_cell_handled # Первое слагаемое — подбитая клетка s/$/\nadd::first_operand = mark_common_cross::cell/ x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::set_add_1/ ; x b copy :mark_common_cross::set_add_1 # Второе слагаемое: 1, 10, -1 или -10 s/\([01]*\)$/\1\nadd::second_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::set_add_2/ ; x b set :mark_common_cross::set_add_2 # Выполняем сложение x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::calculate_cross_cell/ ; x b add :mark_common_cross::calculate_cross_cell # Дополнительная проверка, что клетка результата реально пустая (в ней может быть затопленный корабль) s/$/\nadd::result/ x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::set_cell_to_check/ ; x b get :mark_common_cross::set_cell_to_check s/\([01,]*\)\n\([01,]*\)\n\([01]*\)$/\1\n\2\n\3 \1/ /\n\([01]*\) [01,]*\1[01,]*$/ {s/\n[01, ]*$// ; b mark_common_cross::cross_cell_handled ; } # Чистим артефакт проверки s/\n[01, ]*$// # Задаем номер клетки s/\([^\n]*\)$/\1\nset_symbol::number = add::result/ x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::set_mark_cell/ ; x b copy :mark_common_cross::set_mark_cell # Готовим поле саджестов к обновлению s/$/\nset_symbol::string = mark::field/ x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::set_suggestions_string/ ; x b copy :mark_common_cross::set_suggestions_string # Обновляем поле саджестов x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::perform_field_change/ ; x b set_symbol :mark_common_cross::perform_field_change s/$/\nmark::field = set_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,mark_common_cross::change_suggestions_field/ ; x b copy :mark_common_cross::change_suggestions_field :mark_common_cross::cross_cell_handled # Clean the handled cell s/,[01]*$// b mark_common_cross::loop } s/\n[01]*\n[01]*\n[01,]*\n*$// b return }
Осталось лишь объединить всю логику хода игрока в одну едрить какую огромную функцию:
Ход игрока
:player_move { x ; s/\(call_stack = [^\n]*\)/\1,player_move::draw/ ; x b draw :player_move::draw # Input coordinates n # Анализ хода /^[A-Ja-j] *[0-9]$/ { b player_move::valid_move} s/$/\nlast_action_log = Invalid move, type a single letter and a single digit like A0 or H5./ x ; s/\(call_stack = [^\n]*\)/\1,player_move::invalid_move/ ; x b set :player_move::invalid_move b player_move :player_move::valid_move # Задаем координаты для конвертации s/^\([^\n]*\)$/convert_coordinates::coordinates = \1/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::set_coordinates/ ; x b set :player_move::set_coordinates # Конвертируем координаты x ; s/\(call_stack = [^\n]*\)/\1,player_move::convert_coordinates/ ; x b convert_coordinates :player_move::convert_coordinates # Задаем переменные для анализа клетки поля саджестов s/$/\nget_symbol::string = field::suggestions/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::set_suggestion_string/ ; x b copy :player_move::set_suggestion_string s/$/\nget_symbol::number = convert_coordinates::result/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::set_number/ ; x b copy :player_move::set_number # Получаем клетку с поля саджестов x ; s/\(call_stack = [^\n]*\)/\1,player_move::get_suggestion_cell/ ; x b get_symbol :player_move::get_suggestion_cell # Анализировать клетку поля саджестов будем в основном буфере s/^.*$/get_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::put_suggestion_cell/ ; x b get :player_move::put_suggestion_cell # Если клетка уже проверена, попробуем еще раз /[~DX]/ { s/$/\nlast_action_log = This cell has already been checked, try another one./ x ; s/\(call_stack = [^\n]*\)/\1,player_move::already_checked/ ; x b set :player_move::already_checked b player_move } # Зададим поле для получения символа s/$/\nget_symbol::string = field::computer/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::set_enemy_string/ ; x b copy :player_move::set_enemy_string # Получим нужный символ с поля противника x ; s/\(call_stack = [^\n]*\)/\1,player_move::get_enemy_cell/ ; x b get_symbol :player_move::get_enemy_cell # Анализировать полученную клетку будем в основном буфере s/^.*$/get_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::put_enemy_cell/ ; x b get :player_move::put_enemy_cell # При попадании сперва пометим клетку как поврежденную s/H/D/ # Подготовим обновление поля саджестов s/\([^\n]*\)$/\1\nset_symbol::symbol = \1/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::set_missed_symbol/ ; x b set :player_move::set_missed_symbol s/$/\nset_symbol::number = convert_coordinates::result/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::set_missed_number/ ; x b copy :player_move::set_missed_number s/$/\nset_symbol::string = field::suggestions/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::set_missed_string/ ; x b copy :player_move::set_missed_string # Обновим поле саджестов x ; s/\(call_stack = [^\n]*\)/\1,player_move::change_missed_string/ ; x b set_symbol :player_move::change_missed_string s/$/\nfield::suggestions = set_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::perform_missed/ ; x b copy :player_move::perform_missed # Обработка промаха /~$/ { s/$/\nlast_action_log = You've missed! Now it's my turn./ x ; s/\(call_stack = [^\n]*\)/\1,player_move::log_miss/ ; x b set :player_move::log_miss b player_move::end_move } # Обработка попадания /D$/ { s/$/\nlast_action_log = Ouch! My ship is damaged!/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::log_hit/ ; x b set :player_move::log_hit s/\([^\n]*\)$/\1\nset_symbol::symbol = D/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::set_hit_symbol/ ; x b set :player_move::set_hit_symbol s/$/\nset_symbol::string = field::computer/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::set_hit_string/ ; x b copy :player_move::set_hit_string # Обновляем поле компьютера x ; s/\(call_stack = [^\n]*\)/\1,player_move::change_hit_string/ ; x b set_symbol :player_move::change_hit_string s/$/\nfield::computer = set_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::perform_hit/ ; x b copy :player_move::perform_hit # Проводим анализ состояния корабля s/$/\nanalyze_ship_state::coordinate = convert_coordinates::result/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::prepare_analysis_coordinate/ ; x b copy :player_move::prepare_analysis_coordinate s/$/\nanalyze_ship_state::ships = field::computer_ships/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::prepare_analysis_ships/ ; x b copy :player_move::prepare_analysis_ships s/$/\nanalyze_ship_state::field = field::computer/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::prepare_analysis_field/ ; x b copy :player_move::prepare_analysis_field x ; s/\(call_stack = [^\n]*\)/\1,player_move::analysis/ ; x b analyze_ship_state :player_move::analysis s/$/\nanalyze_ship_state::result/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::get_analysis_result/ ; x b get :player_move::get_analysis_result # Подготовим переменные для маркировки: поле и номер клетки s/$/\nmark::coordinates = convert_coordinates::result/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::prepare_mark_coordinates/ ; x b copy :player_move::prepare_mark_coordinates s/$/\nmark::field = field::suggestions/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::prepare_mark_field/ ; x b copy :player_move::prepare_mark_field # Маркируем соседей по диагонали как пустые клетки x ; s/\(call_stack = [^\n]*\)/\1,player_move::mark_diagonal_cross/ ; x b mark_diagonal_cross :player_move::mark_diagonal_cross s/$/\nfield::suggestions = mark::field/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::set_diagonal_cross/ ; x b copy :player_move::set_diagonal_cross /sunk$/ { s/\n*sunk$// # Обновляем поле саджестов s/$/\nmark::field = field::suggestions/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::update_before_sink/ ; x b copy :player_move::update_before_sink x ; s/\(call_stack = [^\n]*\)/\1,player_move::sink_ship/ ; x b sink_computer_ship :player_move::sink_ship # Получаем координаты корабля s/$/\nmark::coordinates/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::get_hit_cell/ ; x b get :player_move::get_hit_cell s/$/\nfield::computer_ships/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::get_ship_list/ ; x b get :player_move::get_ship_list s/$/\nmark::field = field::suggestions/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::prepare_mark_field_2/ ; x b copy :player_move::prepare_mark_field_2 s/\([^\n]*\)$/ \1/ s/\([01]*\)\n[^\n]* \([01,]*\)\1\([01,]*\)[^\n]*$/,\2\1\3/ s/\([01,]*\)$/mark::ship = \1/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::set_ship_cells/ ; x b set :player_move::set_ship_cells x ; s/\(call_stack = [^\n]*\)/\1,player_move::mark_circle/ ; x b mark_circle :player_move::mark_circle # Обновляем поле саджестов s/$/\nfield::suggestions = mark::field/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::set_suggestions_field/ ; x b copy :player_move::set_suggestions_field s/$/\nlast_action_log = OK, you've managed to sink my ship. Happy?/ x ; s/\(call_stack = [^\n]*\)/\1,player_move::log_sink/ ; x b set :player_move::log_sink } x ; s/\(call_stack = [^\n]*\)/\1,player_move::check_win/ ; x b check_win :player_move::check_win # Make yet another move b player_move } :player_move::end_move b computer_move # Это пока не реализовано, но очень скоро будет }
Можно ли было сделать эту функцию компактнее и разделить ее на логические шаги? Да, черт возьми, можно и нужно! Но на рефакторинг меня уже не хватило :)
⚠️ В последней функции мы работаем еще с одной неупомянутой переменной —
last_action_log. Она еще сыграет свою роль: пока нужно просто иметь в виду, что пользователь будет видеть ее содержимое.
Игра начинает уже что-то собой представлять. Мы можем вручную задать расположение кораблей противника, а потом расстрелять его беспомощный флот вплоть до победы. Теперь научим противника стрелять в ответ.
Ходы компьютера
Ты всего лишь машина, только имитация жизни.
Алан Тьюринг
Как известно, для игры в морской бой существуют хорошие стратегии, которые кроют наивный случайный выбор как бык овцу. И их, в общем, несложно было бы запрограммировать. Проблема, однако, в том, что ветвиться такая стратегия начинает обычно только с первым попаданием, а до этого ходы плюс-минус предсказуемы. Из этого следует, во-первых, что игрок может подстроиться под стратегию компьютерного оппонента, а значит, получить преимущество. Во-вторых, с таким оппонентом станет попросту неинтересно играть, как только игрок выкупит стратегию. Поэтому выбирать клетку для выстрела оппонент будет иначе.
Чтобы построить стратегию для компьютера, начнем со стратегии случайного выбора. Если на поле есть корабли общей площадью , а всего непроверенных клеток у нас
, то вероятность попадания по кораблю при случайном выборе очевидно составляет
. Размышления и эвристики, однако, эту вероятность могут увеличивать, иногда — значительно.

Например, для этой расстановки вероятность попасть методом тыка равна — примерно 26%. Но элементарные рассуждения увеличивают эту вероятность до 50%, а уж если не попадем с первого раза, то на следующий ход линкор будет подбит гарантированно.
Получается, если мы будем манипулировать вероятностью попадания, компьютер будет выигрывать быстрее, а значит, играть с ним будет сложнее. А раз в памяти записано, где стоят корабли игрока, мы можем разбить множество непроверенных клеток на два множества и присвоить разные вероятности стрельбы по ним. Читерство ли это? Безусловно! Становится ли от этого интереснее играть? Черт возьми, да! Бонусом мы получаем возможность элементарно регулировать и даже выбирать уровень сложности: достаточно просто задать нужный модификатор вероятности. Таким образом, сильный противник — это в нашей реализации просто удачливый противник. Ну или противник с хорошей морской разведкой :)

Первая идея по реализации удачи была такой. Зафиксируем удачу как параметр от 0 до 4. Логика выбора (промазать или попасть) будет такой. Допустим, всего есть N непроверенных клеток, из которых k занято кораблями. Тогда мы возьмем число , где
— параметр удачи, и случайным образом выберем число от 0 до
. Если это число не больше
, то компьютер промажет. Если больше — попадет.
Идея в целом рабочая, но поразмыслив, я решил, что сорокапроцентная вероятность попадания со старта уже при удаче, равной 2, — это слишком хардкорно. Поэтому удачу мы слегка понерфим: вероятность попадания будет равна , где параметр
может быть от 1 до 5. Константа 2 хороша тем, что на нее легко умножать и делить, и она при этом дает вполне удовлетворительную кривую роста сложности.
Программируется это так. Сперва генерируем случайное число от 0 до и сравниваем его с
. Если оно меньше
, то делим его на
и получаем число
. Выберем все клетки с живыми кораблями и выстрелим по клетке с полученным номером. Если же случайное число получилось больше
, то отняв от него
, получим число от 0 до
. Поделив его на 2, получим число от 0 до
. Стрелять будем в пустую клетку с этим номером.
Дальше мы проводим тот же анализ поля, который мы уже научились проводить при стрельбе по полю противника, и выставляем нужные символы прямо на поле игрока.
Итак, поехали. Сперва напишем функцию генерации нужной клетки. Она вернет (точнее, запишет в переменные результата) два значения: тип клетки (пустая или полная) и номер клетки такого типа на поле игрока.
Генерация хода противника
:generate_computer_move { # Генерируем случайное число x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::random/ ; x b rng :generate_computer_move::random # Считаем лимит: 2N + kL s/$/\nfield::player/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::get_player_field/ ; x b get :generate_computer_move::get_player_field # Инициализируем счетчики для N и k # Все будет 32-битным, т.к. придется работать еще и со случайными числами s/\n\([^\n]*\)$/00000000000000000000000000000000\n00000000000000000000000000000000\n\1/ :generate_computer_move::calculate_cells_loop /H$/ { # Инлайновый инкремент для k :generate_computer_move::inc_healthy_count_loop /\n0*\n[·~HDX]*$/ {s/\(0*\)0\n\([^\n]*\)$/\11\n\2/ ; b generate_computer_move::inc_healthy_reverseloop; } :generate_computer_move::inc_healthy_loop ; s/1\(_*\)\n\([^\n]*\)$/_\1\n\2/ ; /1_*\n[^\n]*$/ b generate_computer_move::inc_healthy_loop s/0\(_*\)\n\([^\n]*\)$/1\1\n\2/ :generate_computer_move::inc_healthy_reverseloop ; s/_\(_*\)\n\([^\n]*\)$/0\1\n\2/ ; /_\n[^\n]*$/ b generate_computer_move::inc_healthy_reverseloop ; } /[·H]$/ { # Инлайновый инкремент для N :generate_computer_move::inc_total_count_loop /^0*\n[01]*\n[^\n]*$/ {s/\(0*\)0\n\([01]*\)\n\([^\n]*\).$/\11\n\2\n\3/ ; b generate_computer_move::inc_total_reverseloop; } :generate_computer_move::inc_total_loop ; s/1\(_*\)\n\([01]*\)\n\([^\n]*\)$/_\1\n\2\n\3/ ; /1_*\n[01]*\n[^\n]*$/ b generate_computer_move::inc_total_loop s/0\(_*\)\n\([01]*\)\n\([^\n]*\)$/1\1\n\2\n\3/ :generate_computer_move::inc_total_reverseloop ; s/_\(_*\)\n\([01]*\)\n\([^\n]*\)$/0\1\n\2\n\3/ ; /_\n[01]*\n[^\n]*$/ b generate_computer_move::inc_total_reverseloop ; } # Удаляем последний символ s/.$// /[^\n]$/ b generate_computer_move::calculate_cells_loop s/\n$// # Готовим первое слагаемое числителя: 2*k s/\([01]\)\([01]*\)$/\1\2\ngenerate_computer_move::2_healthy_cells = \20/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::save_2_healthy_cells/ ; x b set :generate_computer_move::save_2_healthy_cells # Готовим первое слагаемое знаменателя: 2*N s/[01]\([01]*\)\n\([01]*\)$/\2\ngenerate_computer_move::total_cells_factor = \10/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::save_total_cells_factor/ ; x b set :generate_computer_move::save_total_cells_factor # Готовим второе слагаемое: k*L s/\([01]*\)$/mul::first_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::prepare_healthy_cells_factor_1/ ; x b set :generate_computer_move::prepare_healthy_cells_factor_1 s/$/\nmul::second_operand = luck/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::prepare_healthy_cells_factor_2/ ; x b copy :generate_computer_move::prepare_healthy_cells_factor_2 # Выполняем умножение x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::perform_mul/ ; x b mul :generate_computer_move::perform_mul s/$/\ngenerate_computer_move::healthy_cells_factor = mul::result/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::save_healthy_cells_factor/ ; x b copy :generate_computer_move::save_healthy_cells_factor # Готовим сложение s/$/\nadd::first_operand = mul::result/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::prepare_add_1/ ; x b copy :generate_computer_move::prepare_add_1 s/$/\nadd::second_operand = generate_computer_move::total_cells_factor/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::prepare_add_2/ ; x b copy :generate_computer_move::prepare_add_2 # Выполняем сложение x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::perform_add/ ; x b add :generate_computer_move::perform_add # Делим случайное число на посчитанный лимит s/$/\ndiv::first_operand = rng::current_state/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::prepare_random_limitation_1/ ; x b copy :generate_computer_move::prepare_random_limitation_1 s/$/\ndiv::second_operand = add::result/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::prepare_random_limitation_2/ ; x b copy :generate_computer_move::prepare_random_limitation_2 x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::perform_random_limitation/ ; x b div :generate_computer_move::perform_random_limitation # Посчитаем границу, определяющую промах или попадание s/$/\nadd::first_operand = generate_computer_move::healthy_cells_factor/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::prepare_set_partition_limit_1/ ; x b copy :generate_computer_move::prepare_set_partition_limit_1 s/$/\nadd::second_operand = generate_computer_move::2_healthy_cells/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::prepare_set_partition_limit_2/ ; x b copy :generate_computer_move::prepare_set_partition_limit_2 # Выполним сложение x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::calculate_set_partition_limit/ ; x b add :generate_computer_move::calculate_set_partition_limit # Сравним случайное число с границей промаха/попадания s/$/\ndiv::mod_result/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::get_limited_random/ ; x b get :generate_computer_move::get_limited_random s/$/\nadd::result/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::get_set_partition_limit/ ; x b get :generate_computer_move::get_set_partition_limit # Процедура сравнения /\([01]*\)\n\1$/ b generate_computer_move::miss_result s/\([01]*\)\n\([01]*\)$/ \1\n \2/ :generate_computer_move::compare_loop s/\([01]*\) \([01]\)\([01]*\)\n\([01]*\) \([01]\)\([01]*\)$/\1\2 \3\n\4\5 \6/ /0 [01]*\n[01]*1 [01]*$/ b generate_computer_move::hit_result /1 [01]*\n[01]*0 [01]*$/ b generate_computer_move::miss_result b generate_computer_move::compare_loop :generate_computer_move::hit_result { s/\([01]*\) \([01]*\)\n[01 ]*$/\1\2/ # Делим число на 2+L. s/$/\nluck/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::get_luck/ ; x b get :generate_computer_move::get_luck s/\([01]*\)$/add::first_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::prepare_divisor_1/ ; x b set :generate_computer_move::prepare_divisor_1 # 2 s/$/\nadd::second_operand = 00000000000000000000000000000010/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::prepare_divisor_2/ ; x b set :generate_computer_move::prepare_divisor_2 x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::build_divisor/ ; x b add :generate_computer_move::build_divisor # Еще одно деление s/\([01]*\)$/div::first_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::set_hit_dividend/ ; x b set :generate_computer_move::set_hit_dividend s/\([01]*\)$/div::second_operand = add::result/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::set_hit_divisor/ ; x b copy :generate_computer_move::set_hit_divisor x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::perform_hit_div/ ; x b div :generate_computer_move::perform_hit_div # Запишем результат: H и номер клетки с кораблем s/$/\ngenerate_computer_move::cell_type = H/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::set_hit_type/ ; x b set :generate_computer_move::set_hit_type s/$/\ngenerate_computer_move::cell_number = div::div_result/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::set_hit_number/ ; x b copy :generate_computer_move::set_hit_number b return } :generate_computer_move::miss_result { s/\([01]*\) \([01]*\)\n\([01]*\) \([01]*\)$/\1\2\n\3\4/ # Вычитаем 2*healthy_cells + healthy_cells*luck s/\([01]*\)$/add::second_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::prepare_miss_sub_1/ ; x b set :generate_computer_move::prepare_miss_sub_1 # Из нашего случайного числа s/\([01]*\)$/add::first_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::prepare_miss_sub_2/ ; x b set :generate_computer_move::prepare_miss_sub_2 x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::build_dividend/ ; x b sub :generate_computer_move::build_dividend s/$/\nadd::result/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::get_miss_cell_number/ ; x b get :generate_computer_move::get_miss_cell_number # Пишем результат: · и номер пустой клетки s/\([01]*\)[01]$/\ngenerate_computer_move::cell_number = 0\1/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::set_miss_number/ ; x b set :generate_computer_move::set_miss_number s/$/\ngenerate_computer_move::cell_type = ·/ x ; s/\(call_stack = [^\n]*\)/\1,generate_computer_move::set_miss_type/ ; x b set :generate_computer_move::set_miss_type b return } }
Теперь эти значения нужно конвертировать в обычную координату клетки на поле (число от 0 до 99), а также в человекочитаемые координаты: если компьютер явно не сообщит, по каким клеткам он стрелял, то серию попаданий будет весьма тяжело отслеживать невооруженным глазом. Алгоритм тут строго обратный к тому, что мы уже проделывали:
Конвертация хода компьютера в человекочитаемые координаты
:reverse_convert_coordinates { s/$/\ngenerate_computer_move::cell_type/ x ; s/\(call_stack = [^\n]*\)/\1,reverse_convert_coordinates::get_cell_type/ ; x b get :reverse_convert_coordinates::get_cell_type s/$/\ngenerate_computer_move::cell_number/ x ; s/\(call_stack = [^\n]*\)/\1,reverse_convert_coordinates::get_cell_number/ ; x b get :reverse_convert_coordinates::get_cell_number # Подстрижем номер клетки: нам нужно не больше 7 бит s/[01]*\([01]\{7\}\)$/\1/ # Инициализируем два счетчика s/$/\n0000000\n0000000/ s/$/\nfield::player/ x ; s/\(call_stack = [^\n]*\)/\1,reverse_convert_coordinates::get_player_field/ ; x b get :reverse_convert_coordinates::get_player_field :reverse_convert_coordinates::cell_count_loop # Останавливаемся, когда первый счетчик достиг лимита # И ПРИ ЭТОМ # мы рассматриваем клетку нужного типа /\([·~HDX]\)\n\([01]*\)\n\2\n[01]*\n\1[·~HDX]*$/ b reverse_convert_coordinates::end_loop # Инкрементим первый счетчик, если перед нами клетка нужного типа /\([·~HDX]\)\n[01]*\n[01]*\n[01]*\n\1[·~HDX]*$/ { /\n0*\n[01]*\n[·~HDX]*$/ {s/\(0*\)0\n\([01]*\)\n\([·~HDX]*\)$/\11\n\2\n\3/ ; b reverse_convert_coordinates::first_count_endloop; } :reverse_convert_coordinates::first_inc_loop ; s/1\(_*\)\n\([01]*\)\n\([·~HDX]*\)$/_\1\n\2\n\3/ ; /1_*\n[01]*\n[·~HDX]*$/ b reverse_convert_coordinates::first_inc_loop s/0\(_*\)\n\([01]*\)\n\([·~HDX]*\)$/1\1\n\2\n\3/ :reverse_convert_coordinates::first_inc_reverseloop ; s/_\(_*\)\n\([01]*\)\n\([·~HDX]*\)$/0\1\n\2\n\3/ ; /_\n[01]*\n[·~HDX]*$/ b reverse_convert_coordinates::first_inc_reverseloop; :reverse_convert_coordinates::first_count_endloop } # Безусловный инкремент второго счетчика /\n0*\n[·~HDX]*$/ {s/\(0*\)0\n\([·~HDX]*\)$/\11\n\2/ ; b reverse_convert_coordinates::second_count_endloop; } :reverse_convert_coordinates::second_inc_loop ; s/1\(_*\)\n\([·~HDX]*\)$/_\1\n\2/ ; /1_*\n[·~HDX]*$/ b reverse_convert_coordinates::second_inc_loop s/0\(_*\)\n\([·~HDX]*\)$/1\1\n\2/ :reverse_convert_coordinates::second_inc_reverseloop ; s/_\(_*\)\n\([·~HDX]*\)$/0\1\n\2/ ; /_\n[·~HDX]*$/ b reverse_convert_coordinates::second_inc_reverseloop; :reverse_convert_coordinates::second_count_endloop # Удаляем очередной символ поля s/[·~HDX]\([·~HDX]*\)$/\1/ b reverse_convert_coordinates::cell_count_loop :reverse_convert_coordinates::end_loop # Чистим артефакты, оставляем только номер клетки s/[·~HDX]\n[01]*\n[01]*\n\([01]*\)\n[·~HDX]*$/\1/ # Сохраним итоговый номер клетки s/\([01]*\)$/\1\nreverse_convert_coordinates::result_number = \1/ x ; s/\(call_stack = [^\n]*\)/\1,reverse_convert_coordinates::save_result_number/ ; x b set :reverse_convert_coordinates::save_result_number # Поделим число на 10 s/\([01]*\)$/div::first_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,reverse_convert_coordinates::prepare_coordinate_div_1/ ; x b set :reverse_convert_coordinates::prepare_coordinate_div_1 s/\([01]*\)$/div::second_operand = 0001010/ x ; s/\(call_stack = [^\n]*\)/\1,reverse_convert_coordinates::prepare_coordinate_div_2/ ; x b copy :reverse_convert_coordinates::prepare_coordinate_div_2 x ; s/\(call_stack = [^\n]*\)/\1,reverse_convert_coordinates::perform_coordinate_div/ ; x b div :reverse_convert_coordinates::perform_coordinate_div s/$/\ndiv::div_result/ x ; s/\(call_stack = [^\n]*\)/\1,reverse_convert_coordinates::get_y_coordinate/ ; x b get :reverse_convert_coordinates::get_y_coordinate s/$/\ndiv::mod_result/ x ; s/\(call_stack = [^\n]*\)/\1,reverse_convert_coordinates::get_x_coordinate/ ; x b get :reverse_convert_coordinates::get_x_coordinate # Конвертируем букву s/0000000\n\([01]*\)$/A\n\1/ ; s/0000001\n\([01]*\)$/B\n\1/ ; s/0000010\n\([01]*\)$/C\n\1/ ; s/0000011\n\([01]*\)$/D\n\1/ ; s/0000100\n\([01]*\)$/E\n\1/ ; s/0000101\n\([01]*\)$/F\n\1/ ; s/0000110\n\([01]*\)$/G\n\1/ ; s/0000111\n\([01]*\)$/H\n\1/ ; s/0001000\n\([01]*\)$/I\n\1/ ; s/0001001\n\([01]*\)$/J\n\1/ # Конвертируем цифру s/\n0000000$/0/ ; s/\n0000001$/1/ ; s/\n0000010$/2/ ; s/\n0000011$/3/ ; s/\n0000100$/4/ ; s/\n0000101$/5/ ; s/\n0000110$/6/ ; s/\n0000111$/7/ ; s/\n0001000$/8/ ; s/\n0001001$/9/ # Сохраним человекочитаемые координаты s/\([A-J][0-9]\)$/reverse_convert_coordinates::result_string = \1/ x ; s/\(call_stack = [^\n]*\)/\1,reverse_convert_coordinates::save_result_string/ ; x b set :reverse_convert_coordinates::save_result_string b return }
Еще нам потребуется функция для затопления корабля на поле игрока. Ее можно было реализовать и более универсально через передачу туда отдельной переменной. Но, впрочем, она не так уж и велика, чтобы об этом беспокоиться.
Затопление корабля игрока
:sink_player_ship { # Получим координату попадания s/$/\nreverse_convert_coordinates::result_number/ x ; s/\(call_stack = [^\n]*\)/\1,sink_player_ship::get_ship_cell/ ; x b get :sink_player_ship::get_ship_cell # Получим корабли игрока s/$/\nfield::player_ships/ x ; s/\(call_stack = [^\n]*\)/\1,sink_player_ship::get_all_ships/ ; x b get :sink_player_ship::get_all_ships # Оставим только нужный корабль s/\([^\n]*\)$/ \1/ s/\([01]*\)\n[^\n]* \([01,]*\)\1\([01,]*\)[^\n]*$/\2\1\3/ # Выставим символ затопленного корабля (X) s/\([^\n]*\)$/\1\nset_symbol::symbol = X/ x ; s/\(call_stack = [^\n]*\)/\1,sink_player_ship::set_sink_symbol/ ; x b set :sink_player_ship::set_sink_symbol :sink_player_ship::sink_loop /[01]$/ { # Зададим номер клетки для затопления s/\([01]*\)$/\nset_symbol::number = \1/ x ; s/\(call_stack = [^\n]*\)/\1,sink_player_ship::set_cell_number/ ; x b set :sink_player_ship::set_cell_number # Подготовим поле игрока для обновления s/$/\nset_symbol::string = field::player/ x ; s/\(call_stack = [^\n]*\)/\1,sink_player_ship::set_enemy_string/ ; x b copy :sink_player_ship::set_enemy_string # Обновим поле игрока x ; s/\(call_stack = [^\n]*\)/\1,sink_player_ship::change_enemy_field/ ; x b set_symbol :sink_player_ship::change_enemy_field s/$/\nfield::player = set_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,sink_player_ship::save_enemy_field/ ; x b copy :sink_player_ship::save_enemy_field s/[01]*\n[^\n]$// s/,$// b sink_player_ship::sink_loop } b return }
Дальнейшие же действия — копия логики хода игрока. Надо лишь задать другие переменные для обработки и постобработки. Ну и поле саджестов можно не менять.
Ход компьютерного оппонента
:computer_move { x ; s/\(call_stack = [^\n]*\)/\1,computer_move::draw/ ; x b draw :computer_move::draw # Генерируем x ; s/\(call_stack = [^\n]*\)/\1,computer_move::generate_move/ ; x b generate_computer_move :computer_move::generate_move # Посчитаем итоговую координату на поле x ; s/\(call_stack = [^\n]*\)/\1,computer_move::calculate_coordinates/ ; x b reverse_convert_coordinates :computer_move::calculate_coordinates s/$/\ngenerate_computer_move::cell_type/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::get_ct/ ; x b get :computer_move::get_ct /H$/ { s/[^\n]*$/last_action_log/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::hit_get_log/ ; x b get :computer_move::hit_get_log # Получаем человекочитаемые координаты s/$/\nreverse_convert_coordinates::result_string/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::get_hit_coordinates/ ; x b get :computer_move::get_hit_coordinates s/\([^\n]*\)\n\([A-J][0-9]\)$/\nlast_action_log = \1_\2! Hit ya!/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::hit_set_log/ ; x b set :computer_move::hit_set_log # Меняем символ для поля игрока s/$/\nset_symbol::symbol = D/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::set_hit_symbol/ ; x b set :computer_move::set_hit_symbol s/$/\nset_symbol::number = reverse_convert_coordinates::result_number/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::set_hit_number/ ; x b copy :computer_move::set_hit_number s/$/\nset_symbol::string = field::player/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::set_hit_string/ ; x b copy :computer_move::set_hit_string # Обновляем поле игрока x ; s/\(call_stack = [^\n]*\)/\1,computer_move::change_hit_string/ ; x b set_symbol :computer_move::change_hit_string s/$/\nfield::player = set_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::perform_hit/ ; x b copy :computer_move::perform_hit # Анализируем попадание s/$/\nanalyze_ship_state::coordinate = reverse_convert_coordinates::result_number/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::prepare_analysis_coordinate/ ; x b copy :computer_move::prepare_analysis_coordinate s/$/\nanalyze_ship_state::ships = field::player_ships/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::prepare_analysis_ships/ ; x b copy :computer_move::prepare_analysis_ships s/$/\nanalyze_ship_state::field = field::player/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::prepare_analysis_field/ ; x b copy :computer_move::prepare_analysis_field x ; s/\(call_stack = [^\n]*\)/\1,computer_move::analysis/ ; x b analyze_ship_state :computer_move::analysis s/$/\nanalyze_ship_state::result/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::get_analysis_result/ ; x b get :computer_move::get_analysis_result # Обычное попадание: маркируем соседние по диагонали клетки s/$/\nmark::coordinates = reverse_convert_coordinates::result_number/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::prepare_mark_coordinates/ ; x b copy :computer_move::prepare_mark_coordinates s/$/\nmark::field = field::player/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::prepare_mark_field/ ; x b copy :computer_move::prepare_mark_field x ; s/\(call_stack = [^\n]*\)/\1,computer_move::mark_diagonal_cross/ ; x b mark_diagonal_cross :computer_move::mark_diagonal_cross s/$/\nfield::player = mark::field/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::set_diagonal_cross/ ; x b copy :computer_move::set_diagonal_cross /sunk$/ { s/\n*sunk$// # Обновим поле игрока s/$/\nmark::field = field::player/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::update_before_sink/ ; x b copy :computer_move::update_before_sink x ; s/\(call_stack = [^\n]*\)/\1,computer_move::sink_ship/ ; x b sink_player_ship :computer_move::sink_ship # Получим клетки корабля s/$/\nmark::coordinates/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::get_hit_cell/ ; x b get :computer_move::get_hit_cell s/$/\nfield::player_ships/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::get_ship_list/ ; x b get :computer_move::get_ship_list s/$/\nmark::field = field::player/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::prepare_mark_field_2/ ; x b copy :computer_move::prepare_mark_field_2 s/\([^\n]*\)$/ \1/ s/\([01]*\)\n[^\n]* \([01,]*\)\1\([01,]*\)[^\n]*$/,\2\1\3/ s/\([01,]*\)$/mark::ship = \1/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::set_ship_cells/ ; x b set :computer_move::set_ship_cells x ; s/\(call_stack = [^\n]*\)/\1,computer_move::mark_circle/ ; x b mark_circle :computer_move::mark_circle # Обновим поле игрока s/$/\nfield::player = mark::field/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::update_after_sink/ ; x b copy :computer_move::update_after_sink x ; s/\(call_stack = [^\n]*\)/\1,computer_move::check_lose/ ; x b check_lose :computer_move::check_lose } b computer_move } # Обработка промаха /·$/ { s/[^\n]*$/last_action_log/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::miss_get_log/ ; x b get :computer_move::miss_get_log # Получим человекочитаемые координаты s/$/\nreverse_convert_coordinates::result_string/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::get_miss_coordinates/ ; x b get :computer_move::get_miss_coordinates s/\([^\n]*\)\n\([A-J][0-9]\)$/\nlast_action_log = \1_\2! OK, I missed.../ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::miss_set_log/ ; x b set :computer_move::miss_set_log # Изменим символ на поле игрока s/$/\nset_symbol::symbol = ~/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::set_missed_symbol/ ; x b set :computer_move::set_missed_symbol s/$/\nset_symbol::number = reverse_convert_coordinates::result_number/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::set_missed_number/ ; x b copy :computer_move::set_missed_number s/$/\nset_symbol::string = field::player/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::set_missed_string/ ; x b copy :computer_move::set_missed_string # Обновим поле игрока x ; s/\(call_stack = [^\n]*\)/\1,computer_move::change_missed_string/ ; x b set_symbol :computer_move::change_missed_string s/$/\nfield::player = set_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,computer_move::perform_missed/ ; x b copy :computer_move::perform_missed b player_move } }
Расстановка кораблей игрока
Каждый должен знать свое место!
Влад Цепеш
Основная логика игры готова. Теперь мы можем вручную записать в переменные свои и чужие корабли, задать уровень сложности и даже поиграть! Но не будем останавливаться на достигнутом: со знанием, где расположены корабли противника, играть не шибко интересно. Да и ручная запись в переменные — не самое простое занятие. Начнем с интерфейса для расстановки кораблей игрока.
Итак, положение любого корабля задается тремя параметрами: координаты (или координата) левой верхней клетки, ориентация (горизонталь или вертикаль) и длина. Длины всех кораблей мы знаем заранее и будем сами предлагать их игроку по очереди. Это нас предохранит еще и от особо одаренных игроков, которые начнут расстановку с катеров, чтобы потом на линкор места не хватило. Координаты логично будет принимать буквенно-цифровые — это для игрока достаточно удобно. Тем более, что преобразовывать их в единый номер клетки мы уже умеем. Ну а ориентацию можно задать дополнительной буквой: H или V. Например, положение корабля на клетке B5 и ниже будет задаваться строкой B5V. Ориентацию катеров можно будет задавать любой буквой.
⚠️ По совести, для катеров вообще не надо было проводить проверку ориентации. Я просто поленился сделать для них кастомную валидацию.
Самая трудоемкая часть — это валидация перед размещением. Определенную часть этой логики мы уже успели реализовать, когда обводили затонувшие корабли методами mark_diagonal_cross и mark_common_cross. Этими наработками можно воспользоваться. Итоговый алгоритм получается вот какой:
Инициализируем
field::playerсотней точек, аfield::player_ships— пустотой.Берем очередной корабль.
Транслируем координаты его верхней клетки и ориентацию в список потенциальных координат корабля.
Проверяем все полученные координаты на предмет того, что в их клетках на поле стоят точки.
Если нет — выдаем ошибку и предлагаем ввести другие координаты.
Если все хорошо, дописываем координаты в
field::player_ships.Ставим в
field::playerкорабль.Обводим его волнами.
Когда корабли закончились, превращаем все волны на поле обратно в точки.
Размещение отдельного корабля на поле
:place_ship { # Инициализируем результат s/^.*$/place_ship::result = / x ; s/\(call_stack = [^\n]*\)/\1,place_ship::init_result/ ; x b set :place_ship::init_result # Инициализируем флаг границы, список координат и счетчик s/^.*$/0\n\n0/ s/$/\nplace_ship::length/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::get_length/ ; x b get :place_ship::get_length s/$/\nplace_ship::orientation/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::get_orientation/ ; x b get :place_ship::get_orientation s/$/\nplace_ship::coordinates/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::get_coordinates/ ; x b get :place_ship::get_coordinates # Считаем координаты :place_ship::collecting # Проверим флаг границы, вернем ошибку,если сейчас пересечем ее /^1/ b place_ship::invalid_placement # Если в очередной координате — не точка, вернем ошибку s/$/\nget_symbol::string = place_ship::field/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::set_field_string/ ; x b copy :place_ship::set_field_string s/\([01]*\)$/\1\nget_symbol::number = \1/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::set_coordinate_to_get/ ; x b set :place_ship::set_coordinate_to_get x ; s/\(call_stack = [^\n]*\)/\1,place_ship::get_potential_cell/ ; x b get_symbol :place_ship::get_potential_cell s/$/\nget_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::put_potential_cell/ ; x b get :place_ship::put_potential_cell /[~H]$/ b place_ship::invalid_placement # Добавим в список текущую координату s/^\([01]\)\n\([01,]*\)\n\([0-4]\n[0-4]\n[HVhv]\)\n\([01]*\)\n[·~HDX]$/\1\n\2,\4\n\3\n\4/ # Выставим флаг границы, если мы ее достигли s/\([01]*\)$/\1\ndiv::first_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::div_set_1/ ; x b set :place_ship::div_set_1 s/$/\ndiv::second_operand = 0001010/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::div_set_2/ ; x b set :place_ship::div_set_2 x ; s/\(call_stack = [^\n]*\)/\1,place_ship::execute_div/ ; x b div :place_ship::execute_div # Достигли правого края /[Hh]/ { s/$/\ndiv::mod_result/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::get_mod_result/ ; x b get :place_ship::get_mod_result /0001001$/ s/^0/1/ s/\n[01]*$// } # Достигли нижнего края /[Vv]/ { s/$/\ndiv::div_result/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::get_div_result/ ; x b get :place_ship::get_div_result /0001001$/ s/^0/1/ s/\n[01]*$// } # Смена координат # Зададим первое слагаемое s/\([01]*\)$/\nadd::first_operand = \1/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::prepare_coordinate_change_1/ ; x b set :place_ship::prepare_coordinate_change_1 # Второе слагаемое зависит от ориентации корабля /[Hh]/ s/$/\nadd::second_operand = 0000001/ /[Vv]/ s/$/\nadd::second_operand = 0001010/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::prepare_coordinate_change_2/ ; x b set :place_ship::prepare_coordinate_change_2 x ; s/\(call_stack = [^\n]*\)/\1,place_ship::calculate_next_coordinate/ ; x b add :place_ship::calculate_next_coordinate s/$/add::result/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::get_next_coordinate/ ; x b get :place_ship::get_next_coordinate # Быстрый инкремент счетчика: для всего 4-х вариантов нет смысла тащить полную процедуру. # Поэтому мы и в бинарную форму не переводили длину корабля. s/^\([01]\)\n\([01,]*\)\n3/\1\n\2\n4/ ; s/^\([01]\)\n\([01,]*\)\n2/\1\n\2\n3/ ; s/^\([01]\)\n\([01,]*\)\n1/\1\n\2\n2/ ; s/^\([01]\)\n\([01,]*\)\n0/\1\n\2\n1/ # Завершаем или продолжаем сбор координат /^[01]\n[01,]*\n\([1-4]\)\n\1/ { s/^[01]\n\([01,]*\).*$/\1/ b place_ship::place } b place_ship::collecting :place_ship::invalid_placement { s/^.*$/place_ship::result = error/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::set_error/ ; x b set :place_ship::set_error b return } :place_ship::place s/^\([01,]*\)$/\1\nplace_ship::result = \1/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::update_ship_placement/ ; x b set :place_ship::update_ship_placement # Обновляем поле :place_ship::loop /^,/ { # Mark ship cell s/$/\nset_symbol::symbol = H/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::set_ship_symbol/ ; x b set :place_ship::set_ship_symbol s/\([01]*\)$/\1\nset_symbol::number = \1/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::set_cell_number/ ; x b set :place_ship::set_cell_number s/$/\nset_symbol::string = place_ship::field/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::set_field/ ; x b copy :place_ship::set_field x ; s/\(call_stack = [^\n]*\)/\1,place_ship::mark_ship/ ; x b set_symbol :place_ship::mark_ship s/$/\nplace_ship::field = set_symbol::result/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::update_field/ ; x b copy :place_ship::update_field # Маркируем соседей по диагонали s/\([01]*\)$/\1\nmark::coordinates = \1/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::prepare_diagonal_mark_coordinates/ ; x b set :place_ship::prepare_diagonal_mark_coordinates s/$/\nmark::field = place_ship::field/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::prepare_diagonal_mark_field/ ; x b copy :place_ship::prepare_diagonal_mark_field x ; s/\(call_stack = [^\n]*\)/\1,place_ship::mark_diagonal_cross/ ; x b mark_diagonal_cross :place_ship::mark_diagonal_cross s/$/\nplace_ship::field = mark::field/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::set_diagonal_cross/ ; x b copy :place_ship::set_diagonal_cross s/,[01]*$// # Удаляем обработанную координату b place_ship::loop } # Маркируем соседей по горизонтали и вертикали s/$/\nmark::ship = place_ship::result/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::set_ship_to_circle/ ; x b copy :place_ship::set_ship_to_circle x ; s/\(call_stack = [^\n]*\)/\1,place_ship::mark_circle/ ; x b mark_circle :place_ship::mark_circle s/$/\nplace_ship::field = mark::field/ x ; s/\(call_stack = [^\n]*\)/\1,place_ship::set_circle/ ; x b copy :place_ship::set_circle b return }
Расстановка кораблей игрока
:set_player_ships { s/$/ships_to_place = ,1,1,1,1,2,2,2,3,3,4/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::create_ships/ ; x b set :set_player_ships::create_ships :set_player_ships::place_loop { x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::draw/ ; x b draw :set_player_ships::draw s/^.*$/ships_to_place/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::get_ships_to_place/ ; x b get :set_player_ships::get_ships_to_place s/^\n$// /^$/ b set_player_ships::end_placing s/\([^\n]*\),\([0-9]*\)$/\2\nships_to_place = \1/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::remove_ship/ ; x b set :set_player_ships::remove_ship :set_player_ships::got_ship s/^\(.*\)$/You're placing a ship of length \1. Set its upper left square and its orientation.\nE.g. 'B5V' (vertical) or 'G2H' (horizontal)\n/ ; p s/^.*length \([0-9]\).*$/\1/ N # Первичная валидация инпута /\n[A-Ja-j][0-9][hvHV]$/ b set_player_ships::valid_placement :set_player_ships::invalid_placement { s/$/\nships_to_place/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::prepare_ship_to_return/ ; x b get :set_player_ships::prepare_ship_to_return s/\([0-9]\)\n[^\n]*\n\([,0-9]*\)$/ships_to_place = \2,\1/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::return_ship/ ; x b set :set_player_ships::return_ship s/$/\nlast_action_log = Cannot place a ship. Try again./ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::log_invalid_place/ ; x b set :set_player_ships::log_invalid_place b set_player_ships::place_loop } :set_player_ships::valid_placement { s/$/\nlast_action_log = Place yet another ship./ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::log_valid_place/ ; x b set :set_player_ships::log_valid_place s/\([1-4]\)\n\(..\)\(.\)$/\1\n\3\nconvert_coordinates::coordinates = \2/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::prepare_coordinate_conversion/ ; x b set :set_player_ships::prepare_coordinate_conversion # Конвертируем координаты x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::convert_coordinates/ ; x b convert_coordinates :set_player_ships::convert_coordinates s/$/\nconvert_coordinates::result/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::set_start_coordinate/ ; x b get :set_player_ships::set_start_coordinate # Подготовим значения переменных для задания s/\([1-4]\)\n\(.\)\n\([01]*\)$/place_ship::field = field::player\nplace_ship::length = \1\nplace_ship::coordinates = \3\nplace_ship::orientation = \2/ # Задаем все переменные x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::set_orientation/ ; x b set :set_player_ships::set_orientation x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::set_coordinates/ ; x b set :set_player_ships::set_coordinates x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::set_length/ ; x b set :set_player_ships::set_length x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::set_field/ ; x b copy :set_player_ships::set_field x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::place_ship/ ; x b place_ship :set_player_ships::place_ship s/$/\nplace_ship::result/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::get_placing_result/ ; x b get :set_player_ships::get_placing_result /error/ { s/^.*$/\nplace_ship::length/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::handle_placing_error/ ; x b get :set_player_ships::handle_placing_error s/$/\n/ b set_player_ships::invalid_placement } s/$/\nfield::player = place_ship::field/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::update_field/ ; x b copy :set_player_ships::update_field s/$/\nfield::player_ships/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::get_player_ships/ ; x b get :set_player_ships::get_player_ships s/^,\([01,]*\)\n\([^\n]*\)$/field::player_ships = \2 \1/ s/ / / # Hack for the first replacement x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::update_player_ships/ ; x b set :set_player_ships::update_player_ships } b set_player_ships::place_loop } # Превращаем волны обратно в точки :set_player_ships::end_placing s/^.*$/field::player/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::prepare_final_update/ ; x b get :set_player_ships::prepare_final_update s/~/·/g s/^\n*\(.*\)$/\nfield::player = \1/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::final_update/ ; x b set :set_player_ships::final_update s/$/\nlast_action_log = Let the battle begin!/ x ; s/\(call_stack = [^\n]*\)/\1,set_player_ships::log_game_start/ ; x b set :set_player_ships::log_game_start b return }
Реализуем заодно и выбор уровня сложности. В качестве оммажа третьему Квейку я назвал уровни сложности как в нем: I can win ; Bring it on ; Hurt me plenty ; Hardcore ; Nightmare!
Выбор уровня сложности
:select_difficulty { s/^.*$/\c[[H\c[[J=== Sed Battleship by Red Void ===\n\nSelect a difficulty level:\n1. I can win\n2. Bring it on\n3. Hurt me plenty\n4. Hardcore\n5. Nightmare!\n/ p n /^[1-5]$/ { s/1/001/ ; s/2/010/ ; s/3/011/ ; s/4/100/ ; s/5/101/ s/\([01]*\)$/\nluck = 00000000000000000000000000000\1/ x ; s/\(call_stack = [^\n]*\)/\1,select_difficulty::set_luck/ ; x b set_variable :select_difficulty::set_luck b return } s/^.*$// b select_difficulty }
Расстановка кораблей оппонента
Как может в казино быть колода разложена в другом порядке?! Ты че, бредишь, что ли?!
Джон фон Нейман
Расстановка кораблей — это первое, что делает компьютер. Поэтому инициализировать генератор случайных чисел нам нужно до этого момента. Пришло, наконец, время изобрести источник энтропии, то бишь, метод получения начального значения ГСЧ. Однако у нас нет ничего, что могло бы отличаться от игры к игре, кроме одного — расстановки кораблей игрока и выбора уровня сложности. Так что эта последовательность бит и будет нашим сидом.
В принципе, можно придумать и другие методы. Например, в мастермайнде, исполненном на sed, загаданная строка определяется тем, что игрок настучит на клавиатуре (в предположении, что это более-менее случайная строка). Как вариант, можно было бы добавить к сиду еще и первый выстрел игрока, как это делается в классическом сапере для Виндовса. Но от этих затей я отказался в пользу UX. Первый вариант отпал, потому что я считаю, что у игрока не должно быть необходимости делать что-то кроме того, что требуется непосредственно для игры. Да и многие ли будут играть в эту реализацию настолько часто, что запомнят расстановку? :) Второй же вариант предполагает достаточно долгое ожидание после первого выстрела, пока оппонент не расставится. Да и само по себе это странно: игрок уже выстрелил, а противник еще корабли не расставил. Хватит с нас читерского алгоритма стрельбы :) Но если кого-то не устраивает такой подход к сидированию, в функцию rng_seed вполне можно встроить любую логику. Смело форкайте (с соблюдением лицензии!) и дорабатывайте.
На практике с выбранным подходом возникла неожиданная сложность. Поле представляет собой 100 бит, а удача — еще 32. Сид же нам нужен не больше параметра M, который укладывается в 20 бит. Недолго думая, я решил просто взять остаток от деления всей 132-битной колбасы на M. Но внезапно оказалось, что делит такие числа наш алгоритм уже с изрядным напряжением: приходилось по несколько секунд слушать, как компьютер пытается взлететь на вентиляторах, не в силах терпеть царящую в коде вакханалию. И это одно лишь сидирование, к расстановке мы еще и не приступали!
Что с этим можно сделать? Напрашивается вариант сжатия. Например, 100-битная расстановка очень просто сжимается до 76 бит: достаточно выделить по 8 бит на длинные корабли и по 7 бит — на катера. Тогда 7 бит будут хранить номер верхней левой клетки корабля, а дополнительный бит — ориентацию. Более продвинутые алгоритмы могут обеспечить и лучшее сжатие, но они нам, скорее всего, не подойдут: изначальная проблема была в быстродействии, и если сам алгоритм сжатия будет сложным, его исполнение съест все выигранное на делении время. Поэтому остановимся все же на 76 битах. Сжатие параметра удачи зависит от того, какая планируется расширяемость. Давайте выделим 4 бита: это позволит увеличивать удачу вплоть до 15. С учетом того, что самый сложный уровень в нашей реализации имеет удачу 5, думаю, что этого должно быть достаточно. В итоге получаем 80 бит, что, с учетом квадратичной сложности деления в столбик, значительно лучше, чем изначальные 132.
Я также обдумывал и даже реализовал вариант с хеш-функцией Пирсона: на сложениях и xor’ах можно было бы получить число-сид очень быстро по сравнению с делением. К сожалению, в экспериментах этот алгоритм хеширования дал очень плохую равномерность распределения сидов, поэтому от него пришлось отказаться. Сказать, что мне жаль — ничего не сказать: я ужасно хотел включить в программу вишенкой на торте еще и алгоритм хеширования :) Но не судьба, остановимся на делении сжатых чисел. Итоговый вариант процедуры сидирования получился таким:
Сидирование ГСЧ
:rng_seed { s/$/\nfield::player_ships/ x ; s/\(call_stack = [^\n]*\)/\1,rng_seed::get_rng_seed_parts_1/ ; x b get :rng_seed::get_rng_seed_parts_1 # Сжатие без потерь: 100 -> 76 битов s/\([^01,][01]\{6\}0\)\(,[01]\{6\}1\)[^ ]*/\11/g s/\([^01,][01]\{6\}1\)\(,[01]\{6\}0\)[^ ]*/\11/g s/\([^01,][01]*\),[01,]*/\10/g s/$/\nluck/ x ; s/\(call_stack = [^\n]*\)/\1,rng_seed::get_rng_seed_parts_2/ ; x b get :rng_seed::get_rng_seed_parts_2 # Сжатие с потерями (но только в теории): 32 -> 4 бита s/[01]*\([01]\{4\}\)$/\1/ s/[\n ]//g s/$/\nrng::M/ x ; s/\(call_stack = [^\n]*\)/\1,rng_seed::get_rng_seed_parts_3/ ; x b get :rng_seed::get_rng_seed_parts_3 # В делимом у нас 80 битов, а в делителе 32 # Поэтому добавим в делитель еще 48 ведущих нулей s/\([01]*\)\n\([01]*\)$/div::first_operand = \1\ndiv::second_operand = 000000000000000000000000000000000000000000000000\2/ x ; s/\(call_stack = [^\n]*\)/\1,rng_seed::set_divisor/ ; x b set :rng_seed::set_divisor x ; s/\(call_stack = [^\n]*\)/\1,rng_seed::set_dividend/ ; x b set :rng_seed::set_dividend x ; s/\(call_stack = [^\n]*\)/\1,rng_seed::perform_division/ ; x b div :rng_seed::perform_division s/^.*$/div::mod_result/ x ; s/\(call_stack = [^\n]*\)/\1,rng_seed::get_raw_seed/ ; x b get :rng_seed::get_raw_seed s/.*\([01]\{32\}\)$/rng::current_state = \1/ x ; s/\(call_stack = [^\n]*\)/\1,rng_seed::set_seed/ ; x b set :rng_seed::set_seed b return }
Теперь встает вопрос об алгоритме расстановки. Самый простой алгоритм — брать корабли по убыванию длины и случайно генерировать их ориентацию и положение левой верхней клетки. Если очередной корабль на поле разместить не выходит, повторяем процедуру. Такой алгоритм легко позволит нам определить все клетки, на которых расположены корабли. Памятуя о том, насколько сложна обработка ходов игрока и компьютера, это очень важно.
Насколько этот алгоритм хорош с точки зрения равномерного распределения? Честно говоря, бывает и получше. Дело в том, что настоящая равномерная случайность должна нам гарантировать примерно равную вероятность заполнения любой клетки, а также примерно равную вероятность горизонтальной или вертикальной ориентации линкоров, крейсеров и эсминцев. Однако в эксперименте выяснилось, что если перебрать по разу все возможные сиды, то с ориентацией-то все в порядке, но при этом существуют клетки, вероятности нахождения кораблей в которых отличаются от среднего аж на 11-13%. Но в три сигмы эти значения все же укладываются, так что я решил пойти на этот компромисс. Кроме того, более сложный (пусть и более равномерный) алгоритм почти наверняка бы медленнее работал, а он и так нетороплив.
Чтобы игрок не заскучал, после выставления очередного корабля я решил отрисовывать поле заново и давать какую-то видимость того, что что-то происходит. Поэтому перед переходом к расстановке компьютера мы инициализируем лог действий предупреждением, что сейчас придется подождать, а потом прибавляем к нему точки после каждой попытки — удачной или неудачной.
Вся остальная логика — это просто копипаста логики расстановки кораблей игрока с двумя отличиями. Во-первых, координата клетки получается сразу числом от 0 до 99, поэтому этап первичной валидации строки становится не нужен. Во-вторых, если на поле игрока мы в конце превращали волны в точки, то на поле противника нужно сделать наоборот — превратить точки в волны. После внесения этих изменений оставалось просто заменить названия меток:
Расстановка кораблей оппонента
:set_computer_ships { s/$/ships_to_place = ,1,1,1,1,2,2,2,3,3,4/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::create_ships/ ; x b set :set_computer_ships::create_ships :set_computer_ships::place_loop { x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::draw/ ; x b draw :set_computer_ships::draw s/^.*$/ships_to_place/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::get_ships_to_place/ ; x b get :set_computer_ships::get_ships_to_place s/^\n$// /^$/ b set_computer_ships::end_placing s/\([^\n]*\),\([0-9]*\)$/\2\nships_to_place = \1/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::remove_ship/ ; x b set :set_computer_ships::remove_ship :set_computer_ships::got_ship # Генерируем случайное число x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::gen_random/ ; x b rng :set_computer_ships::gen_random s/$/\nrng::current_state/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::get_random/ ; x b get :set_computer_ships::get_random s/0$/\nH/ ; s/1$/\nV/ s/[01]*\([01]\{20\}\)\n\([HV]\)$/\2\ndiv::first_operand = \1\ndiv::second_operand = 00000000000001100100/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::prepare_coordinate_conversion_1/ ; x b set :set_computer_ships::prepare_coordinate_conversion_1 x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::prepare_coordinate_conversion_2/ ; x b set :set_computer_ships::prepare_coordinate_conversion_2 x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::convert_coordinates/ ; x b div :set_computer_ships::convert_coordinates s/$/\ndiv::mod_result/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::get_coordinates/ ; x b get :set_computer_ships::get_coordinates s/[01]*\([01]\{7\}\)$/\1/ b set_computer_ships::valid_placement :set_computer_ships::invalid_placement { s/$/\nships_to_place/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::prepare_ship_to_return/ ; x b get :set_computer_ships::prepare_ship_to_return s/\([0-9]\)\n[^\n]*\n\([,0-9]*\)$/ships_to_place = \2,\1/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::return_ship/ ; x b set :set_computer_ships::return_ship b set_computer_ships::place_loop } :set_computer_ships::valid_placement { s/$/\nlast_action_log/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::get_current_log/ ; x b get :set_computer_ships::get_current_log s/\([^\n]*\)$/last_action_log = \1./ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::log_valid_place/ ; x b set :set_computer_ships::log_valid_place # Конвертируем длину, готовим переменные для задания значений s/\([1-4]\)\n\(.\)\n\([01]*\)$/place_ship::field = field::computer\nplace_ship::length = \1\nplace_ship::coordinates = \3\nplace_ship::orientation = \2/ # Задаем все переменные x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::set_orientation/ ; x b set :set_computer_ships::set_orientation x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::set_coordinates/ ; x b set :set_computer_ships::set_coordinates x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::set_length/ ; x b set :set_computer_ships::set_length x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::set_field/ ; x b copy :set_computer_ships::set_field x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::place_ship/ ; x b place_ship :set_computer_ships::place_ship s/$/\nplace_ship::result/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::get_placing_result/ ; x b get :set_computer_ships::get_placing_result /error/ { s/^.*$/\nplace_ship::length/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::handle_placing_error/ ; x b get :set_computer_ships::handle_placing_error s/$/\n/ b set_computer_ships::invalid_placement } s/$/\nfield::computer = place_ship::field/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::update_field/ ; x b copy :set_computer_ships::update_field s/$/\nfield::computer_ships/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::get_computer_ships/ ; x b get :set_computer_ships::get_computer_ships s/^,\([01,]*\)\n\([^\n]*\)$/field::computer_ships = \2 \1/ s/ / / # Hack for the first replacement x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::update_computer_ships/ ; x b set :set_computer_ships::update_computer_ships } b set_computer_ships::place_loop } # Превращаем оставшиеся точки в волны :set_computer_ships::end_placing s/^.*$/field::computer/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::prepare_final_update/ ; x b get :set_computer_ships::prepare_final_update s/·/~/g s/^\n*\(.*\)$/\nfield::computer = \1/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::final_update/ ; x b set :set_computer_ships::final_update s/$/\nlast_action_log = Let the battle begin!/ x ; s/\(call_stack = [^\n]*\)/\1,set_computer_ships::log_game_start/ ; x b set :set_computer_ships::log_game_start b return }
И теперь, когда логика игры полностью готова, мы можем смело написать короткую main-функцию, которая будет идти сразу после инициализации переменных. Ее можно уже не прятать под спойлер :)
:main { x ; s/\(call_stack = [^\n]*\)/\1,main::select_difficulty/ ; x b select_difficulty :main::select_difficulty x ; s/\(call_stack = [^\n]*\)/\1,main::set_player_ships/ ; x b set_player_ships :main::set_player_ships s/$/\nlast_action_log = Please wait for a while: I need to place my ships like a pro./ x ; s/\(call_stack = [^\n]*\)/\1,main::log_ships_placement/ ; x b set :main::log_ships_placement x ; s/\(call_stack = [^\n]*\)/\1,main::draw/ ; x b draw :main::draw x ; s/\(call_stack = [^\n]*\)/\1,main::rng_seed/ ; x b rng_seed :main::rng_seed x ; s/\(call_stack = [^\n]*\)/\1,main::set_computer_ships/ ; x b set_computer_ships :main::set_computer_ships b player_move }
Финальные штрихи
Стиль держит меня на плаву поплавком.
Коко Шанель
Пришло время объединить все реализованные шаги. Нам потребуется инициализировать несколько переменных, выбрать уровень сложности, расставить свои корабли и корабли противника. После этого передадим управление в функцию player_move, которая после каждого промаха будет переключаться на computer_move.
Играть можно уже сейчас, все работает! Но игра все еще выглядит не слишком профессионально: остаются артефакты от предыдущих ходов, обои скучные. Давайте это исправим.
Для начала научимся очищать экран. Для этого нам потребуются ANSI-коды. Среди них нас интересует код перемещения курсора (CSI n ; m H) и код очистки экрана (CSI n J). Поэкспериментировав с ними, получим, что чтобы очистить экран, достаточно выполнить команду:
s/^.*$/\c[[H\c[[J/ ; p
Вставим эту команду перед выводом полей во время отрисовки, а также в меню выбора уровня сложности.
Теперь после каждого хода поле отрисовывается заново, а играем мы фактически в полноэкранном режиме :) Сделаем теперь поля не такими однообразными: пусть координаты будут пожирнее, а клетки самого поля — разноцветными. Волны покрасим в синий цвет, здоровые клетки кораблей — в зеленый, подбитые — в желтый, а затопленные — в красный. Красным также выделим последний уровень сложности в меню: Nightmare есть Nightmare!
Кроме того, добавим в функцию отрисовки вывод переменной last_action_log, через которую мы сообщали игроку полезную информацию.
Стилизованная отрисовка
:draw { s/^.*$// s/$/\nfield::player/ x ; s/\(call_stack = [^\n]*\)/\1,draw::get_field1/ ; x b get :draw::get_field1 s/$/\nfield::suggestions/ x ; s/\(call_stack = [^\n]*\)/\1,draw::get_field2/ ; x b get :draw::get_field2 s/\([·~HDX]\)/\1 /g s/$/\n | \c[[1m0 1 2 3 4 5 6 7 8 9\c[[0m | \c[[1m0 1 2 3 4 5 6 7 8 9\c[[0m/ s/$/\n---+-------------------- ---+--------------------/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ \c[[1mA\c[[0m | \1 \c[[1mA\c[[0m | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ \c[[1mB\c[[0m | \1 \c[[1mB\c[[0m | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ \c[[1mC\c[[0m | \1 \c[[1mC\c[[0m | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ \c[[1mD\c[[0m | \1 \c[[1mD\c[[0m | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ \c[[1mE\c[[0m | \1 \c[[1mE\c[[0m | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ \c[[1mF\c[[0m | \1 \c[[1mF\c[[0m | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ \c[[1mG\c[[0m | \1 \c[[1mG\c[[0m | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ \c[[1mH\c[[0m | \1 \c[[1mH\c[[0m | \3/ s/^\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\([·~HDX ]\{20\}\)\([·~HDX ]*\)\n\(.*\)$/\2\n\4\n\5\ \c[[1mI\c[[0m | \1 \c[[1mI\c[[0m | \3/ s/^\([·~HDX ]\{20\}\)\n\([·~HDX ]\{20\}\)\n\(.*\)$/\3\ \c[[1mJ\c[[0m | \1 \c[[1mJ\c[[0m | \2/ # Раскрасим элементы поля s/ D/ \c[[33mD\c[[0m/g s/ X/ \c[[31mX\c[[0m/g s/ H/ \c[[32mH\c[[0m/g s/ ~/ \c[[34m~\c[[0m/g # Добавим лог последних действий s/$/\n\nlast_action_log/ x ; s/\(call_stack = [^\n]*\)/\1,draw::get_log/ ; x b get :draw::get_log # Отформатируем лог: заменим подчеркивания на переносы s/_/\n/g # Добавим еще одну линию, очистку экрана и заголовок s/$/\n/ s/^/\c[[H\c[[J === Sed Battleship by Red Void ===\n\n/ # Выведем поля p # Снова очистим pattern space s/^.*$// b return }

Наконец, последняя деталь. Поскольку расстановка противника целиком зависит от действий игрока, есть шанс, что отдельные хитрые игроки этим воспользуются и предварительно разведают все корабли противника, после чего будет выносить самого злого оппонента с первого же хода. С жульем, допустим, надо бороться. Поэтому я встроил в игру пасхалку. Для ее активации нужно с самого начала сделать десять попаданий подряд. Код пасхалки разбирать тут не буду, чтобы не портить сюрприз, тем более, что алгоритм там крайне простой по сравнению с тем, что уже было реализовано.
Заключение
Бог создал регулярные выражения, все остальное — дело рук человека.
Леопольд Кронекер
Мы прошли долгий и интересный путь. Начав с прибавления к двоичному числу единицы, мы научились складывать и вычитать, умножать и делить, а также генерировать псевдослучайные числа. При помощи обмена данными со вспомогательным буфером и обычного goto мы реализовали переменные и функции. Сделали анализ хода игрока и запрограммировали соответствующие изменения на игровом поле. Используя реализованную арифметику, мы научились делать ходы со стороны противника в зависимости от уровня сложности. Научились почти непредсказуемо расставлять корабли противника, используя генератор случайных чисел. И черт возьми, красиво все это отрисовывать. Неплохо для потокового текстового редактора, основная задача которого — заменять одну регулярку на другую, а?
Зачем нужно было вообще писать эту жуть, да еще и вручную в нашу проклятую эпоху кодогенерации? Ну, я воспринимал этот процесс как создание произведения искусства. И я доволен результатом: думаю, у меня получилось.
В этом месте полагается сказать: «Как же это было легко!» :) Но не буду лукавить, сил, времени и размышлений на этот magnum opus было потрачено изрядно. И все же я не жалею ни о единой потраченной минуте. Надеюсь, вам понравилось читать об этом проекте настолько же, насколько мне — писать его.
Хочу сказать большое спасибо:
Kusanagi Mitsuhisa (mikkun), у которого я подсмотрел классную интерфейсную идею в реализации сапера;
Aurelio Jargas (aureliojargas), который сделал форматтер для sed-скриптов (а также сокобан на sed);
Laurent Le Brun (laurentlb), который придумал любопытный подход к генерации псевдослучайных последовательностей в мастермайнде;
Евгению Степанищеву (bolknote), который написал на sed шахматы, в которые можно играть с компьютером;
Всем, чьи цитаты для эпиграфов я приписывал, переиначивал и бессовестно выдумывал;
И last but not least — моей жене, которая меня поддерживала и вдохновляла на протяжении всей реализации этого проекта.
Полный текст игры лежит в проекте на гитхабе, можно склонировать и поиграть.
P.S. Как же это было легко!
Комментарии (8)

wataru
05.06.2026 10:50Жесть. Мое увожение, но жесть. Не рассматривали ли вы вариант написать что-то вроде компилятора? Вот вы умеете вызвать функции, работать с переменными и выполнять арифметику. Текст на самопальном языке программирования было бы гораздо проще понимать и отлаживать.

red_void Автор
05.06.2026 10:50Да, я над этим думал, но перманентно недооценивал объем оставшейся работы. Каждый раз думал — блин, ну чуть-чуть же осталось, компилятор дольше писать буду.
К моменту, когда я реализовал логику ходов и оставались только расстановки, это даже оказалось правдой)
unreal_undead2
Ждём DOOM на регулярках )
Не знал про бранчи в sed, действительно полноценный язык (хотя на практике, конечно, проще с awk).
alhimik45
Ещё кстати из таких штук bc есть тоже со своим языком. Я как-то через gparted live на ноутбуке разделы двигал на медленном HDD, в дистрибутиве дров на wifi и соответственно интернета не было, так что чтобы себя занять написал бота для крестики колики. В принципе сложностью была не логика, bc тоже проще sed в плане описания логики, а то что bc я никогда до этого не трогал и из справочного материала был только man.
unreal_undead2
Там хотя бы арифметика сразу есть )
red_void Автор
О, если вы на bc программировали, то вам еще вот эта штука может понравиться: https://habr.com/ru/articles/1018738/
alhimik45
Ухх, обратная польская… Я обожаю концепцию форта и ему подобных. Ещё школотроном написал свой интерпретатор форт-подобного языка, с двумя дополнительными стеками чтобы попроще жонглировать значениями было xD. В универе как-то имплементил DES шифрование на Factor. Но господи, как же всё-таки неудобно читать такой код. Мейнстримные языки или даже лисп с префиксной записью настолько более читабельны, что остальные возможности стековых/конкатенативных языков не перевешивают этого минуса
red_void Автор
С DOOM сложно: поскольку в sed любой интерактив должен заканчиваться нажатием Enter'а, придется дополнительную обвязку на шелле сооружать для того, чтобы игра вела себя как шутер. Так в реализациях arkanoid и flappy bird на sed делали.
Но в целом ничего невозможного нет :D