11
\$\begingroup\$

I've been working in C++ for awhile now, but always like for others to look over my code as much as I can get them to. My main concerns are if I've made something overly complicated or if I've broken any general conventions in my code. Anyways, thank you for reviewing and I hope you enjoy playing the game if you choose to.

Code:

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <fstream>
#include <ctime>
#include <sstream>
#include <cctype>
#include <iterator>
#include <cstring>

void prompt(const int& rMaxWrongGuesses, int& rWrongGuesses, std::string& rGuessedLetters, std::string& rSoFar);
char getGuess(std::string& rGuessedLetters, const std::string& rTheWord, std::string& rSoFar, int& rWrongGuesses);
void checkGuess(char guess, const std::string& rTheWord, std::string& rSoFar, int& rWrongGuesses);
void shutDown(int& rWrongGuesses, const int& rMaxWrongGuesses, const std::string& rTheWord);
void drawGallows(int& rWrongGuesses);

int main()
{
    //setup
    const int kMaxWrongGuesses = 8; //maximum number of incorrect guesses allowed


    // Gets list of words from file and assigns them to vector by line
    std::vector<std::string> words; // collection of possible words to guess
    std::ifstream myfile("words.txt");
    std::copy(std::istream_iterator<std::string>(myfile),
        std::istream_iterator<std::string>(),
        std::back_inserter(words));

    // Shuffles  Words
    srand(static_cast<unsigned int>(time(0)));
    random_shuffle(words.begin(), words.end());

    // Split Words and Definitions
    std::stringstream ss(words[0]);
    std::string item;
    char delim = ',';
    while (std::getline(ss, item, delim)) {
        words.push_back(item);
    }

    // Find all characters after a comma in kTheWord and erase them.
    std::string kTheWord = words[0];
    kTheWord = kTheWord.substr(0, kTheWord.find(",", 0));

    // Initialize Hint and strip underscores used in file
    std::string hint = words.back();
    std::replace(hint.begin(), hint.end(), '_', ' ');

    // Set Variables used in game
    int wrongGuesses = 0;
    std::string soFar(kTheWord.size(), '-');
    std::string GuessedLetters = "";
    char guess;

    // References for performance and ease of access
    const int& rMaxWrongGuesses = kMaxWrongGuesses;
    int& rWrongGuesses = wrongGuesses;
    const std::string& rTheWord = kTheWord;
    std::string& rGuessedLetters = GuessedLetters;
    std::string& rSoFar = soFar;

    std::cout << "Welcome to Hangman. Good luck!\n";
    std::cout << "Here is your hint: " << hint << " \n";

    while ((wrongGuesses < kMaxWrongGuesses) && (soFar != kTheWord))
    {
        prompt(rMaxWrongGuesses, rWrongGuesses, rGuessedLetters, rSoFar);
        char guess = getGuess(rGuessedLetters, rTheWord, rSoFar, rWrongGuesses);
        checkGuess(guess, rTheWord, rSoFar, rWrongGuesses);
        drawGallows(rWrongGuesses); 
    }
    shutDown(rWrongGuesses, rMaxWrongGuesses, rTheWord);
}

void prompt(const int& rMaxWrongGuesses, int& rWrongGuesses, std::string& rGuessedLetters, std::string& rSoFar)
{
    std::cout << "\n\nYou have " << (rMaxWrongGuesses - rWrongGuesses);
    std::cout << " incorrect guesses left.\n";
    std::cout << "\nYou've used the following letters:\n" << rGuessedLetters << std::endl;
    std::cout << "\nSo far, the word is:\n" << rSoFar << std::endl;
}

char getGuess(std::string& rGuessedLetters, const std::string& rTheWord, std::string& rSoFar, int& rWrongGuesses)
{
    char guess;
    std::cout << "\n\nEnter your guess: ";
    std::cin >> guess;
    guess = toupper(guess); // make uppercase since secret word is in uppercase
    while (rGuessedLetters.find(guess) != std::string::npos)
    {
        std::cout << "\nYou've already guessed " << guess << std::endl;
        std::cout << "Enter your guess: ";
        std::cin >> guess;
        guess = toupper(guess);
    }

    rGuessedLetters += guess;
    return guess;
}

