Monty Hall Problem in SwiftUI - part 3

I just couldn't let the Monty Hall problem go. What if there were more doors in the problem. In this article I create a simulation of anywhere from 3 to 6 doors with the underlying premise that after selecting a door, all but one of the remaining doors are opened. The contestant is then offered an option to switch doors.

It seems more intuitive, with the increased doors, to switch doors than it did when there were just 3 doors.

The Monty Hall problem is presented in the style of a game show, where a contestant is given a choice of selecting one of three doors. Behind one of the doors is the desired prize of a car, while behind the other two doors are goats. After the contestant selects one of the three doors, the host opens one of the remaining doors to reveal a goat and asks you if you would like to switch doors.



Changes to the Model

There are only some minor changes required to the MontyGameModel and this shows some of the advantages of MVVM. Essentially, the model is made more generic to handle a variable number of doors. A second initializer is added that takes a number for quantity of doors to have in the model. The default initializer is modified to call this initializer with a value of 3. Some guard statements could be added to this to set constraints on the number of doors.

The other change is the openGoatDoor method is replaced with openOtherDoors method. This opens all but one of the non-selected door and so, behaves the same as the openGoatDoor method when there is just 3 doors. Another change that is done is to rename the shuffle method to reset, this is more of a correction to better name what the method is doing. the reset method is used to reset the model for a new game. There are some other refactoring to move logic out of the View and down to the model, but this refactoring would also apply to the original 3-door game as well.

 1struct MontyGameModel {
 2    private var doors: [Door]
 3    private var gameWon = false
 4    private var gameOver = false
 5    private var hostDoorOpened = false
 6    
 7    init() {
 8        self.init(numberOfDoors: 3)
 9    }
10    
11    init(numberOfDoors: Int) {
12        doors = []
13        for i in (0..<numberOfDoors) {
14            i == 0 ? doors.append(Door(emoji: "🚗")) : doors.append(Door(emoji: "🐐"))
15        }
16        doors.shuffle()
17    }
18
19    var isGameWon: Bool {
20        get { gameWon }
21    }
22   
23    var isGameOver: Bool {
24        get { gameOver }
25    }
26   
27    var isHostDoorOpened: Bool {
28        get { hostDoorOpened }
29    }
30   
31    var isDoorSelected: Bool {
32        get { doors.contains { $0.selected } }
33    }
34   
35    var doorCount: Int {
36        get { doors.count }
37    }
38    
39    mutating func selectDoor(index i: Int) {
40        for i in doors.indices {
41            doors[i].selected = false
42        }
43        doors[i].selected = true
44    }
45    
46    mutating func reset() {
47        for i in doors.indices {
48            doors[i].open = false
49            doors[i].selected = false
50        }
51        gameWon = false
52        gameOver = false
53        hostDoorOpened = false
54        self.doors.shuffle()
55    }
56
57    func doorAtIndex(_ i:Int) -> Door {
58        return doors[i]
59    }
60
61    mutating func openOtherDoors() -> Int {
62        let allDoors = Array(0..<doors.count)
63        let carDoorIndex = allDoors.filter { doors[$0].emoji == "🚗" }[0]
64        let selectedDoorIndex = allDoors.filter { doors[$0].selected }[0]
65        var unOpenedDoorIndex: Int
66        if selectedDoorIndex == carDoorIndex {
67            unOpenedDoorIndex = allDoors.filter { $0 != selectedDoorIndex }.randomElement()!
68        } else {
69            unOpenedDoorIndex = carDoorIndex
70        }
71        let doorsToOpen = allDoors.filter { $0 != selectedDoorIndex && $0 != unOpenedDoorIndex }
72        for i in doorsToOpen {
73            self.doors[i].open = true
74        }
75        hostDoorOpened = true
76        return unOpenedDoorIndex
77    }
78    
79    mutating func playGame() {
80        let allDoors = Array(0..<doors.count)
81        if (allDoors.filter { doors[$0].emoji == "🚗" && doors[$0].selected }.count == 1) {
82            gameWon = true
83        }
84        let selectedDoorIndex = allDoors.filter { doors[$0].selected }[0]
85        doors[selectedDoorIndex].open = true
86        gameOver = true
87    }
88}
1struct Door {
2    let emoji: String
3    var label: String {
4        return emoji == "🚗" ? "car" : "goat"
5    }
6    var open = false
7    var selected = false
8}


