Read arrow keys and show color text in an efficient way

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Message
Author
Aacini
Expert
Posts: 1913
Joined: 06 Dec 2011 22:15
Location: México City, México
Contact:

Read arrow keys and show color text in an efficient way

#1 Post by Aacini » 30 Jan 2016 20:56

This topic has been a long request from Batch file programmers: read arrow and other extended keys (like function keys) and process they in efficient way. The conclusion from previous experiences is that read extended keys is not possible using just native Batch commands, so an additional application is required and the most used one is PowerShell. However, current solutions based on PowerShell are very inefficient because the whole PS environment must be executed each time that a key is read, so these programs are slow and unresponsive. Of course, another possible solution would be to write the complete process just in PowerShell, but we all know that this is not the solution we are looking for; we want a Batch-file based solution with the minimal part of non native code.

I developed a method that allows to use PowerShell in a Batch file in a very efficient way: the PS engine is loaded just one time and then repeatedly used to read all keys. The result is a Batch file that must wait just once, when PowerShell is loaded the first time, but that is very responsive after that. This key-entry method may be used in a wide range of applications. For example, the first program below show an horizontal one-line menu that allows to select their options via Home/LeftArrow/RightArrow/End keys:

Code: Select all

@echo off
if "%~1" equ "OptionSelection" goto %1

rem Activate an horizontal menu controlled by cursor keys
rem Antonio Perez Ayala

setlocal EnableDelayedExpansion
cls
echo/
echo Example of horizontal menu
echo/
echo Change selected option with these cursor keys:
echo Home/End = First/Last, Left/Right = Prev/Next.
:loop
   echo/
   echo/
   call :HMenu select="Press Enter to continue, Esc to cancel: " Insert/Append/Update/Delete
   echo Option selected: %select%
if %select% neq 0 goto Loop
echo End of menu example
goto :EOF


This subroutine activate a one-line selection menu controlled by cursor control keys

:HMenu  select=  prompt  option1/option2/...
setlocal EnableDelayedExpansion

rem Separate options
set "options=%~3"
set "lastOption=0"
(
   set "options="
   for %%a in ("%options:/=" "%") do (
      set /A lastOption+=1
      set "option[!lastOption!]=%%~a"
      set "options=!options! %%~a "
   )
)

rem Define working variables
for %%a in ("Enter=13" "Esc=27" "End=35" "Home=36" "LeftArrow=37" "RightArrow=39") do set %%a
for /F %%a in ('copy /Z "%~F0" NUL') do set "CR=%%a"
for /F %%a in ('echo prompt $H ^| cmd') do set "BS=%%a"

rem Define movements for standard keys
set "sel=1"
set "moveSel[%Home%]=set sel=1"
set "moveSel[%End%]=set sel=%lastOption%"
set "moveSel[%LeftArrow%]=set /A sel-=^!^!(sel-1)"
set "moveSel[%RightArrow%]=set /A sel+=^!^!(sel-lastOption)"

rem Read keys via PowerShell  ->  Process keys in Batch
set /P "=Loading menu..." < NUL
PowerShell  ^
   Write-Host 0;  ^
   $validKeys = %End%..%LeftArrow%+%RightArrow%+%Enter%+%Esc%;  ^
   while ($key -ne %Enter% -and $key -ne %Esc%) {  ^
      $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode;  ^
      if ($validKeys.contains($key)) {Write-Host $key}  ^
   }  ^
%End PowerShell%  |  "%~F0" OptionSelection %2
endlocal & set "%~1=%errorlevel%"
echo/
exit /B


:OptionSelection
setlocal EnableDelayedExpansion

rem Wait for PS code start signal
set /P "keyCode="
set /P "="

:nextSelection

   rem Show prompt: options
   for %%s in ("!option[%sel%]!") do set /P "=%BS%!CR!%~2!options: %%~s =[%%~s]!" < NUL

   rem Get a keycode from PowerShell
   set /P "keyCode="
   set /P "="

   rem Process it
   if %keyCode% equ %Enter% goto endSelection
   if %keyCode% equ %Esc% set "sel=0" & goto endSelection
   !moveSel[%keyCode%]!

goto nextSelection
:endSelection
exit %sel%

We may add the findstr color text method in order to highlight the selected option. The next example is a standard CheckList/RadioButton selection form with the options placed vertically; this code also features the selection of options via the first letter of each one.

Code: Select all

@echo off
if "%~1" equ "OptionSelection" goto %1

rem Activate a CheckList/RadioButton controlled by cursor keys
rem Antonio Perez Ayala

setlocal
set "switch="
set "endHeader="
:header
   cls
   echo/
   echo Example of Check List / Radio Button
   echo/
   echo Move selection lightbar with these cursor keys:
   echo Home/End = First/Last, Up/Down = Prev/Next; or via option's first letter.
   echo/
   %endHeader%
   if not defined switch (set "switch=/R") else set "switch="
   call :CheckList select="First option/Second option/Third option/Last option" %switch%
   echo/
   echo/
   if "%select%" equ "0" goto endProg
   echo Option(s) selected: %select%
   pause
goto header
:endProg
echo End of example
goto :EOF


This subroutine activate a CheckList/RadioButton form controlled by cursor control keys

%1 = Variable that receive the selection
%2 = Options list separated by slash
%3 = /R (switch) = Radio Button (instead of Check List)

:CheckList select= "option1/option2/..." [/R]
setlocal EnableDelayedExpansion

rem Process /R switch
if /I "%~3" equ "/R" (
   set "Radio=1"
   set "unmark=( )" & set "mark=(o)"
) else (
   set "Radio="
   set "unmark=[ ]" & set "mark=[X]"
)

rem Separate options
set "options=%~2"
set "lastOption=0"
for %%a in ("%options:/=" "%") do (
   set /A lastOption+=1
   set "option[!lastOption!]=%%~a"
   set "select[!lastOption!]=%unmark%"
   call set "moveSel[%%option[!lastOption!]:~0,1%%]=set sel=!lastOption!"
)
if defined Radio set "select[1]=%mark%"

rem Define working variables
for %%a in ("Enter=13" "Esc=27" "Space=32" "End=35" "Home=36" "UpArrow=38" "DownArrow=40" "LetterA=65" "LetterZ=90") do set %%a
set "letter=ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for /F %%a in ('echo prompt $H ^| cmd') do set "BS=%%a"
echo %BS%%BS%%BS%%BS%%BS%%BS%      >_

rem Define movements for standard keys
set "sel=1"
set "moveSel[%Home%]=set sel=1"
set "moveSel[%End%]=set sel=%lastOption%"
set "moveSel[%UpArrow%]=set /A sel-=^!^!(sel-1)"
set "moveSel[%DownArrow%]=set /A sel+=^!^!(sel-lastOption)"

rem Read keys via PowerShell  ->  Process keys in Batch
set /P "=Loading menu..." < NUL
PowerShell  ^
   Write-Host 0;  ^
   $validKeys = %End%..%Home%+%UpArrow%+%DownArrow%+%Space%+%Enter%+%Esc%+%LetterA%..%LetterZ%;  ^
   while ($key -ne %Enter% -and $key -ne %Esc%) {  ^
      $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode;  ^
      if ($validKeys.contains($key)) {Write-Host $key}  ^
   }  ^
%End PowerShell%  |  "%~F0" OptionSelection
endlocal & set "%~1=%errorlevel%"
del _
exit /B


:OptionSelection
setlocal EnableDelayedExpansion

rem Wait for PS code start signal
set /P "keyCode="
set /P "="

set "endHeader=exit /B"
:nextSelection

   rem Clear the screen and show the list
   call :header
   < NUL (for /L %%i in (1,1,%lastOption%) do (
      if %%i equ %sel% (
         set /P "=!select[%%i]!  "
         findstr /A:70 . "!option[%%i]!\..\_" NUL
      ) else (
         echo !select[%%i]!  !option[%%i]!
      )
   ))
   echo/
   set /P "=Space=(De)Select, Enter=Continue, Esc=Cancel" < NUL

   rem Get a keycode from PowerShell
   set /P "keyCode="
   set /P "="

   rem Process it
   if %keyCode% equ %Enter% goto endSelection
   if %keyCode% equ %Esc% exit 0
   if %keyCode% equ %Space% (
      if defined Radio (
         set "select[%Radio%]=%unmark%"
         set "select[%sel%]=%mark%"
         set "Radio=%sel%"
      ) else (
         if "!select[%sel%]!" equ "%unmark%" (
            set "select[%sel%]=%mark%"
         ) else (
            set "select[%sel%]=%unmark%"
         )
      )
      goto nextSelection
   )
   if %keyCode% lss %LetterA% goto moveSelection
      set /A keyCode-=LetterA
      set "keyCode=!letter:~%keyCode%,1!"
   :moveSelection
   !moveSel[%keyCode%]!

goto nextSelection
:endSelection
set "sel="
for /L %%i in (1,1,%lastOption%) do (
   if "!select[%%i]!" equ "%mark%" set "sel=!sel!%%i"
)
if not defined sel set "sel=0"
exit %sel%

If we already used PS code to read keys, we may also use it for a very small additional point: move the cursor to a certain "screen home" position after each key is read. This simple detail add two important features to the whole application:

  • It completelly eliminates screen flicker because the screen is not cleared in each screen refresh. Instead, the original screen contents is preserved and the refresh just change the parts that needs to be changed. This method presents a very pleasant movement/animation to the user, even if the screen contents is large.
  • It allows to update/refresh just those parts of the screen that needs to be refreshed; this point decrease the time the screen refresh takes and allows faster animations and larger sprites.

Below there is the classical vertical selection menu application; this example add a couple new features: the cursor is hidden while the menu is active in order to present a cleaner screen, and the menu may be displayed at any place in the screen preserving the text above it.

Code: Select all

@echo off
if "%~1" equ "OptionSelection" goto %1

rem Activate a vertical selection menu controlled by cursor keys
rem Antonio Perez Ayala

rem Example: define menu options, menu messages and first letter of options (see description below)
setlocal EnableDelayedExpansion
set "option.length=0"
for %%a in ("First option =Description of first option            "
            "Second option=The option below don't have description"
            "Third option =                                       "
            "Last option  =Description of last option             ") do (
   for /F "tokens=1,2 delims==" %%b in (%%a) do (
      set /A option.length+=1
      set "option[!option.length!]=%%b"
      set "message[!option.length!]=%%c"
      call set "moveSel[%%option[!option.length!]:~0,1%%]=set sel=!option.length!"
   )
)

:loop
   cls
   echo/
   echo Description of :VMenu subroutine: how to show a vertical selection menu.
   echo/
   echo call :VMenu select options [messages]
   echo/
   echo     select     Variable that receives the number of selected option
   echo     options    Name of array with the options
   echo     messages   Name of array with messages, optional
   echo/
   echo "options" is an array with the text to show for each menu option;
   echo all texts must be aligned (filled with spaces) to the same lenght.
   echo/
   echo A variable with same "options" name and ".length" postfix must contain
   echo the number of options in the menu; for example: set options.length=4
   echo/
   echo "messages" is an optional array with companion descriptions for each option;
   echo all descriptions must be aligned to the same lenght (even the empty ones).
   echo/
   echo The highlighted option can be changed with these cursor keys:
   echo Home/End = First/Last, Up/Down = Prev/Next, or via option's first letter;
   echo Enter = Select option and continue, Esc = Cancel selection (return zero).
   echo/
   echo/
   rem  For example:

   call :VMenu select=option message
   echo/
   if %select% equ 0 goto exitLoop
   echo Option selected: %select%
   pause