void checkGuess(char guess, const std::string& rTheWord, std::string& rSoFar, int& rWrongGuesses)
{
    if (rTheWord.find(guess) != std::string::npos)
    {
        std::cout << "That's right! " << guess << " is in the word.\n";

        //update soFar to include newly guessed letter
        for (int i = 0; i < rTheWord.length(); ++i)
        {
            if (rTheWord[i] == guess)
            {
                rSoFar[i] = guess;
            }
        }
    }
    else
    {
        std::cout << "Sorry, " << guess << " isn't in the word.\n";
        ++rWrongGuesses;
    }
}

void shutDown(int& rWrongGuesses, const int& rMaxWrongGuesses, const std::string& rTheWord)
{
    if (rWrongGuesses == rMaxWrongGuesses)
    {
        std::cout << "\nYou've been hanged!";
    }
    else
    {
        std::cout << "\nYou guessed it!";
    }

    std::cout << "\nThe word was " << rTheWord << std::endl;
    system("pause");
}

void drawGallows(int& rWrongGuesses) 
{ 
 if(rWrongGuesses==8) 
 { 

  std::cout<<std::endl<<std::endl 
   <<"   +----+     "<<std::endl 
   <<"   |    |     "<<std::endl 
   <<"   |    O     "<<std::endl 
   <<"   |   /|\\   "<<std::endl 
   <<"   |   / \\   "<<std::endl 
   <<"   |          "<<std::endl 
   <<"  ============"<<std::endl<<std::endl; 
 } 
 else if(rWrongGuesses==7) 
 { 
  std::cout<<std::endl<<std::endl 
   <<"   +----+  "<<std::endl 
   <<"   |    |  "<<std::endl 
   <<"   |    O  "<<std::endl 
   <<"   |   /|\\ "<<std::endl 
   <<"   |     \\ "<<std::endl 
   <<"   |       "<<std::endl 
   <<"  ============"<<std::endl<<std::endl; 
 } 
 else if(rWrongGuesses==6) 
 { 
  std::cout<<std::endl<<std::endl 
   <<"   +----+  "<<std::endl 
   <<"   |    |  "<<std::endl 
   <<"   |    O  "<<std::endl 
   <<"   |   /|\\ "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"  ============="<<std::endl<<std::endl; 
 } 
 else if(rWrongGuesses==5) 
 { 
  std::cout<<std::endl<<std::endl 
   <<"   +----+  "<<std::endl 
   <<"   |    |  "<<std::endl 
   <<"   |    O  "<<std::endl 
   <<"   |   /|  "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"  ============="<<std::endl<<std::endl; 
 } 
 else if(rWrongGuesses==4) 
 { 
  std::cout<<std::endl<<std::endl 
   <<"   +----+  "<<std::endl 
   <<"   |    |  "<<std::endl 
   <<"   |    O  "<<std::endl 
   <<"   |    |  "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"  ============="<<std::endl<<std::endl; 
 } 
 else if(rWrongGuesses==3) 
 { 
 std::cout<<std::endl<<std::endl 
   <<"   +----+  "<<std::endl 
   <<"   |    |  "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"  ============="<<std::endl<<std::endl; 
 }
 else if(rWrongGuesses==2) 
 { 
  std::cout<<std::endl<<std::endl 
   <<"   +----+  "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"  ============="<<std::endl<<std::endl; 
 }
 else if(rWrongGuesses==1) 
 { 
  std::cout<<std::endl<<std::endl 
   <<"   +       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"   |       "<<std::endl 
   <<"  ============="<<std::endl<<std::endl; 
 }
 else {};
 }

Dropbox link of game and required files

\$\endgroup\$
  • 2
    \$\begingroup\$ You are not supposed to edit the question once you have had a review. Otherwise it will confuse future readers that are trying to learn. If you want to update the code and ask for a re-review then post another question. PS. There is still a lot to learn here. \$\endgroup\$ – Martin York Aug 3 '16 at 14:00
15
\$\begingroup\$

Prefer '\n' to std::endl