Changes to the ViewModel

The main change to the MontyHallViewModel is the addition of an initializer that takes the number of doors. There are some renaming of methods to match the changes made to the model.

 1class MontyHallViewModel: ObservableObject {
 2    @Published private var montyModel: MontyGameModel
 3
 4    init() {
 5        montyModel = MontyGameModel()
 6    }
 7
 8    init(numberOfDoors n: Int) {
 9        montyModel = MontyGameModel(numberOfDoors: n)
10    }
11
12    var isGameWon: Bool {
13        get { montyModel.isGameWon }
14    }
15    
16    var isGameOver: Bool {
17        get { montyModel.isGameOver }
18    }
19    
20    var isHostDoorOpened: Bool {
21        get { montyModel.isHostDoorOpened }
22    }
23    
24    var isDoorSelected: Bool {
25        get { montyModel.isDoorSelected }
26    }
27    
28    var doorCount: Int {
29        get { montyModel.doorCount }
30    }
31    
32    func door(index: Int) -> Door {
33        return montyModel.doorAtIndex(index)
34    }
35
36    func reset() {
37        montyModel.reset()
38    }
39    
40    func selectDoor(index i: Int) {
41        montyModel.selectDoor(index: i)
42    }
43
44    func openOtherDoors() {
45        _ = montyModel.openOtherDoors()
46    }
47
48    func playGame() {
49        montyModel.playGame()
50    }
51}


View for Monty Hall Problem with Six Doors

A new view is added to the app to display a variable number of doors. This is similar to the original MontyHallGameView except the view is initialized with 6 doors. There are some layout changes to adjust the size of the doors and buttons. There are also changes to remove ant state variables and use data from the ViewModel to determine the state o the game.

  1struct MultiDoorGameView: View {
  2    @ObservedObject private var montyHallVm: MontyHallViewModel
  3    @ObservedObject private var metrics: MetricsViewModel
  4    
  5    init() {
  6        self.montyHallVm = MontyHallViewModel(numberOfDoors: 6)
  7        self.metrics = MetricsViewModel()
  8        
  9        UISegmentedControl.appearance()
 10            .selectedSegmentTintColor = UIColor(Color("background2"))
 11        UISegmentedControl.appearance()
 12            .setTitleTextAttributes([.foregroundColor: UIColor(Color("lightYellow"))],
 13                                    for: .selected)
 14        UISegmentedControl.appearance()
 15            .setTitleTextAttributes([.foregroundColor: UIColor(Color("background2"))],
 16                                    for: .normal)
 17    }
 18
 19    var body: some View {
 20        ZStack {
 21            Colors.bgGradient
 22                .edgesIgnoringSafeArea(.all)
 23            
 24            VStack {
 25                Spacer().frame(height:20)
 26                
 27                Text("Monty Hall Problem")
 28                    .foregroundColor(Color("mainText"))
 29                    .font(.custom("Helvetica Neue", size: 36, relativeTo: .largeTitle))
 30                    .fontWeight(.bold)
 31                
 32                HStack(spacing:10) {
 33                    ForEach(0..<montyHallVm.doorCount) { i in
 34                        VStack {
 35                            DoorView(number: i,
 36                                     montyHall: montyHallVm)
 37                                .frame(width:50, height:150)
 38                        }
 39                    }
 40                }
 41                
 42                Spacer().frame(height:20)
 43                
 44                ZStack {
 45                    Spacer()
 46                        .frame(height:40)
 47                    if montyHallVm.isGameOver {
 48                        Text(montyHallVm.isGameWon ? "You Won" : "You Loose")
 49                    }
 50                }
 51                .foregroundColor(Color("mainText"))
 52                .font(.largeTitle)
 53                
 54                VStack {
 55                    HStack {
 56                        Button( action: {
 57                            montyHallVm.reset()
 58                        })
 59                        {
 60                            ActionButtonView(label: "New",
 61                                             symbol: "asterisk")
 62                        }
 63                        
 64                        Button( action: {
 65                            montyHallVm.openOtherDoors()
 66                        })
 67                        {
 68                            ActionButtonView(label: "Open",
 69                                             symbol: "arrow.right.square")
 70                        }
 71                        .disabled(montyHallVm.isGameOver || montyHallVm.isHostDoorOpened || !montyHallVm.isDoorSelected)
 72
 73                    }
 74                    .buttonStyle(ActionButtonSmallStyle())
 75                    
 76                    Button( action: {
 77                        montyHallVm.playGame()
 78                        metrics.gameOver(isWon: montyHallVm.isGameWon)
 79                    })
 80                    {
 81                        ActionButtonView(label: "play", symbol: "play.fill")
 82                    }
 83                    .disabled(montyHallVm.isGameOver || !montyHallVm.isDoorSelected)
 84                    .buttonStyle(ActionButtonStyle())
 85                }
 86                .frame(width:300)
 87                
 88                
 89                Spacer().frame(height:30)
 90                
 91                Rectangle()
 92                    .fill(Color("background1").opacity(0.3))
 93                    .frame(height: 5)
 94
 95                GameMetricsView(metrics: metrics)
 96                    .foregroundColor(Color("mainText"))
 97            }
 98        }
 99    }
100}

