Bulls & Cows - Extra Credit: JSON

Hey all, just finished the Bulls and Cows section.

For my feature I decided to add a means for players to supply their own word list as a JSON formatted file to live outside of the UE4 environment. To make this work, inside the BullCowGame.Build.cs I had to append “Json” and “JsonUtilities” to the list for PublicDependencyModuleNames. Another goal with this feature was to learn how to factor out code to be put in separate source files; a portion of working with JSON data was placed in a separate class.

Create your word list as an array named WordList inside a file called words.json and save it in the Content folder of your UE4 project. Here’s an example:

words.json:

{
	"WordList": ["up", "down", "left", "right"]
}

Here is the BullCowCartridge.h file:

#pragma once

#include "utils/jsonassist.h"
#include "CoreMinimal.h"
#include "Console/Cartridge.h"
#include "Misc/Paths.h"
#include "Misc/FileHelper.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "Math/UnrealMathUtility.h"
#include "BullCowCartridge.generated.h"

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 ResetGame();
	static bool ValidateIsogram(FString word);
private:
	FString HiddenWord;
	uint32 NumGuesses;
	jsonassist MyJsonData;
	void DetermineCattle(FString GuessWord, FString CorrectWord, uint32& NumBulls, uint32& NumCows);
};

The BullCowCartridge.cpp file:

#include "BullCowCartridge.h"

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

    FMath::RandInit((uint32)FDateTime::Now().GetTicks());
    MyJsonData = jsonassist("words.json");
    ResetGame();
}

void UBullCowCartridge::OnInput(const FString& Input) // When the player hits enter
{
    uint32 InputLen = Input.Len();
    uint32 HiddenLen = HiddenWord.Len();

    if (InputLen == 0) return;

    if (ValidateIsogram(Input)) {
        if (Input == HiddenWord) {
            PrintLine(TEXT("You guessed the right word!"));
            ResetGame();
            return;
        }
        else if (InputLen != HiddenWord.Len()) {
            PrintLine(TEXT("The mystery word has " + FString::FromInt(HiddenLen) + TEXT(" letters, not ") + FString::FromInt(InputLen) + TEXT(".")));
            return;
        } else {
            PrintLine(TEXT("That's not the word I was thinking of."));
            --NumGuesses;
        }
    } else {
        PrintLine(TEXT("That word isn't an isogram!"));
        if (!ValidateIsogram(HiddenWord)) {
            PrintLine(TEXT("Fix the source list!"));
        }
        return;
    }

    if (NumGuesses <= 0) {
        PrintLine(TEXT("Game over! The word was: " + HiddenWord));
        ResetGame();
        return;
    }

    uint32 bulls = 0, cows = 0;
    DetermineCattle(Input, HiddenWord, bulls, cows);
    PrintLine(TEXT("Bulls: ") + FString::FromInt(bulls) + TEXT(" Cows: ") + FString::FromInt(cows));
    PrintLine(TEXT("You have " + FString::FromInt(NumGuesses) + TEXT(" guesses left.")));
}

void UBullCowCartridge::ResetGame() {
    NumGuesses = 5;
    PrintLine(TEXT("Bull Cows Game!\nHit TAB to enable text input, press ENTER when done."));

    if (MyJsonData.CheckValid()) { // Ensure JSON data is valid
        if (MyJsonData.Get()->HasField(TEXT("WordList"))) { // Make sure WordList key is present
            const TArray<TSharedPtr<FJsonValue>>& words = MyJsonData.Get()->GetArrayField("WordList");
            uint32 NumWords = words.Num();
            PrintLine(TEXT("words.json contains ") + FString::FromInt(NumWords) + TEXT(" words."));            
            HiddenWord = words[FMath::RandRange(0, NumWords - 1)].Get()->AsString(); // Pick one
            PrintLine(TEXT("Using the word \"" + HiddenWord + TEXT("\"")));
            if (!ValidateIsogram(HiddenWord)) {
                PrintLine(TEXT("Warning: \"" + HiddenWord + TEXT("\" from source list isn't an isogram!")));
            }
            PrintLine(TEXT("Mystery word has ") + FString::FromInt(HiddenWord.Len()) + TEXT(" letters."));
        } else {
            PrintLine(TEXT("Oops, cant find WordList key!"));
        }
    } else {
        PrintLine(MyJsonData.GetErrors());
    }

    PrintLine(TEXT("You have " + FString::FromInt(NumGuesses) + TEXT(" guesses left.")));
}