goto loop
:exitLoop
echo End of menu example
goto :EOF
   

This subroutine activate a selection menu controlled by cursor control keys

:VMenu select= option [message]
setlocal

rem Define working variables
for %%a in ("Enter=13" "Esc=27" "End=35" "Home=36" "UpArrow=38" "DownArrow=40" "LetterA=65" "LetterZ=90") do set %%a
set "letter=ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for /F %%a in ('echo prompt $H ^| cmd') do set "BS=%%a"
echo %BS%%BS%%BS%%BS%%BS%%BS%      >_

rem Define selection bar movements for standard keys
set "sel=1"
set "moveSel[%Home%]=set sel=1"
set "moveSel[%End%]=set sel=!%2.length!"
set "moveSel[%UpArrow%]=set /A sel-=^!^!(sel-1)"
set "moveSel[%DownArrow%]=set /A sel+=^!^!(sel-%2.length)"

rem Read keys via PowerShell  ->  Process keys in Batch
set /P "=Loading menu..." < NUL
PowerShell  ^
   $console = $Host.UI.RawUI;  ^
   $curSize = $console.CursorSize;  ^
   $console.CursorSize = 0;  ^
   $curPos = $console.CursorPosition;  ^
   $curPos.X = 0;  ^
   $console.CursorPosition = $curPos;  ^
   Write-Host 0;  ^
   $validKeys = %End%..%Home%+%UpArrow%+%DownArrow%+%Enter%+%Esc%+%LetterA%..%LetterZ%;  ^
   while ($key -ne %Enter% -and $key -ne %Esc%) {  ^
      $key = $console.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode;  ^
      if ($validKeys.contains($key)) {  ^
         if ($key -ne %Enter% -and $key -ne %Esc%) {$console.CursorPosition = $curPos}  ^
         Write-Host $key  ^
      }  ^
   }  ^
   $console.CursorSize = $curSize;  ^
%End PowerShell%  |  "%~F0" OptionSelection %2 %3
endlocal & set "%1=%errorlevel%"
del _
exit /B


:OptionSelection %1 option [message]
setlocal EnableDelayedExpansion

rem Wait for PS code start signal
set /P "keyCode="
set /P "="

:nextSelection

   rem Show menu options
   for /L %%i in (1,1,!%2.length!) do (
      if %%i equ %sel% (
         set /P "=.%BS%    " < NUL
         findstr /A:70 . "!%2[%%i]!\..\_" NUL
      ) else (
         echo     !%2[%%i]!
      )
   )
   if defined %3[%sel%] (
      echo/
      echo(!%3[%sel%]!
   )

   rem Get a keycode from PowerShell
   set /P "keyCode="
   set /P "="

   rem Process it
   if %keyCode% equ %Enter% goto endSelection
   if %keyCode% equ %Esc% set "sel=0" & goto endSelection
   if %keyCode% lss %LetterA% goto moveSelection
      set /A keyCode-=LetterA
      set "keyCode=!letter:~%keyCode%,1!"
   :moveSelection
   !moveSel[%keyCode%]!

goto nextSelection
:endSelection
exit %sel%

Finally, if we already accepted to use PS code to read keys and move the cursor to a home position, we may accept to also use it for two small additional points: move the cursor to any place in the screen, and show text in color. This way, this method would provide the features of my GetKey.exe, CursorPos.exe and ColorShow.exe auxiliary programs, so it would allow to write very fast animated games in color using just original Windows features. I will modify my original Snake.bat, 2048.bat, Tetris.bat and other animated games in order to use the PowerShell features described in this topic; I will post the new versions as soon as they are ready.

Enjoy! :D

Antonio
Last edited by Aacini on 11 Apr 2016 23:04, edited 1 time in total.

Aacini
Expert
Posts: 1913
Joined: 06 Dec 2011 22:15
Location: México City, México
Contact:

Re: Process arrow keys in Batch in an efficient way

#2 Post by Aacini » 31 Mar 2016 14:36

I modified my Tetris.bat game in order to make good use of the PowerShell features previously described. The first step was to change the original Batch's xcopy read keys method by the equivalent PowerShell's one; to do that, I changed this section:

Code: Select all

:Input
set "com[J]=Dx=-1"
set "com[L]=Dx=1"
set "com[K]=del=3"
set "com[I]=R=-1"
set "com[A]=R=1"
set "com[D]=R=-1"
set "com[S]=Dy=-1"
set "com[Y]=Y"
set "com[N]=N=1"
set "com[P]=pause=1"

for /L %%# in () do (
   set "key="
   for /F "delims=" %%k in ('xcopy /W "%~F0" "%~F0" 2^>NUL') do if not defined key set "key=%%k"
   for /F %%k in ("!key:~-1!") do (
      echo(!com[%%k]!
      if /I "%%k" equ "N" exit
   )
)
exit

by this one:

Code: Select all

:Input
set "letter=ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for /L %%i in (0,1,25) do (
   set /A "Letter!letter:~%%i,1!=%%i+65"
)
PowerShell    ^
   $command = @{  ^
      %LetterJ% = 'Dx=-1';  ^
      %LetterL% = 'Dx=1';   ^
      %LetterK% = 'del=3';  ^
      %LetterI% = 'R=-1';   ^
      %LetterA% = 'R=1';    ^
      %LetterD% = 'R=-1';   ^
      %LetterS% = 'Dy=-1';  ^
      %LetterY% = 'Y';      ^
      %LetterN% = 'N=1';    ^
      %LetterP% = 'pause=1';  ^
   };  ^
   while ($key -ne %LetterN%) {  ^
      $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode;  ^
      Write-Output $command[$key];   ^
   }
%End PowerShell%
exit

After this change the program behaves exactly the same than before, but the reading of keys is now performed by PowerShell so it is very easy to replace the %LetterJ%/%LetterL%/%LetterK%/%LetterI% values by the new %LeftArrow%/%RightArrow%/%DownArrow%/%UpArrow% cursor control keys. After tested the new method, I think that the reading of keys via PowerShell is slightly faster than Batch's xcopy method, so the game seems more responsive than before.

I also added the possibility of control the Tetris pieces via movements of the mouse, just as a proof of concept; the direction of the mouse movement emulate the corresponding arrow key. Playing the game using the mouse this way is very funny! :P You may adjust the mouse sensitivity via $mouseSens variable with an inverse value: smaller numbers produce a more responsive mouse.

Code: Select all

@echo off
setlocal EnableDelayedExpansion

if "%~1" neq "" goto %1

title Tetris.BAT by Aacini
rem Written by Antonio Perez Ayala
rem http://www.dostips.com/forum/viewtopic.php?f=3&t=6812
rem Reference: http://colinfahey.com/tetris/tetris.html
rem 2015/11/27 - version 1.0
rem 2016/03/31 - version 2.0: Use PowerShell to read arrow keys and mouse movements
rem http://www.dostips.com/forum/viewtopic.php?f=3&t=6936&p=45980#p45980

cls
echo/
echo ===  Pure .BATch-file Tetris game by Aacini  ===
echo/
echo/
echo Tetris pieces are controlled with these keys:
echo/
echo                              rot.right
echo                                  ^^
echo rot.             rot.     move   ^|    move
echo left ^<- A S D -^> right    left ^<- -^> right
echo           ^|                      ^|
echo           v                      v
echo       soft drop              hard drop
echo/
echo ... and also via mouse movements.
echo/
echo/
echo Press P to pause the game; press N to end game
echo/
echo/
pause
cls

rem Field dimensions
set /A cols=10, lines=20

set /A col=cols+6, lin=lines+8,  lin+=lines+2
mode CON: cols=%col% lines=%lin%
chcp 850 > NUL
cd . > pipeFile.txt

set /P "=Loading PS engine..." < NUL
"%~F0" Input >> pipeFile.txt  |  "%~F0" Main < pipeFile.txt
ping localhost -n 2 > NUL
del pipeFile.txt
goto :EOF



