41
\$\begingroup\$

"Scissors cuts paper, paper covers rock, rock crushes lizard, lizard poisons Spock, Spock smashes scissors, scissors decapitate lizard, lizard eats paper, paper disproves Spock, Spock vaporizes rock. And as it always has, rock crushes scissors."

-- Dr. Sheldon Cooper

So these are the rules.

Building blocks

The first thing I thought was "I need a way to compare the possible selections" - this sounded like IComparable<T>, so I started by implementing that interface in a SelectionBase class. Now because I knew I'd derive Rock, Paper, Scissors, Lizard and Spock classes from this, I decided to include a Name property that returns the type name, and since I also needed a way to display different verbs depending on the type of the opponent's selection, I also included a GetWinningVerb method:

public abstract class SelectionBase : IComparable<SelectionBase>
{
    public abstract int CompareTo(SelectionBase other);
    public string Name { get { return GetType().Name; } }
    public abstract string GetWinningVerb(SelectionBase other);
}

The base class is implemented by each of the possible selections:

public class Rock : SelectionBase
{
    public override string GetWinningVerb(SelectionBase other)
    {
        if (other is Scissors) return "crushes";
        if (other is Lizard) return "crushes";

        throw new InvalidOperationException("Are we playing the same game?");
    }

    public override int CompareTo(SelectionBase other)
    {
        if (other is Rock) return 0;
        if (other is Paper) return -1;
        if (other is Scissors) return 1;
        if (other is Lizard) return 1;
        if (other is Spock) return -1;

        throw new InvalidOperationException("Are we playing the same game?");
    }
}

public class Paper : SelectionBase
{
    public override string GetWinningVerb(SelectionBase other)
    {
        if (other is Rock) return "covers";
        if (other is Spock) return "disproves";

        throw new InvalidOperationException("Are we playing the same game?");
    }

    public override int CompareTo(SelectionBase other)
    {
        if (other is Rock) return 1;
        if (other is Paper) return 0;
        if (other is Scissors) return -1;
        if (other is Lizard) return -1;
        if (other is Spock) return 1;

        throw new InvalidOperationException("Are we playing the same game?");
    }
}

public class Scissors : SelectionBase
{
    public override string GetWinningVerb(SelectionBase other)
    {
        if (other is Paper) return "cuts";
        if (other is Lizard) return "decapitates";

        throw new InvalidOperationException("Are we playing the same game?");
    }

    public override int CompareTo(SelectionBase other)
    {
        if (other is Rock) return -1;
        if (other is Paper) return 1;
        if (other is Scissors) return 0;
        if (other is Lizard) return 1;
        if (other is Spock) return -1;

        throw new InvalidOperationException("Are we playing the same game?");
    }
}

public class Lizard : SelectionBase
{
    public override string GetWinningVerb(SelectionBase other)
    {
        if (other is Paper) return "eats";
        if (other is Spock) return "poisons";

        throw new InvalidOperationException("Are we playing the same game?");
    }

    public override int CompareTo(SelectionBase other)
    {
        if (other is Rock) return -1;
        if (other is Paper) return 1;
        if (other is Scissors) return -1;
        if (other is Lizard) return 0;
        if (other is Spock) return 1;

        throw new InvalidOperationException("Are we playing the same game?");
    }
}

public class Spock : SelectionBase
{
    public override string GetWinningVerb(SelectionBase other)
    {
        if (other is Rock) return "vaporizes";
        if (other is Scissors) return "smashes";

        throw new InvalidOperationException("Are we playing the same game?");
    }

    public override int CompareTo(SelectionBase other)
    {
        if (other is Rock) return 1;
        if (other is Paper) return -1;
        if (other is Scissors) return 1;
        if (other is Lizard) return -1;
        if (other is Spock) return 0;

        throw new InvalidOperationException("Are we playing the same game?");
    }
}

User Input

