0

Working on a Codewar challenge: Rock, Paper,Scissors. The aim is to return which player won! In case of a draw return Draw!.

For example:

rps('scissors','paper') // Player 1 won!
rps('scissors','rock') // Player 2 won!
rps('paper','paper') // Draw!

my code:

def rps(p1, p2)

case p1...p2

when  (p1 == 'rock' && p2 == 'scissors') ||
      (p1 == 'scissors' && p2 == 'paper') ||
      (p1 == 'paper' && p2 == 'rock')
  return "Player 1 won!"
  
when  (p1 == 'scissors' && p2 == 'rock') ||
      (p1 == 'paper' && p2 == 'scissors') ||
      (p1 == 'rock' && p2 == 'paper')
else (p1 == p2)
  return "Draw!"
end
end

outcome:

 player 1 win
Expected: "Player 1 won!", instead got: "Draw!"
Expected: "Player 1 won!", instead got: "Draw!"
Expected: "Player 1 won!", instead got: "Draw!"
 player 2 win
Expected: "Player 2 won!", instead got: "Draw!"
Expected: "Player 2 won!", instead got: "Draw!"
Expected: "Player 2 won!", instead got: "Draw!"
 draw
Test Passed: Value == "Draw!"
Test Passed: Value == "Draw!"
Test Passed: Value == "Draw!"

What do I need to change in my code for it to return "Player 1 Won!" and "Player 2 Won!"?

KobbyAdarkwa
  • 181
  • 2
  • 16

4 Answers4

2

The case expression in Ruby works like this (see section 11.5.2.2.4 The case expression of the ISO/IEC 30170:2012 Information technology — Programming languages — Ruby specification for details):

case object_of_interest
when category1
  some_expression
when category2
  some_other_expression
when category3
  another_expression
else
  an_entirely_different_expression
end

is (roughly) equivalent to

temp = object_of_interest

if category1 === temp
  some_expression
elsif category2 === temp
  some_other_expression
elsif category3 === temp
  another_expression
else
  an_entirely_different_expression
end

The === operator (I call it the case subsumption operator, but that is something I made up, not an official term) checks whether the right-hand operand can be subsumed under the receiver. What do I mean by that? Imagine, you had a drawer labelled with the receiver, would it make sense to put the right-hand operand into it?

So, if you have

foo === bar

this asks "if I have a drawer labelled foo, would it make sense to put bar into it?", or "if I interpret foo as describing a set, would bar be a member of that set?"

The default implementation of Object#=== is more or less just simple equality, it looks a bit like this (see also section 15.3.1.3.2 Kernel#=== of the ISO/IEC 30170:2012 Ruby Language Specification):

class Object
  def ===(other)
    equals?(other) || self == other
  end
end

This is not exactly helpful, because it does not show what I mean by case subsumption. For that, we have to look at some of the overrides. For example, Module#=== (see also section 15.2.2.4.5 Module#=== of the ISO/IEC 30170:2012 Ruby Language Specification):

mod === objtrue or false

Case Equality—Returns true if obj is an instance of mod or an instance of one of mod's descendants. Of limited use for modules, but can be used in case statements to classify objects by class.

So, it is equivalent to

class Module
  def ===(other)
    other.kind_of?(self)
  end
end

So, it implements the question "If I had a drawer named mod, would it make sense to put other in it" as "Is other an instance of mod", and we can see that it makes sense:

Integer === 3       #=> true
Integer === 'Hello' #=> false

String  === 3       #=> false
String  === 'Hello' #=> true

If I have a drawer labelled Integer, does it make sense to put 3 in it? Yes, it does. What about 'Hello'? No, it does not.

Another example is Range#=== (see also section 15.2.14.4.2 Range#=== of the ISO/IEC 30170:2012 Ruby Language Specification):

rng === objtrue or false

Returns true if obj is between begin and end of range, false otherwise (same as cover?).

So, it implements the question "If I had a drawer named rng, would it make sense to put other in it" as "Does the range rng cover other", and we can see that it makes sense:

