Hangman 2

For Words-Collection Source Click Here


::  You can ignore all lines beginning with %debugMode%, they don't do anything unless you change the variable (see below).
::  If you want to see what the program does step by step make this variable blank. Otherwise leave as REM. Any other values will crash the program.

@set debugMode=REM

::   This game relies on the existence of a dictionary file. A (long) good one is available on my website ScrewTheLotOfYou.webs.com. If you want to make your own (with naughty words or in a different language?) you MUST format it correctly or this program will refuse to load it.
::   The format is simple, a text-only file (.txt) full of words. Each word must be on its OWN LINE, with NO SPACES and NO BLANK LINES.
::   The very first line (called the 'Header' throughout this program) must be of the format: '//WordCount=###' (without quotes) where ### is exactly the number of words in your dictionary.
::   The first word is on the line immediately below the header; NO BLANK LINES.


:Main
    @echo off

    setlocal enabledelayedexpansion
    title Hangman 2

    :://CONSTANT random.Max is the maximum number generated by the %random% variable provided by cmd.exe.
    set /a random.Max=32767

    :://There's good reason for setting this limit. Who needs more than 131,000 words anyway?
    set /a dictionary.maxEntries= random.Max * 4

    :://This is arbitrary.
    set /a targetWord.maxLength=100

    set dictionary.defaultPath=Hangman.Dictionary.txt
    set settings.defaultPath=HangmanSettings.inf

    :://Load dictionary on startup.

    :://GetDictionary sets dictionary.filePath and dictionary.wordCount.
    call :GetDictionary

    :://Exceptions are thrown by GetDictionary when user cannot locate a dictionary that passes the tests. In this case the program exits; you can't play hangman without words to guess.
    if defined exception (
        echo !exception!
        pause
        exit /b
    )

    %debugMode% pause

    call :MainMenu
    exit /b





:MainMenu

    set exception=
    set menuChoice=

    :://MenuInterface sets menuChoice to a number between 1 and 3 inclusive.
    call :MenuInterface

    if !menuChoice!==1 (
        call :PrepareGame

        REM //This exception should never occur. Indicative of the dictionary file being changed since checks were made (or a programming error).
        if defined exception (
            echo !exception!
            pause
            exit /b
        )

        call :PlayGame
    )

    if !menuChoice!==2 call :ChangeDictionary

    if !menuChoice!==3 exit /b

    :://Once game has finished or options have been changed, MainMenu procedure is repeated so the user can play another game.
    goto :MainMenu






:MenuInterface

:://Sets the menuChoice variable to a number between 1 and 3.

    cls
    echo Main Menu
    echo =========
    echo.
    echo.
    echo 1. Play
    echo 2. Change dictionaries
    echo 3. Quit
    echo.
    echo Type EXIT at any time to cancel.
    echo.
    set /p menuChoice="Enter the number of your choice [1-3] - "

    if /i "!menuChoice!"=="EXIT" (
        set menuChoice=3
    )

    if !menuChoice! lss 1 goto MenuInterface
    if !menuChoice! gtr 3 goto MenuInterface

    :://ASSERT: menuChoice is now a number between 1 and 3.

    exit /b






:PrepareGame

:://Sets targetWord to a word from the dictionary, sets up displayCharacter[], targetCharacter[], guessedCharacter, displayLine, guessedLine, correctGuessCount and lives.

    :://GetTargetWord sets targetWord.
    call :GetTargetWord

    :://SetupCharacterVariables creates displayCharacter[], targetCharacter[] and displayLine.
    call :SetupCharacterVariables

    set /a lives=12
    set /a correctGuessCount=0
    set /a guessedCharacter.length=0
    set guessedLine=

    exit /b







:GetDictionary (String dictionary.defaultPath)

:://Sets dictionary.filePath, dictionary.wordCount. Or throws exception if valid dictionary cannot be found.
:://Called only when first loading dictionary on program start.

    set exception=

    if exist !settings.defaultPath! (
        %debugMode% echo !settings.defaultPath! found. Reading saved settings.

        REM //LoadSettingsFromFile sets dictionary.defaultPath.
        call :LoadSettingsFromFile

    ) else (
        %debugMode% echo No saved settings found.
        %debugMode% echo Checking if dictionary exists at default location.
    )

    :://Assume the default dictionary.filePath.
    if exist !dictionary.defaultPath! (
        %debugMode% echo Default dictionary found.
        set dictionary.filePath=!dictionary.defaultPath!
    ) else (
        REM //If no default dictionary is available, must ask the user to locate one.
        %debugMode% echo No default dictionary found.
        call :RequestDictionary
    )


    :://Exception is thrown by RequestDictionary if no valid dictionary file could be located.
    if defined exception (
        exit /b
    )


    :://ASSERT: Dictionary file exists and its path is stored in dictionary.filePath.

    call :ReadDictionary
    if defined exception (
        exit /b
    )   

    :://ASSERT: Dictionary file has valid header listing appropriate length.

    %debugMode% echo Dictionary file OK.

    if /i NOT "!dictionary.filePath!"=="!dictionary.defaultPath!" (
        call :SaveDefaultPath
    )
    exit /b





