Bulls & Cows - Just finished the section, here's what I did

I haven’t made any changes to the core gameloop, but I added an intro rather than going right into the game.

I also changed the appearance of the terminal as well as changing the command line starter thingy (I don’t know what it’s actually called) from $> to C:\> in the terminal.cpp file.

I’ll post some screenshots in a minute.

The terminal:

(I took the cartridge part of the class name very literally)

Just recorded a quick video of example gameplay.


As you can see I made it slightly harder as it doesn’t tell you the number of letters, although I might change it as it seems like unnecessary extra guessing.

I’ll post the c++ and header code in a second.

2 Likes

BullCowCartridge.cpp

// Fill out your copyright notice in the Description page of Project Settings.
#include "BullCowCartridge.h"

void UBullCowCartridge::BeginPlay() // When the game starts
{
    Super::BeginPlay();

    // Load wordlist at runtime from text file to reduce compile time
    const FString WordListPath = FPaths::ProjectContentDir() / TEXT("WordLists/HiddenWords.txt");
    FFileHelper::LoadFileToStringArray(WordList, *WordListPath);

    // Replace word list, by reference, with valid words only
    SetValidWords(WordList);

    // Intro
    PrintLine(TEXT("S.I.G.N. Terminal v1.0"));
    PrintLine(TEXT("(Serial Interface Game Node)\n"));
    PrintLine(TEXT("Please enter a command, type help to view all available commands"));
}

void UBullCowCartridge::OnInput(const FString& Input) // When the player hits enter
{
    if (bGameInitialised) // Check if game started, else run through the fake command line interface until user types cartridge load 1
    {
        if (bGameOver)
        {
            ClearScreen();
            PrintLine(TEXT("S.I.G.N. Terminal v1.0"));
            PrintLine("(Serial Interface Game Node)\n");
            PrintLine(TEXT("Please enter a command, type help to view all available commands"));

            bGameInitialised = false;
        }
        else
        {
            ProcessGuess(Input);
        }
    }
    else
    {
        GameIntro(Input);
    }
}

void UBullCowCartridge::InitGame()
{
    HiddenWord = WordList[FMath::RandRange(0, WordList.Num() - 1)];
    Lives = HiddenWord.Len();
    bGameOver = false;

    bGameInitialised = true;

    // Cartridge splash screen
    ClearScreen();
    //PrintLine(TEXT("The hidden word is: ") + HiddenWord); // DEBUG LINE

    PrintLine(TEXT("============================"));
    PrintLine(TEXT("| Welcome to Bulls & Cows! |"));
    PrintLine(TEXT("============================"));
    PrintLine(TEXT("COWCOM, all right reserved\n"));
    PrintLine(TEXT("You have %i lives and the word is %i letters long, good luck!"), Lives, HiddenWord.Len());
}

void UBullCowCartridge::ProcessGuess(const FString& Guess)
{
    // Win check
    if (Guess == HiddenWord) // End game if word guessed successfully
    {
        bGameOver = true;
        EndGame(true);
        return;
    }

    // Input validation
    if (Guess.Contains(TEXT(" "))) // Prevent the player from entering a space or number
    {
        PrintLine(TEXT("No spaces!"));
        return;
    }

    if (Guess.Len() != HiddenWord.Len()) // Tell the player that their word is too short
    {
        PrintLine(TEXT("The hidden word is %i letters long, you entered a %i letter word"), HiddenWord.Len(), Guess.Len());
        return;
    }

    if (!ContainsAllLetters(Guess))
    {
        PrintLine(TEXT("Only lower and upper case letters are allowed!"));
        return;
    }

    if (!IsIsogram(Guess)) // Check if guess is an isogram
    {
        PrintLine(TEXT("%s is not an isogram. No repeating letters!"), *Guess);
        return;
    }

    // Decriment Lives
    --Lives;

    // Loose check
    if (Lives <= 0)
    {
        bGameOver = true;
        EndGame(false);
        return;
    }

    // Show the player bulls and cows
    FBullsAndCowsCount BullCowCount = GetBullsAndCows(Guess);
    PrintLine(TEXT("You have %i bulls, and %i cows"), BullCowCount.Bulls, BullCowCount.Cows);

    // Change life text for proper English
    if (Lives == 1)
    {
        PrintLine(TEXT("%s isn't the word, try guessing again\nYou have 1 life remaining"), *Guess);
    }
    else
    {
        PrintLine(TEXT("%s isn't the word, try guessing again\nYou have %i lives remaining"), *Guess, Lives);
    }
}

void UBullCowCartridge::EndGame(const bool bPlayerWon) const
{
    ClearScreen();

    if (bPlayerWon)
    {
        PrintLine(TEXT("=====| YOU WON! |====="));
    }
    else
    {
        PrintLine(TEXT("=====| GAME OVER |====="));
        PrintLine(TEXT("The hidden word was ") + HiddenWord);
    }

    PrintLine(TEXT("Press enter to continue..."));
}

bool UBullCowCartridge::IsIsogram(const FString& Word) const
{
    // Initialise alphabet array
    //                         a  b  c  d  e  f  g  h  i  j  k  l  m  n  o  p  q  r  s  t  u  v  w  x  y  z
    int32 LetterCounts[26] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

    // Loop through word adding occurences of letter to alphabet array
    for (char Letter : Word.ToLower())
    {
        if (++LetterCounts[Letter - 'a'] > 1)
        {
            return false;
        }
    }

    return true;
}

