5
\$\begingroup\$

I'm writing a small guessing game. I'm writing a points calculation algorithm.

I wrote the following, and it works. But I feel like I'm bringing over procedural background into Ruby, or not leveraging Ruby properly.

How would an experienced Ruby programmer approach the problem? You can test the code on TryRuby.com (copy and paste code in the browser interpreter).

# g = guesses
g = [{ id: 1, elmer­: 5, roger: 7, outcome: "Roger Win" },{ id: 2, elmer: 5, roger: 1, outcome: "Elmer Win" },{ id: 3, elmer: 4, roger: 8, outcome: "Roger Win" }]
# r = actual results
r = [{ id: 1, elmer: 3, roger: 9, outcome: "Roger Win" },{ id: 2, elmer: 7, roger: 9, outcome: "Roger Win" },{ id: 3, elmer: 4, roger: 8, outcome: "Roger Win" }]
# points table
p = []    
# rules: correct outcome = 1 point, perfect match = 5 points.

# Loop over results.
r.each do |result|

  # Loop over guesses.
  g.each do |guess|

    # Make sure we compare corresponding ids.
    # So, compare result 1 to guess 1, r2 to g2, etc....
    if result[:id] == guess[:id]

      # Init a hash to store score
      score = {}

      # Did they guess the correct outcome?
      if result[:outcome] == guess[:outcome]

        # Correct outcome guessed! Make a score hash, give'em a point.
        score[:id]     = result[:id]  # game id
        score[:points] = 1            # point

        # Was it a perfect match?
        if result[:elmer] == guess[:elmer] && result[:roger] == guess[:roger]

          # Perfect match! Give them 4 points.
          # They already got 1 point for guessing the correct outcome.
          score[:points] += 4
        end
      end

      # Add the score to the points table
      p.push(score) unless score.empty?
    end
  end
end
\$\endgroup\$
5
\$\begingroup\$

I first propose how to improve your algorithm without changing the data structures.

Your algorithm is quadratic (two nested loops over all the entries). This can be easily avoided because you don't need to loop over all the g array to find the item you need.

If they are not, sort the arrays by the id key, using sort_by:

r.sort_by! { |e| e[:id] }
g.sort_by! { |e| e[:id] }

Then you can use each_with_index:

r.each_with_index { |result, i|
    guess = g[i]

    # compute the score here
}

or, if speed is not an issue (and this does not seem to be the case), you can also zip the arrays together r.zip(g) and iterate on that:

r.zip(g).each { |result, guess|
    # compute the score here
}

If you can modify the data structure I propose some alternatives.

Often when you have a set of objects with an id you use a hash.

# g = guesses
g = {1 => { elmer­: 5, roger: 7, outcome: "Roger Win" }, 2 => {elmer: 5, roger: 1, outcome: "Elmer Win" }, 3 => { elmer: 4, roger: 8, outcome: "Roger Win" }}
# r = actual results
r = {1 => { elmer: 3, roger: 9, outcome: "Roger Win" }, 2 => {elmer: 7, roger: 9, outcome: "Roger Win" }, 3 => { elmer: 4, roger: 8, outcome: "Roger Win" }}
# points table
p = {}

# Loop over results.
r.each { |id, result|
  guess = g[id]

  # Init a hash to store score
  score = {}

  # compute the score here
  # ...

  p[id] = score unless score.empty?
}

If the id is a simple incremental integer and you add the entries in r and g in the same order, you can avoid the id at all and use the array indices instead:

# g = guesses
g = [{ elmer­: 5, roger: 7, outcome: "Roger Win" }, {elmer: 5, roger: 1, outcome: "Elmer Win" }, { elmer: 4, roger: 8, outcome: "Roger Win" }]
# r = actual results
r = [{ elmer: 3, roger: 9, outcome: "Roger Win" }, {elmer: 7, roger: 9, outcome: "Roger Win" }, { elmer: 4, roger: 8, outcome: "Roger Win" }]
# points table
p = {}

# Loop over results.
r.each_with_index { |result, id|
  guess = g[id]

  # Init a hash to store score
  score = {}

  # compute the score here
  # ...

  p[id] = score unless score.empty?
}

