バッチファイルで「ハノイの塔」
「ハノイの塔」のゲームをプレイできるバッチファイルです。円盤を左端から右端に移動すればクリアです。3段から9段を選択できます。
※ Windows 10 以降で動作します。(Windows 10 以降であれば従来のコマンドプロンプト(コンソールホスト)、Windows Terminalどちらでも動作します。)
ソースコード
※ 以下のソースコードには「」(\x07)と「」(\x1b)の文字が含まれます。正しくコピペできない場合は次のリンクからダウンロードしてください → hanoi.bat
@echo off
setlocal enableextensions enabledelayedexpansion
set STEP_COUNT=0
set PICKED_DISK=
set DISK_DRAW_Y=5
set DISK_COUNT=3
:start
cls
call :select_disk_count
if "%ERRORLEVEL%"=="1" exit /b 0
call :init
call :main_loop
if "%ERRORLEVEL%"=="1" goto start
cls
exit /b 0
:init
cls
set STEP_COUNT=0
call :init_tower
call :draw_tower_all
call :display_status
exit /b
:select_disk_count
choice /C 3456789Q /M "Select disk count (Q for exit)"
if "%ERRORLEVEL%"=="0" exit /b 1
if "%ERRORLEVEL%"=="8" exit /b 1
set /A DISK_COUNT="%ERRORLEVEL% + 2"
call :init_disk_count
exit /b 0
:init_disk_count
set /A DISK_COUNT_MINUS_1="%DISK_COUNT% - 1"
set /A DISK_COUNT_PLUS_1="%DISK_COUNT% + 1"
set /A WIDTH="%DISK_COUNT% * 2 - 1 + 2"
set /A LEAST_STEP="(1 << %DISK_COUNT%) - 1"
exit /b
:main_loop
set /A INFO_Y="%DISK_DRAW_Y% + %DISK_COUNT% + 3 + 2"
call :set_position 0 %INFO_Y%
echo *** Press 1,2,3 to pick disk (press R to reset, press Q to exit) ***
call :wait_for_select_pick_tower CHOICE
if "%ERRORLEVEL%"=="1" exit /b 1
if "%ERRORLEVEL%"=="2" exit /b 0
call :pick_tower PICKED_DISK TOWER%CHOICE% "!TOWER%CHOICE%!"
call :draw_tower %CHOICE% "!TOWER%CHOICE%!"
call :display_status
call :set_position 0 %INFO_Y%
echo *** Press 1,2,3 to put disk (press R to reset, press Q to exit) ***
call :wait_for_select_put_tower CHOICE
if "%ERRORLEVEL%"=="1" exit /b 1
if "%ERRORLEVEL%"=="2" exit /b 0
call :put_tower TOWER%CHOICE% "!TOWER%CHOICE%!" %PICKED_DISK%
call :draw_tower %CHOICE% "!TOWER%CHOICE%!"
set /A STEP_COUNT="%STEP_COUNT% + 1"
set PICKED_DISK=
call :display_status
if "%TOWER3%"=="%TOWER_COMPLETE%" (
call :set_position 1 %INFO_Y%
call :erase_current_line
setlocal disabledelayedexpansion
if "%STEP_COUNT%"=="%LEAST_STEP%" (
echo Congratulations!
) else (
echo Good, but you can move in fewer steps.
)
endlocal
goto main_loop_play_again
)
goto main_loop
:main_loop_play_again
choice.exe /C YN /M "Do you play again"
if not "%ERRORLEVEL%"=="1" exit /b 0
exit /b 1
:display_status
call :set_position 1 1
echo.
echo * Now step: %STEP_COUNT%
if not "%PICKED_DISK%"=="" (
set /P T="Picked: " < NUL
call :draw_disk %PICKED_DISK%
) else (
call :erase_current_line
)
echo.
exit /b
:wait_for_select_pick_tower
setlocal
:wait_for_select_pick_tower_inner
choice.exe /C 123RQ /N > NUL
if "%ERRORLEVEL%"=="0" exit /b 2
if "%ERRORLEVEL%"=="4" exit /b 1
if "%ERRORLEVEL%"=="5" exit /b 2
set CHOICE=%ERRORLEVEL%
call :is_tower_empty !TOWER%CHOICE%!
if "%ERRORLEVEL%"=="0" (
call :beep
goto wait_for_select_pick_tower_inner
)
endlocal & set "%~1=%CHOICE%"
exit /b 0
:wait_for_select_put_tower
setlocal
:wait_for_select_put_tower_inner
choice.exe /C 123RQ /N > NUL
if "%ERRORLEVEL%"=="0" exit /b 2
if "%ERRORLEVEL%"=="4" exit /b 1
if "%ERRORLEVEL%"=="5" exit /b 2
set CHOICE=%ERRORLEVEL%
call :is_tower_puttable %PICKED_DISK% !TOWER%CHOICE%!
if "%ERRORLEVEL%"=="1" (
call :beep
goto wait_for_select_put_tower_inner
)
endlocal & set "%~1=%CHOICE%"
exit /b 0
:init_tower
set TOWER1=
set TOWER2=
set TOWER3=
for /L %%I in (1,1,%DISK_COUNT%) do (
set TOWER1=!TOWER1!%%I
set "TOWER2=!TOWER2! "
set "TOWER3=!TOWER3! "
)
set TOWER_COMPLETE=%TOWER1%
exit /b
:is_tower_empty
if "%~1"=="" exit /b 0
exit /b 1
:is_tower_puttable
setlocal
if "%2"=="" exit /b 0
set T=%2
set T=%T:~0,1%
if "%~1" LSS "%T%" exit /b 0
exit /b 1
:pick_tower
setlocal
set R=
set "TOWER=%~3"
for /L %%I in (0,1,%DISK_COUNT_MINUS_1%) do (
if "!R!"=="" (
if not "!TOWER:~%%I,1!"==" " (
call :pick_tower_inner %%I
)
)
)
endlocal & set "%~1=%R%" & set "%~2=%TOWER%"
exit /b
:pick_tower_inner
set "R=!TOWER:~%1,1!"
set /A "J=%1 + 1"
set "TOWER=!TOWER:~0,%1! !TOWER:~%J%!"
exit /b
:put_tower
setlocal
set "TOWER=%~2"
set "DISK=%~3"
set PUTTED=0
for /L %%I in (0,1,%DISK_COUNT_MINUS_1%) do (
if "!PUTTED!"=="0" (
if not "!TOWER:~%%I,1!"==" " (
call :put_tower_inner %%I
set PUTTED=1
)
)
)
if "%PUTTED%"=="0" (
set "TOWER=!TOWER:~0,%DISK_COUNT_MINUS_1%!%DISK%"
)
endlocal & set "%~1=%TOWER%"
:put_tower_inner
set /A "J=%1 - 1"
set "TOWER=!TOWER:~0,%J%!%DISK%!TOWER:~%1!"
exit /b
:draw_tower_all
setlocal
for /L %%A in (1,1,3) do (
call :draw_tower %%A "!TOWER%%A!"
)
call :move_position_y 2
exit /b
:draw_tower
setlocal
set "TOWER=%~2"
set /A X="1 + (%WIDTH% + 2) * (%~1 - 1)"
set /A Y_BASE="%DISK_DRAW_Y% + 3"
set /A Y_BAR="%DISK_DRAW_Y%"
call :set_position %X% %Y_BAR%
call :move_position_x %DISK_COUNT%
set /P T="%~1" < NUL
call :move_position_y 1
call :move_position_minus_x 1
set /P T="|" < NUL
call :move_position_y 1
call :move_position_minus_x 1
set /P T="|" < NUL
echo.
for /L %%B in (0,1,%DISK_COUNT_MINUS_1%) do (
set /A Y="%Y_BASE% + %%B"
call :set_position %X% !Y!
call :substr DISK "%TOWER%" %%B 1
rem echo "!DISK! %%B %DISK_COUNT_MINUS_1%"
if "!DISK!"==" " (
call :draw_no_disk
) else (
call :draw_disk "!DISK!"
)
echo.
)
exit /b
:draw_no_disk
setlocal
call :spc %DISK_COUNT%
call :strings Z " " %DISK_COUNT%
set /P T="|%Z%" < NUL
exit /b
:draw_disk
setlocal
set /A S="%DISK_COUNT% - %~1"
set /A L="%DISK_COUNT_MINUS_1% - %S% + 1"
call :spc %S%
call :strings P "-" %L%
call :strings Q "-" %L%
call :strings R " " %S%
set /P T="%P%-%Q%%R%" < NUL
exit /b
:substr
setlocal
set "A=%~2"
set "R=!A:~%~3,%~4!"
endlocal & set "%1=%R%"
exit /b
rem -- "set /P" prompt cannot start with space character, so using color-reset sequence and spaces, and adjust the position
:spc
setlocal
call :strings S " " %~1
set /P T="[0m%S%" < NUL
exit /b
:strings
setlocal
set A=
for /L %%I in (1,1,%~3) do set "A=!A!%~2"
endlocal & set "%~1=%A%"
exit /b
:beep
set /P T="" < NUL
exit /b
:set_position
set /P T="[%2;%1H" < NUL
exit /b
:move_position_x
set /P T="[%1C" < NUL
exit /b
:move_position_minus_x
set /P T="[%1D" < NUL
exit /b
:move_position_y
set /P T="[%1B" < NUL
exit /b
:erase_current_line
set /P T="[%2K" < NUL
exit /b
解説など
- このバッチファイルでは、文字位置調整のためエスケープシーケンスを使っています。エスケープシーケンスは「ESC文字+特定の文字」で使用することができます。
- 画面表示を毎回Clsコマンドでリセットするとちらつきが多くなるため、エスケープシーケンスで調整して差分のある箇所のみを表示するようにしています。
- 不要な改行を防ぐため、Setコマンドの「/P」オプションを使って文字出力を行っています。(詳しくはSetコマンドを参照)
- ユーザー入力はChoiceコマンドを用いています。