Ensure changes to Model and ViewModel work for a six-door game
Ensure changes to Model and ViewModel work for a six-door game



Set a variable number of doors

Changing the number of doors dynamically in the View was a bit more complicated that I thought. This is because the doors are laid out using ForEach with the number of doors. The recommendation from Apple is that the number of elements in the array should not change dynamically when using the array to create views. The views can be bound to a collection of Identifiable objects.

The Door object is modified to make it conform to the Identifiable protocol. As the doors get shuffled for every game, a property is added to contain the door position number. A method is added to set this door position number.

 1struct Door: Identifiable {
 2    let id = UUID()
 3    let emoji: String
 4    var label: String {
 5        return emoji == "🚗" ? "car" : "goat"
 6    }
 7    var positionNumber = 0
 8    var open = false
 9    var selected = false
10    
11    mutating func setNumber(_ n:Int) {
12        positionNumber = n
13    }
14}

The doors variable is made accessible in the MontyGameModel. A method to shuffle the doors is added to the model to shuffle the doors and set the position number of the doors.

 1struct MontyGameModel {
 2    var doors: [Door]
 3    private var gameWon = false
 4    private var gameOver = false
 5    private var hostDoorOpened = false
 6    
 7    init() {
 8        self.init(numberOfDoors: 3)
 9    }
10    
11    init(numberOfDoors: Int) {
12        doors = []
13        for i in (0..<numberOfDoors) {
14            i == 0 ? doors.append(Door(emoji: "🚗")) : doors.append(Door(emoji: "🐐"))
15        }
16        shuffleDoors()
17    }
18
19    var isGameWon: Bool {
20        get { gameWon }
21    }
22   
23    var isGameOver: Bool {
24        get { gameOver }
25    }
26   
27    var isHostDoorOpened: Bool {
28        get { hostDoorOpened }
29    }
30   
31    var isDoorSelected: Bool {
32        get { doors.contains { $0.selected } }
33    }
34    
35    mutating func selectDoor(doorNumber n: Int) {
36        for i in doors.indices {
37            doors[i].selected = i == (n-1)
38        }
39    }
40    
41    mutating func reset() {
42        for i in doors.indices {
43            doors[i].open = false
44            doors[i].selected = false
45        }
46        gameWon = false
47        gameOver = false
48        hostDoorOpened = false
49        shuffleDoors()
50    }
51
52    mutating func openOtherDoors() -> Int {
53        let allDoors = Array(0..<doors.count)
54        let carDoorIndex = allDoors.filter { doors[$0].emoji == "🚗" }[0]
55        let selectedDoorIndex = allDoors.filter { doors[$0].selected }[0]
56        var unOpenedDoorIndex: Int
57        if selectedDoorIndex == carDoorIndex {
58            unOpenedDoorIndex = allDoors.filter { $0 != selectedDoorIndex }.randomElement()!
59        } else {
60            unOpenedDoorIndex = carDoorIndex
61        }
62        let doorsToOpen = allDoors.filter { $0 != selectedDoorIndex && $0 != unOpenedDoorIndex }
63        for i in doorsToOpen {
64            self.doors[i].open = true
65        }
66        hostDoorOpened = true
67        return unOpenedDoorIndex + 1
68    }
69    
70    mutating func playGame() {
71        let allDoors = Array(0..<doors.count)
72        if (allDoors.filter { doors[$0].emoji == "🚗" && doors[$0].selected }.count == 1) {
73            gameWon = true
74        }
75        let selectedDoorIndex = allDoors.filter { doors[$0].selected }[0]
76        doors[selectedDoorIndex].open = true
77        gameOver = true
78    }
79    
80    mutating func setNumberOfDoors(n: Int) {
81        doors = []
82        for i in (0..<n) {
83            i == 0 ? doors.append(Door(emoji: "🚗")) : doors.append(Door(emoji: "🐐"))
84        }
85        reset()
86    }
87    
88    private mutating func shuffleDoors() {
89        doors.shuffle()
90        for i in doors.indices {
91            doors[i].positionNumber = i + 1
92        }
93    }
94}

