Massively simplifying / cleanup: Work smart, not hard


#1

Hi!

Just came across someone working on the RPS thing and thought to myself, let's have some fun and turn this into something more elegant.
Then I wanted to share the solution, since it massively reduces the standard code: As a bonus after completing the task. Cleanup and optimization: Let's reduce the problems to something we can better deal with.

The most obvious pitfall of the if/else attempt is the immense redundancy. The good news is: We don't need to bother comparing the different results to each other, that can be reduced to a simple equals (==) check.

Now, everybody knows RPS kind of works in a circle.
Rock is beaten by paper, is beaten by scissors, is again beaten by rock, and it repeats.
So we can imagine the whole thing as a circle. After 2, it begins with 0 again.

And we notice, that a player wins if his value is exactly one higher than his opponent's. (who beats 2 then? Let's imagine the numbers just wrapping around: 3 becomes 0.)

Now: How can we switch between numbers and names for our moves? We can just use an Array, containing the names:

   var choices = ["rock", "paper", "scissors"];

Now when we call choices[0], we get "rock", choices[1] is "paper", choices[2] is "scissors".
Similarly, we can use choices.indexOf("rock") to get 0, and so on.

Thus, we can switch between numbers and names.

And how do we compare those more efficiently?

Let's look at how we check for the player's inputs first.
Player (Human):

    var userChoice = function () {
      var choice = prompt("Do you choose rock, paper or scissors?");
      if (choice !== "rock" && choice !== "paper" && choice !== "scissors") {
        return userChoice();             //  In case the input was invalid, check again and return THAT result instead.
      } else {
        return choice;                     //  Otherwise, return the valid input
      }
    };

And Player2 (Machine):

    var computerChoice = Math.round(Math.random() * 2);

Math.random() creates a random value between 0 and 1. If we want a random value between 0 and 2 (for out possible indices), we can simply multiply that by 2.
Now, we would get some pretty awful floating point numbers, so we have to round. That gives us either 0, 1 or 2.
We can later use those to refer to our move names in the table.

We've established that a wins if his value is equal to b + 1.
So we could ask

    if(a = b+1)

to find out if a won. That would work.
Only, what if b is actually 2? then b+1 would equal 3, which can never be reached, since the index of 3 does not exist in our array.
So we have to wrap b+1 around, if necessary. We COULD to that with another IF, but let's not. Let's do this inline, the neat way.
We can use the Modulo operator (%) to get the remainder of b+1 divided by 3. This means, in this case, b+1 will remain b+1, unless b+1 == 3, because then we will have no remainder (3/3 = 1, no remainder) - so 2+1 will be 0 again.
This way, we've created a way to wrap the number around between 0 and 2. (This also works for ANY other integer, not just for 3)

Now, we won't ever get a number higher than 2:

    if(a = (b+1) % 3)

And it will definitely tell us if a won.
Now we just need to combine that with a check to see if a is actually equal to b (is it a draw?) If not, check who won.
If it wasn't a, then it must have been b.
Let's make a function out of this.
We will call it using the player's pick first (which is entered as a string), and the computer's pick second (which will be an integer: 0, 1 or 2) - like so:

    compare(userChoice(), computerChoice);

We use the return value of the user function directly.
And this shall be our function:

    function compare(a, b) {
      var w = choices[b];          //  let w be our winner; start off with b's move being the winner by default, to save space
      if (choices.indexOf(a) == b) {    //  is it a draw?
        w = "draw";
      } else if (choices.indexOf(a) == (b + 1) % 3) {      //  If a's number is in fact exactly one higher than b's, a wins.
        w = a;
      }
      console.log(a + " vs. " + choices[b] + ": " + w + " won!");     //  tell us who won
    }

We already have the player's move's name and the computer's number in the function, as a and b respectively.
To get the player's "number", we use choices.indexOf(a). to get the computer's move's name, we use choices[b].

And really... that's it. let's look at our whole code:

    var choices = ["rock", "paper", "scissors"];
    
    var userChoice = function () {
      var choice = prompt("Do you choose rock, paper or scissors?");
      if (choice !== "rock" && choice !== "paper" && choice !== "scissors") {
        return userChoice();
      } else {
        return choice;
      }
    };
    
    var computerChoice = Math.round(Math.random() * 2);
    
    function compare(a, b) {
      var w = choices[b];
      if (choices.indexOf(a) == b) {
        w = "draw";
      } else if (choices.indexOf(a) == (b + 1) % 3) {
        w = a;
      }
      console.log(a + " vs. " + choices[b] + ": " + w + " won!");
    }
    compare(userChoice(), computerChoice);     //  actually call the function in the end

And see it working here:
http://jsfiddle.net/svArtist/12nLz7nb/7/

This is a lot shorter than most attempts, and should give you some helpful ideas and practices for the future.

I hope this is useful for many people out there, I thought it couldn't hurt to share my findings.


#2
var userChoice = function () {
  var choice = prompt("Do you choose rock, paper or scissors?");
  if (choice !== "rock" && choice !== "paper" && choice !== "scissors") {
    return userChoice();
  } else {
    return choice;
  }
};

Using your recursion idea, we can further simplify to something along these lines...

var choice, re = /rock|paper|scissors/i;
function userChoice(){
  choice = prompt("Do you choose rock, paper or scissors?");
  if (choice === null) {
      return choice;
  }
  return re.test(choice) ? choice.toLowerCase() : userChoice(); 
}

Going on the assumption of some kind of game loop,

result = compare(userChoice(), computerChoice);
if (result === null) break;

#3

How do you let something go once you are committed? Pounded the leather for a couple hours and morphed into this...

var choices = ["ROCK", "PAPER", "SCISSORS"];
var choice, re = /rock|paper|scissors/i;

function userChoice(){
    choice = prompt("Do you choose rock, paper or scissors?");
    if (choice === null) return choice;
    return re.test(choice) ? choice.toLowerCase() : userChoice(); 
}
function computerChoice(){
    return Math.floor(Math.random() * 3);
}
function compare(a, b){
    if (a === null) return a;
    var u = choices.indexOf(a.toUpperCase());
    return u == b ? 'draw' : u == (b + 1) % 3 ? a + ' wins' : choices[b] + ' wins';
}

// game loop
var result;
do {
    result = compare(userChoice(), computerChoice());
    if (result) console.log(result);
} while (result !== null);
console.log("Bye!");

#4

Good thinking! I just took the approach someone on S/O had and tackled the win check, but you introduced further stability and functionality.
The ? shorthands are nice for brevity but not as well relatable for beginners I guess, I wanted to maintain a high readability, that's why I did my best to comment everything


#5

Your annotations were enough to make the transition to this refactoring easy to understand. The two are hand in hand, and necessary.


#6

Wait.. how come you have syntax highlighting?
My code looks all plain and ugly...

Don't tell me you have to do that manually : D


#7

Three back-ticks before and after code, each on their own line.


#8

I'll be...!
: D

Thanks


#9

Ternary Operator, ?

Expression form:

expression ? true : false

Statement form:

return expression ? true : false;

There are three parts to this statement/expression. Hence, ternary. It's the more declarative form of an if..else statement. There are lots of pitfalls, so this should not be a go-to approach; but, it does have merit in simple one-line operations. We're always looking for ways to minimize the amount of work, and in carefully controlled surroundings, the ternary is a great condensation.