SNAKE.BAT 4.1 - An arcade style game using pure batch

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
dbenham
Expert
Posts: 2461
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

SNAKE.BAT 4.1 - An arcade style game using pure batch

#1 Post by dbenham » 13 Jul 2013 13:43

Important EDIT: I've decided to keep the most up-to-date version of SNAKE.BAT here at the top of this first post. A description of the most recent changes will be placed near the end of the thread. Below the code is the original post. Read the entire thread to get a sense of the evolution of the game..

Code: Select all

:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:: SNAKE.BAT - A pure native Windows batch implementation of the classic game
:: ------------------------------------------------------------------------------
:: Written by Dave Benham with some debugging help and technique pointers from
:: DosTips users - See http://www.dostips.com/forum/viewtopic.php?f=3&t=4741
::
:: The game should work on any Windows machine from XP onward using only batch
:: and native external commands. However, the default configuration will most
:: likely have some screen flicker due to the CLS command issued upon every 
:: screen refresh. There are two ways to eliminate screen flicker:
::
:: 1 - "Pure batch" via VT100 escape sequences:
:: You can eliminate flicker by enabling the VT100 mode within the game's
:: Graphic options menu. However, this mode requires a console that supports
:: VT100 escape sequences. This comes standard with Windows 10 (and beyond).
:: The Windows 10 console must be configured properly for this to work - the
:: "Legacy Console" option must be OFF. Prior to Windows 10, there was no
:: standard Windows console that supported VT100 escape sequences, though you
:: may find a utility that provides that support.
::
:: 2 - CursorPos.exe cheat from Aacini:
:: You can eliminate screen flicker on any Windows version by placing Aacini's
:: CursorPos.exe in the same folder that contains SNAKE.BAT. This method of
:: eliminating flicker is "cheating" in that it is not pure native batch since
:: it relies on a 3rd party tool. A script to create CursorPos.exe is available
:: at http://goo.gl/hr6Kkn.
::
:: Note that user preferences and high scores are stored in %USERPROFILE%\Snake
:: User saved games have an implicit .snake.txt "extension", and are saved and
:: loaded from the current directory.
::
:: Version History
::
:: 4.1  2018-09-08
::   - Fixed bug in Playfield too large error handling that aborted but hung.
::
:: 4.0  2017-04-10
::   - New Field size options, ranging from tiny to large. Original = Medium.
::   - Reconfigured menu
::   - Added support for VT100 mode to eliminate screen flicker by using
::     with "pure" batch VT100 escape sequences.
::
:: 3.8  2015-02-16
::   - Improve performance of Replay abort
::   - Eliminate flicker at game start when using CursorPos.exe
::   - Use symbols (variables) for lock, key and cmd streams.
::
:: 3.7  2014-08-03
::   - Reduced screen flicker when playing without CursorPos.exe by building
::     entire screen in one variable before CLS and ECHOing the screen.
::
:: 3.6  2014-04-09
::   - Pause after displaying CursorPos.exe message at end if game was launced
::     via double click or START menu.
::
:: 3.5  2014-02-03
::   - Made growth rate user configurable. High scores are now stored for each
::     growth rate played.
::   - Added optional support for Aacini's CursorPos.exe to eliminate screen
::     flicker.
::   - Redesigned storage of configuration options within saved games to make
::     it easier to extend in the future. Existing saved games are automatically
::     converted to the new format.
::   - Simplified replay abort mechanics.
::
:: 3.4  2013-12-26
::   - Added ability to abort a game replay.
::
:: 3.3  2013-12-24
::   - Added Pause functionality.
::
:: 3.2  2013-12-08
::   - Fixed a replay bug. Note that attempting to delete a non-existent file
::     does not raise an error!
::   - Added ability to save a previous game or a High score game to a user
::     named file in the current directory.
::   - Added ability to load and replay a user saved game from the current
::     directory.
::
:: 3.1  2013-12-08
::   - Fixed a bug with the game logs. Must include key mappings in log.
::     High scores from version 3.0 should be deleted from %USERPROFILE%\Snake.
::
:: 3.0  2013-12-07
::   - Made control keys user configurable, including option for 2 key
::     (left/right) or 4 key (left/right/up/down) input.
::   - Made graphics user configurable.
::   - Added ability to display replay of previous game.
::   - Added High Score list, with ability to display replay of High Score games.
::
:: 2.3  2013-12-01
::   - Added elapsed game time to the display.
::
:: 2.2  2013-08-06
::   - Improved comments / internal documentation
::   - A few inconsequential code changes
::
:: 2.1  2013-07-20
::   - Reworked interprocess communication. No more hanging games (I hope).
::   - Fixed parameterization of movement key definition.
::   - Temp file location now controlled by TEMP (or TMP) environment variable.
::   - Implemented a game session lock into temp file names so multiple game
::     instances can share the same TEMP folder without interference.
::
:: 2.0  2013-07-17
::   - First attempt at using XCOPY instead of CHOICE. Game now runs as
::     pure native batch on all Windows versions from XP onward.
::
:: 1.0  2013-07-13  to  1.x
::   - Game required CHOICE command, so did not work on XP without download of
::     a non-standard exe or com file.
::
:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

@echo off
if "%~1" == "startGame" goto :game
if "%~1" == "startController" goto :controller


::---------------------------------------------------------------------
:: setup some global variables used by both the game and the controller

