DEV Community

Eduardo
Eduardo

Posted on • Updated on • Originally published at eduherminio.github.io

Rock, Paper, Scissors: a mathematical approach

Introduction

I'm sure all of you have played Rock, Paper, Scissors, a simple hand game that is said to have been around for nearly 2000 years.

Rock, Paper, Scissors!

But has any of you acted as a referee in that game?
Probably not, and that was exactly the purpose of the first problem of Tuenti Challenge 10 (a competitive programming contest that took place early this year): you had to programmatically create a referee for it.

Essentially, the problem consisted of:

Given a pair of inputs, each one of them being R (rock), P (paper) or S (scissors); print either the winning one or - in case of a draw, i.e.:

  • for input R S your program has to print R
  • for input P P your program has to print -

Hopefully that sounds easy. What makes this challenge a good one for beginners is its simplicity: it can be easily solved with a bunch of if/else statements.

However, in this post we'll be exploring an alternative, 'mathematical' and hopefully more interesting approach to solve it. Let's have a look!

Getting analytical

First things first, we need numbers to be able to justify the mathematical word in the title of this article. Let's associate each possibility with a number:

  • 1: Rock
  • 2: Paper
  • 3: Scissors

If we combine those numbers and the rules of the game, we get:

  • Paper (2) covers Rock (1)
  • Scissors (3) cuts Paper (2)
  • Rock (1) smashes Scissors (3)

What do we have here?

[CS reader] "That's.. that's.. a circular linked list!"

Well, indeed, but we're not going down that path today. However, the key fact to observe has to do with exactly that: all combinations follow the same pattern (big number beats small number) but the last one (where small number beats big number).

And that's a fact that remains true even if we shift the possibilities (keeping the same order) and use different, consecutive numbers:

  • Paper (227)
  • Scissors (228)
  • Rock (229)

Which would leave us with

  • Scissors (228) beats Paper (227)
  • Rock (229) smashes Scissors (228)
  • Paper (227) covers Rock (229)

Spotting the difference

Our base conclusion is that all combinations follow the same pattern except for one.

In order to create an algorithm that can solve our problem, the first step now is being able to identify that odd combination, the one that doesn't follow the general rule.

Let's put together our pairs of numbers:

  • 2 beats 1, 228 beats 227
  • 3 beats 2, 229 beats 228
  • 1 beats 3, 227 beats 229

Now it may be easier to spot that the combination that behaves differently is the one whose numbers aren't consecutive.

Putting everything together

Let's summarize what we've got:

  • If both numbers are the same, no one wins
  • If both numbers are consecutive, the bigger one wins
  • If both numbers aren't consecutive, the smaller one wins

Let's try to put those three conditions together in code (I'm using C# 8 syntax here):

// Input
int player1 = 229, player2 = 227;

// Output calculation
var players = new []{ player1, player2 };

var winnerNumber = Math.Abs(player2 - player1)
    switch
    {
        0 => -1,
        1 => players.Max(),
        _ => players.Min()
    };
Enter fullscreen mode Exit fullscreen mode

We can use the result of Math.Abs(player2 - player1) in a switch expression to check if the numbers are equal, consecutive or something else; and act accordingly.

[Disappointed reader] "Hey, but hold on a second, those are numbers, not strings!"

Correct.

We already know that it doesn't really matter which numbers we choose, providing they follow a given order. That allows us to get advantage of enums, which are based on integers, in the following way:

using System;
using System.Linq;

namespace RockPaperScissors
{
    public static class Program
    {
        private enum Item { R, P, S }

        public static void Main()
        {
            // Input
            const string str1 = "R";
            const string str2 = "P";

            // Output calculation
            var player1 = (int)Enum.Parse(typeof(Item), str1);
            var player2 = (int)Enum.Parse(typeof(Item), str2);

            var players = new[] { player1, player2 };

            var result = Math.Abs(player2 - player1)
              switch
              {
                  0 => "-",
                  1 => ((Item)players.Max()).ToString(),
                  _ => ((Item)players.Min()).ToString()
              };

            Console.WriteLine($"Winner: {result}");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's have a look at what's exactly happening here:

  • We create an enum Item with elements that match the strings we'll be getting as inputs. Note that we could give those elements any consecutive values (i.e., 227, 228 and 229, as we did in our example), or just let the language automatically do it.

  • That allows us to use Enum.Parse() to get the correct Item from each input, and cast them to get our numbers.

  • We feed our algorithm with those numbers, which chooses - as output if it declares the game a draw, or the string representation of whoever Item has won (R, S or P).


Hope you liked it!

You can find here the complete implementation I used for the challenge, which includes reading the data from an input file and writing the results to an output one.

Now a small test for you, avid reader:

Can you find a similar, mathematical approach for Rock, Paper, Scissors, Lizard, Spock?

Rock, Paper, Scissors, Lizard, Spock!

Top comments (0)