0..2 === 1 #=> true
0..2 === 3 #=> false

If I have a drawer labelled 0..2, does it make sense to put 1 in it? Yes, it does. What about 3? No, it does not.

The last example is Regexp#=== (see also section 15.2.15.7.4 Regexp#=== of the ISO/IEC 30170:2012 Ruby Language Specification). Imagine a Regexp describing an infinite set of languages that match that Regexp:

/el+/ === 'Hello' #=> true
/el+/ === 'World' #=> false

If I have a drawer labelled /el+/, does it make sense to put 'Hello' in it? Yes, it does. What about 'World'? No, it does not.

So, your case expression:

case p1...p2
when p1 == 'rock' && p2 == 'scissors' || p1 == 'scissors' && p2 == 'paper' || p1 == 'paper' && p2 == 'rock'
  return 'Player 1 won!'
when p1 == 'scissors' && p2 == 'rock' || p1 == 'paper' && p2 == 'scissors' || p1 == 'rock' && p2 == 'paper'
else
  p1 == p2
  return 'Draw!'
end

Note that else doesn't have a conditional. It literally means "in every other case", so there is no need for a conditional. So, the p1 == p2 is actually the first expression inside the else block. However, since it has no side-effects, and its value is completely ignored, it doesn't actually do anything at all.

Let's simplify that to:

case p1...p2
when something_that_is_either_true_or_false
  return 'Player 1 won!'
when something_that_is_either_true_or_false
else
  return 'Draw!'
end

This is equivalent to

temp = p1...p2
if    something_that_is_either_true_or_false === temp
  return 'Player 1 won!'
elsif something_that_is_either_true_or_false === temp
else
  return 'Draw!'
end

Now, depending on how exactly you call the rps method, those conditional expressions may be either true or false, but we don't know exactly which. What we do know, however, is that neither TrueClass#=== nor FalseClass#=== override Object#===, so they still have the same semantics: they simply test for equality. So, this is actually equivalent to

if    something_that_is_either_true_or_false == p1...p2
  return 'Player 1 won!'
elsif something_that_is_either_true_or_false == p1...p2
else
  return 'Draw!'
end

Here's the thing: a boolean will never be equal to a range. They aren't even the same type! What you are effectively asking in your case expression is

"If I have a drawer labelled true, does it make sense to put p1...p2 into it?" or "If I have a drawer labelled false, does it make sense to put p1...p2 into it?" It doesn't matter which of the two situations you have, because the answer is "No" in both cases.

So, there is, in fact, no possible way in which any of the when clauses can ever be true. Therefore, your case expression will always evaluate the else clause, or put another way, your entire method is completely equivalent to

def rps(_, _) # ignore all arguments
  'Draw!'
end

There is a second form of the case expression: when you leave out the object_of_interest, then it becomes equivalent to a series of conditionals, like this:

case
when conditional1
  some_expression
when conditional2
  some_other_expression
when conditional3
  another_expression
else
  an_entirely_different_expression
end

is (roughly) equivalent to

if conditional1
  some_expression
elsif conditional2
  some_other_expression
elsif conditional3
  another_expression
else
  an_entirely_different_expression
end

So, one way to fix your method, is to simply delete the p1...p2:

case # that is all we need to change to make it work
when p1 == 'rock' && p2 == 'scissors' || p1 == 'scissors' && p2 == 'paper' || p1 == 'paper' && p2 == 'rock'
  return 'Player 1 won!'
when p1 == 'scissors' && p2 == 'rock' || p1 == 'paper' && p2 == 'scissors' || p1 == 'rock' && p2 == 'paper'
else
  p1 == p2
  return 'Draw!'
end

That is literally all we need to change to make it work. Just delete that single expression. We can simplify it a bit further: first, as discussed above, we can remove the p1 == p2, because it doesn't do anything. Secondly, the case expression is an expression. It is not a statement. (In fact, there are no statements in Ruby. Everything is an expression.)

Ergo, the case expression evaluates to a value, in particular, it evaluates to the value of the branch that was taken. So, whenever you see something like

