Building a Weighted Lottery Method in Ruby
In this Ruby coding exercise, we're going to build out what is called a weighted lottery method.
Guide Tasks
  • Read Tutorial
  • Watch Guide Video
  • Complete the Exercise
Video locked
This video is viewable to users with a Bottega Bootcamp license

What this essentially means is we need to build out a method that can take a set of weights, and then depending on what the weights are, it can give some behavior that lends itself towards those weights. I know that may sound very confusing, and this is definitely a little bit more on the non-trivial side, from a functionality perspective.

I think you'll be impressed with how well Ruby can handle this type of system. So I break here, I've added quite a bit more documentation to the example, to your starter code, and also to the tests. Just because this is a little bit harder to explain. So let's walk through that before I let you go build this, and then get into the solution.

Right here I have a basic set of weights, and it's not limited to simply 2, we're going to go through an example that has more than 2. So our weights need to be very dynamic. We can't just say: okay like in this example we have a winning and a losing option.

large

If you come down here to the other test, we have winning, breaking_even, and losing. These names also don't have to be hardcoded. This could be gold, silver, and bronze. From the way the system works that has to be completely dynamic. So this is just a basic hash, and the hash is going to be what the argument is.

The way that the system should work is if we call our weighted lottery, and we pass in weights like I have there on line 6. That means that if you play this game 1001 times, then it should come up losing 1000 times and winning only once. This is a heavily skewed lottery.

If you come down to the second example right here. This lottery is not quite as skewed. Imagine, if you want to have kind of a real-world mental framework for when you'd build something like this out, that you're building out a system for a casino or some type of gaming type of system.

You want to make sure that the house always wins, or not that always wins, but that it wins the majority of the time and does not lose money.

large

So what we have here is you have winning 1 time, you have breakeven 100 times, and then you have losing 1000 times. These are the weights. Each one of these values is the respective weight for that key.

If you come down, this is not in relation to the solution at all, it simply is how I'm testing it. So what I do is I run through the entire system 1000 times, and then I call the weighted lottery method. I pass in the weights. In this test, the weights are these three weights right here.

large

Then if the result is losing, then I increment the lost count by 1. If it's breaking_ even I increment it by 1. If they won, so the last scenario, then I increment that. At the very bottom set of tests, I make sure that there are more lost counts than broke_even. Then also that there are more broke_even items than won.

large

This is something that if you ever have tried to build automated tests for random behavior, it can be pretty tricky. In this scenario, I think 1000 times is enough times to make sure that the weights are working at a very high level.

What this means, is it's 100% clear that if you play this game, if you play this lottery 1000 times, then you should definitely get losing popping up the most. You should get breaking_even coming up second, and then winning should be in a pretty far distant third.

Now that you have a good idea for how it looks, what the input is, and then also what the output is: then I want you to pause the video and I want you to go and try to build this. Then when you come back, I will walk through exactly how I personally would build out this solution.

Welcome back. If you tried to build that out. If you were not successful Do not worry this is not a trivial feature to build, in the least. Let's walk through how I personally would build this. I would start with iterating through these weights, and in Ruby, we have a few ways of looping through a hash.

Right here on line 7 we have an example hash, but this could take in 3 weights, 2 ways, it could take in 100 way, It really doesn't matter. Our system needs to be able to handle all of those scenarios, so I'm going to loop over a hash. I want to, as I'm doing that, be building an array. The way I can do that is I can say:

require 'rspec'

def weighted_lottery(weights)
    weights.each_with_object([]) do | 
end

I want to build an array, so I'm going to pass each with object an array. Then I'm going to pass it a block. Now, this is where we're gonna get a little bit tricky because when you're looping through a hash you have the ability to access both the key and the value.

The way that you can do that, because if you're used to most Ruby blocks like working with inject or anything like that, then you're used to having two block variables. You'd have something like x and , and those are your block variables.

When we're iterating through these weights, or when we're iterating through a hash like we are, we need to access the key and the value. We need to access both winning in this case and then 1. Then losing, and 1000.

The way we can do that is inside of the block, we can pass in a set of parens, and we can split up our key and our values. So here I can say:

require 'rspec'

def weighted_lottery(weights)
    weights.each_with_object([]) do |(weight_key, weight_val), container_arr| 