bool UBullCowCartridge::ContainsAllLetters(const FString& Word) const
{
    for (char Letter : Word)
    {
        if (!isalpha(Letter))
        {
            return false;
        }
    }

    return true;
}

void UBullCowCartridge::GameIntro(const FString& Input) // Entire function for desicion tree CLI
{
    TArray<FString> CommandArray;
    Input.ToLower().ParseIntoArray(CommandArray, TEXT(" "));
    
    if (CommandArray.Num() == 0)
    {
        return;
    }

    if (CommandArray[0] == TEXT("help"))
    {
        PrintLine("The following commands are enabled on this terminal:");
        PrintLine("help    cartridge");
    }
    else if (CommandArray[0] == TEXT("cartridge"))
    {
        if (CommandArray.Num() == 1)
        {
            PrintLine(TEXT("cartridge sub commands:"));
            PrintLine(TEXT("help    list    load"));
        }
        else
        {
            if (CommandArray[1] == TEXT("help"))
            {
                PrintLine(TEXT("* Cartridge Command Help Doc *"));
                PrintLine(TEXT("help - this command"));
                PrintLine(TEXT("list - list all cartridge slots and contents"));
                PrintLine(TEXT("load - load a connected cartridge, usage: cartridge load <index>"));
                
            }
            else if (CommandArray[1] == TEXT("list"))
            {
                PrintLine(TEXT("The currently installed cartridges are:"));
                PrintLine(TEXT("[1] Bulls & Cows"));
                PrintLine(TEXT("[2] empty slot"));
            }
            else if (CommandArray[1] == TEXT("load"))
            {
                if (CommandArray.Num() == 3)
                {
                    if (CommandArray[2] == TEXT("1"))
                    {
                        InitGame();
                    }
                    else if (CommandArray[2] == TEXT("2"))
                    {
                        PrintLine(TEXT("No cartridge in slot 2"));
                    }
                    else
                    {
                        PrintLine(TEXT("Invalid slot"));
                    }
                }
                else
                {
                    PrintLine(TEXT("Invalid arguments, see cartridge help for documentation"));
                }
            }
            else
            {
                PrintLine(TEXT("Invalid sub command, type cartridge for a list of commands\nor cartridge help for a discription of each sub command"));
            }
        }
    }
    else
    {
        PrintLine(TEXT("Invalid command, type help to view available commands"));
    }
}

void UBullCowCartridge::SetValidWords(TArray<FString>& Words)
{
    TArray<FString> ValidWords;

    for (FString Word : Words)
    {
        if (Word.Len() >= 4 && Word.Len() <= 8)
        {
            if (IsIsogram(Word))
            {
                ValidWords.Emplace(Word);
            }
        }
    }

    Words = ValidWords;
}

FBullsAndCowsCount UBullCowCartridge::GetBullsAndCows(const FString& Guess) const
{
    FBullsAndCowsCount Count;

    for (int32 i = 0; i < Guess.Len(); i++)
    {
        if (Guess[i] == HiddenWord[i])
        {
            ++Count.Bulls;
            continue;
        }

        for (char Letter : HiddenWord)
        {
            if (Letter == Guess[i])
            {
                ++Count.Cows;
                break;
            }
        }
    }

    return Count;
}

and BullCowCartridge.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Console/Cartridge.h"
#include "BullCowCartridge.generated.h"

struct FBullsAndCowsCount
{
	int8 Bulls = 0;
	int8 Cows = 0;
};

UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class BULLCOWGAME_API UBullCowCartridge : public UCartridge
{
	GENERATED_BODY()

	public:
		virtual void BeginPlay() override;
		virtual void OnInput(const FString& Input) override;
		void InitGame();
		void ProcessGuess(const FString& Guess);
		void GameIntro(const FString& Input);
		void EndGame(const bool bPlayerWon) const;
		bool IsIsogram(const FString& Word) const;
		bool ContainsAllLetters(const FString& Word) const;
		void SetValidWords(TArray<FString>& Words);
		FBullsAndCowsCount GetBullsAndCows(const FString& Guess) const;

	// Your declarations go below!
	private:
		TArray<FString> WordList;
		bool bGameInitialised;
		bool bGameOver;
		FString HiddenWord;
		int32 Lives;
};

Note: if you want to run this yourself, you’ll have to add a folder to the content folder of the project call WordLists, and create a text file in that folder called HiddenWords.txt (This was done to reduce compile times as per a post on this forum).

Less game breaking but still, you’ll have to edit the terminal component to increase the max columns and rows before a line wrap AND change the font size of the text component if you don’t want wrapping issues.
Instructions:
Change the text render component’s Word Size property to 16.0
Change the terminal component’s Max Lines to 18, and Max Columns to 70.

(You’ll find both of those components on the terminal GameObject (I think they’re called actors in unreal))

Edit:
After playing it for a bit I really didn’t like the extra guessing of the length, so I’ve changed the code above to reflect that.

Also here’s the word list I’m using:
http://www.desiquintans.com/nounlist
They’re all nouns and there’s over 6000 total words (around 2500 isograms between 4 and 10 letters)

3 Likes

Privacy & Terms