7
\$\begingroup\$

Looking over Rock, Paper, Scissors. C++ from a beginning programmer prompted me to think about how I'd program this simple game in C++. The RockPaperScissors class contains the essential features of printing and evaluating each round of the game, as well as generation of randomly selected moves. I didn't encode a player input because that didn't seem too interesting; instead the sample program just plays 20 rounds against itself and prints the result of each. No statistics are gathered or stored.

One feature that does exist is that there is a #define to select whether the classic game or the enhanced "Rock-Paper-Scissors-Lizard-Spock" version is used. (With a bit more work, which I chose not to invest, one could allow for a fairly generic \$n\$-ary relation as the basis of the game.) The other feature worth noting is that the operator< is deliberately private because the \$<\$ operation implemented here does not represent a partially ordered set because the operation is not transitive. In simple terms, we would normally expect that \$(a < b) \land (b < c) \implies (a < c)\$ but this is expressly not true here.

I'd be interested in comments on those or any other design decisions or implementation details.

#include <iostream>
#include <string_view>
#include <array>
#include <stdexcept>
#include <random>

// define ENHANCED to non-zero for enhanced version with Spock, Lizard
#ifndef ENHANCED
#define ENHANCED 0
#endif

class RockPaperScissors 
{
public:
    RockPaperScissors(std::size_t num) {
        if (num >= words.size()) {
            throw std::range_error("invalid choice");
        }
        choice = num;
    }
    static RockPaperScissors random();
    const std::string_view& vs(const RockPaperScissors &b) const;
    friend std::ostream& operator<<(std::ostream& out, const RockPaperScissors &rps);
private:
    bool operator==(const RockPaperScissors &b) const;
    bool operator<(const RockPaperScissors &b) const;
#if ENHANCED
    static constexpr std::array<std::string_view, 5> words{ "Rock", "Paper", "Scissors", "Spock", "Lizard", };
#else
    static constexpr std::array<std::string_view, 3> words{ "Rock", "Paper", "Scissors" };
#endif
    static constexpr std::array<std::string_view, 3> results{"LOSE", "TIE", "WIN" };
    std::size_t choice;
};

std::ostream& operator<<(std::ostream& out, const RockPaperScissors &rps) {
    return out << rps.words[rps.choice];
}

RockPaperScissors RockPaperScissors::random() {
    static std::random_device rd;
    static std::mt19937 gen{rd()};
    static std::uniform_int_distribution<> dis(0, RockPaperScissors::words.size()-1);
    std::size_t which{static_cast<std::size_t>(dis(gen))};
    return RockPaperScissors{which};
}

bool RockPaperScissors::operator==(const RockPaperScissors &b) const {
    return choice == b.choice;
}

bool RockPaperScissors::operator<(const RockPaperScissors &b) const {
    return (choice + 1) % words.size() == b.choice || (choice + 3) % words.size() == b.choice;
}

const std::string_view& RockPaperScissors::vs(const RockPaperScissors &b) const {
    if (*this == b) {
        return results[1];
    } else if (*this < b) {
        return results[0];
    }
    return results[2];
}

int main()
{
    for (int trials = 20; trials; --trials) {
        auto a = RockPaperScissors::random();
        auto b = RockPaperScissors::random();
        std::cout << a << " vs. " << b << ": " << a << " " << a.vs(b) << "S!\n";
    }
}
\$\endgroup\$
4
  • \$\begingroup\$ the-big-bang-theory.com/rock-paper-scissors-lizard-spock \$\endgroup\$ Commented Feb 19, 2019 at 23:10
  • \$\begingroup\$ int trials... unsigned int trials maybe? \$\endgroup\$
    – Dair
    Commented Feb 20, 2019 at 0:14
  • \$\begingroup\$ Were you trying to golf your for loop a bit or why the somewhat unusal design? \$\endgroup\$
    – yuri
    Commented Mar 12, 2019 at 17:30
  • 1
    \$\begingroup\$ I see nothing unusual about the for loop. \$\endgroup\$
    – Edward
    Commented Mar 12, 2019 at 17:34

1 Answer 1

3
\$\begingroup\$

After preparing this recent answer it occurred to me that I'd already written something like it. Here's my review of this code.

Make the class more useful

Right now the only thing we can do with the RockPaperScissors class is to get a new one via random or print the results of a contest. If we wanted to do something else, such as collect some statistics, there are pieces missing, such as the ability to get a printable version of the class. Here's one way to add that:

friend std::ostream& operator<<(std::ostream& out, const RockPaperScissors &rps);

Now we can gather some statistics like this:

int main()
{
    std::unordered_map<std::string_view, unsigned> responses;
    std::unordered_map<std::string_view, unsigned> results;
    constexpr unsigned max_trials{300'000'000};
    for (unsigned trials = max_trials; trials; --trials) {
        auto a = RockPaperScissors::random();
        auto b = RockPaperScissors::random();
        ++responses[static_cast<std::string_view>(a)];
        ++responses[static_cast<std::string_view>(b)];
        ++results[a.vs(b)];
    }
    for (const auto& resp: responses) {
        std::cout << std::setw(16) << resp.first << '\t' << resp.second 
            << '\t' << static_cast<double>(resp.second)/max_trials/2
            << '\n';
    }
    for (const auto& resp: results) {
        std::cout << std::setw(16) << resp.first << '\t' << resp.second 
            << '\t' << static_cast<double>(resp.second)/max_trials
            << '\n';
    }
}

This gives a report something like this:

       Paper    200002179   0.333337
    Scissors    199987250   0.333312
        Rock    200010571   0.333351
         WIN    66662424    0.222208
         TIE    100003629   0.333345
        LOSE    133333947   0.444446

Prefer open functions to class functions

The current code has this member function:

RockPaperScissors RockPaperScissors::random() {
    static std::random_device rd;
    static std::mt19937 gen{rd()};
    static std::uniform_int_distribution<> dis(0, RockPaperScissors::words.size()-1);
    std::size_t which{static_cast<std::size_t>(dis(gen))};
    return RockPaperScissors{which};
}

However, selecting a single item at random from within a collection is a very common need. A more generally useful way to write this might be this:

template <typename Iter>
Iter random_one(Iter begin, const Iter& end) {
    static std::random_device rd;
    static std::mt19937 gen{rd()};
    static std::uniform_int_distribution<> dis(0, std::distance(begin, end) - 1);
    std::advance(begin, dis(gen));
    return begin;
}

Now we can get an iterator that points to a particular item in any collection. (To make this template more durable, we'd actually probably want to do some validation of both the Iter type and the values passed, but this conveys the idea.) We could use that with no further modifications as in this:

RockPaperScissors RockPaperScissors::random() {
    auto which = std::distance(words.begin(), random_one(words.begin(), words.end()));
    return RockPaperScissors{which};
}

But that's a bit odd because we are converting an iterator to an ordinal. That leads to the next suggestion.

Store an iterator within the class

We can simplify some of the code for this implementation by changing the definition of choice within the class to be an iterator.

using iter = decltype(words.begin());
iter choice;  

Now the constructor can use the templated function above very simply:

RockPaperScissors::RockPaperScissors() : choice{random_one(words.begin(), words.end())}
{
}

A few other changes are needed, but mostly they simplify the code.

Also note that now the random function is no longer needed.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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