Then I needed a way to get user input. I knew I was going to make a simple console app, but just so I could run unit tests I decided to create an IUserInputProvider interface - the first pass had all 3 methods in the interface, but since I wasn't using them all, I only kept one; I don't think getting rid of GetUserInput(string) would hurt:

public interface IUserInputProvider
{
    string GetValidUserInput(string prompt, IEnumerable<string> validValues);
}

public class ConsoleUserInputProvider : IUserInputProvider
{
    private string GetUserInput(string prompt)
    {
        Console.WriteLine(prompt);
        return Console.ReadLine();
    }

    private string GetUserInput(string prompt, IEnumerable<string> validValues)
    {
        var input = GetUserInput(prompt);
        var isValid = validValues.Select(v => v.ToLower()).Contains(input.ToLower());

        return isValid ? input : string.Empty;
    }

    public string GetValidUserInput(string prompt, IEnumerable<string> validValues)
    {
        var input = string.Empty;
        var isValid = false;
        while (!isValid)
        {
            input = GetUserInput(prompt, validValues);
            isValid = !string.IsNullOrEmpty(input) || validValues.Contains(string.Empty);
        }

        return input;
    }
}

The actual program

class Program
{
    /*
        "Scissors cuts paper, paper covers rock, 
         rock crushes lizard, lizard poisons Spock, 
         Spock smashes scissors, scissors decapitate lizard, 
         lizard eats paper, paper disproves Spock, 
         Spock vaporizes rock. And as it always has, rock crushes scissors." 

                                                          -- Dr. Sheldon Cooper
    */

    static void Main(string[] args)
    {
        var consoleReader = new ConsoleUserInputProvider();
        var consoleWriter = new ConsoleResultWriter();

        var game = new Game(consoleReader, consoleWriter);
        game.Run();
    }
}

IResultWriter

public interface IResultWriter
{
    void OutputResult(int comparisonResult, SelectionBase player, SelectionBase sheldon);
}

public class ConsoleResultWriter : IResultWriter
{
    public void OutputResult(int comparisonResult, SelectionBase player, SelectionBase sheldon)
    {
        var resultActions = new Dictionary<int, Action<SelectionBase, SelectionBase>>
        {
            { 1, OutputPlayerWinsResult },
            { -1, OutputPlayerLosesResult },
            { 0, OutputTieResult }
        };

        resultActions[comparisonResult].Invoke(player, sheldon);
    }

    private void OutputPlayerLosesResult(SelectionBase player, SelectionBase sheldon)
    {
        Console.WriteLine("\n\tSheldon says: \"{0} {1} {2}. You lose!\"\n", sheldon.Name, sheldon.GetWinningVerb(player), player.Name);
    }

    private void OutputPlayerWinsResult(SelectionBase player, SelectionBase sheldon)
    {
        Console.WriteLine("\n\tSheldon says: \"{0} {1} {2}. You win!\"\n", player.Name, player.GetWinningVerb(sheldon), sheldon.Name);
    }

    private void OutputTieResult(SelectionBase player, SelectionBase sheldon)
    {
        Console.WriteLine("\n\tSheldon says: \"Meh. Tie!\"\n");
    }

The actual game

I didn't bother with building a complex AI - we're playing against Sheldon Cooper here, and he systematically plays Spock.

public class Game
{
    private readonly Dictionary<string, SelectionBase> _playable =
        new Dictionary<string, SelectionBase>
            {
                { "1", new Rock() },
                { "2", new Paper() },
                { "3", new Scissors() },
                { "4", new Lizard() },
                { "5", new Spock() }
            };

    private readonly IUserInputProvider _consoleInput;
    private readonly IResultWriter _resultWriter;

    public Game(IUserInputProvider console, IResultWriter resultWriter)
    {
        _consoleInput = console;
        _resultWriter = resultWriter;
    }

    public void Run()
    {
        while (true)
        {
            LayoutGameScreen();

            var player = GetUserSelection();
            if (player == null) return;

            var sheldon = new Spock();
            var result = player.CompareTo(sheldon);

            _resultWriter.OutputResult(result, player, sheldon);
            Pause();
        }
    }