The only difference is that std::endl will flush the stream after putting the '\n' character on it. Flushing the stream manually is almost always a waste of time as the stream will auto flush when required and any attempt by a human is ultimately at the wrong time.

Data Driven programming.

Rather than have a big set of if else blocks. Put the data you want to draw into an array and just print the appropraitare array element.

 if(rWrongGuesses==8) 
 { 
  std::cout<< picture1; 
 } 
 else if(rWrongGuesses==7) 
 { 
  std::cout<<picture2; 
 } 
 else if(rWrongGuesses==6) 
 {
  ....etc

This can be replace by an array access.

 std::cout << picture[rWrongGuesses];

At the top of you program just add the different pictures.

std::string const picture0 = "\n\n" 
                             "   +       \n"
                             "   |       \n" 
                             "   |       \n" 
                             "   |       \n" 
                             "   |       \n" 
                             "   |       \n" 
                             "  =============\n\n"; 
 .... etc
 std::vector<std::string>  picture{ picture0, picture1, picture2 ....};

Loading a vector

std::vector<std::string> words;
std::ifstream myfile("words.txt");
std::copy(std::istream_iterator<std::string>(myfile),
    std::istream_iterator<std::string>(),
    std::back_inserter(words));

Nice try. But you don't need to do all that work. The standard vector can be constructed using two iterators. So we can simplify the above too:

std::ifstream myfile("words.txt");
std::vector<std::string> words(std::istream_iterator<std::string>(myfile),
                               std::istream_iterator<std::string>());

Random Shuffle

// Shuffles  Words
srand(static_cast<unsigned int>(time(0)));
random_shuffle(words.begin(), words.end());

Good try again. But this version (as you seem to know by the use of srand()) uses the old random number generated. This is not a great random number generator. Its absolutely fine for programs like this. But it is worth learning to use the new random number generator.

Also random_shuffle will be removed in C++17 (it was deprecated in C++14). So prefer to use shuffle().

std::random_device rd;
std::mt19937 g(rd());
std::shuffle(words.begin(), words.end(), g);

Data Structures

You put a lot of work into breaking the word and definition up.

// Find all characters after a comma in kTheWord and erase them.
std::string kTheWord = words[0];
kTheWord = kTheWord.substr(0, kTheWord.find(",", 0));

// Initialize Hint and strip underscores used in file
std::string hint = words.back();
std::replace(hint.begin(), hint.end(), '_', ' ');

Note: Also reusing the word array to hold the definitions is bad style. Use a new varaible rather than re-use the same structure for multiple things.

This is all part of the main section of the code. I would separate this into its own class so that you read the words and definitions all in one go.

Assuming we still use the same definition file (but don't replace the space with underscores).

 class Word
 {
     std::string          word;
     std::vector<string>  definitions;

     friend std::istream& operator>>(std::istream& s, Word& data)
     {
         std::string line;
         if (std::getline(s, line))
         {
             std::stringstream linestream(line);
             if (linestream >> data.word)
             {
                 data.definitions = std::vector<string>(std::istream_iterator<std::string>(linestream),
                                                        std::istream_iterator<std::string>());
             }
         }
         return s;
     };


    // Now in main we can simply load the words like this:
    std::ifstream myfile("words.txt");
    std::vector<Words>   words(std::istream_iterator<Words>(myfile),
                               std::istream_iterator<Words>());
\$\endgroup\$
  • \$\begingroup\$ For the Data Driven Programming section, why would this be the best practice? I feel like that just adds a lot of information to the top of the program and would require another parameter for drawGallows(). I may be looking at this the wrong way, so I would like some clarification if possible. \$\endgroup\$ – McLemore Aug 4 '16 at 2:35
  • \$\begingroup\$ Well at the moment we have moved the data to constant variables. But they don't actually need to be part of the code. We could put the pictures into a config file that is read it at run time. Then you can modify the pictures without needing to update the programme. etc.. \$\endgroup\$ – Martin York Aug 4 '16 at 16:08
  • \$\begingroup\$ In general "Data Driven Programming" makes your code more robust (As there is less repeated code). It makes your code easier to modify (you can update the data without changing the program (so no re-compile)). But DDP is a huge subject and I am just brushing the surface. \$\endgroup\$ – Martin York Aug 4 '16 at 16:12
  • \$\begingroup\$ As for another parameter. That's because your code is more procedural based. In C++ it is more usual to see Object based program. But I was going to cover that in a subsequent code review when you had improved the current code. \$\endgroup\$ – Martin York Aug 4 '16 at 16:14
  • \$\begingroup\$ Ok, I see. The code in Loading a vector is broken. It should be std::vector<std::string> words((std::istream_iterator<std::string>(myfile)), std::istream_iterator<std::string>()); This prevents the error found here \$\endgroup\$ – McLemore Aug 5 '16 at 3:17
0
\$\begingroup\$

Please don't upvote this answer.

I may have written it like this for an OO program.

I have basically just wrapped your code with objects (very little other work has been done). You will notice that the game object is self contained and thus you don't actually need to pass parameters around.

This also makes the game easy to reset. Just let the game object go out of scope then create a new one for the next game. The game object set's itself up and thus all the values are reset correctly.

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <fstream>
#include <ctime>
#include <sstream>
#include <cctype>
#include <iterator>
#include <cstring>
#include <random>

using   Word    = std::pair<std::string, std::string>;

class Picture

class Picture
{
    std::string image;
    public:
        friend std::istream& operator>>(std::istream& s, Picture& data)
        {
            std::string line;
            while(std::getline(s, line))
            {
                data.image  += line;
                data.image  += "\n";
                if (line == "")
                {
                    break;
                }
            }
            return s;
        }
        friend std::ostream& operator<<(std::ostream& s, Picture const& data)
        {
            s << data.image;
            return s;
        }
};

class Gallows

class Gallows
{
    std::vector<Picture>                images;
    public:
        Gallows()
        {
            std::ifstream   picStream("hm.data");
            images.insert(std::begin(images),
                          std::istream_iterator<Picture>(picStream),
                          std::istream_iterator<Picture>());
        }

        void drawGallows(int rWrongGuesses)
        {
            if (rWrongGuesses < images.size())
            {
                std::cout << images[rWrongGuesses];
            }
        }
};

class WordMap

class WordMap
{
    using   Words   = std::vector<Word>;

    Words               words;
    std::random_device  randDevice;
    std::mt19937        generator;

    public:
        WordMap()
            : generator(randDevice())
        {
            std::ifstream   wordStream("hm.words");
            std::string     line;

            while(std::getline(wordStream, line))
            {
                std::size_t     comma   = line.find(',');
                std::string     word    = line.substr(0, comma);
                std::string     def     = line.substr(comma);

                words.emplace_back(word, def);
            }
        }
        Word const& getRandomWord()
        {
            std::uniform_int_distribution<int>  distr(0, words.size());
            return words[distr(generator)];
        }
};

class Game

class Game
{
    const int kMaxWrongGuesses = 8; //maximum number of incorrect guesses allowed

    Word const&     word;
    int             wrongGuesses;
    std::string     soFar;
    std::string     GuessedLetters;

    public:
        Game(Word const& word)
            : word(word)
            , wrongGuesses(0)
            , soFar(word.first.size(), '-')
            , GuessedLetters("")
        {
            std::cout << "Welcome to Hangman. Good luck!\n";
            std::cout << "Here is your hint: " << word.second << " \n";
        }
        ~Game()
        {
            if (wrongGuesses == kMaxWrongGuesses)
            {
                std::cout << "\nYou've been hanged!";
            }
            else
            {
                std::cout << "\nYou guessed it!";
            }

            std::cout << "\nThe word was " << word.first << "\n";
        }

        bool makeGuess()
        {
            prompt();
            char guess = getGuess();
            checkGuess(guess);

            return ((wrongGuesses < kMaxWrongGuesses) && (soFar != word.first));
        }

        int getWrongGuesses() const
        {
            return wrongGuesses;
        }

    private:
        void prompt()
        {
            std::cout   << "\n\nYou have " << (kMaxWrongGuesses - wrongGuesses)
                        << " incorrect guesses left.\n"
                        << "\nYou've used the following letters:\n" << GuessedLetters << "\n"
                        << "\nSo far, the word is:\n" << soFar << "\n";
        }

        char getGuess()
        {
            char guess;
            std::cout << "\n\nEnter your guess: ";
            std::cin >> guess;
            guess = toupper(guess); // make uppercase since secret word is in uppercase
            while (GuessedLetters.find(guess) != std::string::npos)
            {
                std::cout << "\nYou've already guessed " << guess << "\n"
                          << "Enter your guess: ";
                std::cin >> guess;
                guess = toupper(guess);
            }

            GuessedLetters += guess;
            return guess;
        }

        void checkGuess(char guess)
        {
            if (word.first.find(guess) != std::string::npos)
            {
                std::cout << "That's right! " << guess << " is in the word.\n";

                //update soFar to include newly guessed letter
                for (int i = 0; i < word.first.length(); ++i)
                {
                    if (word.first[i] == guess)
                    {
                        soFar[i] = guess;
                    }
                }
            }
            else
            {
                std::cout << "Sorry, " << guess << " isn't in the word.\n";
                ++wrongGuesses;
            }
        }
};

int main()

int main()
{
    Gallows     gallows;
    WordMap     wordMap;

    //setup
    Word const& wordToGuess = wordMap.getRandomWord();
    Game        game(wordToGuess);

    while(game.makeGuess())
    {
        gallows.drawGallows(game.getWrongGuesses());
    }
}

Then in the file hm.data

   +
   |
   |
   |
   |
   |
  =============

   +----+
   |
   |
   |
   |
   |
  =============

   +----+
   |    |
   |
   |
   |
   |
  =============

   +----+
   |    |
   |    O
   |    |
   |
   |
  =============

   +----+
   |    |
   |    O
   |   /|
   |
   |
  =============

   +----+
   |    |
   |    O
   |   /|\
   |
   |
  =============

   +----+
   |    |
   |    O
   |   /|\
   |     \
   |
  ============

   |    |
   |    O
   |   /|\
   |   / \
   |
  ============

Word File:

ABATE, REDUCE OR LESSEN
ABDICATE, GIVE UP A POSITION
ABERRATION, SOMETHING UNUSUAL / DIFFERENT FROM THE NORM
ABHOR, TO REALLY HATE
ABSTAIN, TO REFRAIN FROM DOING SOMETHING
ADVERSITY, HARDSHIP / MISFORTUNE
AESTHETIC, PERTAINING TO BEAUTY
AMICABLE, AGREEABLE
ANACHRONISTIC, OUT OF THE CONTEXT OF TIME / OUT OF DATE
ARID, VERY DRY
ASYLUM, SANCTUARY / PLACE OF SAFETY
BENEVOLENT, FRIENDLY / HELPFUL
BIAS, A PREJUDICE TOWARDS SOMETHING OR AGAINST SOMETHING
BOISTEROUS, ENTHUSIASTIC / LOUD
BRAZEN, BOLD
BRUSQUE, SHORT / RUDE
CAMARADERIE, TOGETHERNESS / TRUST / GROUP DYNAMIC OF TRUST
CANNY, CAREFUL
CAPACIOUS, VERY LARGE / SPACIOUS
CAPITULATE, SURRENDER
CLAIRVOYANT, CAN PREDICT THE FUTURE
COLLABORATE, WORK TOGETHER
COMPASSION, SYMPATHY
COMPROMISE, MEETING IN THE MIDDLE / SETTLING DIFFERENCES
CONDESCENDING, PATRONIZING
CONDITIONAL, CONTINGENT UPON SOMETHING ELSE / CONTINGENT UPON
CONFORMIST, SOMEONE WHO FOLLOWS THE MAJORITY
CONVERGENCE, COMING TOGETHER
DELETERIOUS, HARMFUL
DEMAGOGUE, RABBLE-ROUSING LEADER
DIGRESSION, STRAYING FROM MAIN POINT
DILIGENT, HARD-WORKING / DEDICATED
DISCREDIT, DISHONOR SOMEONE / PROVE SOMETHING UNTRUE
DISDAIN, TO REGARD WITH SCORN
DIVERGENT, MOVING APART / GOING IN DIFFERENT DIRECTIONS
EMPATHY, FEELING SOMEONE ELSE’S FEELING
EMULATE, FOLLOWING SOMEONE ELSE'S EXAMPLE
ENERVATING, TIRING
EPHEMERAL, FLEETING / TEMPORARY
EVANESCENT, NOT LASTING LONG
EXEMPLARY, OUTSTANDING
EXTENUATING, SOMETHING THAT MAKES THE SITUATION NOT AS BAD
FLORID, ORNATE
FORBEARANCE, PATIENCE / RESTRAINT
FORTITUDE, STRENGTH
FORTUITOUS, LUCKY
FOSTER, PROMOTE / AID
FRAUGHT, FILLED WITH
FRUGAL, THRIFTY
HACKNEYED, CLICHÉD
HAUGHTY, BEING ARROGANT / TALKING DOWN TO PEOPLE
HEDONIST, PERSON WHO ACTS IN PURSUIT OF PLEASURE
HYPOTHESIS, UNPROVEN THEORY / EDUCATED GUESS
IMPETUOUS, RASH / IMPULSIVE
IMPUTE, TO ASSIGN OR ATTRIBUTE TO SOMEONE
INCONSEQUENTIAL, WITHOUT CONSEQUENCE / TRIVIAL / DOESN'T MATTER
INEVITABLE, UNAVOIDABLE / DEFINITELY GOING TO HAPPEN
INTREPID, FEARLESS
INTUITIVE, KNOWING SOMETHING BY INSTINCT
JUBILATION, EXTREME HAPPINESS / JOY
LOBBYIST, PERSON WHO TAKES ONE SIDE OR THE OTHER AND PERSUADES GOVERNMENT OFFICIALS
LONGEVITY, LONG (PARTICULARLY LONG LIFE)
MUNDANE, BORING / ORDINARY
NONCHALANT, CASUAL / CALM / AT EASE
OPULENT, WEALTHY
ORATOR, SPEAKER
OSTENTATIOUS, FLAUNTING WEALTH
PARCHED, FREED FROM WATER / DRIED UP
PERFIDIOUS, DISLOYAL
PRAGMATIC, PRACTICAL
PRECOCIOUS, GIFTED/TALENTED BEYOND ONE'S YEARS
PRETENTIOUS, BEING SELF IMPORTANT / THINKING YOU ARE BETTER THAN OTHERS
PROCRASTINATE, TO DELAY / OFTEN UNNECESSARILY
PROSAIC, ORDINARY
PROSPERITY, WEALTH
PROVOCATIVE, CAUSES A FUSS / INFLAMMATORY / LIKELY TO GET PEOPLE RILED UP
PRUDENT, CAREFUL / WISE
QUERULOUS, IRRITABLE / PRONE TO ARGUMENT
RANCOROUS, HATEFUL / UNPLEASANT
RECLUSIVE, HERMIT / WITHDRAWN
RECONCILIATION, COMING BACK TOGETHER AFTER A DISAGREEMENT
RENOVATION, BEING NEW / BEING REDONE
RESTRAINED, CONTROLLED / NOT FREE
REVERENCE, DEEP RESPECT
SAGACITY, WISDOM
SCRUTINIZE, TO LOOK AT CAREFULLY
SPONTANEOUS, BEING IMPULSIVE / ACTING WITHOUT THINKING
SPURIOUS, FALSE / UNTRUE
SUBMISSIVE, MILD / MEEK
SUBSTANTIATE, TO CONFIRM / PROVE
SUBTLE, SHY / SMALL / NOT SHOWY
SUPERFICIAL, SHALLOW
SUPERFLUOUS, UNNECESSARY / TOO MUCH
SURREPTITIOUS, SECRET
TACTFUL, POLITE
TENACIOUS, PERSISTENT / STUBBORN
TRANSIENT, TEMPORARY / IMPERMANENT
VENERABLE, RESPECTABLE BECAUSE OF ITS AGE
VINDICATE, TO FREE FROM BLAME
WARY, CAREFUL / WATCHFUL
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.