Bull Cow Extra - increasing difficulty

My extra feature was to have the game increase in difficulty each time you successfully guess a word (starting with x letter word, then x+1, x+2 … ).

I’ve just come from a different c++ course on udemy and learnt a bit about using the STL. So I used ue4’s TMap and TSet, to create a map.

TMap<int32, TSet<FString>> Words; //in header file

I used a random short story I found on the internet and used a different c++ program I wrote to quickly strip out punctuation, and generate a file with one word per line - I couldn’t quite figure out how to do this within the project, using ue4’s file reading/writing tools.

Then for each FString Word I pulled from the file, I put it into the map using the length of the word as the key, before emplacing it into the appropriate set. This way words were sorted into their appropriate ‘difficulty’, and duplicates were ignored.

    FString FileName = FPaths::ProjectDir();
    FileName += "Content/wordlist.txt";

    if (!FPlatformFileManager::Get().GetPlatformFile().FileExists(*FileName))
    {
        return false;
    }
    else
    {
        FFileHelper::LoadANSITextFileToStrings(*(FileName), NULL, Result);
        for (FString Each : Result)
        {
            if (Each.Len() >= StartingDifficulty)
            {
                Each = Each.ToLower(); //otherwise words like Dad were being considered isograms
                if (IsIsogram(Each))
                {
                    Words.FindOrAdd(Each.Len()); //make sure that the key exists in the map, otherwise add it
                    Words[Each.Len()].Emplace(Each); //insert word into set
                }
            }
        }
    }

Then for selecting a random word, i used an iterator to select the “randomly chosen” word from the set.

if (Words.Contains(Difficulty))
{
    int32 Picker = FMath::RandRange(0,Words[Difficulty].Num()-1);
    auto It = Words[Difficulty].CreateIterator();
    for (int32 i{0}; i < Picker; i++)
        ++It; //advance iterator
    HiddenWord = *It;
}

If there is a better way to do this, please let me know!

1 Like

Wow awesome work! Way to go above and beyond!

  • FString FileName = FPaths::ProjectContentDir();
    FileName += "Content/wordlist.txt";
    

    This can just be

    FString FileName = FPaths::ProjectContentDir() / TEXT("wordlist.txt");
    
  • for (FString Each : Result)
    

    This should be a reference I’d also argue “Each” isn’t a great name, Line or Word would be better IMO.

  • Each = Each.ToLower();
    

    This can just be Each.ToLowerInline();

  • Getting into move semantics this should move the string. (more info on that)

    Words[Each.Len()].Emplace(MoveTemp(Each));
    
  • As the way to get a random word from the TSet isn’t great, what you could do instead is have the member variable be TArray<FString> instead and only use the TSet to add words so you don’t get dupes and then use YourSet.Array() to get it as a TArray

  • i used an iterator to select …

    TSet supports range-based for loops so you can write that as

    const int32 RandIndex = FMath::RandRange(0,Words[Difficulty].Num()-1);
    const int32 Index = 0;
    for (const FString& Word : Words[Difficulty])
    {
        if (Index == RandIndex)
        {
            HiddenWord = Word;
            return;
        }
    }
    

Aha! Thanks for the reply, very much appreciated!
I have a couple of followup questions:

Are you saying to just copy the relevant TSet to a TArray each time so we can access the TArray[RandIndex] directly? Or to have the TMap<int32, TSet<FString>> be temporary and copy each set into a TMap<int32, TArray<FString>>?
Do either of these solutions end up having less overhead than iterating through the TSet?

Ahh yes using a range-based for loop definitely makes more sense! Shouldn’t Index not be const and be incremented each loop (like below), or am I missing something?

const int32 RandIndex = FMath::RandRange(0,Words[Difficulty].Num()-1);
int32 Index = 0;
for (const FString& Word : Words[Difficulty])
{
    if (Index == RandIndex)
    {
        HiddenWord = Word;
        return;
    }
    Index++;
}

Thanks again!

Thanks for the link! Just to see if I understand this correctly: does this mean after using Words[Each.Len()].Emplace(MoveTemp(Each)) if I were to try and continue to use Each in some way before the next loop, it would be now pointing to an empty FString, in the same place in memory?

Just the TSet for getting the words and store TMap<int32, TArray<FString>>

Yes since TArray is a contiguous array so Array[6] is just a single instruction. The difference is most likely a few miliseconds for populating it and this is done once so not a huge deal or anything and nanoseconds for access.

An alternative solution is to store just TArray<FString> sorted by length and have another TArray<int32> to mark the boundaries of where the lengths differ.

Yes although not that it’s “pointing” as it isn’t a pointer. It’s underlying storage has been “stolen” so to speak.

Yup, just habit.

1 Like

Just realising there’s AddUnique so no need for the set at all.

Oh cool! I can’t seem to find anything that does that with normal c++ Arrays! So then is the only reason to ever use a TSet over a TArray in UE4 if you know you will never need to directly access its elements?

You would use TSet for the same reason you would use a TMap except that the key is also the value.

Privacy & Terms