case foo
when bar
  return baz
when qux
  return frobz
end

that is equivalent to

return case foo
when bar
  baz
when qux
  frobz
end

Also, the last expression evaluated in a method body (or block body, lambda body, class definition body, or module definition body) is the value of the whole method (block, lambda, class definition, module definition), so in your case, the return is redundant there, too.

Lastly, your second when has no body, so it isn't doing anything, which means we can just remove it.

Which means the entire method becomes

case
when p1 == 'rock' && p2 == 'scissors' || p1 == 'scissors' && p2 == 'paper' || p1 == 'paper' && p2 == 'rock'
  'Player 1 won!'
else
  'Draw!'
end

Since there is only one condition, it doesn't really make sense for it to be a case expression at all, so we make it a conditional expression instead:

if p1 == 'rock' && p2 == 'scissors' || p1 == 'scissors' && p2 == 'paper' || p1 == 'paper' && p2 == 'rock'
  'Player 1 won!'
else
  'Draw!'
end

An alternative way of solving this with a case expression would be to actually use the fact that, when not specifically overridden, === just means equality:

case [p1, p2]
when ['rock', 'scissors'], ['scissors', 'paper'], ['paper', 'rock']
  'Player 1 won!'
when ['scissors', 'rock'], ['paper', 'scissors'], ['rock', 'paper']
'Player 2 won!'
else
  'Draw!'
end
KobbyAdarkwa
  • 181
  • 2
  • 16
Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • You're a legend mate! Just had a proper read through this and will definitely be bookmarking this for future! – KobbyAdarkwa Sep 14 '20 at 09:57
  • BTW, as the answer is already very long, I left this out, but the *really correct* way to handle this would be to have objects representing rock, paper, and scissors, and implement a `beats?` method, or maybe implementing `<`, `<=`, `==`, `>`, `>=` operators. – Jörg W Mittag Sep 14 '20 at 10:49
1

This is not the completed code, but I believe you'll get the idea

def game(player1, player2)
  hash_game = {
    'scissors' => {
      'rock' => 'loss',
      'paper' => 'win'
    },
    'rock' => {
      ...
    },
    'paper' => {
      ...
    }
  }
  result = hash_game[player1][player2]
  "Player 1 #{result}!"
end
megas
  • 21,401
  • 12
  • 79
  • 130
1

Consider writing that as follows.

WINNERS = { rock: :scissors, paper: :rock, scissors: :paper }
  #=> {:rock=>:scissors, :paper=>:rock, :scissors=>:paper

def rps(p1,p2)
  puts case
  when WINNERS[p1] == p2
    "Player 1 won!"
  when WINNERS[p2] == p1
    "Player 2 won!"
  else
    "Draw!"
  end
end
rps(:rock, :scissors)     -> Player 1 won!
rps(:rock, :paper)        -> Player 2 won!
rps(:rock, :rock)         -> Draw!

rps(:paper, :rock)        -> Player 1 won!
rps(:paper, :scissors)    -> Player 2 won!
rps(:paper, :paper)       -> Draw!

rps(:scissors, :paper)    -> Player 1 won!
rps(:scissors, :rock)     -> Player 2 won!
rps(:scissors, :scissors) -> Draw!
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
0

Similar to what @megas said, I would personally create a static class variable for each game possibility like so

@@GAMES = {}.tap do |h|
    win, lose, draw = ["Player 1 Wins!", "Player 2 Wins!", "Draw!"]

    h["rock"] = { "rock" => draw, "paper" => lose, "scissors" => win }
    h["paper"] = { "rock" => win, "paper" => draw, "scissors" => lose }
    h["scissors"] = { "rock" => lose, "paper" => win, "scissors" => draw }
end

Note that tap just lets you run code and alter the object your "tapping" before returning the object itself. This would allow you to just do

def play_game(p1, p2)
    puts @@GAMES[p1][p2]
end

This method also has the added benefit of being easy to change the messages for each outcome, and even add more outcomes, in case you wanted to implement "Rock Paper Scissors Lizard Spock" or something.