    private void Pause()
    {
        Console.WriteLine("\nPress <ENTER> to continue.");
        Console.ReadLine();
    }

    private void LayoutGameScreen()
    {
        Console.Clear();
        Console.WriteLine("Rock-Paper-Scissors-Lizard-Spock 1.0\n{0}\n", new string('=', 40));

        foreach (var item in _playable)
            Console.WriteLine("\t[{0}] {1}", item.Key, item.Value.Name);

        Console.WriteLine();
    }

    private SelectionBase GetUserSelection()
    {
        var values = _playable.Keys.ToList();
        values.Add(string.Empty); // allows a non-selection

        var input = _consoleInput.GetValidUserInput("Your selection? <ENTER> to quit.", values);
        if (input == string.Empty) return null;

        return _playable[input];
    }
}

Output

  • Rock: "Spock vaporizes Rock. You lose!"
  • Paper: "Paper disproves Spock. You win!"
  • Scissors: "Spock smashes Scissors. You lose!"
  • Lizard: "Lizard poisons Spock. You win!"
  • Spock: "Meh. Tie!"
\$\endgroup\$
43
\$\begingroup\$

Let's look at your code from an extensibility point of view:

If Sheldon decides to add a new item to the game then you have to go to n classes to adjust the comparisons and winning verbs. I usually try to avoid such designs because whenever you require a developer to change stuff in n places when something new is added then he/she is bound to forget one place.

So how can we change the design? Well, a game seems to be suited for a rules approach especially since the rules are fairly simple and always of the same structure in this case:

enum Item
{
    Rock, Paper, Scissors, Lizard, Spock
}

class Rule
{
    public readonly Item Winner;
    public readonly Item Loser;
    public readonly string WinningPhrase;

    public Rule(item winner, string winningPhrase, item loser)
    {
        Winner = winner;
        Loser = loser;
        WinningPhrase = winningPhrase;
    }

    public override string ToString()
    {
         return string.Format("{0} {1} {2}", Winner, WinningPhrase, Loser);
    }
}

Assuming you have a list of rules somewhere:

    static List<Rule> Rules = new List<Rule> {
            new Rule(Item.Rock, "crushes", Item.Scissors),
            new Rule(Item.Spock, "vaporizes", Item.Rock),
            new Rule(Item.Paper, "disproves", Item.Spock),
            new Rule(Item.Lizard, "eats", Item.Paper),
            new Rule(Item.Scissors, "decapitate", Item.Lizard),
            new Rule(Item.Spock, "smashes", Item.Scissors),
            new Rule(Item.Lizard, "poisons", Item.Spock),
            new Rule(Item.Rock, "crushes", Item.Lizard),
            new Rule(Item.Paper, "covers", Item.Rock),
            new Rule(Item.Scissors, "cut", Item.Paper)
    }

You now can make a decision:

class Decision
{
    private bool? _HasPlayerWon;
    private Rule _WinningRule;

    private Decision(bool? hasPlayerWon, Rule winningRule)
    {
        _HasPlayerWon = hasPlayerWon;
        _WinningRule = winningRule;
    }

    public static Decision Decide(item player, item sheldon)
    {
        var rule = FindWinningRule(player, sheldon);
        if (rule != null)
        {
            return new Decision(true, rule);
        }

        rule = FindWinningRule(sheldon, player);
        if (rule != null)
        {
            return new Decision(false, rule);
        }

        return new Decision(null, null);
    }

    private static Rule FindWinningRule(item player, item opponent)
    {
        return Rules.FirstOrDefault(r => r.Winner == player && r.Loser == opponent);
    }