bool UBullCowCartridge::ValidateIsogram(FString word)
{
    TSet<TCHAR>  CorrectLetterMap;    
    for (TCHAR CurrentLetter : word) {
        if (!CorrectLetterMap.Contains(CurrentLetter)) {
            CorrectLetterMap.Add(CurrentLetter);
        } else {
            return false;
            break;
        }
    }

    return true;
}

void UBullCowCartridge::DetermineCattle(FString GuessWord, FString CorrectWord, uint32& NumBulls, uint32& NumCows)
{
    TSet<TCHAR>  CorrectLetterMap;

    for (TCHAR CurrentLetter : CorrectWord) {
        if (!CorrectLetterMap.Contains(CurrentLetter)) {
            CorrectLetterMap.Add(CurrentLetter);
        }
    }

    TCHAR* GuessLetters = GuessWord.GetCharArray().GetData();
    TCHAR* CorrectLetters = CorrectWord.GetCharArray().GetData();
    
    NumBulls = 0;
    NumCows = 0;
    for (int32 i = 0; i < GuessWord.Len(); ++i) {
        if (GuessLetters[i] == CorrectLetters[i]) {
            ++NumBulls;
        } else if (CorrectLetterMap.Contains(GuessLetters[i])) {
            ++NumCows;
        }
    }
}

My jsonassist.h header:

#pragma once

#include "CoreMinimal.h"
#include "Misc/Paths.h"
#include "Misc/FileHelper.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"

class BULLCOWGAME_API jsonassist
{
private:
	bool valid;
	FString Errors;
	TSharedPtr<FJsonObject> JsonObject;

public:
	jsonassist();
	~jsonassist();
	jsonassist(FString path);
	bool CheckValid();
	FString GetErrors();
	TSharedPtr<FJsonObject>& Get();
};

And the jsonassist.cpp source file:

#include "jsonassist.h"

jsonassist::jsonassist() {}
jsonassist::~jsonassist() {}

jsonassist::jsonassist(FString path) {

    Errors = TEXT("");
    valid = true;

    FString RootDir = FPaths::ProjectContentDir().Append(path);
    const TCHAR* RootDir_TCHAR = RootDir.GetCharArray().GetData();

    JsonObject = MakeShareable(new FJsonObject());
    FString JsonData;

    if (FPaths::FileExists(RootDir)) {

        if (!FFileHelper::LoadFileToString(JsonData, RootDir_TCHAR)) {
            Errors += TEXT("Something screwed up reading in JSON data...\n");
            valid = false;
            return;
        } else {
            Errors += TEXT("Loaded JSON file...");
            TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(JsonData);
            // Deserialize the data
            if (FJsonSerializer::Deserialize(JsonReader, JsonObject)) {
                Errors += TEXT("Deserialization successful.");
            } else {
                Errors += TEXT("Deserialization NOT successful.");
                valid = false;
                return;
            }
            if (JsonObject.IsValid()) {
                Errors += TEXT("JSON valid");
            } else {
                Errors += TEXT("JSON NOT valid");
                valid = false;
                return;
            }
        }
    }
}


bool jsonassist::CheckValid()
{
    return valid;
}

FString jsonassist::GetErrors()
{
    return Errors;
}

TSharedPtr<FJsonObject>& jsonassist::Get()
{
    return JsonObject;
}

I haven’t exhaustively tested everything, this is mostly a product of loads of googling and getting familiar with the UE4 docs, but the project seems to work well enough. Feel free to poke around with it, hopefully you might find something useful there for your own projects.

Have fun!

Privacy & Terms