Свой сапер на своих батниках

:

image

Однажды захотелось мне написать Minesweeper… на батниках. И я его написал.

Встречайте!!! Minesweeper for cmd.exe

Итак, особенности данного продукта:

  • Оригинальное лого
  • Двухцветный текстовой графический интерфейс (фон — чёрный, текст — серый)
  • Возможность воспроизведения программы практически на любом компьютере
В общем, это настоящий сапер (а не те жалкие подобия — KMines и сапер for Windows) для настоящих мужчин. И далее вы сможете прочитать как сделать свой крутой сапер.

image
Скриншот игры.

Любая серьезная bat-программа не должна оставлять после себя следов, типа лишних переменных. Для этого используются команды setlocal (в начале) и endlocal (в конце). Больше информации: setlocal /?.. Ладно, перейдем к самой игре.

В первую очередь мы создадим два массива — реальное и видимые поля. Так как массивов нет, то будем импровизировать — создадим кучу переменных вида mfield34 и rfield 69. Для этого мы будем использовать цикл for.

for /L %%x in (1,1,9) do for /L %%y in (1,1,9) do set mfield%%x%%y=?
for /L %%x in (1,1,9) do for /L %%y in (1,1,9) do set rfield%%x%%y=?
Вообще, for помогает в решении большого кол-ва задач. Подробнее вы сможете прочитать выполнив cmd.exe -> for /?

Теперь нужно проставить бомбы на поле. Для этого используем переменную %random% (содержит десятичное число между 0 и 32767) и строки расширения. Подробности: cmd.exe -> set /?..

set /a bx=%random:~-2%
set bx=%bx:~0,1%
Объяснения:
1. set /a используется для использования переменной как числа и выполнения арифметических операций (опять же, читаем cmd.exe -> set /?)
2. %random:~-2% — число между 0 и 99
3. %bx:~0,1% — первая цифра получившегося бреда.

Теперь пытаемся создать новую бомбу (и добавить еденицу к счетчику бомб) с помощью команды call, т.е. вызвем другую процедуру и передадим ей два параметра: содержание переменной и имя переменной (переменная — часть массива). Кстати, содержание переменной можно получить только с помощью команды call (пример: call: процедура %%переменная%счетчик1%%счетчик2%%%). Кстати, REM — это комментарий в бат-файлах.

:genbomb
call :newbomb %%rfield%r1%%r2%%% rfield%r1%%r2%
REM Проверяем на кол-во бомб. Если равно максимальному, то выходим из процедуры.
if "%bombs%" == "%maxbombs%" goto:eof

:newbomb
if not "%1"=="X" (
	set %2=X
	set /a bombs=%bombs%+1
	)
REM goto:eof - это быстрый возрат из процедуры.
goto:eof 
Также необходимо вызвать процедуру :genbomb из цикла инициализации (т.е. при старте новой игры) с помощью call.

Теперь нам необходимо проставить числа во всех клеточках, не заполненных бомбами. С помощью цикла for вызываем специальную процедуру, которой передаем четыре параметра. В этой процедуре мы просто считаем количество стоящих рядом бомб. Необходимо учесть, что для точки (4;4) будет 8 соседей, а для точки (1;1) — всего три.

for /L %%x in (1,1,9) do for /L %%y in (1,1,9) do call :dosumfield %%x %%y %%rfield%%x%%y%% rfield%%x%%y

:dosumfield
REM Если в клеточке уже что-то есть (бомба), то выходим.
if not "%3"=="?" goto:eof
REM Устанавливаем координаты первой соседней клетки.
set /a x1=%1 - 1 
set /a y1=%2 + 1

REM ..... Пропускаем код .....

set sum=0
REM Если координаты первой точки входят в массив, то вызываем процедуру для увеличения счетчика кол-ва бомб
if %1 GTR 1 if %2 LSS 9 call :newsum %%rfield%x1%%y1%%%

REM ..... Пропускаем код .....

:newsum
if "%1"=="X" set /a sum+=1
goto:eof

Итак, поле у нас есть, теперь нужно его вывести. Опять же, необходимо использовать цикл for (ну, если вам скучно, то вы можете и вручную все прописать). Кстати, тут используется новая особенность команды call: первым аргументом можно ввести другую команду (echo, set). И поэтому не нужно создавать однострочные процедуры.

for /L %%y in (1,1,9) do call set line%%y=:%%y:  %%mfield%%y1%%  %%mfield%%y2%%  %%mfield%%y3%%  %%mfield%%y4%%  %%mfield%%y5%%  %%mfield%%y6%%  %%mfield%%y7%%  %%mfield%%y8%%  %%mfield%%y9%%  :%%y:
REM Все клеточки, не имеющие соседей-бомб не отображаем
for /L %%y in (1,1,9) do call set line%%y=%%line%%y:0= %%
echo %line0%
echo :---------------------------------:
for /L %%y in (1,1,9) do call echo %%line%%y%%
echo :---------------------------------:
echo %line0%

Теперь попытаемся считать команды пользователя. Первый символ — команда, второй и третий — координата. На всякий случай удалим все пробелы. Для того, чтобы исключить batch-injection (выполнение посторонних команд с помощью ввода текста) или просто смерть батника, работаем только с тремя символами. Также необходимо проверить, являются ли последние два символа — цифрами.

REM Заносим в input магическую строку, на случай если пользователь забьет на ввод данных.
set input=0 00
set /p "input=Input: "
set input=%input: =%
REM Первая буква - необходимое действие.
set action=%input:~0,1%

REM Пример действия
if "%action%"=="h" (
	cls
	REM Вызываем справку (call) и идем в цикл игры (goto)
	call :help
	goto:gamecycle
	)

REM Если первый символ не является командой, то рассказываем кое-что пользователю.
if not "%action%"=="q" if not "%action%"=="h" if not "%action%"=="o" if not "%action%"=="f" if not "%action%"=="n" call:errorIO2
REM Если второй и третий символ - не координаты, то назначим их равными нулю. 
set ix=0
set iy=0
for /L %%a in (1,1,9) do if "%%a"=="%input:~1,1%" set ix=%%a
for /L %%a in (1,1,9) do if "%%a"=="%input:~2,1%" set iy=%%a
REM Если второй/третий символ не является координатой, то выведем ему сообщение (call) и перейдем к игровому циклу.
if "%ix%"=="0" (
	call :errorIO1
	goto:gamecycle
	)
if "%iy%"=="0" (
	call :errorIO1
	goto:gamecycle
	)
REM Дальше идут команды требующие правильных координат

Теперь осталось расписать как открывать клетки поля. В сапере есть одна особенность: если рядом с клеткой 0 бомб, то открываем рядом стоящие клетки (не по диагонали). Т.е. необходимо использовать рекурсию.

REM Если команда пользователя - открыть клетку, то запускаем спец. процедуру с параметрами: координаты, значение клетки в реальном поле, значение клетки в видимом поле.
if "%action%"=="o" (
	call :openpoint %ix% %iy% %%rfield%ix%%iy%%% %%mfield%ix%%iy%%%
	goto:gamecycle
	)
REM ..... Пропускаем много кода .....

:openpoint
REM Если клетка не пуста - рассказываем пользователю много интересного, если в клетке бомба - уже поздно что-либо рассказывать.
if not "%4"=="?" (
	echo Point x=%1 y=%2 already opened
	pause>nul
	goto:eof
	)
if "%3"=="X" (
	REM Ставим переменную die в единицу, после чего делаем выводы. 
	set die=1
	for /L %%x in (1,1,9) do for /L %%y in (1,1,9) do call set mfield%%x%%y=%%rfield%%x%%y%%
	goto:eof
	)
REM А если ни то, ни другое, то пытаемся открыть эту и ближние клетки.
call :oaf %1 %2 %3 %4
goto:eof 

REM А вот как раз и рекурсивная функция
:oaf
REM Если клетка пуста (выход за пределы поля) или в ней бомба - уходим отсюда.
if "%3"=="" goto:eof
if "%3"=="X" goto:eof
REM Открываем данную клетку
call set mfield%1%2=%%rfield%1%2%%
REM Если в данной клетке 0 бомб, то пытаемся открыть все ближние клетки.
if not "%3" == "0" goto:eof

REM xn, yn - координаты следующей клетки. Диагональ не проверяется.
set /a xn=%1
set /a yn=%2 + 1
REM Проверка, открыта ли эта клетка на видимом поле.
set dooaf=0
call :checkoaf %%mfield%xn%%yn%%%
REM Если нет (doaf==1), то вызываем себя, но с другими координатами.
if %dooaf%==1 call :oaf %xn% %yn% %%rfield%xn%%yn%%% %%mfield%xn%%yn%%%

REM ..... Еще очень много кода .....
goto:eof

Всего у нас осталось две интересные вещи — установка флагов и проверка, выиграл ли пользователь. Первое не интересно, разве что стоит заметить, что процедура должна обеспечивать установку и снятие флага с поля одной командой.

Пользователь победил в двух случаях:

  • все флаги проставленны правильно;
  • сумма проставленных флагов и неоткрытых клеточек равна кол-ву бомб;
Пользователь не победил в одном из трех случаев:
  • количество флагов больше количества бомб;
    if %flags% GTR %maxbombs% goto:eof
  • не все флаги проставленны правильно;
  • сумма проставленных флагов и неоткрытых клеточек не равна кол-ву бомб;
Последние два варианта определяются так:
REM Считаем кол-во правильно поставленных флагов и не открытых клеточек.
set nopoints=0
set rflags=0
for /L %%x in (1,1,9) do for /L %%y in (1,1,9) do call :checkfo %%mfield%%x%%y%% %%rfield%%x%%y%%
REM ..... Здесь много кода .....
:checkfo
if "%1"=="?" set /a nopoints+=1
if "%1"=="!" if "%2"=="X" set /a rflags+=1
goto:eof
После отработки такой процедуры в переменной nopoints будет содержаться кол-во пустых флагов, а в rflags — кол-во правильно проставленных флагов.

Естественно, я расписал не весь код, а только его часть. Сам код можно посмотреть здесь: Google Docs, html или здесь: Plain Text

P.S: Если будут вопросы — почему так, а не иначе — задавайте, я отвечу. Я, к сожалению, не каждую строку расписал, а только необходимые (на мой взгляд). Если вам что-то не понятно — задавайте вопросы. Также прошу прощения за мой английский и имена переменных/процедур.
P.P.S: Jabber на батниках поддерживать лень, поэтому я его забросил :-)

UPD1: Исправления:

  • Улучшенная генерация поля
  • Невозможность умереть на первом ходе
  • Уменьшено количество бомб до 17
  • Добавлена секретная команда 'r', выводящая таблицу рекордов (файл records.log)

UPD2: Исправления:
  • Дополнен принцип ввода команды: теперь можно ввести только координаты, чтобы открыть клеточку.
  • Изменен принцип открытия клеток
  • Знаки вопроса (?) заменены на точку — "."

UPD3: Исправления:
  • Исправлена проблема с records.log