:LoadSettingsFromFile (String settings.defaultPath)
:://Sets dictionary.defaultPath based on settings found in settings.defaultPath

    for /f "tokens=1* delims==" %%I in (!settings.defaultPath!) do (
        if /i NOT "%%I"=="//DefaultDictionaryPath" (
            %debugMode% echo Settings file is incorrectly formatted.
            exit /b
        )
        set dictionary.defaultPath=%%J
        %debugMode% echo Settings file lists default dictionary as '!dictionary.defaultPath!'
        exit /b
    )

    %debugMode% echo Settings file is empty.
    exit /b







:SaveDefaultPath
:://Asks user if they want to make the current dictionary the default one. If so, saves settings to settings.defaultPath

    echo.
    echo Would you like to make this dictionary the default?
    set /p boolean="[Y/N] - "
    if /i "!boolean!"=="N" exit /b

    :://Make the current dictionary the default.
    set dictionary.defaultPath=!dictionary.filePath!

    :://Write settings to settings file, so the default is remembered.
    echo //DefaultDictionaryPath=!dictionary.filePath!>!settings.defaultPath!
    exit /b






:RequestDictionary
:://Sets dictionary.filePath from user input. Throws exception if no valid file can be given.

    echo.
    echo Please enter the path a valid dictionary file.
    echo Alternatively, drag the dictionary file onto this window (the path should appear).
    echo Put speech marks (" ") around the path if necessary.
    echo Type EXIT to cancel.
    echo.
    set /p dictionary.filePath="Path - "
    if /i "!dictionary.filePath!"=="EXIT" (
        set exception=Valid dictionary file could not be located.
        exit /b
    )

    if exist !dictionary.filePath! (
        exit /b
    ) else (
        echo.
        echo File not found. Try speech marks around the path.
        pause
        cls
        goto RequestDictionary
    )

:://Never reaches here.






:ChangeDictionary (String dictionary.filePath, Int dictionary.wordCount)
:://Changes the dictionary file by changing dictionary.filePath and dictionary.Wordcount

    cls

    :://ASSERT:Current dictionary is valid.
   
    :://Save the old settings in case a revert is needed.
    set saved.filePath=!dictionary.filePath!
    set saved.wordCount=!dictionary.wordCount!

    set exception=

    :://RequestDictionary sets dictionary.filePath to the path of any file specified by the user.
    call :RequestDictionary
    cls

    :://Revert changes if user failed to locate file.
    if defined exception (
        set exception=
        set dictionary.filePath=!saved.filePath!
        set dictionary.wordCount=!saved.wordCount!
        exit /b
    )

    :://ReadDictionary checks this file is a dictionary, and sets/checks dictionary.wordCount. Otherwise throws exception.
    call :ReadDictionary

    :://Revert changes if user failed to provide suitable file.
    if defined exception (
        set exception=
        set dictionary.filePath=!saved.filePath!
        set dictionary.wordCount=!saved.wordCount!
        exit /b
    )

    if /i NOT "!dictionary.filePath!"=="!dictionary.defaultPath!" (
        call :SaveDefaultPath
    )

    %debugMode% echo.
    %debugMode% echo Dictionary change success.
    %debugMode% echo.
    %debugMode% pause

    exit /b
   





:ReadDictionary (String dictionary.filePath)
:://Reads the dictionary file header to get the number of words, sets dictionary.wordCount and checks that the count is correct.

    %debugMode% echo Testing "!dictionary.filePath!".
    set exception=

    :://GetDictionaryLength sets dictionary.wordCount
    call :GetDictionaryLength dictionary.filePath

    :://GetDictionaryLength may throw an exception if dictionary file is invalid, in which case a different dictionary file is requested.
    if defined exception (
        echo !exception!
        set exception=

        REM //RequestDictionary sets dictionary.filePath. It throws an exception when user cannot located dictionary file.
        call :RequestDictionary
        if NOT "!exception!"=="" (
            exit /b
        )
        cls

        REM //Once new file has been specified, its header must also be checked.
        goto ReadDictionary
    )


    :://ASSERT dictionary.wordCount is the stated dictionary length and is an appropriate integer

    call :CheckDictionaryLength

    :://CheckDictionaryLength throws an exception when the dictionary length listed by the header is too small.
    if defined exception (
        echo !exception!
        set exception=

        REM //RequestDictionary sets dictionary.filePath. It throws an exception when user cannot locate a dictionary file.
        call :RequestDictionary
        if NOT "!exception!"=="" (
            exit /b
        )
        cls
        goto ReadDictionary
    )

    exit /b






