I think it’s pretty much done now. I’ve got a map of maps for picking random words of given lengths. I probably could have done with a better structure instead of a map within the map, though. Simple ASCII art title generated from here http://patorjk.com/software/taag/#p=display&h=0&f=Graceful&t=Bulls%20And%20Cows
Main.cpp
/*
This is the console executable that makes use of the BullCow class
This acts as the view in an MVC pattern and is responsible for all user
interaction. For game logic see the FBullCowGame class.
*/
#include <iostream>
#include <string>
#include "FBullCowGame.h"
using FText = std::string;
using int32 = int;
void PrintIntro();
void PlayGame();
FText GetValidGuess();
bool AskToPlayAgain();
void PrintWinMessage();
void PrintLoseMessage();
FBullCowGame BCGame = FBullCowGame(6);
int main()
{
do{
PrintIntro();
PlayGame();
} while (AskToPlayAgain());
return 0;
}
// introduce the game
void PrintIntro()
{
std::cout << "\n ____ _ _ __ __ ____ __ __ _ ____ ___ __ _ _ ____ \n( _ \\/ )( \\( ) ( ) / ___) / _\\ ( ( \\( \\ / __) / \\ / )( \\/ ___)\n ) _ () \\/ (/ (_/\\/ (_/\\\\___ \\ / \\/ / ) D ( ( (__ ( O )\\ /\\ /\\___ \\\n(____/\\____/\\____/\\____/(____/ \\_/\\_/\\_)__)(____/ \\___) \\__/ (_/\\_)(____/\n";
std::cout << "\n\nWelcome to Bulls and Cows, a fun word game.\n";
std::cout << "Can you guess the isogram I'm thinking of?\n\n";
return;
}
inline bool isInteger(const FString & s)
{
if (s.empty() || ((!isdigit(s[0])) && (s[0] != '-') && (s[0] != '+'))) return false;
char * p;
strtol(s.c_str(), &p, 10);
return (*p == 0);
}
int GetDesiredWordLength()
{
int minLength = 3;
int maxLength = 6;
int WordLength = 5;
const int32 maxInvalidTries = 10;
std::cout << "How long would you like the mystery word to be? Enter a number between " << minLength << " and " << maxLength << ":\n";
int Attempts = 0;
while (Attempts < maxInvalidTries) {
FText input;
std::getline(std::cin, input);
if (!isInteger(input)) {
std::cout << "\n could not convert input into a whole number, please try again\n";
}else{
char *p;
long i = strtol(input.c_str(), &p, 10);
if (i < minLength || i > maxLength) {
std::cout << "\n number was not between " << minLength << " and " << maxLength << ", please try again\n";
}else {
WordLength = i;
break;
}
}
Attempts++;
}
return WordLength;
}
FText GetValidGuess()
{
int32 CurrentTry = BCGame.GetCurrentTry();
const int MaxInvalidGuessAttempts = 10;
int Attempts = 0;
EGuessStatus GuessStatus = EGuessStatus::Invalid_Status;
while (Attempts < MaxInvalidGuessAttempts){
//Get a guess from the player
std::cout << "Try " << CurrentTry << " of " << BCGame.GetMaxTries() << ". Enter your guess: ";
FText Guess = "";
std::getline(std::cin, Guess);
GuessStatus = BCGame.CheckGuessValidity(Guess);
switch (GuessStatus)
{
case EGuessStatus::Wrong_Length:
std::cout << "Your guess was of incorrect length ( " << Guess.length() << " ) . Please enter a " << BCGame.GetHiddenWordLength() << " letter lowercase isogram.";
break;
case EGuessStatus::Not_Lower_Case:
std::cout << "Your guess was not lower case, the hidden word is lower case.";
case EGuessStatus::Not_Isogram:
std::cout << "Your guess was not an isogram, isograms do not contain repeat characters.";
break;
case EGuessStatus::OK:
return Guess;
break;
default:
return "";
break;
}
std::cout << "\n\n";
Attempts++;
}
std::cout << "Failed to provide a valid guess in " << MaxInvalidGuessAttempts << " attempts.";
return "";
}
void PlayGame()
{
int wordLength = GetDesiredWordLength();
BCGame.Reset(wordLength);
std::cout << "Try to guess the " << wordLength << " letter isogram\n";
int32 MaxTries = BCGame.GetMaxTries();
int32 CurrentTry = BCGame.GetCurrentTry();
//loop for the number of guesses
FText Guess = "";
while (BCGame.GetCurrentTry() <= MaxTries && !BCGame.IsGameWon()){
std::cout << "Please enter your guess followed by the return key >";
Guess = GetValidGuess();
FBullCowCount BullCowCount = BCGame.SubmitValidGuess(Guess);
std::cout << "Bulls = " << BullCowCount.Bulls;
std::cout << ". Cows = " << BullCowCount.Cows << std::endl;
std::cout << "Your guess was: \"" << Guess << "\"\n\n";
}
if (BCGame.IsGameWon()){
PrintWinMessage();
}
else{
PrintLoseMessage();
}
//TODO add a game summary
return;
}
bool AskToPlayAgain()
{
std::cout << "Do you want to play again? (y/n) ";
FText Response = "";
std::getline(std::cin, Response);
bool bWantsToPlayAgain = { Response[0] == 'y' || Response[0] == 'Y' };
std::cout << std::endl;
return bWantsToPlayAgain;
}
void PrintWinMessage()
{
std::cout << "Congratulations, you won Bulls & Cows!\n";
}
void PrintLoseMessage()
{
std::cout << "Sorry, you lost Bulls & Cows!\n";
}
FBullCowGame.h
#pragma once
#include <string>
using FString = std::string;
using int32 = int;
struct FBullCowCount
{
int32 Bulls = 0;
int32 Cows = 0;
};
enum class EGuessStatus
{
Invalid_Status,
OK,
Not_Isogram,
Wrong_Length,
Not_Lower_Case
};
class FBullCowGame
{
public:
FBullCowGame(int wordLength);
int32 GetMaxTries() const;
int32 GetCurrentTry() const;
int32 GetHiddenWordLength() const;
bool IsGameWon() const;
EGuessStatus CheckGuessValidity(FString Guess) const;
void Reset(int wordLength); //TODO make a more rich return value.
FBullCowCount SubmitValidGuess(FString Guess);
private:
int32 MyCurrentTry;
FString MyHiddenWord;
bool bHasWon;
bool IsIsogram(FString Word) const;
bool IsLowerCase(FString Word) const;
};
FBullCowGame.cpp
#include "FBullCowGame.h"
#include <iostream>
#include <map>
#include <string>
#define TMap std::map
FBullCowGame::FBullCowGame(int wordLength)
{
Reset(wordLength);
}
void FBullCowGame::Reset(int wordLength)
{
TMap<int, TMap<int, FString>> isogramListByWordLength = {
{
3,{
{ 0,"low" },
{ 1,"sin" },
{ 2,"tin" },
{ 3,"bin" },
{ 4,"gin" },
{ 5,"fin" },
{ 6,"bat" },
{ 7,"cat" },
{ 8,"mat" },
{ 9,"fat" }
}
},
{
4,{
{ 0,"crys" },
{ 1,"plan" },
{ 2,"clan" },
{ 3,"sang" },
{ 4,"sort" },
{ 5,"torn" },
{ 6,"tows" },
{ 7,"sewn" },
{ 8,"born" },
{ 9,"yawn" },
{ 10,"hues" }
}
},
{
5,{
{ 0,"plane" },
{ 1,"crane" },
{ 2,"train" },
{ 3,"slain" },
{ 4,"align" },
{ 5,"night" },
{ 6,"bites" },
{ 7,"dawns" },
{ 8,"yawns" },
{ 9,"warns" }
}
},
{
6,{
{ 0,"planet" },
{ 1,"planes" },
{ 2,"cranes" },
{ 3,"cringe" },
{ 4,"singed" },
{ 5,"trains" },
{ 6,"clawed" },
{ 7,"thawed" },
{ 8,"things" },
{ 9,"ignore" },
{ 10,"planks" },
{ 11,"thanks" }
}
}
};
FString HIDDEN_WORD = "planet";
TMap<int, FString> availableWords = isogramListByWordLength[wordLength];
MyHiddenWord = availableWords[rand()%availableWords.size()];
MyCurrentTry = 1;
bHasWon = false;
}
int32 FBullCowGame::GetMaxTries() const {
TMap<int32, int32> WordLengthToMaxTries{ {3,5},{4,7},{5,9},{6,11},{7,12} };
return WordLengthToMaxTries[GetHiddenWordLength()];
}
int32 FBullCowGame::GetCurrentTry() const { return MyCurrentTry; }
bool FBullCowGame::IsGameWon() const { return bHasWon; }
bool FBullCowGame::IsIsogram(FString Word) const
{
TMap<char, bool> LetterSeen;
if (Word.length() > 1) {
for (auto Letter : Word) //foreach
{
Letter = tolower(Letter);
if (LetterSeen.count(Letter)) {
return false;
}
else {
LetterSeen[Letter] = true;
}
}
}
return true;
}
bool FBullCowGame::IsLowerCase(FString Word) const
{
for (auto Letter : Word) {
if (Letter< 'a' || Letter > 'z') { return false; }
}
return true;
}
EGuessStatus FBullCowGame::CheckGuessValidity(FString Guess) const
{
if (!IsIsogram(Guess))
{
return EGuessStatus::Not_Isogram;
}
else if (!IsLowerCase(Guess)) // if the guess isn't all lowercase
{
return EGuessStatus::Not_Lower_Case;
}
else if (Guess.length() != GetHiddenWordLength()) // if the word length isn't correct
{
return EGuessStatus::Wrong_Length;
}
else {
return EGuessStatus::OK;
}
}
int32 FBullCowGame::GetHiddenWordLength() const { return MyHiddenWord.length(); }
// counts Bulls & Cows, then increases try #, assumes valid input
FBullCowCount FBullCowGame::SubmitValidGuess(FString Guess)
{
MyCurrentTry++;
// setup a return variable
FBullCowCount BullCowCount;
BullCowCount.Bulls = 0;
// loop through all letters in the guess
int32 WordLength = MyHiddenWord.length(); //valid guesses have the same length for guess and actual word
for (int32 GIndex = 0; GIndex < WordLength; GIndex++) {
//compare letters against the hidden word
for (int32 HWIndex = GIndex; HWIndex < WordLength; HWIndex++) {
int32 index = MyHiddenWord.find(Guess[GIndex]);
if (index == HWIndex) {
BullCowCount.Bulls++;
break; //skip checking the other letters in the hidden word, we've already found a match
}
else if (index != std::string::npos) {
BullCowCount.Cows++;
break; //skip checking the other letters in the hidden word, we've already found a match
}
else {
//miss
}
}
}
if (BullCowCount.Bulls == WordLength) {
bHasWon = true;
}
return BullCowCount;
}