The ViewModel is modified with a read-write property to make the Doors array accesible to the View.

 1class MontyHallViewModel: ObservableObject {
 2    @Published private var montyModel: MontyGameModel
 3
 4    init() {
 5        montyModel = MontyGameModel()
 6    }
 7
 8    init(numberOfDoors n: Int) {
 9        montyModel = MontyGameModel(numberOfDoors: n)
10    }
11
12    var doors: [Door] {
13        get { montyModel.doors }
14        set(newDoors) { montyModel.doors = newDoors }
15    }
16
17    var isGameWon: Bool {
18        get { montyModel.isGameWon }
19    }
20    
21    var isGameOver: Bool {
22        get { montyModel.isGameOver }
23    }
24    
25    var isHostDoorOpened: Bool {
26        get { montyModel.isHostDoorOpened }
27    }
28    
29    var isDoorSelected: Bool {
30        get { montyModel.isDoorSelected }
31    }
32    
33    func reset() {
34        montyModel.reset()
35    }
36    
37    func selectDoor(index i: Int) {
38        montyModel.selectDoor(doorNumber: i)
39    }
40
41    func openOtherDoors() {
42        _ = montyModel.openOtherDoors()
43    }
44
45    func playGame() {
46        montyModel.playGame()
47    }
48    
49    func resetDoors(numberOfDoors: Int) {
50        montyModel.setNumberOfDoors(n: numberOfDoors)
51    }
52}

The DoorView is modified to use an instance of a Door to display the properties for the door and to use a reference to the ViewModel to set when the door is selected.

 1struct DoorView: View {
 2    var door: Door
 3    @ObservedObject var montyHall: MontyHallViewModel
 4    
 5    var body: some View {
 6        Button(action: {
 7            montyHall.selectDoor(index: door.positionNumber)
 8        }) {
 9            VStack {
10                ZStack {
11                    DoorOpenView(emoji: door.emoji, label: door.label)
12                    DoorClosedView(doorNumber: door.positionNumber)
13                        .rotation3DEffect(
14                            Angle.degrees(door.open ? -87 : 0),
15                            axis: (0,1,0),
16                            anchor: .leading,
17                            perspective: 0.2
18                        )
19                }
20                .animation(.easeInOut(duration: door.open ? 1.0 : 0.0))
21                
22                Image(systemName: door.selected ? "checkmark.seal.fill" : "seal")
23                    .font(.system(size: 30))
24                    .foregroundColor(Color("mainText"))
25                    .padding(.vertical, 5)
26            }
27        }
28        .buttonStyle(DoorButtonStyle())
29        .disabled(montyHall.isGameOver || door.open || door.selected )
30    }
31}
  1struct MultiDoorGameView: View {
  2    @ObservedObject private var montyHallVm: MontyHallViewModel
  3    @ObservedObject private var metrics: MetricsViewModel
  4    
  5    init() {
  6        self.montyHallVm = MontyHallViewModel()
  7        self.metrics = MetricsViewModel()
  8        
  9        UISegmentedControl.appearance()
 10            .selectedSegmentTintColor = UIColor(Color("background2"))
 11        UISegmentedControl.appearance()
 12            .setTitleTextAttributes([.foregroundColor: UIColor(Color("lightYellow"))],
 13                                    for: .selected)
 14        UISegmentedControl.appearance()
 15            .setTitleTextAttributes([.foregroundColor: UIColor(Color("background2"))],
 16                                    for: .normal)
 17    }
 18    
 19    var body: some View {
 20        ZStack {
 21            Colors.bgGradient
 22                .edgesIgnoringSafeArea(.all)
 23            
 24            VStack {
 25                VStack(spacing:0) {
 26                    Text("Monty Hall Problem")
 27                        .foregroundColor(Color("mainText"))
 28                        .font(.custom("Helvetica Neue", size: 32, relativeTo: .title))
 29                        .fontWeight(.bold)
 30                    
 31                    NumberOfDoorsView(montyHallVm: montyHallVm,
 32                                      numOfDoors: montyHallVm.doors.count)
 33                }
 34                  
 35                HStack(spacing:10) {
 36                    ForEach(montyHallVm.doors) { door in
 37                        VStack {
 38                            DoorView(door: door,
 39                                     montyHall: montyHallVm)
 40                        }
 41                    }
 42                }
 43                .frame(width:360, height:150)
 44
 45                ZStack {
 46                    Spacer()
 47                        .frame(height:30)
 48                    if montyHallVm.isGameOver {
 49                        Text(montyHallVm.isGameWon ? "You Won" : "You Loose")
 50                    }
 51                }
 52                .foregroundColor(Color("mainText"))
 53                .font(.title2)
 54                
 55                VStack {
 56                    HStack {
 57                        Button( action: {
 58                            montyHallVm.reset()
 59                        })
 60                        {
 61                            ActionButtonView(label: "New",
 62                                             symbol: "asterisk")
 63                        }
 64                        
 65                        Button( action: {
 66                            montyHallVm.openOtherDoors()
 67                        })
 68                        {
 69                            ActionButtonView(label: "Open",
 70                                             symbol: "arrow.right.square")
 71                        }
 72                        .disabled(montyHallVm.isGameOver || montyHallVm.isHostDoorOpened || !montyHallVm.isDoorSelected)
 73
 74                    }
 75                    .buttonStyle(ActionButtonStyle())
 76                    
 77                    Button( action: {
 78                        montyHallVm.playGame()
 79                        metrics.gameOver(isWon: montyHallVm.isGameWon)
 80                    })
 81                    {
 82                        ActionButtonView(label: "play", symbol: "play.fill")
 83                    }
 84                    .disabled(montyHallVm.isGameOver || !montyHallVm.isDoorSelected)
 85                    .buttonStyle(ActionButtonStyle())
 86                }
 87                .frame(width:300)
 88                
 89                Rectangle()
 90                    .fill(Color("background1").opacity(0.3))
 91                    .frame(height: 5)
 92
 93                GameMetricsView(metrics: metrics)
 94                    .foregroundColor(Color("mainText"))
 95                
 96                Spacer()
 97            }
 98        }
 99    }
100}

The section to display the number of Doors is split out into a separate SwiftUI view.

 1struct NumberOfDoorsView: View {
 2    @ObservedObject var montyHallVm: MontyHallViewModel
 3    @State var numOfDoors: Int
 4    
 5    let doorOptions = [3, 4, 5, 6]
 6    
 7    var body: some View {
 8        ZStack {
 9            BackGroundCardView()
10            
11            VStack {
12                Text("Number of Doors")
13                    .font(.custom("Helvetica Neue", size: 22, relativeTo: .largeTitle))
14                    .fontWeight(.bold)
15                
16                Picker("", selection: $numOfDoors) {
17                    ForEach(doorOptions, id: \.self) {
18                        Text("\($0)")
19                            .fontWeight(.heavy)
20                    }
21                }
22                .onChange(of: numOfDoors) { _ in
23                    montyHallVm.resetDoors(numberOfDoors: numOfDoors)
24                }
25                .pickerStyle(.segmented)
26                .background(Color("lightYellow"))
27            }
28            .padding(.horizontal, 35)
29        }
30        .padding()
31    }
32}

Monty Hall Problem with configurable number of doors
Monty Hall Problem with configurable number of doors



Add Simulation to the View

The simulator Model and ViewModel only need slight modifications. The runSimulation method takes a parameter specifying the number of doors. The selected door is updated to use the door number rather than the door index.

 1struct SimulatorModel {
 2    private var targetRunCount: Int
 3    private var metrics: MetricsModel
 4    
 5    init() {
 6        targetRunCount = 10
 7        metrics = MetricsModel()
 8    }
 9    
10    var targetRuns: Int {
11        get { targetRunCount }
12    }
13    
14    var wins: Int {
15        get { metrics.won }
16    }
17    
18    var losses: Int {
19        get { metrics.lost }
20    }
21    
22    var gamesComplete: Int {
23        get { metrics.played }
24    }
25    
26    var percentageWon: Double {
27        get { metrics.percentageWon }
28    }
29    
30    mutating func setTargetNumber(_ n:Int) {
31        targetRunCount = n
32    }
33    
34    mutating func reset() {
35        targetRunCount = 0
36        metrics.reset()
37    }
38    
39    mutating func runSimulation(alwaysSwitch: Bool, numOfdoors:Int) {
40        // Loop for target count
41        for _ in 1...targetRunCount {
42            // 1. Create a new instance of MontyGameModel
43            var game = MontyGameModel(numberOfDoors: numOfdoors)
44            
45            // 2. Select one of the three doors at random
46            let allDoorNumbers = Array(1...game.doors.count)
47            let selectedDoorNumber: Int = allDoorNumbers.randomElement()!
48            game.selectDoor(doorNumber: selectedDoorNumber)
49            
50            // 3. Open one of the other doors
51            let unOpenedNumber = game.openOtherDoors()
52            
53            // 4. Switch selected door if required
54            if alwaysSwitch {
55                game.selectDoor(doorNumber: unOpenedNumber)
56            }
57            
58            // 5. Evaluate the game
59            game.playGame()
60            
61            // 6. Add result to metrics as either win or loss
62            metrics.gameOver(isWon: game.isGameWon)
63        }
64    }
65}

