Skip to content

Instantly share code, notes, and snippets.

@dacr
Last active May 27, 2023 06:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dacr/7e7e3a8e4cd942e14110fa709ca0dd4a to your computer and use it in GitHub Desktop.
Save dacr/7e7e3a8e4cd942e14110fa709ca0dd4a to your computer and use it in GitHub Desktop.
Monty Hall paradox simulation (probability puzzle) / published by https://github.com/dacr/code-examples-manager #57afcd4b-a9bc-4204-82f8-d28a8338ee69/6e4d429ef956c63bdc615daa16fafe017f9232a3
// summary : Monty Hall paradox simulation (probability puzzle)
// keywords : scala, math, probability, puzzle, paradox, simulation, monty-hall, @testable
// publish : gist
// authors : David Crosson
// license : Apache NON-AI License Version 2.0 (https://raw.githubusercontent.com/non-ai-licenses/non-ai-licenses/main/NON-AI-APACHE2)
// id : 57afcd4b-a9bc-4204-82f8-d28a8338ee69
// created-on : 2020-12-29T21:01:25Z
// managed-by : https://github.com/dacr/code-examples-manager
// run-with : scala-cli $file
// ---------------------
//> using scala "3.3.0"
//> using dep "org.scalatest::scalatest:3.2.16"
//> using objectWrapper
// ---------------------
/*
Monty Hall paradox wikipedia : https://en.wikipedia.org/wiki/Monty_Hall_problem
*/
import org.scalatest._
import flatspec._
import matchers._
import scala.annotation.tailrec
import scala.math._
import scala.util.Random
object MontyHall {
type Door = Int
sealed trait BehindDoor
object Goat extends BehindDoor
object NewCar extends BehindDoor
object Game {
def apply(doorsCount: Int = 3): Game = {
val state =
1.to(doorsCount)
.map(n => n -> Goat)
.toMap
.updated(Random.nextInt(doorsCount) + 1, NewCar)
new Game(state,None)
}
}
case class Game(state: Map[Door, BehindDoor], played:Option[Door]) {
val doors = state.keys.toSet
def chooseDoor(door:Door):Game = this.copy(played=Some(door))
def play(door:Door):Boolean = state.get(door).exists(_ == NewCar)
def revealDoor(): Door = {
val doors = state.collect{case (door,Goat) if played.isEmpty || played.get != door => door}.toArray
doors(Random.nextInt(doors.size))
}
}
def simulate(rounds:Int=100_000)(strategy: (Door,Door,Set[Door])=>Door):Double = {
@tailrec
def play(round:Int, winCount:Int):Int = {
if (round==0) winCount else {
val gameInitialState = Game()
val doors = gameInitialState.doors
val firstChosenDoor = doors.to(Array)(Random.nextInt(doors.size))
val gameAfterGivenChoice = gameInitialState.chooseDoor(firstChosenDoor)
val revealedDoor = gameAfterGivenChoice.revealDoor()
val finalChoice = strategy(firstChosenDoor, revealedDoor, doors)
val result = gameAfterGivenChoice.play(finalChoice)
play(round-1, winCount + (if (result) 1 else 0))
}
}
play(rounds,0).toDouble/rounds
}
def simulateChangeMind(rounds:Int=100_000):Double = simulate(rounds) {
(firstChosenDoor, revealedDoor, doors) =>
(doors - firstChosenDoor - revealedDoor).head
}
def simulateKeepChoice(rounds:Int=100_000):Double = simulate(rounds) {
(firstChosenDoor, revealedDoor, doors) => firstChosenDoor
}
}
class MontyHallTest extends AnyFlatSpec with should.Matchers {
override def suiteName: String = "MontyHallTest"
import MontyHall._
"Monty Hall paradox" should "give 33% of win chance if I keep my initial choice" in {
simulateKeepChoice(500_000) shouldBe 0.33d +- 0.01d
}
it should "give 66% of win chance if I change my mind" in {
simulateChangeMind(500_000) shouldBe 0.66d +- 0.01d
}
}
org.scalatest.tools.Runner.main(Array("-oDF", "-s", classOf[MontyHallTest].getName))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment