Однажды захотелось мне написать Minesweeper… на батниках. И я его написал.
Встречайте!!! Minesweeper for cmd.exe
Итак, особенности данного продукта:
- Оригинальное лого
- Двухцветный текстовой графический интерфейс (фон — чёрный, текст — серый)
- Возможность воспроизведения программы практически на любом компьютере
Скриншот игры.
Любая серьезная bat-программа не должна оставлять после себя следов, типа лишних переменных. Для этого используются команды setlocal (в начале) и endlocal (в конце). Больше информации: setlocal /?.. Ладно, перейдем к самой игре.
В первую очередь мы создадим два массива — реальное и видимые поля. Так как массивов нет, то будем импровизировать — создадим кучу переменных вида mfield34 и rfield 69. Для этого мы будем использовать цикл for.
Вообще, for помогает в решении большого кол-ва задач. Подробнее вы сможете прочитать выполнив cmd.exe -> 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=?
Теперь нужно проставить бомбы на поле. Для этого используем переменную %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.: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
Теперь нам необходимо проставить числа во всех клеточках, не заполненных бомбами. С помощью цикла 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
- не все флаги проставленны правильно;
- сумма проставленных флагов и неоткрытых клеточек не равна кол-ву бомб;
После отработки такой процедуры в переменной nopoints будет содержаться кол-во пустых флагов, а в rflags — кол-во правильно проставленных флагов.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
Естественно, я расписал не весь код, а только его часть. Сам код можно посмотреть здесь: Google Docs, html или здесь: Plain Text
P.S: Если будут вопросы — почему так, а не иначе — задавайте, я отвечу. Я, к сожалению, не каждую строку расписал, а только необходимые (на мой взгляд). Если вам что-то не понятно — задавайте вопросы. Также прошу прощения за мой английский и имена переменных/процедур.
P.P.S: Jabber на батниках поддерживать лень, поэтому я его забросил :-)
UPD1: Исправления:
- Улучшенная генерация поля
- Невозможность умереть на первом ходе
- Уменьшено количество бомб до 17
- Добавлена секретная команда 'r', выводящая таблицу рекордов (файл records.log)
UPD2: Исправления:
- Дополнен принцип ввода команды: теперь можно ввести только координаты, чтобы открыть клеточку.
- Изменен принцип открытия клеток
- Знаки вопроса (?) заменены на точку — "."
UPD3: Исправления:
- Исправлена проблема с records.log