HiveBrain v1.2.0
Get Started
← Back to all entries
patternrubyMinor

Multifunctional Monty Hall Simulator

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
simulatormultifunctionalhallmonty

Problem

Based on this question on math.SE regarding probabilities in variations on the Monty Hall problem, I cobbled up a simulator in Ruby to give myself an introduction to the language.

Since this is my first Ruby program, I'm especially looking for feedback regarding Ruby style, but I would love any general code improvement suggestions as well (all the results seem correct, so I think the math is good). I'm more comfortable with Python and shell scripting, so I'm sure my preferences show through here.

Current functionality:

The program allows simulations of the following scenarios:

  • The host (Monty Hall) opens all but 1 door and the player switches to the door Monty left closed (the classic version)



  • Monty opens 1 extra door, leaving the rest closed, and you decide to switch to a different closed door (the premise of the math.SE question)



  • You pick a door and don't switch no matter what Monty does



Currently, it's hard-coded to simulate scenario #2 with 5 doors 1000 times, but that can be changed by switching the comment on the second-to-last line.

```
puts "Hello Monty!"

$wins = 0.0
$goats = 0.0

def choose_doors(num_doors)
door_range = (1..num_doors)

winning_door = rand(door_range)
player_choice = rand(door_range)

losing_doors = door_range.to_a - [winning_door] - [player_choice]
monty_open = losing_doors.sample # used when Monty opens 1 non-winning door

return player_choice, monty_open, winning_door
end

def switch_door_multiple(choice, revealed, num_doors)
# Monty revealed only 1 other door
door_range = (1..num_doors)
door_choices = door_range.to_a - [choice] - [revealed]

new_choice = door_choices.sample
end

def count_result(final_choice, winning_door)
if final_choice == winning_door
$wins += 1
else
$goats += 1
end
end

def simulate(num_doors, runs, strategy)
for i in 1..runs
player_door, open_door, winning_door = choose_doors(num_doors)
if strategy == 1 # switch, 1 choice
if player_door != winning_do

Solution

Style is generally ok, but I'd recommend a more object-oriented approach - especially if you want to learn Ruby, since Ruby is thoroughly object-oriented.

Things I noticed:

-
Global variables (apart from built-in ones like $ARGV) are rare in Ruby. They're rare because you'll usually want to encapsulate your data in objects.

-
No need to write return at the end of methods; the result of the last expression is always implicitly returned.

-
You've quite a few methods that take 3 arguments, and some that return tuples. This is a general code-smell, in my opinion, because it's not very descriptive. Long parameter lists aren't easy to remember or parse, and multiple return values are also a fairly opaque data structure.

-
Overall structure is indeed very script'y and procedural.

Basically, you're passing all your data around, letting the methods modify them. A bit of basic object modelling could make your code better by encapsulation all that.

For instance, here's one way to model the game itself (not the full simulation). Just to give you something to chew on.

# A really simple Door class
class Door
  def initialize
    @open = false
  end

  def open?
    @open
  end

  def open!
    @open = true
  end
end

# This class models 1 instance/round of the game
class Game
  attr_reader :doors

  # Init a new game with +door_count+ doors, and randomly
  # picks one of them to be the winning door.
  # Raises an exception if door count is less than 3
  def initialize(door_count)
    raise ArgumentError, "There must be at least 3 doors" if door_count.to_i < 3
    @doors = Array.new(door_count.to_i) { Door.new }
    @winning_door = @doors.sample
  end

  # Set a random door as the player's pick
  # Raises an exception if there are no doors left to pick
  def pick_door
    raise "No doors to pick" if pickable_doors.empty?
    @picked_door = pickable_doors.sample
  end

  # open +count+ number of doors
  def open_doors(count)
    shuffled = openable_doors.shuffle
    [count, shuffled.count].min.times do 
      shuffled.pop.open!
    end
  end

  # Returns true if the player's pick is the winning door
  def won?
    @picked_door == @winning_door
  end

  # Array of doors that an be picked by the player
  def pickable_doors
    closed_doors - [@picked_door]
  end

  # Array of doors that can be safely opened by Monty
  def openable_doors
    closed_doors - [@picked_door, @winning_door]
  end

  # Array of closed doors
  def closed_doors
    @doors.reject(&:open?)
  end
end


The Door class may seem sort of pointless (and indeed there are many ways to do things), but instead of using, say, an array of plain booleans, an array of door objects allows us to reference specific doors. Not as array indices, but as the doors themselves.

Again, I'm not saying this is the only - or even a particularly great - way of doing things. But it reads pretty well, and is easy to use.

For instance, this code will play a basic game with 3 doors, of which 1 will be revealed, and the player changes their pick:

game = Game.new(3)
game.pick_door     # initial pick
game.open_doors(1) # reveal what's behind a door
game.pick_door     # change pick
game.won?          # => (~67% chance of a win)


You could make classes for Choice, Prize, or, heck, even Goat, if you want to completely model everything. But I'd at least make a SimulationRun (or similar) class.

This would encapsulate the notion of "a run" as in "N rounds of the game", and keep state out of the global scope.

I'd also consider modelling the actors (the player and Monty), and inject them into the simulation.

For instance, imagine being able to say something like:

sim = Simulation.new(3, Monty::Classic, Player::Fickle)
win_ratio = sim.run(1000)


i.e. create a simulation using 3 doors, run it 1000 times, using the "classic" Monty rules that leave two doors unrevealed, and a player that'll always pick a different door second time around.

Monty and the player could be classes or simply lambdas. For instance, the player can simply be a lambda that returns true/false when asked whether to change the pick. And "the Monty" could simply return the number of doors to open. Or you could make them a lot more complex, letting the player's reaction depend on, say, the phase of the Moon or something.

Again, I'm not saying that the above is the only way - or even a particularly great way - to do things, but I'd certainly encourage you to try doing object modelling of some kind.

All of this is of course presuming you don't just want to do the math, but actually want to be empirical :)

Code Snippets

# A really simple Door class
class Door
  def initialize
    @open = false
  end

  def open?
    @open
  end

  def open!
    @open = true
  end
end

# This class models 1 instance/round of the game
class Game
  attr_reader :doors

  # Init a new game with +door_count+ doors, and randomly
  # picks one of them to be the winning door.
  # Raises an exception if door count is less than 3
  def initialize(door_count)
    raise ArgumentError, "There must be at least 3 doors" if door_count.to_i < 3
    @doors = Array.new(door_count.to_i) { Door.new }
    @winning_door = @doors.sample
  end

  # Set a random door as the player's pick
  # Raises an exception if there are no doors left to pick
  def pick_door
    raise "No doors to pick" if pickable_doors.empty?
    @picked_door = pickable_doors.sample
  end

  # open +count+ number of doors
  def open_doors(count)
    shuffled = openable_doors.shuffle
    [count, shuffled.count].min.times do 
      shuffled.pop.open!
    end
  end

  # Returns true if the player's pick is the winning door
  def won?
    @picked_door == @winning_door
  end

  # Array of doors that an be picked by the player
  def pickable_doors
    closed_doors - [@picked_door]
  end

  # Array of doors that can be safely opened by Monty
  def openable_doors
    closed_doors - [@picked_door, @winning_door]
  end

  # Array of closed doors
  def closed_doors
    @doors.reject(&:open?)
  end
end
game = Game.new(3)
game.pick_door     # initial pick
game.open_doors(1) # reveal what's behind a door
game.pick_door     # change pick
game.won?          # => (~67% chance of a win)
sim = Simulation.new(3, Monty::Classic, Player::Fickle)
win_ratio = sim.run(1000)

Context

StackExchange Code Review Q#70756, answer score: 3

Revisions (0)

No revisions yet.