:GetDictionaryLength (String dictionary.filePath)
:://Throws an exception if the header does not exist or is not correctly formatted. Sets dictionary.wordCount.

    :://ASSERT: !dictionary.filePath! contains the path of the dictionary file.

    :://For loop only applied to first line of dictionary file before subroutine exits.

    %debugMode% echo Reading dictionary header.


    :://Splits the first line of the dictionary into two pieces. The part of the line on the left hand side of the equals sign is stored in %%I, the right hand side is stored in %%J.
    for /f "tokens=1,2 delims==" %%I in (!dictionary.filePath!) do (
        if /i NOT "%%I"=="//WordCount" set exception=Dictionary file has invalid header.&& exit /b
        if %%J gtr !dictionary.maxEntries! set exception=Dictionary file has invalid header: dictionary length cannot exceed %dictionary.maxEntries%.&& exit /b
        if %%J lss 1 set exception=Dictionary file has invalid header: dictionary length must be greater than zero.&& exit /b

        set dictionary.wordCount=%%J

        REM //ASSERT dictionary.wordCount is the stated dictionary length and is an appropriate integer

        %debugMode% echo Dictionary header OK.
        exit /b
    )

    :://ASSERT: Interpreter only reaches this point if dictionary file is empty/cannot be read.
    set exception=Dictionary file is empty.
    exit /b






:CheckDictionaryLength (String dictionary.filePath, Int dictionary.wordCount)

:://Checks that the dictionary is indeed at least the length specified by the header.

    set exception=
    set correctLength=false

    %debugMode% echo Checking dictionary length is correct.

    :://Inside of FOR loop is only executed if text exists at the %dictionary.wordCount%'th line. It will be skipped if the dictionary isn't as long as specified.
    for /f "skip=%dictionary.wordCount% delims=" %%I in (%dictionary.filePath%) do (
        set correctLength=true
    )

    if "%correctLength%"=="false" (
        set correctLength=
        set exception=The dictionary file header lists an incorrect length.
        exit /b
    )

    :://ASSERT: Dictionary is at least as long as stated in the header.
    set correctLength=

    %debugMode% echo Dictionary length OK.
    exit /b





:GetTargetWord (String dictionary.filePath, Int dictionary.wordCount)

:://Retrieves a randomly selected word from the dictionary, sets targetWord.

    ::ASSERT: dictionary.filePath contains the path of the dictionary file && dictionary.wordCount is the dictionary length

    :://Ten !randoms! are added together to allow the maximum dictionary size to be 4*random.Max without too much statistical unevenness.
    set /a dictionary.offset= 1 + ( !random! + !random! + !random! + !random! + !random! + !random! + !random! + !random! + !random! + !random!) %% !dictionary.wordCount!

    :://The inside of the FOR loop will ONLY be executed if the offset generated using the dictionary header is appropriate.

    for /f "skip=%dictionary.offset% delims=" %%I in (!dictionary.filePath!) do (
        set targetWord=%%I

        REM //ASSERT targetWord now contains a randomly selected word from the dictionary file
        exit /b
    )

    :://ASSERT: the code below is only reached when an invalid offset was used, caused by an incorrect dictionary length in the dictionary file header. This has been checked for previously and can only happen if dictionary file is changed AFTER the program started.

    set exception=Dictionary file has invalid header: stated dictionary length is incorrect. Program will terminate.
    exit /b






:SetupCharacterVariables (String targetWord)

    :://Sets up the targetCharacter[] and displayCharacter[] arrays and their length.
    :://ASSERT: targetWord conatins the target word.

    set tempWord=!targetWord!
    set /a targetCharacter.length=0

    :://displayLine is the line displayed to the user showing current progress. Initially just underscores, as the user guesses correctly actual letters are revealed in their corresponding positions.
    set displayLine=

    for /L %%I in (0,1,%targetWord.maxLength%) do (

        REM //When all characters are consumed, exit loop.
        if "!tempWord!"=="" (

            REM //The lengths of the two arrays are the same, by design.
            set /a displayCharacter.length=!targetCharacter.length!
            exit /b
        )

        REM //Copy the first character of the remaining string into targetCharacter[].
        set targetCharacter[!targetCharacter.length!]=!tempWord:~0,1!

        REM //For each character in the actual word, there is an _ displayed initially.
        set displayCharacter[!targetCharacter.length!]=_

        REM //Extend the display line to include the new display character.
        set displayLine=!displayLine! ^^!displayCharacter[!targetCharacter.length!]^^!

        REM //Increment the array length.
        set /a targetCharacter.length+=1

        REM //Remove the first character of the string.
        set tempWord=!tempWord:~1!

        REM //Repeat.
    )