The runSimulation method in the SimulatorViewModel is updated to have an optional parameter for the number of doors. This has a default value of 3 so the original Simulation view does not need to be changed.

 1class SimulatorViewModel: ObservableObject {
 2    @Published private var simModel: SimulatorModel
 3
 4    init() {
 5        simModel = SimulatorModel()
 6    }
 7
 8    var targetRuns: Int {
 9        get { simModel.targetRuns }
10    }
11    
12    var wins: Int {
13        get { simModel.wins }
14    }
15    
16    var losses: Int {
17        get { simModel.losses }
18    }
19    
20    var gamesComplete: Int {
21        get { simModel.gamesComplete }
22    }
23    
24    var percentageWon: Double {
25        get { simModel.percentageWon }
26    }
27    
28    func runSimulation(targerNumber n:Int, alwaysSwitch: Bool, numberOfDoors:Int = 3) {
29        simModel.reset()
30
31        simModel.setTargetNumber(n)
32
33        simModel.runSimulation(alwaysSwitch: alwaysSwitch, numOfdoors: numberOfDoors)
34    }
35}

A separate simulation SwiftUI view is defined to present a section for simulation in the new multi-door view.

 1struct SimulationView: View {
 2    var numOfDoors: Int
 3    @ObservedObject var montySimulator: SimulatorViewModel
 4    @State private var targetNumber = 100
 5    @State private var switchDoors = true
 6    
 7    var body: some View {
 8        VStack {
 9            ZStack {
10                BackGroundCardView()
11                
12                VStack {
13                    Text("Select the Number of runs")
14                        .font(.custom("Helvetica Neue", size: 22, relativeTo: .largeTitle))
15                        .fontWeight(.bold)
16                    
17                    Picker("", selection: $targetNumber) {
18                        ForEach([10,100,500,1000], id: \.self) {
19                            Text("\($0)")
20                                .fontWeight(.heavy)
21                        }
22                    }
23                    .pickerStyle(.segmented)
24                    .background(Color("lightYellow"))
25                    
26                    
27                    HStack {
28                        Toggle(isOn: $switchDoors, label: {
29                            Text("Always switch doors")
30                                .font(.custom("Helvetica Neue", size: 22, relativeTo: .largeTitle))
31                                .fontWeight(.bold)
32                        })
33                    }
34                    Button( action: {
35                        montySimulator.runSimulation(
36                            targerNumber: targetNumber,
37                            alwaysSwitch: switchDoors,
38                            numberOfDoors: numOfDoors)
39                    })
40                    {
41                        ActionButtonView(label: "Run Simulation",
42                                         symbol: "play.fill")
43                    }
44                    .buttonStyle(ActionButtonStyle())
45                    
46                    ResultBarView(won: montySimulator.wins,
47                                  lost: montySimulator.losses)
48                        .frame(height:20)
49                    
50                    HStack {
51                        Text("\(montySimulator.wins) Won")
52                        Spacer()
53                        Text("\(montySimulator.losses) Lost")
54                    }
55                }
56                .padding()
57            }
58            .padding(5)
59        }
60    }
61}