setlocal disableDelayedExpansion
set "toggleVT100="
:getSession
if defined temp (set "tempFileBase=%temp%\") else if defined tmp set "tempFileBase=%tmp%\"
set "tempFileBase=%tempFileBase%Snake%time::=_%"
set "keyFile=%tempFileBase%_key.txt"
set "cmdFile=%tempFileBase%_cmd.txt"
set "gameLock=%tempFileBase%_gameLock.txt"
set "gameLog=%tempFileBase%_gameLog.txt"
set "signal=%tempFileBase%_signal.txt"
set "saveLoc=%userprofile%\Snake"
set "userPref=%saveLoc%\SnakeUserPref.txt"
set "hiFile=%saveLoc%\Snake!size!!growth!Hi"
set "keyStream=9"
set "cmdStream=8"
set "lockStream=7"


::------------------------------------------
:: Lock this game session and launch.
:: Loop back and try a new session if failure.
:: Cleanup and exit when finished

call :launch %lockStream%>"%gameLock%" || goto :getSession
del "%tempFileBase%*"
exit /b


::------------------------------------------
:launch the game and the controller

call :fixLogs
:relaunch
copy nul "%keyFile%" >nul
copy nul "%cmdFile%" >nul
start "" /b cmd /c ^""%~f0" startController %keyStream%^>^>"%keyFile%" %cmdStream%^<"%cmdFile%" 2^>nul ^>nul^"
cmd /c ^""%~f0" startGame %keyStream%^<"%keyFile%" %cmdStream%^>^>"%cmdFile%" ^<nul^"
echo(


::--------------------------------------------------------------
:: Upon exit, wait for the controller to close before returning

:close
2>nul (>>"%keyFile%" call )||goto :close
if "%=exitcode%" equ "00000002" (
  set "toggleVT100=1"
  goto :relaunch
) else if "%=exitcode%" equ "00000001" (
  echo Game play can be improved by installing
  echo Aacini's CursorPos.exe, available at
  echo http://goo.gl/hr6Kkn
  echo(
  echo Alternatively, if your console supports
  echo VT100 escape sequences, then you can
  echo enable VT100 mode within the SNAKE.BAT
  echo Graphic options menu.
  echo(
  echo %cmdcmdline%|find /i "%~f0">nul&&pause
)
exit /b 0


::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:game
title %~nx0
cls

::---------------------------------------
:: Default playfield size
:: max playing field: (width-2)*(height-2) <= 1365

set "size=Medium"
set "dispWidth=40"   max=99
set "dispHeight=25"  max=99
set "defaultSize=%size%"
set /a "defaultWidth=dispWidth, defaultHeight=dispHeight"

::----------------------------
:: Other default values

set "moveKeys=4"

set "up=W"
set "down=S"
set "left=A"
set "right=D"
set "pause=P"

set "space= "
set "bound=#"
set "food=+"
set "head=@"
set "body=O"
set "death=X"

set "vt0=OFF"
set "vt1=ON"
set "vt=0"

set "growth=1"

::--- sendCmd macro ---
:: sendCmd  command
:::  sends a command to the controller
set "sendCmd=>&%cmdStream% echo"

::---------------------------
:: Load user preferences
if exist "%userPref%" for /f "usebackq delims=" %%V in ("%userPref%") do set "%%V"
call :resize

::---------------------------
:: Variable constants

set "configOptions=diffCode difficulty growth moveKeys up down left right size dispWidth dispHeight"

for %%S in (
 "T Tiny   15 10"
 "S Small  25 15"
 "M Medium 40 25"
 "L Large  47 32"
 "W Wide   82 19"
 "N Narrow 20 40"
) do for /f "tokens=1-4" %%A in (%%S) do (
  set "size%%A=%%B"
  set /a "width%%A=%%C, height%%A=%%D"
)

set "spinner1=-"
set "spinner2=\"
set "spinner3=|"
set "spinner4=/"
set "spinner= spinner1 spinner2 spinner3 spinner4 "

set "delay1=20"
set "delay2=15"
set "delay3=10"
set "delay4=7"
set "delay5=5"
set "delay6=3"

set "desc1=Sluggard"
set "desc2=Crawl"
set "desc3=Slow"
set "desc4=Normal"
set "desc5=Fast"
set "desc6=Insane"

set "spinnerDelay=3"

:: define LF as a Line Feed (newline) character
set ^"LF=^

^" Above empty line is required - do not remove

:: define CR as a Carriage Return character
for /f %%A in ('copy /Z "%~dpf0" nul') do set "CR=%%A"

:: define BS as a BackSpace character
for /f %%A in ('"prompt $H&for %%B in (1) do rem"') do set "BS=%%A"

set "upper=A B C D E F G H I J K L M N O P Q R S T U V W X Y Z"
set "invalid=*~="

::---------------------------
:: define macros

if %vt% equ 1 (
  for /f "delims=" %%E in (
    'forfiles /p "%~dp0." /m "%~nx0" /c "cmd /c echo(0x1B"'
  ) do (
    cls
    <nul set /p "=%%E7"
    set "cls=<nul set /p "=%%E8""
    set "ClearLine=<nul set /p "=%%E[K""
    set "ClearPrev=echo(&echo(%%E[F%%E[K"
    set "Up4=echo(%%E[F%%E[F%%E[F%%E[F%%E[F"
    set "ShowCursor=<nul set /p "=%%E[?25h""
    set "HideCursor=<nul set /p "=%%E[?25l""
    set "exitCode=0"
  )
) else if exist "%~dp0CursorPos.exe" (
  set "cls=CursorPos 0 0"
  set "ClearLine=echo(                                   &CursorPos 0 -1"
  set "ClearPrev=CursorPos 0 -0&echo(                                   "
  set "Up4=CursorPos 0 -4"
  set "ShowCursor="
  set "HideCursor="
  set "exitCode=0"
) else (
  set "cls=cls"
  set "ClearLine="
  set "ClearPrev="
  set "Up4="
  set "ShowCursor="
  set "HideCursor="
  set "exitCode=1"
)

:: define a newline with line continuation
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"

:: setErr
:::  Sets the ERRORLEVEL to 1
set "setErr=(call)"

:: clrErr
:::  Sets the ERRORLEVEL to 0
set "clrErr=(call )"

:: getKey  [ValidKey]  [ValidKey...]
::: Check for keypress from the controller. Only accept a ValidKey.
::: Token delimiters and poison characters must be quoted.
::: Accept any key if no ValidKey specified.
::: Return result in Key variable. Key is undefined if no valid keypress.
set getKey=%\n%
for %%# in (1 2) do if %%#==2 (%\n%
  set key=%\n%
  set inKey=%\n%
  set keyTest=%\n%
  ^<^&%keyStream% set /p "inKey="%\n%
  if defined inKey (%\n%
    set inKey=!inKey:~0,-1!%\n%
    for %%C in (!args!) do set /a keyTest=1^&if /i !inKey! equ %%~C set key=!inKey!%\n%
  )%\n%
  if not defined keyTest set key=!inKey!%\n%
) else set args=


:: draw
:::  draws the board
set draw=%\n%
set screen=%\n%
for /l %%Y in (0,1,!height!) do set screen=!screen!!line%%Y!!LF!%\n%
set screen=!screen!Speed = !Difficulty! !replay!!LF!Growth Rate = !growth!   HighScore = !hi!!LF!Score = !score!   Time = !m!:!s!%\n%
if defined replay if not defined replayFinished (%\n%
  set screen=!screen!!LF!!LF!Press a key to abort the replay%\n%
)%\n%
%cls%^&echo(!screen!

:: test  X  Y  ValueListVar
:::  tests if value at coordinates X,Y is within contents of ValueListVar
set test=%\n%
for %%# in (1 2) do if %%#==2 (for /f "tokens=1-3" %%1 in ("!args!") do (%\n%
  for %%A in ("!line%%2:~%%1,1!") do if "!%%3:%%~A=!" neq "!%%3!" %clrErr% else %setErr%%\n%
)) else set args=


:: plot  X  Y  ValueVar
:::  places contents of ValueVar at coordinates X,Y
set plot=%\n%
for %%# in (1 2) do if %%#==2 (for /f "tokens=1-3" %%1 in ("!args!") do (%\n%
  set "part2=!line%%2:~%%1!"%\n%
  set "line%%2=!line%%2:~0,%%1!!%%3!!part2:~1!"%\n%
)) else set args=


::--------------------------------------
:: start the game
setlocal enableDelayedExpansion
if not exist "%saveLoc%\" md "%saveLoc%"
set "replay= Aborting... "
set "replayAvailable="
if exist "!gameLog!" set "replayAvailable=R"
call :loadHighScores
call :mainMenu


::--------------------------------------
:: main loop (infinite loop)
for /l %%. in () do (

  %=== check for and process abort signal if in replay mode ===%
  if defined replay if exist "%signal%" (
    del "%signal%"
    set "replayFinished=1"
    %draw%
    echo(
    %ClearLine%
    <nul set /p "=Aborting... "
    findstr "^" >nul <&%keyStream%
    for %%A in (!configOptions!) do set "%%A=!%%ASave!"
    %ShowCursor%
    call :mainMenu
  )

  %=== compute time since last move ===%
  for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do set /a "t2=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100, tDiff=t2-t1"
  if !tDiff! lss 0 set /a tDiff+=24*60*60*100

  if !tDiff! geq !delay! (
    %=== delay has expired, so time for movement ===%
    set /a t1=t2

    %=== compute game time ===%
    if not defined gameStart set "gameStart=!t2!"
    set /a "gameTime=(t2-gameStart)"
    if !gameTime! lss 0 set /a "gameTime+=24*60*60*100"
    set /a "gameTime=(gameTime-pauseTime)/100, m=gameTime/60, s=gameTime%%60"
    if !m! lss 10 set "m=0!m!"
    if !s! lss 10 set "s=0!s!"

    %=== get keypress ===%
    %getKey% !keys!
    if /i !key! equ !pause! (

      %=== pause game ===%
      echo(
      %ShowCursor%
      call :ask "PAUSED - Press a key to continue..."
      %HideCursor%
      %ClearPrev%
      %sendCmd% go
      for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do set /a "t2=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100, tDiff=t2-t1"
      if !tDiff! lss 0 set /a tDiff+=24*60*60*100
      set /a pauseTime+=tDiff

    ) else (

      %=== establish direction ===%
      if not defined replay (echo(!key!.) >>"!gameLog!"
      for %%K in (!key!) do if !moveKeys! equ 2 (
        set /a "xDiff=xTurn%%K*!yDiff!, yDiff=yTurn%%K*!xDiff!"
      ) else if "!%%KAxis!" neq "!axis!" (
        set /a "xDiff=xDiff%%K, yDiff=yDiff%%K"
        set "axis=!%%KAxis!"
      )

      %=== erase the tail ===%
      set "TX=!snakeX:~-2!"
      set "TY=!snakeY:~-2!"
      set "snakeX=!snakeX:~0,-2!"
      set "snakeY=!snakeY:~0,-2!"
      %plot% !TX! !TY! space

      %=== compute new head location and attempt to move ===%
      set /a "X=PX+xDiff, Y=PY+yDiff"
      set "X= !X!"
      set "Y= !Y!"
      set "X=!X:~-2!"
      set "Y=!Y:~-2!"
      (%test% !X! !Y! playerSpace) && (

        %=== move successful ===%

        %=== remove the new head location from the empty list ===%
        for %%X in ("!X!") do for %%Y in ("!Y!") do set "empty=!empty:#%%~X %%~Y=!"

        %=== eat any food that may be present ===%
        (%test% !X! !Y! food) && (
          %=== initiate growth ===%
          set /a grow+=growth

          %=== locate and draw new food ===%
          if defined replay (
            <&%keyStream% set /p "F="
          ) else (
            set /a "F=(!random!%%(emptyCnt-1))*6+1"
            (echo !F!) >>"!gameLog!"
          )
          for %%F in (!F!) do (%plot% !empty:~%%F,5! food)
        )

        if !grow! gtr 0 (
          %=== restore the tail ===%
          %plot% !TX! !TY! body
          set "snakeX=!snakeX!!TX!"
          set "snakeY=!snakeY!!TY!"
          set /a emptyCnt-=1

          %=== manage score ===%
          set /a "score+=1, grow-=1"
          if not defined replay if !score! gtr !hi! set /a "hi+=1, newHi=1"

        ) else (
          %=== add the former tail position to the empty list ===%
          set "empty=!empty!#!TX! !TY!"
        )

        %=== draw the new head ===%
        if defined snakeX (%plot% !PX! !PY! body)
        %plot% !X! !Y! head

        %=== Add the new head position to the snake strings ===%
        set "snakeX=!X!!snakeX!"
        set "snakeY=!Y!!snakeY!"
        set "PX=!X!"
        set "PY=!Y!"

        %draw%

      ) || (

        %=== failed move - game over ===%
        set "replayFinished=1"
        %plot% !TX! !TY! body
        call :spinner !PX! !PY! death
        %draw%
        if defined newHi (
          echo(
          echo New High Score - Congratulations^^!
          set "hi!diffCode!=!score!"
          copy "!gameLog!" "%hiFile%!diffCode!.txt" >nul
          >>"%hiFile%!diffCode!.txt" echo ::!score!
        )
        echo(
        %ClearLine%
        %ShowCursor%
        call :ask "Press a key to continue..."
        for %%A in (!configOptions!) do set "%%A=!%%ASave!"
        call :mainMenu
      )
    )
  )
)


::-------------------------------------
:getString  Prompt  Var  MaxLen
:: Prompt for a string with max lengh of MaxLen.
:: Valid keys are alpha-numeric, space, underscore, and dash
:: String is terminated by Enter
:: Backspace works to delete previous character
:: Result is returned in Var
set /a "maxLen=%3"
set "%2="
%sendCmd% prompt
<nul set /p "=%~1 "
call :purge
:getStringLoop
(%getKey% !upper! 0 1 2 3 4 5 6 7 8 9 " " _ - {Enter} !BS!)
if defined key (
  if !key! equ {Enter} (
    echo(
    exit /b
  )
  if !key! neq !BS! if !maxLen! gtr 0 (
    set /a maxLen-=1
    <nul set /p "=.!BS!!key!"
    set "%2=!%2!!key!
  )
  if !key! equ !BS! if defined %2 (
    set /a maxLen+=1
    <nul set /p "=!BS! !BS!"
    set "%2=!%2:~0,-1!"
  )
)
if defined inKey %sendCmd% one
goto :getStringLoop


::-------------------------------------
:ask  Prompt  ValidKey [Validkey]...
:: Prompt for a keypress.
:: Wait until a ValidKey is pressed and return result in Key variable.
:: Token delimiters, ), and poison characters must be quoted.
%sendCmd% prompt
<nul set /p "=%~1 "
(set validKeys=%*)
(set validKeys=!validKeys:%1=!)
call :purge
:getResponse
(%getKey% !validKeys!)
if not defined key (
  if defined inKey %sendCmd% one
  goto :getResponse
)
exit /b


:purge
set "inKey="
for /l %%N in (1 1 1000) do (
  set /p "inKey="
  if "!inKey!" equ "{purged}." exit /b
)<&%keyStream%
goto :purge


::-------------------------------------
:spinner  X  Y  ValueVar
set /a d1=-1000000
for /l %%N in (1 1 5) do for %%C in (%spinner%) do (
  call :spinnerDelay
  %plot% %1 %2 %%C
  %draw%
)
call :spinnerDelay
(%plot% %1 %2 %3)
exit /b


::-------------------------------------
:delay  centiSeconds
setlocal
for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do set /a "spinnerDelay=%1, d1=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100"
:: fall through to :spinnerDelay

::-------------------------------------
:spinnerDelay
for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do set /a "d2=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100, dDiff=d2-d1"
if %dDiff% lss 0 set /a dDiff+=24*60*60*100
if %dDiff% lss %spinnerDelay% goto :spinnerDelay
set /a d1=d2
exit /b


::-------------------------------------
:mainMenu
if defined toggleVT100 call :graphicOptions
cls
call :resize
set "loadAvailable="
echo SNAKE.BAT v4.0 by Dave Benham
echo(
echo Main Menu:
echo(
echo   N - New game
echo   F - Field size..... !size!
echo   W - groWth rate.... !growth!
echo   C - Control options
echo   G - Graphic options

if defined replayAvailable echo   R - Replay previous game
if defined saveAvailable   echo   S - Save a game
if exist *.snake.txt       echo   L - Load and watch a saved game&set "loadAvailable=L"

echo   Q - Quit
echo(
set "hiAvailable="
for /l %%N in (1 1 6) do if defined hi%%N (
  if not defined hiAvailable (
    echo Replay High Score:
    echo(
  )
  set "desc=!desc%%N!........"
  set "hiAvailable=!hiAvailable! %%N"
  echo   %%N - !desc:~0,8! !hi%%N!
)
if defined hiAvailable echo(
set "keys=N F W C G Q !hiAvailable! !replayAvailable! !saveAvailable! !loadAvailable!"
call :ask ">" !keys!
if /i !key! equ Q (
  %sendCmd% quit
  cls
  exit %exitCode%
) else if /i !key! equ N (
  set "replay="
  set "replayAvailable=R"
  set "saveAvailable=S"
  goto :initialize
) else if /i !key! equ S (
  if defined replayAvailable (
    call :ask "HighScore # or P for Previous:" !hiAvailable! P
  ) else (
    call :ask "HighScore #:" !hiAvailable!
  )
  echo !key!
  if /i !key! equ P (set "src=!gameLog!") else set "src=%hiFile%!key!.txt"
  call :getString "Save file name:" file 20
  copy "!src!" "!file!.snake.txt"
  call :ask "Press a key to continue..."
) else if /i !key! equ L (
  call :getString "Load file name:" file 20
  if exist "!file!.snake.txt" (
    set "replay=!file!.snake.txt"
    goto :initialize
  )
  echo Error: File "!file!.snake.txt" not found
  call :ask "Press a key to continue..."
) else if /i !key! equ R (
  set "replay=!gameLog!"
  goto :initialize
) else if /i !key! equ C (
  call :controlOptions
) else if /i !key! equ G (
  call :graphicOptions
) else if /i !key! equ F (
  call :sizeOptions
) else if /i !key! equ W (
  call :ask "Press a digit for growth rate (0 = 10)" 0 1 2 3 4 5 6 7 8 9
  if !key! equ 0 set "key=10"
  set "growth=!key!"
  call :loadHighScores
) else if !key! geq 1 if !key! leq 6 (
  set "replay=%hiFile%!key!.txt"
  goto :initialize
)
goto :mainMenu


::-------------------------------------
:sizeOptions
cls
set "keys=T S M L W N"
echo Field Size Options:
echo(
echo   T - Tiny   15 x 10
echo   S - Small  30 x 20
echo   M - Medium 40 x 25
echo   L - Large  47 x 32
echo   W - Wide   82 x 19
echo   N - Narrow 15 x 40
echo(
call :ask ">" !keys!
set "size=!size%key%!"
set /a "dispWidth=!width%key%!, dispHeight=!height%key%!"
call :loadHighScores
goto :saveUserPrefs
exit /b


::-------------------------------------
:controlOptions
cls
set "keys={Enter} T L R P"
if !moveKeys! equ 4 set "keys=!keys! U D"
                    echo Control Options:
                    echo(
                    echo   T - Type... = !moveKeys! keys
                    echo(
                    echo   L - Left... = !left!
                    echo   R - Right.. = !right!
if !moveKeys! equ 4 echo   U - Up..... = !up!
if !moveKeys! equ 4 echo   D - Down... = !down!
                    echo(
                    echo   P - Pause.. = !pause!
                    echo(
                    echo   {Enter} - Return to Main Menu
                    echo(
call :ask ">" !keys!
if !key! equ {Enter} goto :saveUserPrefs
if /i !key! equ T (
  if !moveKeys! equ 2 (set "moveKeys=4") else set "moveKeys=2"
  goto :controlOptions
)
set "option= LLeft RRight UUp DDown PPause"
for /f %%O in ("!option:* %key%=!") do (
  call :ask "Press a key for %%O:"
  for %%K in (0 1 2) do if "!key!" equ "!invalid:~%%K,1!" goto :controlOptions
  for %%C in (!upper!) do set "key=!key:%%C=%%C!"
  set "%%O=!key!"
)
goto :controlOptions


::-------------------------------------
:graphicOptions
set "toggleVT100="
cls
echo Graphic Options:
echo(
echo   B - Border...... = !bound!
echo   E - Empty space. = !space!
echo   H - snake Head.. = !head!
echo   S - Snake body.. = !body!
echo   F - Food........ = !food!
echo   D - Death....... = !death!
echo(
echo   V - VT100 mode.. = !vt%vt%!
echo(
echo   {Enter} - Rturn to Main Menu
echo(
call :ask ">" B E H S F D V {Enter}
if !key! equ {Enter} goto :saveUserPrefs
if /i !key! equ V (
  set /a "vt=^!vt"
  call :saveUserPrefs
  %sendCmd% quit
  exit 2
) else (
  set "option=-BBorder:bound:-EEmpty Space:space:-HSnake Head:head:-SSnake Body:body:-FFood:food:-DDeath:death:"
  for /f "tokens=1,2 delims=:" %%A in ("!option:*-%key%=!") do (
    call :ask "Press a key for %%A"
    for %%K in (0 1 2) do if "!key!" equ "!invalid:~%%K,1!" goto :graphicOptions
    set "%%B=!key!"
  )
)
goto :graphicOptions


::------------------------------------
:saveUserPrefs
(for %%V in (moveKeys up down left right space bound food head body death pause growth vt size dispWidth dispHeight) do echo %%V=!%%V!) >"%userPref%"
exit /b


::-------------------------------------
:initialize
cls
if defined replay (
  echo Replay Speed Options:
) else (
  echo Speed Options:
)
echo                       delay
echo    #   Description  (seconds)
echo   ---  -----------  ---------
for /l %%N in (1 1 6) do (
  set "delay=0!delay%%N!"
  set "desc=!desc%%N!           "
  echo    %%N   !desc:~0,11!    0.!delay:~-2!
)
echo(
call :ask "Pick a speed (1-6):" 1 2 3 4 5 6
set "difficulty=!desc%key%!"
set "delay=!delay%key%!"
set "diffCode=%key%"
echo %key% - %difficulty%
echo(
<nul set /p "=Initializing."
for %%A in (!configOptions!) do set "%%ASave=!%%A!"
if defined replay (
  %sendCmd% replay
  %sendCmd% !replay!
  call :waitForSignal
  set "replay=(REPLAY at !difficulty!)"
  set "size=%defaultSize%"
  set /a "dispWidth=defaultWidth, dispHeight=defaultHeight"
  :loadReplayConfig
  <&%keyStream% set /p "ln="
  if "!ln!" neq "END" set "!ln!" & goto :loadReplayConfig
  call :resize
)
set "axis=X"
set "xDiff=+1"
set "yDiff=+0"
set "empty="
set /a "PX=1, PY=height/2, FX=width/2+1, FY=PY, score=0, emptyCnt=0, t1=-1000000"
set "gameStart="
set "m=00"
set "s=00"
set "snakeX= %PX%"
set "snakeY= %PY%"
set "snakeX=%snakeX:~-2%"
set "snakeY=%snakeY:~-2%"
for /l %%Y in (0 1 %height%) do (
  <nul set /p "=."
  set "line%%Y="
  for /l %%X in (0,1,%width%) do (
    set "cell="
    if %%Y equ 0        set "cell=%bound%"
    if %%Y equ %height% set "cell=%bound%"
    if %%X equ 0        set "cell=%bound%"
    if %%X equ %width%  set "cell=%bound%"
    if %%X equ %PX% if %%Y equ %PY% set "cell=%head%"
    if not defined cell (
      set "cell=%space%"
      set "eX= %%X"
      set "eY= %%Y"
      set "empty=!empty!#!eX:~-2! !eY:~-2!"
      set /a emptyCnt+=1
    )
    if %%X equ %FX% if %%Y equ %FY% set "cell=%food%"
    set "line%%Y=!line%%Y!!cell!"
  )
)
set "replayFinished="
if defined replay (
  set "keys="
  set "hi=0"
  for /f "delims=:" %%A in ('findstr "^::" "%hiFile%!diffCode!.txt" 2^>nul') do set "hi=%%A"
  %HideCursor%
  cls
  (%draw%)
  call :delay 100
) else (
  if defined hi%diffCode% (set "hi=!hi%diffCode%!") else set "hi=0"
  cls
  (%draw%)
  >"!gameLog!" ( 
    for %%A in (!configOptions!) do (echo %%A=!%%A!)
    (echo END)
  )
  echo(
  if !moveKeys! equ 4 (
    echo Controls: !up!=up !down!=down !left!=left !right!=right !pause!=pause
  ) else (
    echo Controls: !left!=left !right!=right !pause!=pause
  )
  echo Avoid running into yourself (!body!!body!!head!^) or wall (!bound!^)
  echo Eat food (!food!^) to grow.
  echo(
  call :ask "Press a key to start..."
  %HideCursor%
  %sendCmd% go
)
set "pauseTime=0"
set "xDiff!up!=+0"
set "xDiff!down!=+0"
set "xDiff!left!=-1"
set "xDiff!right!=+1"
set "yDiff!up!=-1"
set "yDiff!down!=+1"
set "yDiff!left!=+0"
set "yDiff!right!=+0"
set "!up!Axis=Y"
set "!down!Axis=Y"
set "!left!Axis=X"
set "!right!Axis=X"
set "xTurn!left!=1"
set "xTurn!right!=-1"
set "yTurn!left!=-1"
set "yTurn!right!=1"
set "playerSpace=!space!!food!"
set ^"keys="!left!" "!right!" "!pause!"^"
set "newHi="
set "grow=0"
if !moveKeys! equ 4 set ^"keys=!keys! "!up!" "!down!"^"
if defined Up4 if not defined replay (
  %Up4%
  for /l %%N in (1 1 5) do (echo(                                             )
)
exit /b


::-------------------------------------
:waitForSignal
if not exist "%signal%" goto :waitForSignal
del "%signal%"
exit /b


::-------------------------------------
:loadHighScores
set "saveAvailable="
for /l %%N in (1 1 6) do (
  set "hi%%N="
  for /f "delims=:" %%A in ('findstr "^::" "%hiFile%%%N.txt" 2^>nul') do (
    set "hi%%N=%%A"
    set "saveAvailable=S"
  )
)
exit /b


::----------------------------
:resize the console window
set /a cols=dispWidth+1, lines=dispHeight+10, area=(dispWidth-2)*(dispHeight-2)
if %area% gtr 1365 (
  echo ERROR: Playfield area too large
  %sendCmd% quit
  exit
)
if %lines% lss 25 set lines=25
if %cols% lss 46 set cols=46
mode con: cols=%cols% lines=%lines%
set /a "width=dispWidth-1, height=dispHeight-1"
set "resize="
exit /b


::-------------------------------------
:fixLogs
setlocal enableDelayedExpansion
for %%F in (*.snake) do (
  ren "%%F" "%%F.txt"
  call :fixLog "%%F.txt"
)
pushd "%SaveLoc%"
for /f "delims=" %%F in ('dir /b SnakeHi*.txt 2^>nul') do (
  set "file=%%~nF"
  set "file=Snake1Hi!file:~-1!.txt"
  ren "%%F" "!file!"
  call :fixLog "!file!"
)
for /f "tokens=1* delims=eE" %%A in (
  'dir /b Snake*Hi*.txt ^| findstr /i "^Snake[0-9]"'
) do ren "Snake%%B" "SnakeMedium%%B"
popd
exit /b

:fixLog  filePath
>"%~1.new" (
  <"%~1" (
    for %%A in (diffCode difficulty moveKeys up down left right) do (
      set /p "val="
      (echo %%A=!val!)
    )
  )
  (echo growth=1)
  (echo END)
  more +7 "%~1"
)
move /y "%~1.new" "%~1" >nul
exit /b


::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:controller
:: Detects keypresses and sends the information to the game via a key file.
:: The controller has various modes of input that are activated by commands sent
:: from the game via a cmd file.
::
:: Modes:
::
::   hold   - No input, wait for command
::
::   go     - Continuously get/send key presses
::
::   prompt - Send {purged} marker to allow game to purge input buffer, then
::            get/send a single key press and hold
::
::   one    - Get/send a single key press and hold
::
::   replay - Copy a game log to the key file. The next line in cmd file
::            specifies name of log file to copy. During replay, the controller
::            will send an abort signal to the game if a key is pressed.
::
::   quit   - Immediately exit the controller process
::
:: As written, this routine incorrectly reports ! as ), but that doesn't matter
:: since we don't need that key. Both <CR> and Enter key are reported as {Enter}.
:: An extra character is appended to the output to preserve any control chars
:: when read by SET /P.

setlocal enableDelayedExpansion
for /f %%a in ('copy /Z "%~dpf0" nul') do set "CR=%%a"
set "cmd=hold"
set "inCmd="
set "key="
for /l %%. in () do (
  if "!cmd!" neq "hold" (
    for /f "delims=" %%A in ('xcopy /w "%~f0" "%~f0" 2^>nul') do (
      if not defined key set "key=%%A"
    )
    set "key=!key:~-1!"
    if !key! equ !CR! set "key={Enter}"
  )
  <&%cmdStream% set /p "inCmd="
  if defined inCmd (
    if !inCmd! equ quit exit
    set "cmd=!inCmd!"
    if !inCmd! equ replay (
      <&%cmdStream% set /p "file="
      type "!file!" >&%keyStream%
      copy nul "%signal%"
    )
    set "inCmd="
  )
  if defined key (
    if "!cmd!" equ "prompt" (echo {purged}.)
    if "!cmd!" equ "replay" (
      copy nul "%signal%" >nul
      set "cmd=go"
    ) else (echo(!key!.)
    if "!cmd!" neq "go" set "cmd=hold"
    set "key="
  )>&%keyStream%
)
====================================================================================================
Here begins the original post

For a while now I've been messing around with the concept of creating an arcade style game using only pure native batch. I ran across an interesting implementation of the classic SNAKE game (BATCHSNAKE.BAT) at http://www.youtube.com/watch?v=RblizDDxaBA
EDIT: I updated the link to point to the original author's YouTube post. The prior link was by an impostor claiming credit

BATCHSNAKE.BAT uses an interesting technique to emulate the ability to detect a key press without pausing the game. The main game runs in one console window, and it launches a controller batch process in a new window that uses CHOICE to read keys. The controller process writes the key presses to a file, and the main game has stdin redirected to the file, so it can read the keys using SET /P. The nice feature is that SET /P reads the next key if available, but immediately moves on with no input if currently at the end of the file. What I don't like is the reliance on two windows, it is confusing. Also, the implementation seems to sometimes drop key presses. BATCHSCRIPT.BAT also uses multiple batch scripts.

BATCHSNAKE.BAT works fairly well, and has a lot of nice features. But the screen has a lot of flicker, and the maximum speed is severely limited by the number of CALL statements, as well as to a lesser extent by the complexity of some logic and dependence of randomly finding a free space to locate the next piece of food. Performance on XP running via Win 7 Virtual PC is abysmal, and the controller process is somehow broken on the virtual PC (it launches but then closes).

I decided to create my own version of SNAKE using pure batch, concentrating on performance and controller reliability and ease of use. I chose not to implement features like high score, user defined maps, rocks, etc. Those features could easily be added.

Controller improvements
I put all code within one batch script (SNAKE.BAT). I launch the controller process using START /B so the controller runs in the same console window. The controller has various modes to allow the game to detect individual key presses, as well as to prompt and wait for input. The game communicates with the controller by writing commands to a command file that the controller then reads. Getting the correct behavior within a shared console window requires carefully constructed output redirection for the controller, as well as input redirection for the game.

The controller requires the CHOICE command. Unfortunately, the CHOICE command is not standard for XP. But XP users can still use the game if they download a port of the CHOICE command. SNAKE.BAT detects if the CHOICE command is available, and displays URLs where it may be downloaded if it is not available. The calling syntax for CHOICE varies depending on the version of CHOICE. SNAKE.BAT detects which version is available and sets up a simple "macro" to account for the version.

Speed improvements
This is a BIG topic. There are a number of important basic design principles.

1) Minimize use of GOTO

The main game loop is an infinite FOR /L loop. The infinite loop is exited via the EXIT command. The main game is launched from SNAKE.BAT via a new CMD.EXE so that necessary cleanup actions can take place after the loop exits. Use of GOTO for the main loop would slow down the program.

2) Minimize use of CALL

CALLed Batch "functions" with arguments are convenient, but the CALL mechanism is comparatively very slow. SNAKE.BAT dramatically improves speed via extensive use of batch "macros" with arguments. See Batch "macros" with arguments for more info on how to use macros. The link gives a good background on the macro concept. It provides a link to a more recent thread with major macro enhancements by jeb that more closely resemble the macros used in SNAKE.BAT, as well as a link to the initial thread where Ed Dyreen introduced the macro with arguments concept.

3) Encapsulate logic in data structures.

A lot of IF logic is avoided by defining variables with carefully constructed names, coupled with staged variable expansion. For example, if %%K contains the currently selected direction key (W A S or D), and xDiffW, xDiffA, xDiffS, and xDiffD contain the possible movement deltas for the x axis, then !xDiff%%K! will yield the currently selected movement x delta. The actual code uses SET /A to read the value of xDiff%%K directly without the need for delayed expansion.

4) Efficiently "plot" characters as "pixels"

The screen is composed of a set of environment variables holding fixed length strings that are ECHOed, one string per graphic line. Each character in a string represents a graphic "pixel". The variable name specifies the Y coordinate, and the position within the string the X coordinate. All "graphic" lines are created pixel by pixel just once upon initialization. From then on, a Plot macro uses simple SET substring operations to set the value of individual "pixels" as needed. There is no need to recompose the entire screen.

5) Snake definition

The snake is represented as variables containing a string of coordinates, one variable for X, and another for Y. The beginning of the string is the head, and the end is the tail. As the snake "moves" forward, simple SET substring operations are used to place a new coordinate at the front, and remove a coordinate from the rear.

6) Random food placement

A list of empty coordinates is maintained in a string variable so that food can be randomly placed without fear of colliding with an already occupied pixel. A random number between 0 and the count of available pixels is generated, and a SET substring operation is used to extract the available position. Then a SET search and replace is used to remove the newly occupied coordinates from the empty list. When the snake tail moves, the newly vacated pixel coordinates are appended to the end of the empty list.

7) Collision detection

Classic pixel probing is used to detect collisions. When the snake head is moving, a Test macro probes the value of the new position using a SET substring operation. The macro compares the current value against a list of allowable values. Disallowed values represent a collision.

8) Smooth motion

A fixed delay between each main loop iteration can result in jerky movement because the amount of work required by an iteration varies depending on context. More work equates to more time. Instead of introducing a fixed delay between each iteration, I note the time the previous movement ended, and then continuously check how much time has elapsed since the last movement. I only initiate movement when the delay time is exceeded. As long as the time spent on any one iteration is always less then the delay time, then the motion will always be smooth.

Results
I am extremely pleased with the speed and responsivenes of SNAKE.BAT. Setting the delay to 0 demonstrates just how fast the program executes - it is rediculously fast. A speed of 6 (delay = 30 milliseconds) is totally smooth on my Windows 7 machine, but it is so fast it is unplayable. I tend to play with a speed of 3 (delay = 100 milliseconds). At that speed, I get no flicker on my screen except for the top most border line. Faster speeds result in some flicker throughout the screen, but the motion remains smooth.

The game even works well when I run it on an XP virtual machine within Windows 7. There is much more flicker, but it still remains fairly smooth.

Possible Enhancements
Obviously things like high score lists, user defined maps, rocks, etc. that are available in the original BATCHSNAKE.BAT could be imlemented.

The following ideas interest me more. At one point I was considering implementing the following ideas, but I ran out of steam.

1) Increased play field size

The empy pixel list is maintained in a variable with a max string length of 8191 bytes. That restricts the total area of the play field. The snake definition variables also restrict the size, but they support a larger area then the empty list. Maximum playfield size could be increased by splitting the empty list and snake definition over multiple variables. But that complicates the management of those lists.

2) Introduce enemy snakes

If the food is not eaten within a time limit, then it can spawn into an enemy snake. The enemy snake(s) can eat food or chase you. Shorter snakes move faster than longer snakes. A small snake can nibble at the tail of a large snake. A large snake can swallow a small snake whole if eaten head first. Running into the side of a snake results in a collision (death). This sounds complicated, but the logic need not be all that complex. And I believe there is plenty of performance bandwidth left to handle the added complexity without hurting the animation quality.

So, here at last is the code for SNAKE.BAT
Embedded in a comment at the top are instructions for how to disable the awfull beep that occurs when an invalid key is pressed.

Code: Select all

:: Disable that awful beep (commands must be run with administrator privilege)
::   current session only:  net stop beep
::   permanently:  sc config beep start= disabled

@echo off
if "%~1" == "startGame" goto :game
if "%~1" == "startController" goto :controller


::------------------------------------------------------------
:: verify existence of CHOICE command
:: set up a macro appropriately depending on available version

set "choice="
2>nul >nul choice /c:yn /t 0 /d y
if errorlevel 1 if not errorlevel 2 set "choice=choice /cs"
if not defined choice (
  2>nul >nul choice /c:yn /t:y,1
  if errorlevel 1 if not errorlevel 2 set "choice=choice /s"
)
if not defined choice (
  echo ERROR: This game requires the CHOICE command, but it is missing.
  echo Game aborted. :(
  echo(
  echo A 16 bit port of CHOICE.EXE from FREEDOS is available at
  echo http://winsupport.org/utilities/freedos-choice.html
  echo(
  echo A 32 bit version from ??? suitable for 64 bit machines is available at
  echo http://hp.vector.co.jp/authors/VA007219/dkclonesup/choice.html
  echo(
  exit /b
)


::---------------------------------------------------------------------
:: setup some global variables used by both the game and the controller

set "keys=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
set "keyFile=key.txt"
set "cmdFile=cmd.txt"


::------------------------------------------
:: launch the game and the controller

copy nul "%keyFile%" >nul
start "" /b "%~f0" startController 9^>^>%keyFile% 2^>nul ^>nul
cmd /c "%~f0" startGame 9^<%keyFile% ^<nul
echo(

::--------------------------------------------------------------------------------
:: Upon exit, wait for the controller to close before deleting the temp input file

:close
2>nul (>>"%keyFile%" call )||goto :close
del "%keyFile%"
exit /b


::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:game
setlocal disableDelayedExpansion
title %~nx0
cls

::----------------------------
:: user configurable options

set "up=W"
set "down=S"
set "left=A"
set "right=D"

set "width=40"   max=99
set "height=25"  max=99
:: max playing field: (width-2)*(height-2) <= 1365

::----------------------------
:: resize the console window

set /a cols=width+1, lines=height+10, area=(width-2)*(height-2)
if %area% gtr 1365 (
  echo ERROR: Playfield area too large
  >"%cmdFile%" (echo quit)
  exit
)
if %lines% lss 14 set lines=14
if %cols% lss 46 set cols=46
mode con: cols=%cols% lines=%lines%

::----------------------------
:: define variables

set "spinner1=-"
set "spinner2=\"
set "spinner3=|"
set "spinner4=/"
set "spinner= spinner1 spinner2 spinner3 spinner4 "

set "space= "
set "bound=#"
set "food=+"
set "head=@"
set "body=O"
set "death=X"
set "playerSpace=%space%%food%"

set "xDiff%up%=+0"
set "xDiff%down%=+0"
set "xDiff%left%=-1"
set "xDiff%right%=+1"

set "yDiff%up%=-1"
set "yDiff%down%=+1"
set "yDiff%left%=+0"
set "yDiff%right%=+0"

set "%up%Axis=Y"
set "%down%Axis=Y"
set "%left%Axis=X"
set "%right%Axis=X"

set "delay1=20"
set "delay2=15"
set "delay3=10"
set "delay4=7"
set "delay5=5"
set "delay6=3"
set "delay0=0"

set "desc1=Sluggard"
set "desc2=Crawl"
set "desc3=Slow"
set "desc4=Normal"
set "desc5=Fast"
set "desc6=Insane"
set "desc0=Unplayable"

set "spinnerDelay=3"

set /a "width-=1, height-=1"


::---------------------------
:: define macros

::define a Line Feed (newline) string (normally only used as !LF!)
set LF=^


::Above 2 blank lines are required - do not remove

::define a newline with line continuation
set ^"\n=^^^%LF%%LF%^%LF%%LF%^^"

:: setErr
:::  Sets the ERRORLEVEL to 1
set "setErr=(call)"

:: clrErr
:::  Sets the ERRORLEVEL to 0
set "clrErr=(call )"


:: getKey  ValidKeys
::: Check for keypress. Only accept keys listed in ValidKeys
::: Return result in Key variable. Key is undefined if no valid keypress.
set getKey=%\n%
for %%# in (1 2) do if %%#==2 (for /f "eol= delims= " %%1 in ("!args!") do (%\n%
  set "validKeys=%%1"%\n%
  set "key="%\n%
  ^<^&9 set /p "key="%\n%
  if defined key if "!key!" neq ":" (%\n%
    set /a key-=1%\n%
    for %%K in (!key!) do set "key=!keys:~%%K,1!"%\n%
  )%\n%
  for %%K in (!key!) do if "!validKeys:%%K=!" equ "!validKeys!" set "key="%\n%
)) else set args=


:: draw
:::  draws the board
set draw=%\n%
cls%\n%
for /l %%Y in (0,1,%height%) do echo(!line%%Y!%\n%
echo Speed=!Difficulty!%\n%
echo Score=!score!


:: test  X  Y  ValueListVar
:::  tests if value at coordinates X,Y is within contents of ValueListVar
set test=%\n%
for %%# in (1 2) do if %%#==2 (for /f "tokens=1-3" %%1 in ("!args!") do (%\n%
  for %%A in ("!line%%2:~%%1,1!") do if "!%%3:%%~A=!" neq "!%%3!" %clrErr% else %setErr%%\n%
)) else set args=


:: plot  X  Y  ValueVar
:::  places contents of ValueVar at coordinates X,Y
set plot=%\n%
for %%# in (1 2) do if %%#==2 (for /f "tokens=1-3" %%1 in ("!args!") do (%\n%
  set "part2=!line%%2:~%%1!"%\n%
  set "line%%2=!line%%2:~0,%%1!!%%3!!part2:~1!"%\n%
)) else set args=


::--------------------------------------
:: start the game
setlocal enableDelayedExpansion
call :initialize


::--------------------------------------
:: main loop (infinite loop)
for /l %%. in (1 0 1) do (

  %=== compute time since last move ===%
  for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do set /a "t2=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100, tDiff=t2-t1"
  if !tDiff! lss 0 set /a tDiff+=24*60*60*100

  if !tDiff! geq !delay! (
    %=== delay has expired, so time for movement ===%

    %=== establish direction ===%
    %getKey% ASDW
    for %%K in (!key!) do if "!%%KAxis!" neq "!axis!" (
      set /a "xDiff=xDiff%%K, yDiff=yDiff%%K"
      set "axis=!%%KAxis!"
    )

    %=== erase the tail ===%
    set "TX=!snakeX:~-2!"
    set "TY=!snakeY:~-2!"
    set "snakeX=!snakeX:~0,-2!"
    set "snakeY=!snakeY:~0,-2!"
    %plot% !TX! !TY! space

    %=== compute new head location and attempt to move ===%
    set /a "X=PX+xDiff, Y=PY+yDiff"
    set "X= !X!"
    set "Y= !Y!"
    set "X=!X:~-2!"
    set "Y=!Y:~-2!"
    (%test% !X! !Y! playerSpace) && (

      %=== move successful ===%

      %=== remove the new head location from the empty list ===%
      for %%X in ("!X!") do for %%Y in ("!Y!") do set "empty=!empty:#%%~X %%~Y=!"

      (%test% !X! !Y! food) && (
        %=== moving to food - eat it ===%

        %=== restore the tail ===%
        %plot% !TX! !TY! body
        set "snakeX=!snakeX!!TX!"
        set "snakeY=!snakeY!!TY!"

        %=== increment score and locate and draw new food ===%
        set /a "score+=1, F=(!random!%%(emptyCnt-=1))*6+1"
        for %%F in (!F!) do (%plot% !empty:~%%F,5! food)

      ) || (
        %=== moving to empty space ===%

        %=== add the former tail position to the empty list ===%
        set "empty=!empty!#!TX! !TY!"
      )

      %=== draw the new head ===%
      if defined snakeX (%plot% !PX! !PY! body)
      %plot% !X! !Y! head

      %=== Add the new head position to the snake strings ===%
      set "snakeX=!X!!snakeX!"
      set "snakeY=!Y!!snakeY!"
      set "PX=!X!"
      set "PY=!Y!"

      %draw%

    ) || (

      %=== failed move - game over ===%
      %plot% !TX! !TY! body
      call :spinner !PX! !PY! death
      %draw%
      echo(
      call :ask "Would you like to play again? (Y/N)" YN
      if /i "!key!" equ "N" (
        >"%cmdFile%" (echo quit)
        exit
      ) else (
        call :initialize
      )
    )

    set /a t1=t2
  )
)

:ask  Prompt  ValidKeys
:: Prompt for a keypress. ValidKeys is a list of acceptable keys
:: Wait until a valid key is pressed and return result in Key variable
>"%cmdFile%" (echo prompt)
<nul set /p "=%~1 "
:purge
(%getKey% :)
if not defined key goto :purge
:getResponse
(%getKey% %2)
if not defined key (
  >"%cmdFile%" (echo one)
  goto :getResponse
)
exit /b


:spinner  X  Y  ValueVar
set /a d1=-1000000
for /l %%N in (1 1 5) do for %%C in (%spinner%) do (
  call :spinnerDelay
  %plot% %1 %2 %%C
  %draw%
)
call :spinnerDelay
(%plot% %1 %2 %3)
exit /b

:spinnerDelay
for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do set /a "d2=(((1%%a*60)+1%%b)*60+1%%c)*100+1%%d-36610100, dDiff=d2-d1"
if %dDiff% lss 0 set /a dDiff+=24*60*60*100
if %dDiff% lss %spinnerDelay% goto :spinnerDelay
set /a d1=d2
exit /b


::-------------------------------------
:initialize
cls

echo Speed Options:
echo                       delay
echo    #   Description  (seconds)
echo   ---  -----------  ---------
echo    1   Sluggard        0.20
echo    2   Crawl           0.15
echo    3   Slow            0.10
echo    4   Normal          0.07
echo    5   Fast            0.05
echo    6   Insane          0.03
echo    0   Unplayable      none
echo(
call :ask "Pick a speed (1-6, 0):" 1234560
set "difficulty=!desc%key%!"
set "delay=!delay%key%!"
echo %key% - %difficulty%
echo(
<nul set /p "=Initializing."
set "axis=X"
set "xDiff=+1"
set "yDiff=+0"
set "empty="
set /a "PX=1, PY=height/2, FX=width/2+1, FY=PY, score=0, emptyCnt=0, t1=-1000000"
set "snakeX= %PX%"
set "snakeY= %PY%"
set "snakeX=%snakeX:~-2%"
set "snakeY=%snakeY:~-2%"
for /l %%Y in (0 1 %height%) do (
  <nul set /p "=."
  set "line%%Y="
  for /l %%X in (0,1,%width%) do (
    set "cell="
    if %%Y equ 0        set "cell=%bound%"
    if %%Y equ %height% set "cell=%bound%"
    if %%X equ 0        set "cell=%bound%"
    if %%X equ %width%  set "cell=%bound%"
    if %%X equ %PX% if %%Y equ %PY% set "cell=%head%"
    if not defined cell (
      set "cell=%space%"
      set "eX= %%X"
      set "eY= %%Y"
      set "empty=!empty!#!eX:~-2! !eY:~-2!"
      set /a emptyCnt+=1
    )
    if %%X equ %FX% if %%Y equ %FY% set "cell=%food%"
    set "line%%Y=!line%%Y!!cell!"
  )
)
(%draw%)
echo(
echo Movement keys: %up%=up %down%=down %left%=left %right%=right
echo Avoid running into yourself (%body%%body%%head%) or wall (%bound%)
echo Eat food (%food%) to grow.
echo(
call :ask "Press any alpha-numeric key to start..." %keys%
>"%cmdFile%" (echo go)
exit /b


::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:controller

setlocal enableDelayedExpansion
set "cmd=hold"
set "key="
for /l %%. in (1 0 1) do (
  if "!cmd!" neq "hold" (
    %choice% /n /c:!keys!
    set "key=!errorlevel!"
  )
  if exist "%cmdFile%" (
    <"%cmdFile%" set /p "cmd="
    del "%cmdFile%"
  )
  if "!cmd!" equ "quit" exit
  if defined key (
    if "!cmd!" equ "prompt" >&9 (echo :)
    >&9 (echo !key!)
    if "!cmd!" neq "go" set "cmd=hold"
    set "key="
  )
)
EDIT: Added /C: option to the test for CHOICE to account for some internationalization issues
EDIT2: Added comma to DELIMS option when parsing time to support European decimal point
EDIT3: Changed 2nd CHOICE test to wait 1 second to support downloaded 16 bit version and fixed disappearing food
bug

EDIT4: One more CHOICE test change to account for fact that 16 bit version returns 0 if invalid arguments


Dave Benham
Last edited by dbenham on 16 Feb 2015 10:53, edited 26 times in total.

aGerman
Expert
Posts: 4654
Joined: 22 Jan 2010 18:01
Location: Germany

Re: SNAKE.BAT - An arcade style game using pure batch

#2 Post by aGerman » 13 Jul 2013 14:27

Hi Dave

I gave it a shot but it complains that CHOICE was missing even if that is definitely not true (Win7 x86). Think you should revise that test :wink:
I commended some lines out ...

Code: Select all

::------------------------------------------------------------
:: verify existence of CHOICE command
:: set up a macro appropriately depending on available version

::set "choice="
::2>nul >nul choice /t 0 /d y
::if errorlevel 1 if not errorlevel 2 (
  set "choice=choice /cs"
::) else (
::  2>nul >nul choice /t:y,-1
::  if errorlevel 1 if not errorlevel 2 set "choice=choice /s"
::)

... but still doesn't work.

Code: Select all

#                                      #
#                                      #
#                                      #
#                                      #
#                                      #
#                                      #
#                                      #
#                                    O@#
#                                      #
#                                      #
#                                      #
#                                      #
#                                      #
#                                      #
#                                      #
#                       +              #
#                                      #
#                                      #
#                                      #
########################################
Speed=Crawl
Score=1
Ungültige Zahl. Numerische Konstanten sind ent
weder dezimale (17),
hexadezimale (0x11) oder oktale (021) Zahlen.
Ungültige Zahl. Numerische Konstanten sind ent
weder dezimale (17),
hexadezimale (0x11) oder oktale (021) Zahlen.
Ungültige Zahl. Numerische Konstanten sind ent
weder dezimale (17),
hexadezimale (0x11) oder oktale (021) Zahlen.
Ungültige Zahl. Numerische Konstanten sind ent
weder dezimale (17),
hexadezimale (0x11) oder oktale (021) Zahlen.

There must be a wrong SET /A somewhere ...

Regards
aGerman

penpen
Expert
Posts: 1991
Joined: 23 Jun 2013 06:15
Location: Germany

Re: SNAKE.BAT - An arcade style game using pure batch

#3 Post by penpen » 13 Jul 2013 15:11

You should avoid creating files, as this is slower than reading memory.
This example is selfexplaining:

Code: Select all

:: source.bat
@echo off
setlocal
rem for %%a in (a b c d e f g h i j k l m n o p q) do <nul set /p "=%%a"
for %%a in (a b c d e f g) do <nul set /p "=%%a"
for %%a in (h i j k l m n o p) do <nul set /p "=%%a"
<nul set /p "=q"

Code: Select all

:: pipeOut.bat
@echo off
   setlocal
   echo:%0: started
:readkey
   set INPUT=
   set /p INPUT=
   if DEFINED INPUT goto :inputAvailable
   goto :readkey

:inputAvailable
   echo:%0: "%INPUT%"
   if %INPUT% == %INPUT:q=z% goto :readkey
   echo:%0: leaving
   endlocal

Started with:

Code: Select all

source.bat | pipeOut.bat 


And for a bigger field you may use something like this, but i don't know, how fast this is:

Code: Select all

set "MAX_X=100"
set "MAX_Y=100"

set "STATE_NO_SNAKE = 0"
set "STATE_OWN_SNAKE_TAIL = 1"
set "STATE_OWN_SNAKE_HEAD = 2"
set "STATE_OWN_SNAKE_BODY = 3"
set "STATE_ENEMY_SNAKE_TAIL = 4"
set "STATE_ENEMY_SNAKE_TAIL = 8"
set "STATE_ENEMY_SNAKE_TAIL = 12"
set "STATE_SNAKE_HORIZONTAL = 16"
:: ...
set "FIELD.LENGTH=%MAX_X%
for /l %%x in (1,1,%MAX_X%) do (
   set "FIELD[%%x].LENGTH=%MAX_X%
   for /l %%a in (1,1,%MAX_Y%) do (
      set FIELD[%%x][%%y]=%STATE_NO_SNAKE%
   )
)


penpen

dbenham
Expert
Posts: 2461
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: SNAKE.BAT - An arcade style game using pure batch

#4 Post by dbenham » 13 Jul 2013 15:27

aGerman wrote:Hi Dave

I gave it a shot but it complains that CHOICE was missing even if that is definitely not true (Win7 x86). Think you should revise that test :wink:
I commended some lines out ...

... but still doesn't work.

:shock: :? Are you sure you copied the script correctly? I copied what I posted into a new script and it runs fine on my Windows 7 64 bit machine, even using an XP virtual machine. I believe I also tested this on a Windows 7 32 bit machine at work. I'm totally stumped.


penpen wrote:You should avoid creating files, as this is slower than reading memory.

Of course memory operations are generally much faster than writing and reading a file. But the file operations are critical to the whole non-blocking key press detection algorithm.

Besides, the use of temp files gets an unfair bad rap. With today's high speed disk drives, file operations are extremely fast as long as you are not on a shared network drive with a lot of traffic. For example, there are threads somewhere on this site that demonstrate it is more efficient to write lengthy command output to a temp file and then process the temp file instead of processing the command output directly within a FOR /F statement.


Dave Benham

dbenham
Expert
Posts: 2461
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: SNAKE.BAT - An arcade style game using pure batch

#5 Post by dbenham » 13 Jul 2013 15:42

@penpen - I reread your response, and you may be onto something. I'm mildly surprised that a batch script running on the left side of a pipe can properly get interactive key presses using CHOICE.

I'll have to do some experiments to see if I can cleanly eliminate the keypress file as you suggest. However, I don't see a way to eliminate the communication via file when sending a command from the game to the controller. The pipe is unidirectional.


Dave Benham

aGerman
Expert
Posts: 4654
Joined: 22 Jan 2010 18:01
Location: Germany

Re: SNAKE.BAT - An arcade style game using pure batch

#6 Post by aGerman » 13 Jul 2013 15:47

Yes, copied again with the same result.
I already fixed the "missing choice". In line ...

Code: Select all

2>nul >nul choice /t 0 /d y

... option /C was missing.

Code: Select all

2>nul >nul choice /c y /t 0 /d y


Regards
aGerman

dbenham
Expert
Posts: 2461
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: SNAKE.BAT - An arcade style game using pure batch

#7 Post by dbenham » 13 Jul 2013 15:53

@aGerman

:shock: That is very strange. The /C option is supposed to be optional in all versions that I have seen. It defaults to YN.

Does the program work now that you have gotten past that issue?


Dave Benham

aGerman
Expert
Posts: 4654
Joined: 22 Jan 2010 18:01
Location: Germany

Re: SNAKE.BAT - An arcade style game using pure batch

#8 Post by aGerman » 13 Jul 2013 16:06

The /C option is supposed to be optional in all versions that I have seen. It defaults to YN.

It defaults to JN (Ja Nein) under my German OS. Don't ask me why Microsoft permanently tries to disimprove their tools by doing such useless adjustments to another language.

Due to the SET /A - No that doesn't fix it. I'll try to figure out why it fails.

aGerman
Expert
Posts: 4654
Joined: 22 Jan 2010 18:01
Location: Germany

Re: SNAKE.BAT - An arcade style game using pure batch

#9 Post by aGerman » 13 Jul 2013 16:22

It's somewhere while the %getKey% macro is executed in a loop. But with "echo on" it iterates too fast. I can't perceive what happens.

dbenham
Expert
Posts: 2461
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: SNAKE.BAT - An arcade style game using pure batch

#10 Post by dbenham » 13 Jul 2013 16:29

@penpen - I ran a quick test. SET /P becomes blocking when reading from a pipe and there is no input waiting. It is non-blocking when reading from a file when there is no input waiting. So I cannot use a pipe to eliminate the keypress temp file.


aGerman wrote:
dbenham wrote:The /C option is supposed to be optional in all versions that I have seen. It defaults to YN.

It defaults to JN (Ja Nein) under my German OS.

The important thing is that CHOICE has a default /C option, so it should be optional. I don't see why my test should require the /C option. Oh well, whatever works.

Good luck with the SET /A issue. I find it frustrating that my code does not work for you "out of the box". I'm usually good at writing generic code that is platform agnostic.


Dave Benham

aGerman
Expert
Posts: 4654
Joined: 22 Jan 2010 18:01
Location: Germany

Re: SNAKE.BAT - An arcade style game using pure batch

#11 Post by aGerman » 13 Jul 2013 16:38

The important thing is that CHOICE has a default /C option, so it should be optional. I don't see why my test should require the /C option.

You were not able to find that error. But since Y is not part of my default JN option /C is required to make it language independent.

Good luck with the SET /A issue. I find it frustrating that my code does not work for you "out of the box".

I'll do my best effort :wink:

dbenham
Expert
Posts: 2461
Joined: 12 Feb 2011 21:02
Location: United States (east coast)

Re: SNAKE.BAT - An arcade style game using pure batch

#12 Post by dbenham » 13 Jul 2013 16:43

aGerman wrote:It's somewhere while the %getKey% macro is executed in a loop. But with "echo on" it iterates too fast. I can't perceive what happens.


Well, there is only one SET /A in %getKey%. It is decrementing the value of the key variable, whose value comes from the keyfile. It should never be anything other than a number or a colon. And the number should never be 0 prefixed. A value of 08 or 09 will give the error that you are seeing.

So my guess is that the controller is somehow passing a zero prefixed number on your machine. The controller simply reads the ERRORLEVEL from the choice command, so I don't see how that could be happening.

Try modifying the script a bit so that you can type out the contents of key.txt after you have pressed a few keys. Look for a zero prefixed number. If you find it, then we have to figure out how it is getting there.


Dave Benham

miskox
Posts: 553
Joined: 28 Jun 2010 03:46

Re: SNAKE.BAT - An arcade style game using pure batch

#13 Post by miskox » 13 Jul 2013 17:09

I receive the same errors

Code: Select all

Invalid number.  Numeric constants are either
decimal (17)....


I get these errors when the game ends (if this is of any help).

Is it possible that the set /a operations with date/time are causing this error?

What format should the date/time have?

key.txt does not contain any numbers with leading zero(s).

Saso

aGerman
Expert
Posts: 4654
Joined: 22 Jan 2010 18:01
Location: Germany

Re: SNAKE.BAT - An arcade style game using pure batch

#14 Post by aGerman » 13 Jul 2013 17:15

I typed 1 for Sluggard - everything fine so far.
Then typed B to start the game. Now it returned the error perhaps 3 or 4 times.

key.txt

Code: Select all

:
54
:
2


Could it happen that the colons will cause the error?

aGerman
Expert
Posts: 4654
Joined: 22 Jan 2010 18:01
Location: Germany

Re: SNAKE.BAT - An arcade style game using pure batch

#15 Post by aGerman » 13 Jul 2013 17:22

@miskox

Good point. That's it!

%time% contains H:MM:SS,nn since the decimal separator is comma instead of point in Germany.

Changed these lines

Code: Select all

  for /f "tokens=1-4 delims=:.," %%a in ("!time: =0!") do ..."

Now it works :D

Post Reply