:://Should never reach here.






:PlayGame (String displayLine, Char Array targetCharacter[], Char Array displayCharacter[], Int lives, Char Array guessedCharacter[], String guessedLine, Int correctGuessCount)

    call :RefreshDisplay
    set errorMessage=
    set guess=
    set /p guess="Guess - "
    if /i "!guess!"=="EXIT" (
        call :ClearUpGame
        exit /b
    )
    if NOT "!guess:~1!"=="" (
        set errorMessage=You must enter one letter.
        goto PlayGame
    )

    :://CheckGuess sets matchCount to the number of letters in the targetWord that match the guessed letter.
    call :CheckGuess

    :://CheckGuess throws exceptions if the guessed letter has already been guessed
    if defined exception (
        set errorMessage=%exception%
        set exception=
        goto PlayGame
    )

    :://matchCount is 0 if the guess was wrong i.e. no letters matched the guess, so player loses a life.
    if %matchCount% equ 0 (
        set /a lives-=1
        if !lives! leq 0 (
            call :LoseScreen
            exit /b
        )
    ) else (
        set /a correctGuessCount+=matchCount
        if !correctGuessCount! geq %targetCharacter.length% (
            call :WinScreen
            exit /b
        )
    )
    goto PlayGame






:RefreshDisplay (String displayLine, Int lives, string guessedLine, String errorMessage)

    :://Displays current progress and lives.

    cls
    echo %displayLine%
    echo.
    echo.

    if %lives% neq 1 (
        echo You have %lives% lives remaining.
    ) else (
        echo You have 1 life remaining.
    )
    echo Guessed letters: !guessedLine!
    echo.
    echo.%errorMessage%
    echo.
    echo Type EXIT at any time to exit.
    echo.
    exit /b



:CheckGuess (Char guess, Char Array targetCharacter[], Char Array displayCharacter[], Char Array guessedCharacter[], String guessedLine)

    :://Sets matchCount to the number of matches found.

    set matchCount=0

    set /a tempCount=%guessedCharacter.length% - 1

    :://Check that the letter has not already been guessed by comparing the guess to all the guessed letters
    for /L %%I in (0,1,%tempCount%) do (
        if /i "!guessedCharacter[%%I]!"=="!guess!" (
            set exception=You have already guessed that letter.
            exit /b
        )
    )

    :://ASSERT: code only reaches this point if the letter has NOT been guessed before.

    :://Add the letter to guessLine and guessedLetters[]
    set guessedLine=!guessedLine! !guess!
    set guessedCharacter[%guessedCharacter.length%]=!guess!
    set /a guessedCharacter.length+=1


    set /a tempCount=%targetCharacter.length% - 1

    :://Check if the letter is in the targetWord, if so display each instance of that letter.
    for /L %%I in (0,1,%tempCount%) do (
        if /i "!targetCharacter[%%I]!"=="!guess!" (
            set /a matchCount+=1
            set displayCharacter[%%I]=!targetCharacter[%%I]!
        )
    )

    exit /b





:WinScreen
    call :RefreshDisplay
    echo You win^^! The word was '!targetWord!'.
    pause
    call :ClearUpGame
    exit /b


:LoseScreen
    call :RefreshDisplay
    echo You lose^^! The word was '!targetWord!'.
    pause
    call :ClearUpGame
    exit /b



:ClearUpGame
:://Clears all the variables created from one game to the next.

    set /a tempCount=%targetCharacter.length% - 1

    for /l %%I in (0,1,%tempCount%) do (
        set targetCharacter[%%I]=
        set displayCharacter[%%I]=
    )
    set targetCharacter.length=0
    set displayCharacter.length=0

    set /a tempCount=%guessedCharacter.length% - 1

    for /l %%I in (0,1,%tempCount%) do (
        set guessedCharacter[%%I]=
    )
    set guessedCharacter.length=0

    set tempCount=
    set matchCount=
    set lives=
    set correctGuessCount=
    set guessedLine=
    set displayLine=
    set guess=

    exit /b