Finally the MultiDoorGameView is updated to contain a section for simulation as well as the section for playing the game with multiple doors.

  1struct MultiDoorGameView: View {
  2    @ObservedObject private var montyHallVm: MontyHallViewModel
  3    @ObservedObject private var metrics: MetricsViewModel
  4    @ObservedObject private var montySimulator: SimulatorViewModel
  5    
  6    init() {
  7        self.montyHallVm = MontyHallViewModel()
  8        self.metrics = MetricsViewModel()
  9        self.montySimulator = SimulatorViewModel()
 10        
 11        UISegmentedControl.appearance()
 12            .selectedSegmentTintColor = UIColor(Color("background2"))
 13        UISegmentedControl.appearance()
 14            .setTitleTextAttributes([.foregroundColor: UIColor(Color("lightYellow"))],
 15                                    for: .selected)
 16        UISegmentedControl.appearance()
 17            .setTitleTextAttributes([.foregroundColor: UIColor(Color("background2"))],
 18                                    for: .normal)
 19    }
 20    
 21    var body: some View {
 22        ZStack {
 23            Colors.bgGradient
 24                .edgesIgnoringSafeArea(.all)
 25            
 26            VStack {
 27                VStack(spacing:0) {
 28                    Text("Monty Hall Problem")
 29                        .foregroundColor(Color("mainText"))
 30                        .font(.custom("Helvetica Neue", size: 32, relativeTo: .title))
 31                        .fontWeight(.bold)
 32                    
 33                    NumberOfDoorsView(montyHallVm: montyHallVm,
 34                                      numOfDoors: montyHallVm.doors.count)
 35                }
 36                  
 37                HStack(spacing:10) {
 38                    ForEach(montyHallVm.doors) { door in
 39                        VStack {
 40                            DoorView(door: door,
 41                                     montyHall: montyHallVm)
 42                        }
 43                    }
 44                }
 45                .frame(width:360, height:150)
 46
 47                ZStack {
 48                    Spacer()
 49                        .frame(height:30)
 50                    if montyHallVm.isGameOver {
 51                        Text(montyHallVm.isGameWon ? "You Won" : "You Loose")
 52                    }
 53                }
 54                .foregroundColor(Color("mainText"))
 55                .font(.title2)
 56                
 57                VStack {
 58                    HStack {
 59                        Button( action: {
 60                            montyHallVm.reset()
 61                        })
 62                        {
 63                            ActionButtonView(label: "New",
 64                                             symbol: "asterisk")
 65                        }
 66                        
 67                        Button( action: {
 68                            montyHallVm.openOtherDoors()
 69                        })
 70                        {
 71                            ActionButtonView(label: "Open",
 72                                             symbol: "arrow.right.square")
 73                        }
 74                        .disabled(montyHallVm.isGameOver || montyHallVm.isHostDoorOpened || !montyHallVm.isDoorSelected)
 75
 76                    }
 77                    .buttonStyle(ActionButtonStyle())
 78                    
 79                    Button( action: {
 80                        montyHallVm.playGame()
 81                        metrics.gameOver(isWon: montyHallVm.isGameWon)
 82                    })
 83                    {
 84                        ActionButtonView(label: "play", symbol: "play.fill")
 85                    }
 86                    .disabled(montyHallVm.isGameOver || !montyHallVm.isDoorSelected)
 87                    .buttonStyle(ActionButtonStyle())
 88                }
 89                .frame(width:300)
 90                
 91                ResultBarView(won: metrics.won, lost: metrics.lost)
 92                    .frame(width: 350, height: 20, alignment: .center)
 93
 94                Rectangle()
 95                    .fill(Color("background1").opacity(0.3))
 96                    .frame(height: 5)
 97
 98                SimulationView(numOfDoors: montyHallVm.doors.count,
 99                               montySimulator: montySimulator)
100
101                Spacer()
102            }
103        }
104    }
105}

Monty Hall problem with six doors and simulation of 1000 runs
Monty Hall problem with six doors and simulation of 1000 runs



Simulation of Monty Hall Problem with three to six doors

Simulation of Monty Hall Problem with three to six doors




Conclusion

This is a variation of the Monty Hall Problem, where the number of doors is configurable from three to six. When the contestant chooses a door, the host open all but one of the remaining doors and offers the contestant the option to switch doors. It seems more intuitive to switch doors when there is an increased number of doors than it does when there are just three doors. The simulation demonstrates that it is always better to switch doors. The final screen shows that it is probably better, from a design point of view, to separate the game display and the simulation onto separate screens.