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
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Jan 30, 2014 at 1:13
\$\endgroup\$

3 Answers 3

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
answered Jan 30, 2014 at 8:56
\$\endgroup\$
6
  • 1
    \$\begingroup\$ Wow. This is superb. I learnt so much from your answer. Thank you. \$\endgroup\$ Commented Jan 30, 2014 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\$ Commented Jan 30, 2014 at 11:17
  • 1
    \$\begingroup\$ Very good answer. Any reason to have :outcome, considering that it is easily determined from the score? \$\endgroup\$ Commented Jan 31, 2014 at 7:03
  • \$\begingroup\$ @CarySwoveland, I can't see any reason indeed. That's why I dropped it in the last example \$\endgroup\$ Commented Jan 31, 2014 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\$ Commented Feb 1, 2014 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]}
answered Jan 30, 2014 at 5:54
\$\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

answered Jan 30, 2014 at 10:24
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.