    public override string ToString()
    {
        if (_HasPlayerWon == null)
        {
            return "Meh. Tie!";
        }
        else if (_HasPlayerWon == true)
        {
            return string.Format("{0}. You win!", _WinningRule);
        }
        else
        {
            return string.Format("{0}. You lose!", _WinningRule);
        }
    }
}

If you want to add another item to the game then you add another entry into the enum and some additional rules and you are done.

One thing to improve with my version is that rules just effectively define winning rules and all other cases are implicitly ties which in the context of this game makes sense but could be made more explicit.

\$\endgroup\$
  • 1
    \$\begingroup\$ @dreza: Yeah, first I had the constructor as (winner, loser, phrase) but then I was writing the rules and thought, well that would read better if swapped :) \$\endgroup\$ – ChrisWue Nov 30 '13 at 8:50
  • \$\begingroup\$ This is pretty clearly a better factoring of the problem than OP and dreza's answers. It makes the decision complex the operative entity, separate from the choice data. \$\endgroup\$ – Karl Damgaard Asmussen Nov 30 '13 at 11:04
15
\$\begingroup\$

this sounded like IComparable<T>

But it's not. The documentation of Compare() states that the relation has to be transitive:

If A.CompareTo(B) returns a value x that is not equal to zero, and B.CompareTo(C) returns a value y of the same sign as x, then A.CompareTo(C) is required to return a value of the same sign as x and y.

This isn't true in your case, which means that your types shouldn't implement IComparable<T> and that ideally, the comparing method shouldn't be called CompareTo() to avoid confusion.

\$\endgroup\$
7
\$\begingroup\$

We could perhaps do away with the bulk of the children class code by implementing a bit of logic in the base class.

A starting point for discussion would be something along the lines of:

public abstract class SelectionBase : IComparable<SelectionBase>
{
    private readonly List<WinningPlay> _winsAgainst;

    protected SelectionBase(List<WinningPlay> winsAgainst)
    {
        _winsAgainst = winsAgainst;
    }

    public virtual int CompareTo(SelectionBase other)
    {
        if (GetType() == other.GetType()) return 0; // draws against itself

        if (_winsAgainst.Any(p => p.Winner == other.GetType())) return 1; // wins

        return -1; // otherwise loses.
    }

    public virtual string Name { get { return GetType().Name; } }

    public virtual string GetWinningVerb(SelectionBase other)
    {
        var winner = _winsAgainst.SingleOrDefault(p => p.Winner == other.GetType());

        if (winner == null)
            throw new InvalidOperationException("Are we playing the same game?");
        else
            return winner.Verb;
    }

    protected class WinningPlay
    {
        public Type Winner { get; private set; }
        public string Verb { get; private set; }

        public WinningPlay(Type type, string verb)
        {
            Winner = type;
            Verb = verb;
        }
}

And the children classes become:

public class Rock : SelectionBase
{