:Input
set "letter=ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for /L %%i in (0,1,25) do (
   set /A "Letter!letter:~%%i,1!=%%i+65"
)
set /A "LeftArrow=37, UpArrow=38, RightArrow=39, DownArrow=40"
PowerShell  ^
   $mouseSens = 10;  ^
   $command = @{     ^
      %LeftArrow%  = 'Dx=-1';  ^
      %RightArrow% = 'Dx=1';   ^
      %DownArrow%  = 'del=3';  ^
      %UpArrow%    = 'R=-1';   ^
      %LetterA%    = 'R=1';    ^
      %LetterD%    = 'R=-1';   ^
      %LetterS%    = 'Dy=-1';  ^
      %LetterY%    = 'Y';      ^
      %LetterN%    = 'N=1';    ^
      %LetterP%    = 'pause=1';  ^
   };  ^
   [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') ^| %%{}; ^
   $lX = ([System.Windows.Forms.Control]::MousePosition.X); ^
   $lY = ([System.Windows.Forms.Control]::MousePosition.Y); ^
   Write-Output 0;  ^
   while ( $key -ne %LetterN% ) {  ^
      if ( $Host.UI.RawUI.KeyAvailable ) {  ^
         $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyUp').VirtualKeyCode;  ^
         Write-Output $command[$key];  ^
      } else {  ^
        $mX = ([System.Windows.Forms.Control]::MousePosition.X);  ^
        $mY = ([System.Windows.Forms.Control]::MousePosition.Y);  ^
        $dX = $mX - $lX;  ^
        $dY = $mY - $lY;  ^
        if ( [math]::abs($dX) -gt $mouseSens ) {  ^
           if ( $dX -lt 0 ) {  ^
              Write-Output $command[%LeftArrow%];  ^
           } else {  ^
              Write-Output $command[%RightArrow%];  ^
           }  ^
           $lX = $mX; ^
           $lY = $mY; ^
        }  ^
        if ( [math]::abs($dY) -gt $mouseSens ) {  ^
           if ( $dY -lt 0 ) {  ^
              Write-Output $command[%UpArrow%];  ^
           } else {  ^
              Write-Output $command[%DownArrow%];  ^
           }  ^
           $lX = $mX; ^
           $lY = $mY; ^
        }  ^
      }  ^
      Start-Sleep -m 100;  ^
   }
%End PowerShell%
echo Ending game... > CON
exit



:Main

(
   for /F "delims==" %%v in ('set') do set "%%v="
   set /A cols=%cols%, lines=%lines%
)

rem Initialize the Field
for /L %%i in (1,1,%cols%) do set "spc=!spc! "
for /L %%i in (1,1,%lines%) do set "F%%i=  ³%spc%³"
set /A top=lines+1
set "F%top%=  Ú" & set "F0=  À"
for /L %%i in (1,1,%cols%) do set "F%top%=!F%top%!Ä" & set "F0=!F0!Ä"
set "F%top%=!F%top%!¿" & set "F0=%F0%Ù"
set "F-1=  Level: 1" & set "Level=1"
set "F-2=   Rows: 0" & set "Rows=0"
set "F-3=  Score: 0" & set "Score=0"
for /L %%i in (1,1,%cols%) do set "blk=!blk!Û"
set /A top=lines+3, delay=50,   linesP2=lines+2, linesP1=lines+1

rem Define all ":orientations:" of the O I S Z L J T pieces via "triplets":
rem (offset Y . offset X . length X); one "triplet" for each horizontal line
for %%t in ( "O:0.-1.2 -1.-1.2"
             "I:0.-2.4:1.0.1 0.0.1 -1.0.1 -2.0.1"
             "S:0.0.2 -1.-1.2:1.0.1 0.0.2 -1.1.1"
             "Z:0.-1.2 -1.0.2:1.1.1 0.0.2 -1.0.1"
             "L:0.-1.3 -1.-1.1:1.0.1 0.0.1 -1.0.2:1.1.1 0.-1.3:1.-1.2 0.0.1 -1.0.1"
             "J:0.-1.3 -1.1.1:1.0.2 0.0.1 -1.0.1:1.-1.1 0.-1.3:1.0.1 0.0.1 -1.-1.2"
             "T:0.-1.3 -1.0.1:1.0.1 0.0.2 -1.0.1:1.0.1 0.-1.3:1.0.1 0.-1.2 -1.0.1" ) do (
   set "pc=%%~t"
   set "i=-1"
   for /F "delims=" %%p in (^"!pc::^=^
% New line %
!^") do (
      if !i! lss 0 (set "pc=%%p") else set "!pc!!i!=%%p"
      set /A i+=1
   )
   set "!pc!N=!i!"
)
set "pcs=OISZLJT"


:WaitPS for PowerShell start signal
   set /P "com="
if not defined com goto WaitPS
set "com="


set "init=1"
for /L %%# in () do (

   if defined init (
      setlocal EnableDelayedExpansion
      set "init="

      rem Create the first "previous" piece
      for /L %%i in (0,1,!time:~-1!) do set /A p=!random!%%7
      for %%p in (!p!) do set "p2=!pcs:~%%p,1!"
      for %%p in (!p2!) do set "p3=!%%p0!" & set "p4=!%%pN!"

      set "new=1"
   )

   if defined new (
      set "new="

      rem Take the "previous" piece as current one
      set "pc=!p2!" & set "p0=!p3!" & set "pN=!p4!"

      rem Create a new "previous" piece
      for /L %%i in (1,1,2) do (
         set /A p=!random!*7/32768
         for %%p in (!p!) do (
            set "p=!pcs:~%%p,1!"
            if !p! neq !pc! set "p2=!p!"
         )
      )
      for %%p in (!p2!) do set "p3=!%%p0!" & set "p4=!%%pN!"

      rem Insert the new "previous" piece in its place, above Field
      set /A x=3+cols/2, y=top, yp=top-1
      set "F!yp!=   %spc%"
      for %%p in (!p3!) do (
         for /F "tokens=1-3 delims=." %%i in ("%%p") do (
            set /A yp=y+%%i, xp=x+%%j, xL=xp+%%k
            for /F "tokens=1-3" %%a in ("!yp! !xp! !xL!") do (
               set "F%%a=!spc:~0,%%b!!blk:~0,%%k!!spc:~%%c!"
            )
         )
      )

      rem Try to insert the new current piece in the Field...
      set /A x=3+cols/2, y=lines,   b=1
      for %%p in (!p0!) do (
         for /F "tokens=1-3 delims=." %%i in ("%%p") do (
            set /A yp=y+%%i, xp=x+%%j, xL=xp+%%k
            for /F "tokens=1-3" %%a in ("!yp! !xp! !xL!") do (
               if "!F%%a:~%%b,%%k!" neq "!spc:~0,%%k!" set     "b="
               set "F%%a=!F%%a:~0,%%b!!blk:~0,%%k!!F%%a:~%%c!"
            )
         )
      )
      cls
      for /L %%i in (%top%,-1,%linesP2%) do echo(!F%%i!& echo(!F%%i!
      echo(!F%linesP1%!
      for /L %%i in (%lines%,-1,1) do echo(!F%%i!& echo(!F%%i!
      for /L %%i in (0,-1,-3) do echo(!F%%i!

      rem ... if that was not possible:
      if not defined b call :endGame & endlocal

      set "p1=!p0!"
      set /A "pI=0, del=delay, b=1!time:~-2!"

   )

   rem Control module: move the piece as requested via a key, or down one row each %del% centiseconds
   set "move="
   set /A "Dy=Dx=0"
   set /P "com="
   if defined com (
      set /A "!com!, move=1"
      set "com="
      if defined N exit
      if defined pause call :Pause & set "move="
      set "b=1!time:~-2!"
   ) else (
      set /A "e=1!time:~-2!, elap=e-b, elap-=(elap>>31)*100"
      if !elap! geq !del! set /A b=e, Dy=move=-1
   )

   if defined move (

      rem Delete the piece from its current position, and store current coordinates
      set i=0
      for %%p in (!p0!) do for /F "tokens=1-3 delims=." %%i in ("%%p") do (
         set /A yp=y+%%i, xp=x+%%j, xL=xp+%%k
         for /F "tokens=1-3" %%a in ("!yp! !xp! !xL!") do (
            set "F%%a=!F%%a:~0,%%b!!spc:~0,%%k!!F%%a:~%%c!"
            set /A i+=1
            set "c!i!=%%a %%b %%c %%k"
         )
      )

      rem If move is Rotate: get rotated piece
      if defined R (
         set /A "p=(pI+R+pN)%%pN"
         for /F "tokens=1,2" %%i in ("!pc! !p!") do set "p1=!%%i%%j!"
      )

      rem Test if the piece can be placed at the new position, and store new coordinates
      set j=0
      for %%p in (!p1!) do if defined move (
         for /F "tokens=1-3 delims=." %%i in ("%%p") do (
            set /A yp=y+%%i+Dy, xp=x+%%j+Dx, xL=xp+%%k
            for /F "tokens=1-3" %%a in ("!yp! !xp! !xL!") do (
               if "!F%%a:~%%b,%%k!" equ "!spc:~0,%%k!" (
                  set /A j+=1
                  set "n!j!=%%a %%b %%c %%k"
               ) else (
                  set "move="
               )
            )
         )
      )

      if defined move (

         rem Place the piece at the new position
         for /L %%j in (1,1,!j!) do (
            for /F "tokens=1-4" %%a in ("!n%%j!") do (
               set "F%%a=!F%%a:~0,%%b!!blk:~0,%%d!!F%%a:~%%c!"
            )
         )

         rem Update the Field in screen
         cls
         for /L %%i in (%top%,-1,%linesP2%) do echo(!F%%i!& echo(!F%%i!
         echo(!F%linesP1%!
         for /L %%i in (%lines%,-1,1) do echo(!F%%i!& echo(!F%%i!
         for /L %%i in (0,-1,-3) do echo(!F%%i!

         rem Update any changes in the piece
         set /A y+=Dy, x+=Dx
         if defined R set "p0=!p1!" & set "pI=!p!" & set "R="

      ) else (   rem The piece can not be moved

         rem Recover the piece at its current position
         for /L %%i in (1,1,!i!) do (
            for /F "tokens=1-4" %%a in ("!c%%i!") do (
               set "F%%a=!F%%a:~0,%%b!!blk:~0,%%d!!F%%a:~%%c!"
            )
         )
         if defined R set "p1=!p0!" & set "R="

         if !Dy! neq 0 (   rem The piece "lands"

            rem Count completed lines
            set "j=0"
            for /L %%i in (1,1,!i!) do for /F %%a in ("!c%%i!") do (
               if "!F%%a:~3,%cols%!" equ "%blk%" (
                  set "F%%a=  ³%spc: ==%³"
                  set /A j+=1
               )
            )

            if !j! neq 0 (
               rem Update scores (See N-Blox at http://www.tetrisfriends.com/help/tips_appendix.php#rankingsystem)
               set /A "xp=Level*(40+((j-2>>31)+1)*60+((j-3>>31)+1)*200+((j-4>>31)+1)*900), Score+=xp, Rows+=j, xL=Level, Level=(Rows-1)/10+1"
               set "F-2=!F-2:~0,8!+!j!     "
               set "xp=!xp!     "
               set "F-3=!F-3:~0,8!+!xp:~0,6!"
               echo  BEL Ctrl-G Ascii-7
               cls
               for /L %%i in (%top%,-1,%linesP2%) do echo(!F%%i!& echo(!F%%i!
               echo(!F%linesP1%!
               for /L %%i in (%lines%,-1,1) do echo(!F%%i!& echo(!F%%i!
               for /L %%i in (0,-1,-3) do echo(!F%%i!
               set "F-1=!F-1:~0,8! !Level!"
               set "F-2=!F-2:~0,8! !Rows!"
               set "F-3=!F-3:~0,8! !Score!"
               if !Level! neq !xL! if !delay! gtr 5 set /A delay-=5

               rem Remove completed lines
               set "i=1"
               for /L %%i in (1,1,%lines%) do (
                  set "F!i!=!F%%i!"
                  if "!F%%i:~3,1!" neq "=" set /A i+=1
               )
               for /L %%i in (!i!,1,%lines%) do set "F%%i=  ³%spc%³"
               call :Delay 95
               cls
               for /L %%i in (%top%,-1,%linesP2%) do echo(!F%%i!& echo(!F%%i!
               echo(!F%linesP1%!
               for /L %%i in (%lines%,-1,1) do echo(!F%%i!& echo(!F%%i!
               for /L %%i in (0,-1,-3) do echo(!F%%i!
            )

            rem Request to show a new piece
            set "new=1"

         )

      )

   )

)

:endGame
set /P "=Play again? " < NUL
:choice
   set /P "com="
if not defined com goto choice
if /I "%com%" equ "Y" exit /B
if /I "%com:~0,1%" neq "N" set "com=" & goto choice
echo N
exit


:Pause
set "pause=!F%lines%!"
set "F%lines%=  ³%spc:          =  PAUSED  %³"
cls & for /L %%i in (%top%,-1,%linesP2%) do echo(!F%%i!& echo(!F%%i!
echo(!F%linesP1%!
for /L %%i in (%lines%,-1,1) do echo(!F%%i!& echo(!F%%i!
for /L %%i in (0,-1,-3) do echo(!F%%i!
:wait
   set /P "com="
if not defined com goto wait
set "com="
set "F%lines%=%pause%"
cls & for /L %%i in (%top%,-1,%linesP2%) do echo(!F%%i!& echo(!F%%i!
echo(!F%linesP1%!
for /L %%i in (%lines%,-1,1) do echo(!F%%i!& echo(!F%%i!
for /L %%i in (0,-1,-3) do echo(!F%%i!
set "pause="
exit /B


:Delay centisecs
set "b=1%time:~-2%"
:wait2
   set /A "e=1%time:~-2%, elap=e-b, elap-=(elap>>31)*100"
if %elap% lss %1 goto wait2
set "b=1%time:~-2%"
exit /B

The next step is the already described in the first post of this thread: establish a backwards communication from the Batch code to the "resident" (active) PowerShell one via a file, so the Batch code may request PowerShell to execute several commands, like move the cursor, show text in color, etc. See the first post in this thread for further details.

Antonio
Last edited by Aacini on 15 Aug 2016 10:47, edited 1 time in total.

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

Re: Process arrow keys in Batch in an efficient way

#3 Post by aGerman » 31 Mar 2016 16:43

That's neat :D Thanks Antonio!

I had to change the code at one point.

Code: Select all

      if ($validKeys.contains($key)) {Write-Host $key}  ^

PowerShell complained that the object doesn't have a "contains" method.
After some tests this worked for me:

Code: Select all

      if ($validKeys -contains $key) {Write-Host $key}  ^


Regards
aGerman

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

Re: Process arrow keys in Batch in an efficient way

#4 Post by dbenham » 31 Mar 2016 19:09

Very nice :!:

Aacini wrote:The next step is the already described in the first post of this thread: establish a backwards communication from the Batch code to the "resident" (active) PowerShell one via a file, so the Batch code may request PowerShell to execute several commands, like move the cursor, show text in color, etc. See the first post in this thread for further details.
That is exactly what I was thinking when I finished CONSOLE.BAT. Currently CONSOLE.BAT is too slow for many applications. Having the utility running in the background, ready to set color, cursor position, get keys, etc. on demand could make the utility extremely useful.

I haven't studied your first post carefully, but I don't see where you talk about or implement two way communication. I'm assuming you have yet to do this.

One additional aspect of the PowerShell rawUI object that intrigues me are all of the buffer methods. It looks to me like you can address the buffer space directly and "plot" characters, along with color, without having to move a cursor and write the data. You can also fill entire areas with a character with a single command. It also looks like you can scroll up and down, left and right. And also I think you can have multiple buffers. While one is displayed, you could prepare the next frame, and then once complete, swap it with the displayed buffer for "instantaneous" full screen updates. I see some interesting scrolling game possibilities. But I wonder if it would be better to simply develop the whole thing in PowerShell.

The one thing I don't like about PowerShell scripts is the need to enable scripts while having admin rights. In contrast, the mini command line "script" can be run right out of the box, without configuration. But there is a limit to the size of such a command line "script", which is one small argument for the hybrid approach.


Dave Benham

Aacini
Expert
Posts: 1913
Joined: 06 Dec 2011 22:15
Location: México City, México
Contact:

Re: Read arrow keys and show color text in an efficient way

#5 Post by Aacini » 11 Apr 2016 23:10

:arrow: The second part of this application is ready :!: :D 8)

Image

2016-04-12: Code modified to fix a synchronization problem and a bug when the window/buffer sizes are changed.

Code: Select all

@echo off
setlocal EnableDelayedExpansion

if "%~1" neq "" goto %1

rem Written by Antonio Perez Ayala
rem http://www.dostips.com/forum/viewtopic.php?f=3&t=6812
rem Reference: http://colinfahey.com/tetris/tetris.html
rem 2015/11/27 - version 1.0
rem 2016/03/31 - version 2.0: Use PowerShell to read arrow keys and mouse movements
rem 2016/04/11 - version 3.0: Use PowerShell to show text in color
rem 2016/04/12 - version 3.1: Synchronization problem fixed, change window/buffer size bug fixed
rem http://www.dostips.com/forum/viewtopic.php?f=3&t=6936&p=46206#p46206

rem The best appearance is obtained with a square font, like Raster Font 8x8

color 0F
chcp 850 > NUL
cls
echo/
echo ===  Pure .BATch-file Tetris game by Aacini  ===
echo/
echo/
echo Tetris pieces are controlled with these keys:
echo/
echo                                 rot.right
echo                                     ^^
echo rot.               rot.     move    ^|     move
echo left ^<ÄÄ A S D ÄÄ^> right    left ^<ÄÄúÄÄ^> right
echo            ^|                        ^|
echo            v                        v
echo        soft drop                hard drop
echo/
echo Mouse movements emulate arrow key presses,
echo left button pause/continue the game.
echo/
echo/
echo Press P to pause/continue; press N to end game.
echo/
echo/
pause
cls

rem Field dimensions
set /A cols=10, lines=20

set /P "=Loading PS engine..." < NUL
cd . > pipeFile.txt
"%~F0" Input >> pipeFile.txt  |  "%~F0" Main < pipeFile.txt  |  "%~F0" Output
ping localhost -n 2 > NUL
del pipeFile.txt
goto :EOF



:Input
set "letter=ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for /L %%i in (0,1,25) do (
   set /A "Letter!letter:~%%i,1!=%%i+65"
)
set /A "LeftArrow=37, UpArrow=38, RightArrow=39, DownArrow=40"
PowerShell  ^
   $mouseSens = 10;  ^
   $command = @{     ^
      %LeftArrow%  = 'Dx=-1';  ^
      %RightArrow% = 'Dx=1';   ^
      %DownArrow%  = 'del=3';  ^
      %UpArrow%    = 'R=-1';   ^
      %LetterA%    = 'R=1';    ^
      %LetterD%    = 'R=-1';   ^
      %LetterS%    = 'Dy=-1';  ^
      %LetterY%    = 'Y';      ^
      %LetterN%    = 'N=1';    ^
      %LetterP%    = 'pause=1';  ^
   };  ^
   [Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') ^| Out-Null; ^
   $lX = [System.Windows.Forms.Control]::MousePosition.X; ^
   $lY = [System.Windows.Forms.Control]::MousePosition.Y; ^
   Start-Sleep -m 1500;  ^
   Write-Output 0;  ^
   while ( $key -ne %LetterN% ) {  ^
      if ( $Host.UI.RawUI.KeyAvailable ) {  ^
         $key = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyUp').VirtualKeyCode;  ^
         Write-Output $command[$key];  ^
      } else {  ^
         $mX = [System.Windows.Forms.Control]::MousePosition.X;  ^
         $mY = [System.Windows.Forms.Control]::MousePosition.Y;  ^
         $mB = [System.Windows.Forms.Control]::MouseButtons;     ^
         $dX = $mX - $lX;  $dY = $mY - $lY;  ^
         if ( [math]::abs($dX) -gt $mouseSens ) {   ^
            $if = if ($dX -lt 0) {%LeftArrow%} else {%RightArrow%};  ^
            Write-Output $command[$if];  ^
            $lX = $mX;  $lY = $mY;  ^
         } else { if ( [math]::abs($dY) -gt $mouseSens ) {  ^
            $if = if ($dY -lt 0) {%UpArrow%} else {%DownArrow%};  ^
            Write-Output $command[$if];  ^
            $lX = $mX;  $lY = $mY;  ^
         } else { if ( $mB -eq 'Left' ) {  ^
            Write-Output $command[%LetterP%];  ^
         }}}  ^
      }  ^
      Start-Sleep -m 100;  ^
   }
%End PowerShell%
echo Ending game... > CON
exit



:Output
rem Parameters received via lines read have one of these four forms:
rem - 'cls' = Clear screen and initialize the field
rem - X,Y  =  Move cursor to X,Y position
rem - X,Y,'ColorString' = Move cursor to such position and show the "Color Codes String"
rem - X,Y,Color,Wide = Move cursor to such position and show Wide chars in given Color;
rem                    if Color is '=', show an equal-sign character Wide times
rem - 'del',Filename = Delete the given file (for synchro purposes)
PowerShell  ^
   function ClearScreen () {  ^
      cls; for ( $i=0; $i -lt $lnN; ++$i ) { Write-Host; Write-Host }  ^
      Write-Host -NoNewLine (' '*2); Write-Host "Ú$frame¿";  ^
      for ( $i=1; $i -le %lines%*$lnN; ++$i ) { Write-Host -NoNewLine (' '*2); Write-Host "³$spc³" }  ^
      Write-Host -NoNewLine (' '*2); Write-Host "À$frameÙ";  ^
      Write-Host -NoNewLine (' '*2); Write-Host ' Level: 1';  ^
      Write-Host -NoNewLine (' '*3); Write-Host ' Rows: 0';  ^
      Write-Host -NoNewLine (' '*2); Write-Host ' Score: 0';  ^
   }  ^
   $console = $Host.UI.RawUI;  ^
   $console.WindowTitle = 'Tetris.BAT by Aacini';  ^
   $curSize = $console.CursorSize;  ^
   $console.CursorSize = 0;  ^
   for ( $i=1; $i -le 3; ++$i ) {  ^
      if ( $i*(%lines%+2)+6 -le $console.MaxPhysicalWindowSize.Height ) { $lnN = $i } ^
   }  ^
   $col = $lnN*%cols%+6;  $lin = $lnN*(%lines%+2)+6;  ^
   $winSize = $console.WindowSize; $bufSize = $console.BufferSize;  ^
   $winSize.Width = $col;  $bufSize.Width = $col;  ^
   if ( $col -lt $console.WindowSize.Width ) {  ^
      $console.WindowSize = $winSize; $console.BufferSize = $bufSize;  ^
   } else {  ^
      $console.BufferSize = $bufSize; $console.WindowSize = $winSize;  ^
   }  ^
   $winSize.Height = $lin; $bufSize.Height = $lin;  ^
   if ( $lin -lt $console.WindowSize.Height ) {  ^
      $console.WindowSize = $winSize; $console.BufferSize = $bufSize;  ^
   } else {  ^
      $console.BufferSize = $bufSize; $console.WindowSize = $winSize;  ^
   }  ^
   $ln = ( ('X'), ('Úª','ÀÙ'), ('ÚÄ¿','³ ³','ÀÄÙ') )[$lnN-1];  ^
   $frame = ''; $spc = ''; for ( $i=1; $i -le $col-6; ++$i ) { $frame+='Ä'; $spc+=' '; }  ^
   $coords = $console.CursorPosition;  ^
   foreach ( $line in $input ) {  ^
      $X,$Y,$Color,$Wide = $line.Split(',');  ^
      if ( $X -eq 'cls' ) {  ^
         ClearScreen  ^
      } else { if ($X -eq 'del') {  ^
         del $Y  ^
      } else {  ^
         $coords.X = 3 + ([int]$X-1) * $lnN;  ^
         $Y = [int]$Y; $coords.Y = (%lines%+3-$Y) * $lnN ;  ^
         if ( $Y -le %lines% ) { $coords.Y -= $lnN-1 }  ^
         if ( $Y -lt 0 ) { $coords.Y += $Y*($lnN-1) }  ^
         $console.CursorPosition = $coords;  ^
         if ( $Wide ) {  ^
            $Wide = [int]$Wide;  ^
            if ( $Color -ne '=' ) {  ^
               $Color = [int]('0x'+$Color);  ^
               for ( $i=0; $i -lt $lnN; ++$i ) {  ^
                  for ( $j=0; $j -lt $Wide; ++$j ) {  ^
                     Write-Host  -BackgroundColor $Color  -ForegroundColor 'Black'  -NoNewLine  $ln[$i]  ^
                  }  ^
                  ++$coords.Y; $console.CursorPosition = $coords;  ^
               }  ^
            } else {  ^
               for ( $i=0; $i -lt $lnN; ++$i ) {  ^
                  Write-Host  ('='*($Wide*$lnN));  ^
                  ++$coords.Y; $console.CursorPosition = $coords;  ^
               }  ^
            }  ^
         } else { if ( $Color ) {  ^
            for ( $i=0; $i -lt $lnN; ++$i ) {  ^
               for ( $j=0; $j -lt $Color.Length; ++$j ) {  ^
                  $colr = [int]('0x'+$Color[$j]);  ^
                  Write-Host  -BackgroundColor $colr  -ForegroundColor 'Black'  -NoNewLine  $ln[$i]  ^
               }  ^
               ++$coords.Y; $console.CursorPosition = $coords;  ^
            }  ^
         }}  ^
      }}  ^
   }  ^
   $console.CursorSize = $curSize;
%End PowerShell%
exit /B



:Main

(
   for /F "delims==" %%v in ('set') do set "%%v="
   set /A cols=%cols%, lines=%lines%
)

rem Initialize the Field
for /L %%i in (1,1,%cols%) do set "spc=!spc!0"
for /L %%i in (1,1,%lines%) do set "F%%i=³%spc%³"
set /A "Level=1, Rows=0, Score=0,  top=lines+3, delay=50"

rem The pieces are defined via 3 groups of values:  Piece : Color : Orientations
rem The ":orientations:" (piece positions) are defined via "triplets":
rem (offset Y . offset X . length X); one "triplet" for each horizontal line
rem See: http://colinfahey.com/tetris/tetris.html
set "pcs="
for %%t in ( "O:9:0.-1.2 -1.-1.2"
             "I:C:0.-2.4:1.0.1 0.0.1 -1.0.1 -2.0.1"
             "S:A:0.0.2 -1.-1.2:1.0.1 0.0.2 -1.1.1"
             "Z:B:0.-1.2 -1.0.2:1.1.1 0.0.2 -1.0.1"
             "L:D:0.-1.3 -1.-1.1:1.0.1 0.0.1 -1.0.2:1.1.1 0.-1.3:1.-1.2 0.0.1 -1.0.1"
             "J:7:0.-1.3 -1.1.1:1.0.2 0.0.1 -1.0.1:1.-1.1 0.-1.3:1.0.1 0.0.1 -1.-1.2"
             "T:E:0.-1.3 -1.0.1:1.0.1 0.0.2 -1.0.1:1.0.1 0.-1.3:1.0.1 0.-1.2 -1.0.1" ) do (
   set "pc=%%~t" & set "i=-2"
   for /F "delims=" %%p in (^"!pc::^=^
% New line %
!^") do (
      if !i! equ -2 (
         set "pc=%%p" & set "pcs=!pcs!%%p"
      ) else if !i! equ -1 (
         set "!pc!R=%%p"
      ) else (
         set "!pc!!i!=%%p"
      )
      set /A i+=1
   )
   set "!pc!N=!i!"
)

:WaitPS for PowerShell Input part start signal
   set /P "com="
if not defined com goto WaitPS
set "com="

set "init=1"
for /L %%# in () do (

   if defined init (
      setlocal EnableDelayedExpansion
      set "init="
      echo cls

      rem Create the first "previous" -hidden- piece
      for /L %%i in (0,1,!time:~-1!) do set /A p=!random!%%7
      for %%p in (!p!) do set "p2=!pcs:~%%p,1!"
      for %%p in (!p2!) do set "p3=!%%p0!" & set "p4=!%%pN!" & set "p5=!%%pR!!%%pR!!%%pR!!%%pR!"

      set "new=1"
   )

   if defined new (
      set "new="

      rem Take the "previous" piece as current one
      set "pc=!p2!" & set "p0=!p3!" & set "pN=!p4!" & set "pR=!p5!"

      rem Create a new "previous" piece
      for /L %%i in (1,1,2) do (
         set /A p=!random!*7/32768
         for %%p in (!p!) do (
            set "p=!pcs:~%%p,1!"
            if !p! neq !pc! set "p2=!p!"
         )
      )
      for %%p in (!p2!) do set "p3=!%%p0!" & set "p4=!%%pN!" & set "p5=!%%pR!!%%pR!!%%pR!!%%pR!"

      rem Show the new "previous" piece in its place, above Field
      set /A x=cols/2-1
      for %%p in (!p3!) do (
         for /F "tokens=1-3 delims=." %%i in ("%%p") do (
            set /A yp=top+%%i, xp=2+%%j, xL=xp+%%k
            for /F "tokens=1,2" %%a in ("!xp! !xL!") do (
               set "pce=0000"
               set "pce=!pce:~0,%%a!!p5:~0,%%k!!pce:~%%b!"
               echo !x!,!yp!,!pce!
            )
         )
      )
      if !p2! equ I set /A yp=top-1 & echo !x!,!yp!,0000

      rem Try to insert the new current piece in the Field...
      set /A x=cols/2+1, y=lines,   b=1
      for %%p in (!p0!) do (
         for /F "tokens=1-3 delims=." %%i in ("%%p") do (
            set /A yp=y+%%i, xp=x+%%j, xL=xp+%%k
            for /F "tokens=1-3" %%a in ("!yp! !xp! !xL!") do (
               if "!F%%a:~%%b,%%k!" neq "!spc:~0,%%k!" set     "b="
               set "F%%a=!F%%a:~0,%%b!!pR:~0,%%k!!F%%a:~%%c!"
               echo %%b,%%a,!F%%a:~%%b,1!,%%k
            )
         )
      )

      rem ... if that was not possible:
      if not defined b call :endGame & endlocal

      set "p1=!p0!"
      set /A "pI=0, del=delay, b=1!time:~-2!"

   )

   rem Control module: move the piece as requested via a key, or down one row each %del% centiseconds
   set "move="
   set /A "Dy=Dx=0"
   set /P "com="
   if defined com (
      set /A "!com!, move=1"
      set "com="
      if defined N exit
      if "!pause!" equ "1" call :Pause & set "move="
      set "b=1!time:~-2!"
   ) else (
      set /A "e=1!time:~-2!, elap=e-b, elap-=(elap>>31)*100"
      if !elap! geq !del! set /A b=e, Dy=move=-1
   )

   if defined move (

      rem Delete the piece from its current position in the field, and store current coordinates
      set i=0
      for %%p in (!p0!) do for /F "tokens=1-3 delims=." %%i in ("%%p") do (
         set /A yp=y+%%i, xp=x+%%j, xL=xp+%%k
         for /F "tokens=1-3" %%a in ("!yp! !xp! !xL!") do (
            set "F%%a=!F%%a:~0,%%b!!spc:~0,%%k!!F%%a:~%%c!"
            set /A i+=1
            set "c!i!=%%a %%b %%c %%k"
         )
      )

      rem If move is Rotate: get rotated piece
      if defined R (
         set /A "p=(pI+R+pN)%%pN"
         for /F "tokens=1,2" %%i in ("!pc! !p!") do set "p1=!%%i%%j!"
      )

      rem Test if the piece can be placed at the new position, and store new coordinates
      set j=0
      for %%p in (!p1!) do if defined move (
         for /F "tokens=1-3 delims=." %%i in ("%%p") do (
            set /A yp=y+%%i+Dy, xp=x+%%j+Dx, xL=xp+%%k
            for /F "tokens=1-3" %%a in ("!yp! !xp! !xL!") do (
               if "!F%%a:~%%b,%%k!" equ "!spc:~0,%%k!" (
                  set /A j+=1
                  set "n!j!=%%a %%b %%c %%k"
               ) else (
                  set "move="
               )
            )
         )
      )

      if defined move (

         rem Clear the piece from its current position, on the screen
         for /L %%i in (1,1,!i!) do (
            for /F "tokens=1-4" %%a in ("!c%%i!") do (
               echo %%b,%%a,!F%%a:~%%b,1!,%%d
            )
         )

         rem Place the piece at the new position, both in field and screen
         for /L %%j in (1,1,!j!) do (
            for /F "tokens=1-4" %%a in ("!n%%j!") do (
               set "F%%a=!F%%a:~0,%%b!!pR:~0,%%d!!F%%a:~%%c!"
               echo %%b,%%a,!F%%a:~%%b,1!,%%d
            )
         )

         rem Update any changes in the piece
         set /A y+=Dy, x+=Dx
         if defined R set "p0=!p1!" & set "pI=!p!" & set "R="

      ) else (   rem The piece can not be moved

         rem Recover the piece at its current position, in the field
         for /L %%i in (1,1,!i!) do (
            for /F "tokens=1-4" %%a in ("!c%%i!") do (
               set "F%%a=!F%%a:~0,%%b!!pR:~0,%%d!!F%%a:~%%c!"
            )
         )
         if defined R set "p1=!p0!" & set "R="

         if !Dy! neq 0 (   rem The piece "lands"

            rem Check completed lines
            set "j=" & set "m=0"
            for /L %%i in (1,1,!i!) do for /F %%a in ("!c%%i!") do (
               if "!F%%a:0=!" equ "!F%%a!" (
                  set "F%%a=X"
                  set "j=!j! %%a"
                  set /A m+=1
               )
            )

            if !m! gtr 0 (

               rem Blink completed lines on screen
               for %%i in (!j!) do (
                  echo 1,%%i,=,%cols%
               )

               rem Update level and scores
               rem See: N-Blox at http://www.tetrisfriends.com/help/tips_appendix.php#rankingsystem
               set /A "xp=Level*(40+((j-2>>31)+1)*60+((j-3>>31)+1)*200+((j-4>>31)+1)*900)"
               set /A "Score+=xp, Rows+=j, xL=Level, Level=(Rows-1)/10+1"
               if !Level! neq !xL! if !delay! gtr 5 set /A delay-=5
               rem BEL Ctrl-G Ascii-7:
               set /P "=" < NUL > CON

               rem Remove completed lines from field
               set "i=1"
               for /L %%i in (1,1,%lines%) do (
                  set "F!i!=!F%%i!"
                  if "!F%%i!" neq "X" set /A i+=1
               )
               for /L %%i in (!i!,1,%lines%) do set "F%%i=³%spc%³"

               rem Update scores and the whole field on screen
               echo 1,-1
               call :Delay 95
               (
               echo Level: !Level!
               echo     Rows: !Rows!
               echo    Score: !Score!
               ) > CON
               echo X > PSbusy.txt
               for /L %%i in (%lines%,-1,1) do echo 1,%%i,!F%%i:~1,-1!
               echo del,PSbusy.txt
               call :WaitPSbusy

            )

            rem Request to show a new piece
            set "new=1"

         )

      )

   )

)


:endGame
echo 1,-4
echo X > PSbusy.txt
echo del,PSbusy.txt
call :WaitPSbusy
set /P "=Play again? " < NUL > CON
:choice
   set /P "com="
if not defined com goto choice
if "%com%" equ "Y" exit /B
if "%com:~0,1%" neq "N" set "com=" & goto choice
echo N > CON
exit


:WaitPSbusy
   if exist PSbusy.txt goto WaitPSbusy
exit /B


:Pause
title PAUSED
:wait
   set /P "com="
   if not defined com goto wait
if "%com%" neq "pause=1" set "com=" & goto wait
set "com="
set "pause="
title Tetris.BAT by Aacini
exit /B


:Delay centisecs
set "b=1%time:~-2%"
:wait2
   set /A "e=1%time:~-2%, elap=e-b, elap-=(elap>>31)*100"
if %elap% lss %1 goto wait2
set "b=1%time:~-2%"
exit /B


Antonio
Last edited by Aacini on 12 Apr 2016 20:16, edited 4 times in total.

Squashman
Expert
Posts: 4486
Joined: 23 Dec 2011 13:59

Re: Read arrow keys and show color text in an efficient way

#6 Post by Squashman » 12 Apr 2016 06:18

Fantastic!

Aacini
Expert
Posts: 1913
Joined: 06 Dec 2011 22:15
Location: México City, México
Contact:

Re: Read arrow keys and show color text in an efficient way

#7 Post by Aacini » 12 Apr 2016 20:06

After several tests I found a synchronization problem that may cause an undesirable behavior when the selected font is small and the computer is slow. Also, there was a subtle bug that cause an error when the window/buffer sizes are changed in certain cases. I fixed two previous points and posted the new code in the same place of the original, as usual.

Antonio

carlos
Expert
Posts: 503
Joined: 20 Aug 2010 13:57
Location: Chile
Contact:

Re: Read arrow keys and show color text in an efficient way

#8 Post by carlos » 12 Apr 2016 22:22

Very good game Aacini.
For play it I needed in windows 8 I add this before the cls:

Code: Select all

cls
path %path%;%SystemRoot%\syswow64\WindowsPowerShell\v1.0;

npocmaka_
Posts: 516
Joined: 24 Jun 2013 17:10
Location: Bulgaria
Contact:

Re: Read arrow keys and show color text in an efficient way

#9 Post by npocmaka_ » 13 Apr 2016 03:12

another way with c# and platform invoke (code was stolen shamelessly from here : https://blogs.msdn.microsoft.com/toub/2 ... hook-in-c/ )

Code: Select all

// 2>nul||@goto :batch
/*
:batch
@echo off
setlocal

:: find csc.exe
set "csc="
for /r "%SystemRoot%\Microsoft.NET\Framework\" %%# in ("*csc.exe") do  set "csc=%%#"

if not exist "%csc%" (
   echo no .net framework installed
   exit /b 10
)

if not exist "%~n0.exe" (
   call %csc% /nologo /warn:0 /out:"%~n0.exe" "%~dpsfnx0" || (
      exit /b %errorlevel%
   )
)
%~n0.exe %*
endlocal & exit /b %errorlevel%

*/

using System;
using System.Diagnostics;
using System.Windows.Forms;
using System.Runtime.InteropServices;
class InterceptKeys
{
    private const int WH_KEYBOARD_LL = 13;
    private const int WM_KEYDOWN = 0x0100;
    private static LowLevelKeyboardProc _proc = HookCallback;
    private static IntPtr _hookID = IntPtr.Zero;
    public static void Main()
    {
        _hookID = SetHook(_proc);
        Application.Run();
        UnhookWindowsHookEx(_hookID);
    }
    private static IntPtr SetHook(LowLevelKeyboardProc proc)
    {
        using (Process curProcess = Process.GetCurrentProcess())
        using (ProcessModule curModule = curProcess.MainModule)
        {
            return SetWindowsHookEx(WH_KEYBOARD_LL, proc,
                GetModuleHandle(curModule.ModuleName), 0);
        }
    }
    private delegate IntPtr LowLevelKeyboardProc(
        int nCode, IntPtr wParam, IntPtr lParam);
    private static IntPtr HookCallback(
        int nCode, IntPtr wParam, IntPtr lParam)
    {
        if (nCode >= 0 && wParam == (IntPtr)WM_KEYDOWN)
        {
            int vkCode = Marshal.ReadInt32(lParam);
            Console.WriteLine((Keys)vkCode);
        }
        return CallNextHookEx(_hookID, nCode, wParam, lParam);
    }
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr SetWindowsHookEx(int idHook,
        LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool UnhookWindowsHookEx(IntPtr hhk);
    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode,
        IntPtr wParam, IntPtr lParam);
    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr GetModuleHandle(string lpModuleName);
}

Aacini
Expert
Posts: 1913
Joined: 06 Dec 2011 22:15
Location: México City, México
Contact:

Re: Read arrow keys and show color text in an efficient way

#10 Post by Aacini » 15 Apr 2016 11:06

carlos wrote:For play it I needed in windows 8 I add this before the cls:

Code: Select all

cls
path %path%;%SystemRoot%\syswow64\WindowsPowerShell\v1.0;



In my computer (Windows 8.1 Spanish 64-Bits) the %path% had the value "C:\Windows\System32\WindowsPowerShell\v1.0" predefined on it, so I would needed to insert the new path "C:\Windows\syswow64\WindowsPowerShell\v1.0" before the standard path in order to start the 64-bits PS version instead. I did it as a test and saw no difference in the program behavior...

I think that all computers with PowerShell preinstalled have also the right value in %path%, so no adjustment is necessary.


npocmaka_ wrote:another way with c# and platform invoke...


Well, in the first paragraph of this thread I said: "we want a Batch-file based solution with the minimal part of non native code", and the original name of the posted game is "Pure .BATch-file Tetris game". The purpose of this thread is develop a method to read arrow keys and show color text using just preinstalled Windows features that be efficient enough that can be used in an animated game.

Anyway, if a third party application could be used in these programs, a much simpler solution would be to just use my auxiliary programs, as I did already in the 2048.bat game.

Antonio

Aacini
Expert
Posts: 1913
Joined: 06 Dec 2011 22:15
Location: México City, México
Contact:

Re: Read arrow keys and show color text in an efficient way

#11 Post by Aacini » 24 Apr 2016 10:06

I modified my 2048.bat game program in order to read cursor keys and show color text in the way described in this thread. This modification is different than the Tetris game one because in this case the original program read cursor keys and show color text already using my GetInput.exe, CursorPos.exe, and ColorChar.exe auxiliary programs. I defined the format of the "control lines" received by the :Output PowerShell code section so they have the same format of the original lines used to execute the auxiliary .exe programs. For example, the original code use this line to move the cursor:

Code: Select all

CursorPos X Y

This way, the new :Output PowerShell section was designed to get a line with this format: "CursorPos X Y" and move the cursor to the position specified, and do the same thing with "ColorChar ..." line. This design makes possible that the modification of the original code, from using auxiliary .exe programs to use PowerShell output section, just consist in add "echo" commands:

Code: Select all

echo CursorPos X Y

The "echo" part may even be stored in a variable that select the method used (original .exe programs or the new PowerShell code) so the resulting program may work in anyone of these two ways by just defining a variable:

Code: Select all

rem The line below select original .exe programs or new PowerShell method:
set "PS=echo"

%PS% CursorPos X Y

if not defined PS (
   .exe version specific lines
) else (
   PowerShell version specific lines
)

A simple visual comparison of the two methods indicate that the .exe auxiliary programs are still faster than the PowerShell method, but the difference is not big.

The :Input PowerShell code section can not be managed in the same way, because the original execution of GetInput.exe auxiliary program is changed by a simple SET /P command that read equivalent input from the piped PowerShell code section. However, the definition of the valid keys and mouse click zones is done via a line passed to the :Input PowerShell section that have the same format of GetInput command. This new :Input section features a standard management of mouse left button: it converts the screen-wide pixel coordinates of a mouse click into cmd.exe text window column/line coordinates and return the correct value when a mouse click was done in the specified zones (in the same way as GetInput.exe auxiliary program). The mouse management is the most complex part of this section, indeed; if this section would read just arrow keys, it would be much simpler.

Code: Select all

@echo off
setlocal EnableDelayedExpansion

if "%~1" neq "" goto %1

rem 2048 game: http://gabrielecirulli.github.io/2048/
rem Windows Batch file version by Antonio Perez Ayala
rem Version 3: http://www.dostips.com/forum/viewtopic.php?f=3&t=6936&p=46413#p46413

rem This version 3 may use CursorPos.exe, ColorChar.exe and GetInput.exe auxiliary programs
rem (to get them, download and execute 2048-Support.bat file from:
rem  http://www.dostips.com/forum/viewtopic.php?f=3&t=5701&p=35407#p35407)
rem OR it may use PowerShell modules to perform the same tasks; the line below select the metod used:
rem PS var defined = use PowerShell modules; PS var not defined = use the 3 auxiliary .exe programs
set "PS=echo"

cls
echo/
echo 2048 game, original version by Gabriele Cirulli
echo http://gabrielecirulli.github.io/2048/
echo/
echo This Windows batch file version by Antonio Perez Ayala
echo/
echo HOW TO PLAY:
echo - Use your arrow keys to move the tiles
echo   or click on the central tiles of the side to move
echo - When two tiles with same number touch, they merge into one
echo - Join the numbers and get to the 2048 TILE
echo/
echo Press Esc key to start a new game, press Del key to exit
echo or click on the corresponding NewGame/Exit buttons
echo/

rem Load a previously saved game, if any
cd "%~P0"
set "prevGame="
if exist 2048.dat (
   echo/
   for %%a in (2048.dat) do echo --^> A previous game exists saved on %%~Ta
   choice /M "Do you want to load it "
   if !errorlevel! equ 1 (
      for /F "delims=" %%a in (2048.dat) do set %%a
      del 2048.dat
      set prevGame=true
      echo Game loaded
   )
   echo/
   echo/
) else (
   set score=0
)
call :getBestScore
set lastBestScore=%bestScore%
pause

title 2048 game by Aacini
mode con cols=68 lines=36
color F0
cls

if not defined PS goto Main
set /P "=Loading PS engine..." < NUL
rem           Left Up  Right Down Esc End      Left        Up          Right        Down         NewGame    Exit
echo GetInput 293  294 295   296  27  302  /M  0 16 17 26  20 7 47 14  50 16 67 26  20 28 47 35  36 4 45 6  52 4 59 6 > input.def
"%~F0" Input  |  "%~F0" Main  |  "%~F0" Output
del input.def
goto :EOF



:Input
PowerShell  ^
   $GetConsoleWindow = Add-Type 'A' -PassThru -MemberDefinition '  ^
      [DllImport(\"Kernel32.dll\")]  ^
      public static extern int GetConsoleWindow();  ^
   ';  ^
   $GetClientRect,$RECT = Add-Type 'B' -PassThru -MemberDefinition '  ^
      [DllImport(\"user32.dll\")]  ^
      public static extern bool GetClientRect(IntPtr hWnd, ref RECT lpRect);  ^
      [StructLayout(LayoutKind.Sequential)]  ^
      public struct RECT { public int Left, Top, Right, Bottom; }  ^
   ';  ^
   $ScreenToClient,$POINT = Add-Type 'C' -PassThru -MemberDefinition '  ^
      [DllImport(\"user32.dll\")]  ^
      public static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint);  ^
      [StructLayout(LayoutKind.Sequential)]  ^
      public struct POINT { public int X, Y; }  ^
   ';  ^
   $Prog,$Params = (Get-Content input.def).Split(' ',[StringSplitOptions]::RemoveEmptyEntries);  ^
   if ( $Prog -eq 'GetInput' ) {  ^
      $validKeys = '';  $pos = 0; $mouseSwitch = $false;  $mouseZone = @();  ^
      while ( $Params ) {  ^
         if ( -not $mouseSwitch ) {  ^
            if ( $Params[0] -eq '/M' ) {  ^
               $mouseSwitch = $true;  ^
               $null,$Params = $Params;  ^
            } else {  ^
               [int]$Param,$Params = $Params;  ^
               if ( $Param -ge 256 ) { $Param -= 256 }  ^
               $validKeys += [char]$Param;  ^
            }  ^
         } else {  ^
            $Rectangle = New-Object -TypeName $RECT.FullName;  ^
            $Rectangle.Left,$Rectangle.Top,$Rectangle.Right,$Rectangle.Bottom,$Params = $Params;  ^
            $mouseZone += $Rectangle;  ^
         }  ^
      }  ^
   }  ^
   $CmdWindowHandle = $GetConsoleWindow::GetConsoleWindow();  ^
   $ClientRect = New-Object -TypeName $RECT.FullName;  ^
   $null = $GetClientRect::GetClientRect($CmdWindowHandle,[ref]$ClientRect);  ^
   $console = $Host.UI.RawUI;  ^
   $fontWidth  = $ClientRect.Right / $console.WindowSize.Width;  ^
   $fontHeight = $ClientRect.Bottom / $console.WindowSize.Height;  ^
   $null = [Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); ^
   $control = [System.Windows.Forms.Control];  ^
   $MouseCoord = New-Object -TypeName $POINT.FullName;  ^
   Start-Sleep -m 1500;  ^
   Write-Output 0;  ^
   while ( $pos -lt $validKeys.length ) {  ^
      if ( $console.KeyAvailable ) {  ^
         $key = $console.ReadKey('NoEcho,IncludeKeyUp').VirtualKeyCode;  ^
         if ( $pos = $validKeys.IndexOf($key)+1 ) { Write-Output $pos }  ^
      } else {  ^
         $mouseButn = $control::MouseButtons;  ^
         if ( $mouseButn -ne $lastButn ) {  ^
            $lastButn = $mouseButn;  ^
            if ( $mouseButn -eq 'Left' ) {  ^
               $MouseCoord.X = $control::MousePosition.X; $MouseCoord.Y = $control::MousePosition.Y;  ^
               $null = $ScreenToClient::ScreenToClient($CmdWindowHandle,[ref]$MouseCoord);  ^
               $mouseCol = $MouseCoord.X/$fontWidth; $mouseRow = $MouseCoord.Y/$fontHeight;  ^
               for ( $pos=$i=0; $i -lt $mouseZone.length; ++$i ) {  ^
                  if ( ($mouseCol -ge $mouseZone[$i].Left)  -and  ($mouseCol -le $mouseZone[$i].Right)  -and  ^
                       ($mouseRow -ge $mouseZone[$i].Top)   -and  ($mouseRow -le $mouseZone[$i].Bottom) ) {  ^
                     $pos = $i+1; break;  ^
                  }  ^
               }  ^
               if ( $pos ) { Write-Output $pos }  ^
            }  ^
         }  ^
      }  ^
      Start-Sleep -m 100;  ^
   }
%End PowerShell%
echo Ending game... > CON
exit



:Output
rem Parameters received via lines read have one of these three forms:
rem - cls
rem - CursorPos X Y
rem - ColorChar standard parameters of ColorChar.exe auxiliary program
PowerShell  ^
   $console = $Host.UI.RawUI;  ^
   $console.WindowTitle = '2048 game by Aacini';  ^
   $curSize = $console.CursorSize;  ^
   $console.CursorSize = 0;  ^
   $coords = $console.CursorPosition;  ^
   foreach ( $line in $input ) {  ^
      $Prog,$Params = $line.Split(' ',[StringSplitOptions]::RemoveEmptyEntries);  ^
      if ( $Prog -eq 'cls' ) {  ^
         cls  ^
      } elseif ( $Prog -eq 'CursorPos' ) {  ^
         $coords.X = [int]$Params[0];  ^
         $coords.Y = [int]$Params[1];  ^
         $console.CursorPosition = $coords;  ^
      } elseif ( $Prog -eq 'ColorChar' ) {  ^
         while ( $Params ) {  ^
            $Param,$Params = $Params;  ^
            if ( $Param[0] -eq '/' ) {  ^
               $color = [int]('0x'+$Param.substring(1));  ^
               $back = $color -shr 4; $fore = $color%%16;  ^
            } elseif ( $Param[0] -eq '\^"' ) {  ^
               Write-Host -BackgroundColor $back -ForegroundColor $fore -NoNewLine $Param.substring(1,$Param.length-2);  ^
            } elseif ( $Param -eq '13' ) {  ^
               Write-Host;  ^
               $Params = '';  ^
            } else {  ^
               [int]$code,[int]$times = $Param.Split('x');  ^
               if ( -not $times ) { $times = 1 }  ^
               if ( $code -ne 219 ) {  ^
                  $str = [string][char]$code * $times;  ^
                  Write-Host -BackgroundColor $back -ForegroundColor $fore -NoNewLine $str;  ^
               } else {  ^
                  $str = ' ' * $times;  ^
                  Write-Host -BackgroundColor $fore -ForegroundColor $back -NoNewLine $str;  ^
               }  ^
            }  ^
         }  ^
      }  ^
   }  ^
   $console.CursorSize = $curSize;
%End PowerShell%
exit /B



:Main

rem Define the numbers used in output (5 lines each)
for %%m in (
"    |70|32x13|32x13|32x13|32x13|32x13"
"   2|F0|32x5    219x3   32x5|32x7        219 32x5|32x5 219x3 32x5|32x5 219        32x7|32x5 219x3 32x5"
"   4|70|32x5 219 32 219 32x5|32x5 219 32 219 32x5|32x5 219x3 32x5|32x7        219 32x5|32x7   219 32x5"
"   8|C7|32x5    219x3   32x5|32x5 219 32 219 32x5|32x5 219x3 32x5|32x5 219 32 219 32x5|32x5 219x3 32x5"
"  16|CF|32x4   219  32    219x3  32x4|32x4 219 32 219     32x6     |32x4  219 32 219x3  32x4|32x4 219 32 219 32 219  32x4 |32x4 219   32 219x3 32x4"
"  32|47|32x3 219x3 32    219x3   32x3|32x5        219 32x3 219 32x3|32x3 219x3 32 219x3 32x3|32x5        219 32 219   32x5|32x3 219x3 32 219x3 32x3"
"  64|4F|32x3 219x3 32 219 32 219 32x3|32x3 219 32x3 219 32 219 32x3|32x3 219x3 32 219x3 32x3|32x3 219 32 219 32x3 219 32x3|32x3 219x3 32x3 219 32x3"
" 128|27|32x2 219 32 219x3 32 219x3 32x2|32x2 219 32x3 219 32 219 32 219 32x2|32x2 219 32 219x3 32 219x3 32x2|32x2 219 32   219 32x3 219 32 219 32x2|32x2 219 32 219x3 32 219x3 32x2"
" 256|2F|32 219x3 32 219x3 32 219x3 32  |32x3  219  32      219 32x3 219 32x3|32 219x3 32 219x3 32 219x3 32  |32   219 32x5 219 32   219 32 219 32  |32 219x3 32 219x3 32 219x3 32  "
" 512|E6|32x2 219x3 32 219 32 219x3 32x2|32x2 219 32x3      219 32x3 219 32x2|32x2 219x3 32 219 32 219x3 32x2|32x4 219 32   219 32          219 32x4|32x2 219x3 32 219 32 219x3 32x2"
"1024|E0|219 32 219x3 32 219x3 32 219 32 219|219 32 219 32 219 32x3 219 32 219 32 219|219 32 219 32 219 32 219x3 32 219x3|219 32 219 32 219 32 219 32x5 219|219 32 219x3 32 219x3 32x3 219"
           ) do (
   for /F "tokens=1-7 delims=|" %%a in (%%m) do (
      set "num[1,%%a]=/%%b %%c"
      set "num[2,%%a]=/%%b %%d"
      set "num[3,%%a]=/%%b %%e"
      set "num[4,%%a]=/%%b %%f"
      set "num[5,%%a]=/%%b %%g"
   )
)

rem Define the mapping indices tables
set j=0
rem               LEFT               UP               RIGHT             DOWN
for %%m in ("1,1|1,2|1,3|1,4   1,1|2,1|3,1|4,1   1,4|1,3|1,2|1,1   4,1|3,1|2,1|1,1"
            "2,1|2,2|2,3|2,4   1,2|2,2|3,2|4,2   2,4|2,3|2,2|2,1   4,2|3,2|2,2|1,2"
            "3,1|3,2|3,3|3,4   1,3|2,3|3,3|4,3   3,4|3,3|3,2|3,1   4,3|3,3|2,3|1,3"
            "4,1|4,2|4,3|4,4   1,4|2,4|3,4|4,4   4,4|4,3|4,2|4,1   4,4|3,4|2,4|1,4") do (
   set /A j+=1
   for /F "tokens=1-4" %%a in (%%m) do (
      set "map[1,!j!]=%%a"
      set "map[2,!j!]=%%b"
      set "map[3,!j!]=%%c"
      set "map[4,!j!]=%%d"
   )
)

rem Wait for PowerShell Input part start signal
if defined PS set /P "=" & set /P "="

:showHeader
%PS% cls
%PS% CursorPos 0 1
%PS% ColorChar /F0 32x38                                                                /8F 32 "SCORE" 32     /F0 32x8 /8F 32 "BEST" 32     13 10
%PS% ColorChar /F0 32x3 219x5 32x2 219x5 32x2 219 32x3 219 32x2 219x5              32x9 /8F 32 "12345" 32     /F0 32x8 /8F "123456"         13 10
%PS% ColorChar /F0 32x7 219 32x2 219 32x3 219 32x2 219 32x3 219 32x2 219 32x3 219                                                           13 10
%PS% ColorChar /F0 32x3 219x5 32x2 219 32x3 219 32x2 219x5 32x2 219x5              32x7 /8F 32x10             /F0 32x6 /8F 32x8             13 10
%PS% ColorChar /F0 32x3 219 32x6 219 32x3 219 32x6 219 32x2 219 32x3 219           32x7 /8F 32  "New" 32 "Game" 32 /F0 32x6 /8F 32x2 "Exit" 32x2 13 10
%PS% ColorChar /F0 32x3 219x5 32x2 219x5 32x6 219 32x2 219x5                       32x7 /8F 32x2 "(Esc)" 32x3 /F0 32x6 /8F 32x2 "(Del)" 32
if defined prevGame goto showMove
:newGame
for /L %%i in (1,1,4) do (
   for /L %%j in (1,1,4) do (
      set "board[%%i,%%j]=    "
   )
)
call :tileSpawn
call :tileSpawn
set /A score=0, points=0, maxTile=0
goto showMove

:mainLoop
if not defined anyHole (
   echo Not empty space left, end of this game.
   set /P "=Press Enter key to start a new game " < CON
   set "prevGame="
   goto showHeader
) > CON
call :tileSpawn

:showMove
if %maxTile% equ 2048 (
   echo Congratulations: YOU WON
   pause
   color
   cls
   goto :EOF
) > CON
set /A score+=points
if %score% gtr %bestScore% set bestScore=%score%
if not defined PS (
   set "scoreOut=    %score%"
   set "bestOut=    %bestScore%"
) else (
   set "scoreOut=ÿÿÿÿ%score%"
   set "bestOut=ÿÿÿÿ%bestScore%"
)
%PS% CursorPos 39 2
%PS% ColorChar /8F "%scoreOut:~-5%" 32 /F0 32x8 /8F "%bestOut:~-6%" 13 10
%PS% CursorPos 0 9
%PS% ColorChar /F0 32x3 /08 219x62  13 10
for /L %%i in (1,1,4) do (
   for /L %%n in (1,1,5) do (
      set "numLine=/08 219x2"
      for /L %%j in (1,1,4) do for /F "delims=" %%k in ("!board[%%i,%%j]!") do (
         set "numLine=!numline! !num[%%n,%%k]! /08 219x2"
      )
      %PS% ColorChar /F0 32x3 !numLine!  13 10
   )
   %PS% ColorChar /F0 32x3 /08 219x62  13 10
)

:getKey     Left Up  Right Down Esc End      Left        Up          Right        Down         NewGame    Exit
if not defined PS (
   GetInput 293  294 295   296  27  302  /M  0 16 17 26  20 7 47 14  50 16 67 26  20 28 47 35  36 4 45 6  52 4 59 6
   set "option=!errorlevel!"
) else (
   set /P "option=" & set /P "="
)
if %option% gtr 4 goto option-%option%

rem Assemble equivalent horizontal rows for move to left
for /L %%i in (1,1,4) do (
   for /F "tokens=1-4 delims=|" %%a in ("!map[%option%,%%i]!") do (
      set "row[%%i]=|!board[%%a]!|!board[%%b]!|!board[%%c]!|!board[%%d]!|"
   )
)
rem Move and merge tiles to left
call :moveLeft
rem Return moved tiles to their original positions in board
for /L %%i in (1,1,4) do (
   for /F "tokens=1-8 delims=|" %%a in ("!map[%option%,%%i]!!row[%%i]!") do (
      set "board[%%a]=%%e" & set "board[%%b]=%%f" & set "board[%%c]=%%g" & set "board[%%d]=%%h"
   )
)
goto mainLoop

:option-5 New game
goto newGame

:option-6 Exit
if %maxTile% gtr 4 (
   set points=0
   for %%a in (score points maxTile) do echo %%a=!%%a!
   set board[
) > 2048.dat
color
cls
if %bestScore% leq %lastBestScore% goto :EOF

rem Save the new best score
setlocal DisableDelayedExpansion
(for /F "usebackq delims=" %%a in ("%~F0") do (
   set "line=%%a"
   setlocal EnableDelayedExpansion   
   echo !line!
   if "!line!" equ ":getBestScore" goto continue
   endlocal
)) > "%~PN0.tmp"
:continue
echo set bestScore=%bestScore%>> "%~PN0.tmp"
del "%~F0" & ren "%~PN0.tmp" "%~NX0" & goto :EOF


:moveLeft
set points=0
set "anyHole="
for /L %%i in (1,1,4) do (
   set "row=!row[%%i]!"
   set "d="
   rem Eliminate empty places and move tiles to left
   for /F "tokens=1-4 delims=|" %%a in ("!row:|    =!") do (
      set "a=%%a" & set "b=%%b" & set "c=%%c" & set "d=%%d"
      rem Merge tiles with same number
      if defined a (
         if !a! equ !b! (
            set /A "a+=b, points+=a" & set "b=!c!" & set "c=!d!" & set "d="
            if !a! gtr !maxTile! set maxTile=!a!
         )
         set "a=   !a!" & set "row=|!a:~-4!|"
      ) else set "row=|    |"
      if defined b (
         if !b! equ !c! (
            set /A "b+=c, points+=b" & set "c=!d!" & set "d="
            if !b! gtr !maxTile! set maxTile=!b!
         )
         set "b=   !b!" & set "row=!row!!b:~-4!|"
      ) else set "row=!row!    |"
      if defined c (
         if !c! equ !d! (
            set /A "c+=d, points+=c" & set "d="
            if !c! gtr !maxTile! set maxTile=!c!
         )
         set "c=   !c!" & set "row=!row!!c:~-4!|"
      ) else set "row=!row!    |"
      if defined d (
         set "row=!row!!d!|"
      ) else set "row=!row!    |"
   )
   if not defined d set anyHole=true
   set "row[%%i]=!row!"
)
exit /B

:tileSpawn
set /A newI=%random% %% 4 + 1, newJ=!random! %% 4 + 1
if "!board[%newI%,%newJ%]!" neq "    " goto tileSpawn
set /A newVal=%random% %% 10/9*2 + 2
set "board[%newI%,%newJ%]=   %newVal%"
exit /B

rem To reset the best score, modify the value below
:getBestScore
set bestScore=0


PS - I did it! :mrgreen: The first image is the one 9 moves before the game end.

Image Image

Antonio

neorobin
Posts: 47
Joined: 01 May 2012 12:18

Re: Read arrow keys and show color text in an efficient way

#12 Post by neorobin » 07 Oct 2016 04:23

Aacini wrote::arrow: The second part of this application is ready :!: :D 8)
2016-04-12: Code modified to fix a synchronization problem and a bug when the window/buffer sizes are changed.



Aacini

Nice effect! I like it!

Your Tetris game code a bit long ( viewtopic.php?f=3&t=6936&p=46206#p46206 ),

so I can not easily find those special characters that used to display blocks and borders.

My system is running the 936 code page,

The copy code of the forum is UNICODE, there are some coding problems.

please tell me, in the code, which lines have special display characters.

In this way, I can do some modification, so that it is good to run on 936 code page

Thanks!

Aacini
Expert
Posts: 1913
Joined: 06 Dec 2011 22:15
Location: México City, México
Contact:

Re: Read arrow keys and show color text in an efficient way

#13 Post by Aacini » 07 Oct 2016 10:18

@neorobin:

The characters used in the field frame are given in these lines (in PowerShell code):

Code: Select all

121:      Write-Host -NoNewLine (' '*2); Write-Host "Ú$frame¿";  ^
122:      for ( $i=1; $i -le %lines%*$lnN; ++$i ) { Write-Host -NoNewLine (' '*2); Write-Host "³$spc³" }  ^
123:      Write-Host -NoNewLine (' '*2); Write-Host "À$frameÙ";  ^

150:   $frame = ''; $spc = ''; for ( $i=1; $i -le $col-6; ++$i ) { $frame+='Ä'; $spc+=' '; }  ^

The characters used to draw the pieces are these ones:

Code: Select all

149:   $ln = ( ('X'), ('Úª','ÀÙ'), ('ÚÄ¿','³ ³','ÀÄÙ') )[$lnN-1];  ^

Antonio

IcarusLives
Posts: 175
Joined: 17 Jan 2016 23:55

Re: Read arrow keys and show color text in an efficient way

#14 Post by IcarusLives » 29 Jan 2017 11:51

Aacini wrote:

Code: Select all

@echo off
:Output
rem Parameters received via lines read have one of these three forms:
rem - cls
rem - CursorPos X Y
rem - ColorChar standard parameters of ColorChar.exe auxiliary program
PowerShell  ^
   $console = $Host.UI.RawUI;  ^
   $console.WindowTitle = '2048 game by Aacini';  ^
   $curSize = $console.CursorSize;  ^
   $console.CursorSize = 0;  ^
   $coords = $console.CursorPosition;  ^
   foreach ( $line in $input ) {  ^
      $Prog,$Params = $line.Split(' ',[StringSplitOptions]::RemoveEmptyEntries);  ^
      if ( $Prog -eq 'cls' ) {  ^
         cls  ^
      } elseif ( $Prog -eq 'CursorPos' ) {  ^
         $coords.X = [int]$Params[0];  ^
         $coords.Y = [int]$Params[1];  ^
         $console.CursorPosition = $coords;  ^
      } elseif ( $Prog -eq 'ColorChar' ) {  ^
         while ( $Params ) {  ^
            $Param,$Params = $Params;  ^
            if ( $Param[0] -eq '/' ) {  ^
               $color = [int]('0x'+$Param.substring(1));  ^
               $back = $color -shr 4; $fore = $color%%16;  ^
            } elseif ( $Param[0] -eq '\^"' ) {  ^
               Write-Host -BackgroundColor $back -ForegroundColor $fore -NoNewLine $Param.substring(1,$Param.length-2);  ^
            } elseif ( $Param -eq '13' ) {  ^
               Write-Host;  ^
               $Params = '';  ^
            } else {  ^
               [int]$code,[int]$times = $Param.Split('x');  ^
               if ( -not $times ) { $times = 1 }  ^
               if ( $code -ne 219 ) {  ^
                  $str = [string][char]$code * $times;  ^
                  Write-Host -BackgroundColor $back -ForegroundColor $fore -NoNewLine $str;  ^
               } else {  ^
                  $str = ' ' * $times;  ^
                  Write-Host -BackgroundColor $fore -ForegroundColor $back -NoNewLine $str;  ^
               }  ^
            }  ^
         }  ^
      }  ^
   }  ^
   $console.CursorSize = $curSize;
%End PowerShell%
exit /B

Antonio


Hello Aacini,

I'm really interested how this code here works. This is your :Output from the 2048 game you posted where you could use PS or aux interchangeably. I'm unfamiliar with powershell, but I love how it is much more efficient at displaying colors than batch. I'm rather curious how I could utilize this for my own projects. I just had some questions :P

How does this work? Does it accept arguments? I saw you were multi-threading it, so perhaps it collects data from :Main elsewhere? How would you go about this for more casual use? I'm unsure if I'm asking the right questions, but this would be extremely useful for me to learn. Thank you :)

Aacini
Expert
Posts: 1913
Joined: 06 Dec 2011 22:15
Location: México City, México
Contact:

Re: Read arrow keys and show color text in an efficient way

#15 Post by Aacini » 30 Jan 2017 15:21

IcarusLives wrote:Hello Aacini,

I'm really interested how this code here works. This is your :Output from the 2048 game you posted where you could use PS or aux interchangeably. I'm unfamiliar with powershell, but I love how it is much more efficient at displaying colors than batch. I'm rather curious how I could utilize this for my own projects. I just had some questions :P

How does this work? Does it accept arguments? I saw you were multi-threading it, so perhaps it collects data from :Main elsewhere? How would you go about this for more casual use? I'm unsure if I'm asking the right questions, but this would be extremely useful for me to learn. Thank you :)


I am afraid I don't fathom out what exactly is your question. All my programs have plenty of remarks that describe all aspects related to the code.

The 2048.bat game is the 6th program posted in this thread. The first three programs are examples of how use the communication with a small PowerShell code segment, that was started at left side of a pipe, to read input keys from it. The 4th example is my Tetris.bat program that was modified in the same way (to read input keys from PowerShell code), although in this case the PS segment is much larger. The line that starts the communication in this Tetris.bat version 2.0 is this:

Code: Select all

"%~F0" Input >> pipeFile.txt  |  "%~F0" Main < pipeFile.txt

The next example is Tetris.bat game again, but this time it uses two simultaneous PowerShell code segments: one to read input keys as before, and another one to show text in color. The communication between Batch code and the PowerShell segments is done via pipes like before, that is, the Input PS code is placed at left side of a pipe and the Output PS code is placed at right side of a pipe. This is the line that starts the whole process:

Code: Select all

"%~F0" Input >> pipeFile.txt  |  "%~F0" Main < pipeFile.txt  |  "%~F0" Output

This means that the Output PowerShell code segment read lines via its Stdin and takes the parameters needed to show text from they; these parameters are described in the code, for example: "cls", "del", etc. In this way, when the Batch code needs to request an action from the Output PS segment, it just needs to output the desired command via its Stdout like in these parts:

Code: Select all

   if defined init (
      setlocal EnableDelayedExpansion
      set "init="
      echo cls


               rem Blink completed lines on screen
               for %%i in (!j!) do (
                  echo 1,%%i,=,%cols%
               )

The next example is the 2048.bat program. This game is entirely similar to the last version of Tetris.bat with two differences: 2048.bat is not an animated game, but a static one that waits until the user press a key to react, so the Input part don't needs to be a non-blocking one. In this case the game is started this way:

Code: Select all

"%~F0" Input  |  "%~F0" Main  |  "%~F0" Output

The second difference is that the commands given to the Output part have the same format of the auxiliaty .exe programs that was originally used for the same task; this point is described with detail in the text.

So?

Antonio

Post Reply