Programming Field

バッチファイルで「ハノイの塔」

「ハノイの塔」のゲームをプレイできるバッチファイルです。円盤を左端から右端に移動すればクリアです。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="%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コマンドを用いています。