    public Rock() 
        : base(new List<WinningPlay>
            {
                new WinningPlay(typeof(Scissors), "crushes"),
                new WinningPlay(typeof(Lizard), "crushes")
            })
    {
    }
}

public class Paper : SelectionBase
{
    public Paper()
        : base(new List<WinningPlay>
            {
                new WinningPlay(typeof (Rock), "covers"),
                new WinningPlay(typeof (Spock), "disproves")
            })
    {
    }
}

public class Scissors : SelectionBase
{
    public Scissors()
        : base(new List<WinningPlay>
            {
                new WinningPlay(typeof (Rock), "cuts"),
                new WinningPlay(typeof (Spock), "decapitates")
            })
    {
    }
}

public class Lizard : SelectionBase
{
    public Lizard()
        : base(new List<WinningPlay>
            {
                new WinningPlay(typeof (Paper), "eats"),
                new WinningPlay(typeof (Spock), "poisons")
            })
    {
    }
}

public class Spock : SelectionBase
{
     public Spock()
         : base(new List<WinningPlay>
            {
                new WinningPlay(typeof (Rock), "Vaporizes"),
                new WinningPlay(typeof (Scissors), "smashes")
            })
    {
    }
}
\$\endgroup\$
  • \$\begingroup\$ Amazing. I was exactly 2 seconds away from submitting an edit that would have totally mootinated this superb answer! I mean, this is almost exactly the code I was just about to commit - it's the next logical refactoring step. I actually also got rid of GetWinningVerb and all these useless exceptions. You're reading my mind! \$\endgroup\$ – Mathieu Guindon Nov 30 '13 at 7:55
  • \$\begingroup\$ @retailcoder cheers. it's a start. I guess good think about refactoring is doing it once, taking a step back and then doing it again. I sort of wonder. Do you even need children classes. Would a simple Enum do? \$\endgroup\$ – dreza Nov 30 '13 at 8:17
4
\$\begingroup\$

I'm somewhat old-school in that I don't think OO is the right answer to every problem. Here's my effort:

void Main()
{
    foreach (var left in Enumerable.Range(0, (int)A.Count).Cast<A>()) {
        foreach (var right in Enumerable.Range(0, (int)A.Count).Cast<A>()) {
            Result result;
            string report;
            Play(left, right, out result, out report);
            Console.WriteLine(left + " vs " + right + ": " + report + " -- " + result);
        }
    }
}

enum A { Rock, Paper, Scissors, Lizard, Spock, Count };

static string[,] Defeats;

static void InitDefeats()
{
    Defeats = new string[(int)A.Count, (int)A.Count];
    Action<A, string, A> rule = (x, verb, y) => { Defeats[(int)x, (int)y] = verb; };
    rule(A.Rock, "crushes", A.Lizard);
    rule(A.Rock, "blunts", A.Scissors);
    rule(A.Paper, "wraps", A.Rock);
    rule(A.Paper, "disproves", A.Spock);
    rule(A.Scissors, "cut", A.Paper);
    rule(A.Scissors, "decapitates", A.Lizard);
    rule(A.Lizard, "poisons", A.Spock);
    rule(A.Lizard, "eats", A.Paper);
    rule(A.Spock, "smashes", A.Scissors);
    rule(A.Spock, "vaporizes", A.Rock);
}

enum Result { LeftWins, Tie, RightWins };

static void Play(A left, A right, out Result result, out string report)
{
    if (Defeats == null) InitDefeats();
    var lr = Defeats[(int)left, (int)right];
    var rl = Defeats[(int)right, (int)left];
    if (lr != null) {
        result = Result.LeftWins;
        report = (left + " " + lr + " " + right).ToLower();
        return;
    }
    if (rl != null) {
        result = Result.RightWins;
        report = (right + " " + rl + " " + left).ToLower();
        return;
    }
    result = Result.Tie;
    report = (left + " vs " + right + " is a tie").ToLower();
}

Which prints out:

Rock vs Rock: rock vs rock is a tie -- Tie
Rock vs Paper: paper wraps rock -- RightWins
Rock vs Scissors: rock blunts scissors -- LeftWins
Rock vs Lizard: rock crushes lizard -- LeftWins
Rock vs Spock: spock vaporizes rock -- RightWins
Paper vs Rock: paper wraps rock -- LeftWins
Paper vs Paper: paper vs paper is a tie -- Tie
Paper vs Scissors: scissors cut paper -- RightWins
Paper vs Lizard: lizard eats paper -- RightWins
Paper vs Spock: paper disproves spock -- LeftWins
Scissors vs Rock: rock blunts scissors -- RightWins
Scissors vs Paper: scissors cut paper -- LeftWins
Scissors vs Scissors: scissors vs scissors is a tie -- Tie
Scissors vs Lizard: scissors decapitates lizard -- LeftWins
Scissors vs Spock: spock smashes scissors -- RightWins
Lizard vs Rock: rock crushes lizard -- RightWins
Lizard vs Paper: lizard eats paper -- LeftWins
Lizard vs Scissors: scissors decapitates lizard -- RightWins
Lizard vs Lizard: lizard vs lizard is a tie -- Tie
Lizard vs Spock: lizard poisons spock -- LeftWins
Spock vs Rock: spock vaporizes rock -- LeftWins
Spock vs Paper: paper disproves spock -- RightWins
Spock vs Scissors: spock smashes scissors -- LeftWins
Spock vs Lizard: lizard poisons spock -- RightWins
Spock vs Spock: spock vs spock is a tie -- Tie

EDIT 14-Jul-2014: in response to Malachi's comment, I've rewritten the Play method to return an object rather than two out parameters. The code is the same length and it's arguable whether it's any clearer. Here's the updated version:

void Main()
{
    foreach (var left in Enumerable.Range(0, (int)A.Count).Cast<A>()) {
        foreach (var right in Enumerable.Range(0, (int)A.Count).Cast<A>()) {
            var outcome = Play(left, right);
            Console.WriteLine(left + " vs " + right + ": " + outcome.Report + " -- " + outcome.Result);
        }
    }
}

enum A { Rock, Paper, Scissors, Lizard, Spock, Count };

static string[,] Defeats;

static void InitDefeats() 
{
    Defeats = new string[(int)A.Count, (int)A.Count];
    Action<A, string, A> rule = (x, verb, y) => { Defeats[(int)x, (int)y] = verb; };
    rule(A.Rock, "crushes", A.Lizard);
    rule(A.Rock, "blunts", A.Scissors);
    rule(A.Paper, "wraps", A.Rock);
    rule(A.Paper, "disproves", A.Spock);
    rule(A.Scissors, "cut", A.Paper);
    rule(A.Scissors, "decapitates", A.Lizard);
    rule(A.Lizard, "poisons", A.Spock);
    rule(A.Lizard, "eats", A.Paper);
    rule(A.Spock, "smashes", A.Scissors);
    rule(A.Spock, "vaporizes", A.Rock);
}

class Outcome {
    internal string Report;
    internal Result Result;
    internal Outcome(A left, A right, string lr, string rl)
    {
        Report = ( lr != null ? left + " " + lr + " " + right
                 : rl != null ? right + " " + rl + " " + left
                 :              left + " vs " + right + " is a tie"
                 ).ToLower();
        Result = ( lr != null ? Result.LeftWins
                 : rl != null ? Result.RightWins
                 :              Result.Tie
                 );
    }
}

enum Result { LeftWins, Tie, RightWins };

static Outcome Play(A left, A right)
{
    if (Defeats == null) InitDefeats();
    var lr = Defeats[(int)left, (int)right];
    var rl = Defeats[(int)right, (int)left];
    return new Outcome(left, right, lr, rl);
}
\$\endgroup\$
  • 1
    \$\begingroup\$ I +1'd because I find your answer brings an interesting point of view. However a downvote was also compelling, because this answer could just as well be posted as a new question for reviewing - it's not a review, it's a rewrite... and FWIW I really don't like the enum type being called A. \$\endgroup\$ – Mathieu Guindon Feb 4 '14 at 2:53
  • \$\begingroup\$ @lol.upvote, fair comment -- today I've been rattling off code on this forum in coffee breaks, which is perhaps against the point! I did have a point to make, though, which is that the usual OO approach (classes, interfaces, design pattern, etc.) can lead you away from the simplest/er solutions. That's what I was trying to demonstrate. Re: A, I chose A to read nicely, albeit tongue in cheek. \$\endgroup\$ – Rafe Feb 4 '14 at 3:37
  • \$\begingroup\$ @Rafe This is not a forum, this is a Q&A site. \$\endgroup\$ – syb0rg Feb 4 '14 at 3:43
  • \$\begingroup\$ @syb0rg, I see no guidelines on this site indicating that replies should not present alternative approaches for consideration. If such guidelines exist and I have missed them, then you have my apologies. If not, then I think my contribution is valid. \$\endgroup\$ – Rafe Feb 4 '14 at 3:59
  • \$\begingroup\$ @Rafe You pointed out in a comment that you've been "rattling off code on this forum in coffee breaks". I was just pointing out that this is a Q&A site. Also, extended conversations should be held in the chatroom, not in the comments. \$\endgroup\$ – syb0rg Feb 4 '14 at 4:01

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.