You can create a class to represent each entry of the array (let's call each entry a Match) and then implement the scoring algorithm as a method:

class Match
  attr_accessor :elmer, :roger, :outcome

  def initialize(elmer, roger, outcome)
    @elmer = elmer
    @roger = roger
    @outcome = outcome
  end

  def compare(m)
    score = 0

    score = score + 1 if @outcome == m.outcome
    score = score + 4 if @elmer == m.elmer and @roger == m.roger

    score
  end
end

Example usage:

guess = Match.new(5, 7, "Roger Win")
result = Match.new(3, 9, "Roger Win")
puts guess.compare(result) # => 1

guess = Match.new(5, 1, "Elmer Win")
result = Match.new(7, 9, "Roger Win")
puts guess.compare(result) # => 0

guess = Match.new(5, 7, "Roger Win")
result = Match.new(5, 7, "Roger Win")
puts guess.compare(result) # => 5

puts guess.compare(guess) # => 5

If the string argument just says who is the winner based on the numbers, you can compute it:

class Match
  attr_accessor :elmer, :roger

  def initialize(elmer, roger)
    @elmer = elmer
    @roger = roger
  end

  def outcome
    if @elmer > @roger
      :elmer
    elsif @roger > @elmer
      :roger
    else
      :tie
    end
  end

  def compare(m)
    if @elmer == m.elmer and @roger == m.roger
      5
    elsif outcome == m.outcome
      1
    else
      0
    end
  end
end

Example usage:

guess = Match.new(5, 7)
result = Match.new(3, 9)
puts guess.compare(result) # => 1

guess = Match.new(5, 1)
result = Match.new(7, 9)
puts guess.compare(result) # => 0

guess = Match.new(5, 7)
result = Match.new(5, 7)
puts guess.compare(result) # => 5

puts guess.compare(guess) # => 5

guess = Match.new(2, 2)
result = Match.new(3, 3)
puts guess.compare(result) # => 1

guess = Match.new(2, 2)
result = Match.new(2, 2)
puts guess.compare(result) # => 5
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ Wow. This is superb. I learnt so much from your answer. Thank you. \$\endgroup\$ – Jumbalaya Wanton Jan 30 '14 at 10:11
  • \$\begingroup\$ I'm not sure about the id. I'm going to use MongoDB to store the guesses. So they will all have an id, only the MongoDB id not numeric like RDBS, so it has no value for sorting. The other thing is, given I'm using a document database, I can just add the score to the guess document. \$\endgroup\$ – Jumbalaya Wanton Jan 30 '14 at 11:17
  • 1
    \$\begingroup\$ Very good answer. Any reason to have :outcome, considering that it is easily determined from the score? \$\endgroup\$ – Cary Swoveland Jan 31 '14 at 7:03
  • \$\begingroup\$ @CarySwoveland, I can't see any reason indeed. That's why I dropped it in the last example \$\endgroup\$ – Domenico De Felice Jan 31 '14 at 7:33
  • 1
    \$\begingroup\$ I missed that. In compare(), perhaps if @elmer == m.elmer && @roger == m.roger; 5; elsif outcome == m.outcome; 1; else; 0; end. \$\endgroup\$ – Cary Swoveland Feb 1 '14 at 2:08
1
\$\begingroup\$

Assuming your lists are already sorted by the ID and contain results/guesses for exactly the same ID you can do:

r.zip(g).each do |result, guess|
    score = {}
    #...
end

This dramatically reduces runtime from quadratic to linear time. If they are not already sorted, but contain exactly the same IDs you can do the following to sort them.

r.sort_by! {|e| e[:id]}
g.sort_by! {|e| e[:id]}
| improve this answer | |
\$\endgroup\$
1
\$\begingroup\$

How does this look to you?

g.zip(r).collect do |guess, result|
  points = (guess == result) ? 5 : guess[:outcome] == result[:outcome] ? 1 : 0   
  {:id => guess[:id], :points => points} 
end

Note: This assumes g and r are sorted by :id; if not, you can sort prior to doing this =) Also, I would define: FULL_SCORE = 5 and CORRECT_OUTCOME_SCORE = 1

| improve this answer | |
\$\endgroup\$

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.