end

Now inside of here, it's going to loop through. Now that we have this in place this is just going to, in our example there on line 9, this is only going to loop twice. Now, inside of that loop, so each through each one of those, what I want to do is I want to loop through it as many times as there are weights.

For winning, I want to create a single nested loop. For losing, I want to create 1000 nested loops, so I want to iterate 1000 times. What I'm going to do here is I'll say:

require 'rspec'

def weighted_lottery(weights)
    weights.each_with_object([]) do |(weight_key, weight_val), container_arr| 
        weight_val.times do
            container_array << weight_key
        end
    end
end

When we're going through each with object, with each one of those iterations, we're able to build out that collection. I'm going to say container_arr, and then I'm just going to add the weight_key. What that's going to do is it's going to add in our example. Whether it's winning or losing, it's going to pass that symbol and it's just going to add that to the array.

Let's see exactly how this would work right here. So I'm going to call this and right here you can see what the output is.

large

When I call weighted_lottery, it returns an array right now, and as you can see it the first time it picked out winning so it actually picked out that very first item. After that, it's going to go 1000 times with losing.

This is getting us very close to the solution that we need because as you notice there are no more entries for losing. This isn't perfect yet because remember we want to pick a single item out.

The logic that I'm wanting to implement here is something I can do with a single word, just one single method call inside of Ruby. If you think about this, just at a high-level, don't even think about it from a coding perspective.

What I want to do is imagine this is a bag of words. So think of it as a literal bag, almost like Scrabble letters or something like that. You have 1001 items in the bag, and 1000 of them say losing. 1 of them says winning.

What I want to do is I want to reach in and grab a word. It has to be at random. What that means is that from a probability perspective, I will pick out winning only 1 out of every 1001 times, which is exactly what we want. This is the type of weighting system that we're talking about.

What I can do in Ruby is instead of creating all of that myself, at the very end of this block I can just say:

require 'rspec'

def weighted_lottery(weights)
    weights.each_with_object([]) do |(weight_key, weight_val), container_arr| 
        weight_val.times do
            container_array << weight_key
        end
    end.sample
end

If you think about it, going back to our analogy on sampling or picking out something from that bag of words, I want to sample one of those words. Sample is Ruby. It's Ruby's method for being able to pick out a random sample, a single item from the array.

Now it's come down, so instead of it returning that entire array, it's only a return a random sample of it. If I run this now, it's losing. If I run it again, it's losing. Run it again, losing, and this should happen. It should be incredibly rare that it shows winning. That is working really nicely.

Let's also test it out. We have winning, losing, and let's just make this 10 so that it gives us a little bit easier to read behavior. We can say winning, and we can add another one. I will say break_even, and then break_even. Let's say they should break_even three times or something like that.

weights = {
    winning: 1,
    break_even: 3,
    losing: 10
}

If I run this again, now you can see that it gave us winning because now the ability to win is much higher.

large

Now you have, if you count these up, you have 1 + 3 is 4, 4 + 10 = 14. You have a 1 in 14 chance of winning. Now if I run this again it's losing, which is kind of what we'd expect. Running again. Losing. Running again. Losing.

Again, this is working perfectly because losing should be there the majority of the time. Each time you run this we have a 10 in 14 chance of losing coming up.

This is doing pretty much the exact behavior that you'd want. You can also play around with this if you just want to test out and make sure that it's doing exactly what you want. You could make these even much closer.

You can just say OK I want winning do you only have a 1 chance of a 1 out of 5 chance, or one out six chance. breaking_even one out of three and then 50 percent time you should get losing. So let's look on line 17. You have losing, then you are breaking_even, then you have losing, and you are breaking_even.

large

This is working really nicely. Now that we have all of that, let's just make sure that our tests are working. Open up rspec, and run this. This is for the respec may/23_spec.rb exercise. If I run that we have 2 examples, 0 failures.

That means that as we go through these pretty extensive tests. We tested it manually maybe a dozen times. These tests went through had a couple thousand times to make sure that the right weights were there, and there in proportion.

Very nice if you went through that. I know that was a nontrivial type of exercise to build out if you've never done that before, so very good work. You now know how to build out a weighted lottery method in Ruby.