├── monty_hall_refactored.rb ├── monty_hall.rb ├── monty_hall_opens_door.rb └── README.md /monty_hall_refactored.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This program simulates the "Monty Hall" brain teaser. Run with 4 | # 5 | # ruby monty_hall_refactored.rb [ COUNT ] 6 | # 7 | # This program purposefully avoids some Rubyisms to make it easier for non-Rubyists 8 | # to understand. 9 | 10 | # Run a single simulation of the Monty Hall problem. 11 | # Returns: 12 | # 'switch' if the contestant should switch doors to win. 13 | # 'stay' if the contestant should NOT switch doors. 14 | def play 15 | # Randomly choose a winning door. 16 | winning_door = rand(3) 17 | 18 | # Contestant randomly chooses a door. 19 | contestant_choice = rand(3) 20 | puts "Contestant chooses door #{contestant_choice}" 21 | puts "Winning door is #{winning_door}" 22 | 23 | # Determine if the contestant should switch or stay. 24 | if contestant_choice == winning_door 25 | return 'stay' 26 | else 27 | return 'switch' 28 | end 29 | end 30 | 31 | if ARGV[ 0 ] == nil 32 | num_plays = 1 # If user didn't specify a count we'll assume 1 33 | else 34 | num_plays = ARGV[ 0 ].to_i 35 | end 36 | 37 | # Initialize the final totals as a hash map. 38 | totals = { 39 | 'stay' => 0, 40 | 'switch' => 0 41 | } 42 | 43 | # Simulate the game 44 | (1..num_plays).each do 45 | contestant_should = play 46 | puts "Contestant should #{contestant_should}" 47 | totals[contestant_should] += 1 48 | end 49 | 50 | puts "#{totals}" 51 | -------------------------------------------------------------------------------- /monty_hall.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This program simulates the "Monty Hall" brain teaser. Run with 4 | # 5 | # ruby monty_hall.rb [ COUNT ] 6 | # 7 | # This program purposefully avoids some Rubyisms to make it easier for non-Rubyists 8 | # to understand. 9 | 10 | # Run a single simulation of the Monty Hall problem. 11 | # Returns: 12 | # 'switch' if the contestant should switch doors to win. 13 | # 'stay' if the contestant should NOT switch doors. 14 | def play 15 | doors = [ false, false, false ] 16 | 17 | # Randomly choose a winning door. 18 | winning_door = rand(3) 19 | doors[ winning_door ] = true 20 | 21 | # Contestant randomly chooses a door. 22 | contestant_choice = rand(3) 23 | puts "Contestant chooses door #{contestant_choice}" 24 | 25 | # Randomly choose doors for Monty to open until we find one that is: 26 | # - Not the winner 27 | # - Not the door the contestant chose. 28 | monty_opens = nil 29 | while ( monty_opens == nil ) 30 | temp_door = rand(3) 31 | if temp_door == contestant_choice 32 | next # Can't be the contestant's door 33 | end 34 | 35 | if temp_door == winning_door 36 | next # Can't be the winning door. 37 | end 38 | 39 | monty_opens = temp_door 40 | end 41 | 42 | puts "Monty opens door #{monty_opens}" 43 | puts "Winning door is #{winning_door}" 44 | 45 | # Determine if the contestant should switch or stay. 46 | if contestant_choice == winning_door 47 | return 'stay' 48 | else 49 | return 'switch' 50 | end 51 | end 52 | 53 | if ARGV[ 0 ] == nil 54 | num_plays = 1 # If user didn't specify a count we'll assume 1 55 | else 56 | num_plays = ARGV[ 0 ].to_i 57 | end 58 | 59 | # Initialize the final totals as a hash map. 60 | totals = { 61 | 'stay' => 0, 62 | 'switch' => 0 63 | } 64 | 65 | # Simulate the game 66 | (1..num_plays).each do 67 | contestant_should = play 68 | puts "Contestant should #{contestant_should}" 69 | totals[contestant_should] += 1 70 | end 71 | 72 | puts "#{totals}" 73 | -------------------------------------------------------------------------------- /monty_hall_opens_door.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This program simulates the "Monty Hall" brain teaser with a difference: Monty will 4 | # randomly open a door and sometimes it is the winner. Run with 5 | # 6 | # ruby monty_hall_opens_door.rb [ COUNT ] 7 | # 8 | # This program purposefully avoids some Rubyisms to make it easier for non-Rubyists 9 | # to understand. 10 | 11 | # Run a single simulation of the Monty Hall problem. 12 | # Returns: 13 | # 'switch' if the contestant should switch doors to win. 14 | # 'stay' if the contestant should NOT switch doors. 15 | # 'lose' if Monty opens the winning door. 16 | def play 17 | # Randomly choose a winning door. 18 | winning_door = rand(3) 19 | 20 | # Contestant randomly chooses a door. 21 | contestant_choice = rand(3) 22 | puts "Contestant chooses door #{contestant_choice}" 23 | 24 | # Randomly choose doors for Monty to open until we find one that is: 25 | # - Not the door the contestant chose. 26 | monty_opens = nil 27 | while ( monty_opens == nil ) 28 | temp_door = rand(3) 29 | if temp_door == contestant_choice 30 | next # Can't be the contestant's door 31 | end 32 | 33 | monty_opens = temp_door 34 | end 35 | 36 | puts "Monty opens door #{monty_opens}" 37 | puts "Winning door is #{winning_door}" 38 | 39 | # If Monty opened the winning door the contestant loses. 40 | if monty_opens == winning_door 41 | return 'lose' 42 | end 43 | 44 | # Determine if the contestant should switch or stay. 45 | if contestant_choice == winning_door 46 | return 'stay' 47 | else 48 | return 'switch' 49 | end 50 | end 51 | 52 | if ARGV[ 0 ] == nil 53 | num_plays = 1 # If user didn't specify a count we'll assume 1 54 | else 55 | num_plays = ARGV[ 0 ].to_i 56 | end 57 | 58 | # Initialize the final totals as a hash map. 59 | totals = { 60 | 'lose' => 0, 61 | 'stay' => 0, 62 | 'switch' => 0 63 | } 64 | 65 | # Simulate the game 66 | (1..num_plays).each do 67 | contestant_should = play 68 | puts "Contestant should #{contestant_should}" 69 | totals[contestant_should] += 1 70 | end 71 | 72 | puts "#{totals}" 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Understanding the Monty Hall Problem through code. 3 | 4 | The Monty Hall problem is an interesting probability puzzler, made famous by Marilyn vos Savant in 1990. It is a problem that has puzzled many great mathematicians like Paul Erdős but it is easier to understand if you write out the code to simulate it. 5 | 6 | [Wikipedia has a lot more detail](https://en.wikipedia.org/wiki/Monty_Hall_problem) but here's the problem as specified by vos Savant: 7 | 8 | > Suppose you're on a game show, and you're given the choice of three doors: Behind one door is a car; behind the others, goats. You pick a door, say No. 1, and the host, who knows what's behind the doors, opens another door, say No. 3, which has a goat. He then says to you, "Do you want to pick door No. 2?" Is it to your advantage to switch your choice? 9 | 10 | The answer, which is at first counter-intuitive, is that you should always switch. You are twice as likely to win if you switch, but how can that be? Let's look at the code... 11 | 12 | ## The Code 13 | 14 | Below is the algorithm written as a method in Ruby. The full program can be found in this repository as [monty_hall.rb](monty_hall.rb). Note: the code purposefully avoids some Rubyisms to make it easier for non-Rubyists to understand. 15 | 16 | ```ruby 17 | # Run a single simulation of the Monty Hall problem. 18 | # Returns: 19 | # 'switch' if the contestant should switch doors to win. 20 | # 'stay' if the contestant should NOT switch doors. 21 | def play 22 | doors = [ false, false, false ] 23 | 24 | # Randomly choose a winning door. 25 | winning_door = rand(3) 26 | doors[ winning_door ] = true 27 | 28 | # Contestant randomly chooses a door. 29 | contestant_choice = rand(3) 30 | puts "Contestant chooses door #{contestant_choice}" 31 | 32 | # Randomly choose doors for Monty to open until we find one that is: 33 | # - Not the winner 34 | # - Not the door the contestant chose. 35 | monty_opens = nil 36 | while ( monty_opens == nil ) 37 | temp_door = rand(3) 38 | if temp_door == contestant_choice 39 | next # Can't be the contestant's door 40 | end 41 | 42 | if temp_door == winning_door 43 | next # Can't be the winning door. 44 | end 45 | 46 | monty_opens = temp_door 47 | end 48 | 49 | puts "Monty opens door #{monty_opens}" 50 | puts "Winning door is #{winning_door}" 51 | 52 | # Determine if the contestant should switch or stay. 53 | if contestant_choice == winning_door 54 | return 'stay' 55 | else 56 | return 'switch' 57 | end 58 | end 59 | ``` 60 | 61 | ## Run the simulation 62 | 63 | To run 1000 simulations of the game execute the program like this: 64 | 65 | `ruby monty_hall.rb 1000` 66 | 67 | A typical run will look like this: 68 | 69 | ``` 70 | ... 71 | {"stay"=>343, "switch"=>657} 72 | ``` 73 | 74 | In this example the contestent would win 343 times (out of a 1000) if they stayed and 657 times if they switched, which is what vos Savant predicted. 75 | 76 | ## Explanation 77 | 78 | There are two areas to pay attention to. The first area is the logic that chooses what door Monty opens: 79 | 80 | ```ruby 81 | monty_opens = nil 82 | while ( monty_opens == nil ) 83 | temp_door = rand(3) 84 | if temp_door == contestant_choice 85 | next # Can't be the contestant's door 86 | end 87 | 88 | if temp_door == winning_door 89 | next # Can't be the winning door. 90 | end 91 | 92 | monty_opens = temp_door 93 | end 94 | ``` 95 | 96 | It randomly selects a door until it finds an appropriate door. Note that--as stated by the problem--**Monty never opens the winning door**. If the game allowed Monty to occasionally open the winning door then the outcomes would be different (we'll discuss that later). 97 | 98 | The second area to inspect is the logic that determins if the contestant should switch: 99 | 100 | ```ruby 101 | if contestant_choice == winning_door 102 | return 'stay' 103 | else 104 | return 'switch' 105 | end 106 | ``` 107 | 108 | Note that the variable `monty_opens` isn't used! In fact, other than printing it out, the door that Monty opens has no bearing on the rest of the code. The crux of understanding the apparent paradox is this: despite opening a door Monty hasn't given the contestant any new information, which is evident in the code because the variable `monty_opens` is never referenced. 109 | 110 | ## Refactored 111 | 112 | Since we know that `monty_opens` is never used let's refactor the code. You might have also noticed that the `doors` array isn't really used, either, so we can factor that out. The method now looks like this: 113 | 114 | ```ruby 115 | def play 116 | # Randomly choose a winning door. 117 | winning_door = rand(3) 118 | 119 | # Contestant randomly chooses a door. 120 | contestant_choice = rand(3) 121 | puts "Contestant chooses door #{contestant_choice}" 122 | puts "Winning door is #{winning_door}" 123 | 124 | # Determine if the contestant should switch or stay. 125 | if contestant_choice == winning_door 126 | return 'stay' 127 | else 128 | return 'switch' 129 | end 130 | end 131 | ``` 132 | 133 | If you didn't care about printing the choosen doors, you could get really agressive with refactoring, down to just a few lines: 134 | 135 | ```ruby 136 | def play 137 | # If the contestant randomly chooses the winning door they should stay, otherwise switch. 138 | return rand(3) == rand(3) ? 'stay' : 'switch' 139 | end 140 | ``` 141 | 142 | In the end it only matters if the contestant chose the winning door. What Monty says matters not at all. 143 | 144 | ## Whoa 145 | 146 | How can this be? It certainly seems like Monty is giving you new information when he opens a door. The answer lies in his constraints: 147 | 148 | * Monty will always open a door and make the offer to switch (i.e. we assume no gamemanship from Monty). 149 | * He will never open the winning door. 150 | 151 | The second constraint is the important one. What if instead of opening a door, Monty just tells the contestant about the other doors: 152 | 153 | > *Contestant:* I choose door X 154 | 155 | > *Monty:* Would you rather have what's behind door X or behind door Y *and* Z? 156 | 157 | > *Contestant:* I'll switch and take doors Y and Z. 158 | 159 | > *Monty:* Are you sure? One of those doors has to be empty. 160 | 161 | > *Contestant:* Yes, I still want Y and Z. 162 | 163 | In his last statement Monty has not said anything we don't already know: one of the two doors has to be empty. The contestant still wants to switch because they know that the chances of Y and Z both being empty is less than just X being empty. 164 | 165 | ## What if Monty could open the winning door? 166 | 167 | To see why the constraint matters, let's rewrite the code to allow Monty to randomly open the winning door. The code is similar to the original with a few lines removed: 168 | 169 | ```ruby 170 | monty_opens = nil 171 | while ( monty_opens == nil ) 172 | temp_door = rand(3) 173 | if temp_door == contestant_choice 174 | next # Can't be the contestant's door 175 | end 176 | 177 | monty_opens = temp_door 178 | end 179 | ``` 180 | 181 | But now when we determine if the contestant should switch or not, we also have to factor in the times that Monty opens the winning door, which means the contestant immediate loses: 182 | 183 | ```ruby 184 | # If Monty opened the winning door the contestant loses. 185 | if monty_opens == winning_door 186 | return 'lose' 187 | end 188 | 189 | # Determine if the contestant should switch or stay. 190 | if contestant_choice == winning_door 191 | return 'stay' 192 | else 193 | return 'switch' 194 | end 195 | ``` 196 | 197 | Unlike the original example, we use `monty_opens` to help determine the final outcome. In this case Monty has given us new information! This version is saved as [monty_hall_opens_door.rb](monty_hall_opens_door.rb) and if you run it you get a different outcome: 198 | 199 | ``` 200 | ... 201 | {"lose"=>355, "stay"=>322, "switch"=>323} 202 | ``` 203 | 204 | In this scenario it doesn't matter whether the contestant stays or switches. --------------------------------------------------------------------------------