I kind of deviated from the lecture along the way, so there wasn’t anything else I wanted to add.
Mostly this includes a few extra functions, some separation of roles, and a more complex end game function.
Turns out, I was really close to figuring out BullCows before I got to the lecture, I was being stubborn about something I didn’t need to be stubborn about. I’m also not from a programming world where continue
and break
are used too often.
I tried to add a bit of personality to this where the game is really dramatic about you dying, and is more melodramatic about you winning.
Anyway, here’s my .h file:
// 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 FBullCowCount
{
int32 Bulls = 0;
int32 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& PlayerInput) override;
// Game Loop
void SetUpGame();
void WelcomePlayer() const;
void EndGame();
// Processing Player Guess
void ProcessGuess(const FString& Guess);
void PlayerDidNotWin(const FString& Guess);
void PlayerLostLife();
// Processing Words
FBullCowCount GetBullCows(const FString &Guess) const;
bool IsIsogram(const FString& Word) const;
TArray<FString> GetValidWords(const TArray<FString>&) const;
// Your declarations go below!
private:
TArray<FString> Words;
TArray<FString> Isograms;
FString HiddenWord;
int32 Lives;
bool bGameOver;
};
My .cpp file
// Fill out your copyright notice in the Description page of Project Settings.
#include "BullCowCartridge.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
void UBullCowCartridge::BeginPlay() // When the game starts
{
Super::BeginPlay();
// Advanced Course Lesson to reduce compile times!
const FString WordListPath = FPaths::ProjectContentDir() / TEXT("WordLists/HiddenWordList.txt");
FFileHelper::LoadFileToStringArray(Words, *WordListPath);
// We only need this done once
Isograms = GetValidWords(Words);
SetUpGame();
// PrintLine(TEXT("The HiddenWord is %s"), *HiddenWord); //Debug Line
// PrintLine(TEXT("It is %i characters Long"), HiddenWord.Len()); //Debug Line
WelcomePlayer();
}
void UBullCowCartridge::OnInput(const FString& PlayerInput) // When the player hits enter
{
ClearScreen();
// If Game is over, ClearScreen() and SetupGame()
if (bGameOver) {
EndGame();
return; // Using a return here to exit early to prevent Normal Game Loop
}
// Normal Game Loop
// Check if Valid
ProcessGuess(PlayerInput);
}
void UBullCowCartridge::SetUpGame()
{
HiddenWord = Isograms[FMath::RandRange(0, Isograms.Num() - 1)];
Lives = HiddenWord.Len();
bGameOver = false;
}
void UBullCowCartridge::WelcomePlayer() const
{
// Separating these may make it easier to simplify dialogue later
PrintLine(TEXT("Welcome to Bull Cows!"));
PrintLine(TEXT("Guess the %i letter word!"), HiddenWord.Len());
PrintLine(TEXT("You have %i lives."), Lives);
PrintLine(TEXT("Please enter your guess and \nPress enter to continue..."));
}
void UBullCowCartridge::ProcessGuess(const FString& Guess)
{
// No reason to check everything else if it already works
if (Guess == HiddenWord)
{
PrintLine(TEXT("The word is %s"), *HiddenWord);
EndGame();
return;
}
if (HiddenWord.Len() != Guess.Len()) // Check Character Length
{
// Prompt to GuessAgain
PrintLine(TEXT("The Hidden Word is %i characters long."), HiddenWord.Len());
PrintLine(TEXT("You still have %i lives remaining. \nTry again!"), Lives);
return;
}
if (!IsIsogram(Guess)) // Check if it is an Isogram
{
PrintLine(TEXT("No repeating Letters!\n"));
PrintLine(TEXT("NOT"));
PrintLine(TEXT("AN"));
PrintLine(TEXT("ISOGRAM!!"));
PrintLine(TEXT("You still have %i lives remaining. \nTry again!"), Lives);
return;
}
// Answer is Valid, but not a victory, so now T or C
PlayerDidNotWin(Guess);
}
void UBullCowCartridge::PlayerDidNotWin(const FString& Guess)
{
//Didn't win? Now the bad stuff happens!
FBullCowCount Score = GetBullCows(Guess);
PrintLine(TEXT("You have %i Bulls and %i Cows"), Score.Bulls, Score.Cows);
PlayerLostLife();
}
// This is taking care of itself, because there is only one way to win.
// Bull Cows Will be calculated separately
void UBullCowCartridge::PlayerLostLife()
{
--Lives; // Keep it here because otherwise I'll be writting it twice.
// Ask you dead or dying?
if (Lives <= 0)
{
// If no lives, inform player they have perished.
EndGame();
return;
}
if (Lives == 1)
{
// Warn them about how close they are to dying
PrintLine(TEXT("YOU HAVE ONE FINAL LIFE!"));
PrintLine(TEXT("YOU WILL PERISH IF YOU FAIL AGAIN"));
return;
}
// If your not dead, your alive!
// Prompt user for another guess
PrintLine(TEXT("YOU MUST TRY AGAIN OR PERISH!"));
PrintLine(TEXT("You have %i lives left"), Lives);
}
FBullCowCount UBullCowCartridge::GetBullCows(const FString &Guess) const
{
FBullCowCount Count;
for (int32 GuessIndex = 0; GuessIndex < Guess.Len(); GuessIndex++)
{
if (Guess[GuessIndex] == HiddenWord[GuessIndex])
{
Count.Bulls++;
continue;
}
for (int32 HiddenIndex = 0; HiddenIndex < HiddenWord.Len(); HiddenIndex++)
{
if (Guess[GuessIndex] == HiddenWord[HiddenIndex])
{
Count.Cows++;
break;
}
}
}
return Count;
}
bool UBullCowCartridge::IsIsogram(const FString& Word) const
{
// So used to brute forcing these, I'm rially happy making it more efficient
// I'm checking up to the second to last because that was mentioned.
for (int32 Index = 0; Index < Word.Len() - 1; ++Index)
{
for (int32 Comparison = Index + 1; Comparison < Word.Len(); ++Comparison)
{
if (Word[Index] == Word[Comparison])
{
return false;
}
}
}
return true;
}
void UBullCowCartridge::EndGame()
{
// If They've confirmed that they want to replay the game
// Then let them re-initialize the game
// This goes first so I have a way to exit early
if (bGameOver) {
// This is here because I feel like the EndGame function,
// Should handle the events that occur EndGame()
SetUpGame();
WelcomePlayer();
return; // Using a return here to exit early
}
// Set GameOver here so that next time function is called, we set the game up again
bGameOver = true;
// Extra shame to those that have failed
// Also show user the hidden word for some closure
if (Lives == 0) {
PrintLine(TEXT("THEN PERISH"));
PrintLine(TEXT("The hidden word was %s\n"), *HiddenWord);
}
else
{
PrintLine(TEXT("Oh yay. You won."));
PrintLine(TEXT("The hidden word was %s"), *HiddenWord);
}
// Play Nice now
PrintLine(TEXT("Press enter to play again..."));
}
TArray<FString> UBullCowCartridge::GetValidWords(const TArray<FString>& WordList) const
{
TArray<FString> ValidWords;
for (FString Word : WordList)
{
if ((Word.Len() >= 4) && (Word.Len() <= 8) && IsIsogram(Word))
{
ValidWords.Emplace(Word);
}
}
return ValidWords;
}