Écrire correctement les applets de commande Powershell et simuler le paradoxe de Monty Hall

Habr est définitivement familier avec le paradoxe, mais probablement pas avec certaines caractéristiques du pavé, alors voici plus à ce sujet.









Utilisation du pipeline dans Powershell



L'algorithme est simple, le premier est le générateur de porte aléatoire, puis le générateur de choix de l'utilisateur, puis la logique d'ouverture de la porte du présentateur, une autre action de l'utilisateur et le comptage des statistiques.



Et nous serons aidés avec cela par l'outil électrique ValueFromPipeline



, qui nous permet de spécifier l'applet de commande une par une, en transformant l'objet étape par étape. Notre pipeline devrait ressembler à ceci:



New-Doors | Select-Door | Open-Door | Invoke-UserAction
      
      





New-Doors



génère de nouvelles portes, dans l'équipe le Select-Door



joueur choisit l'une des portes, le Open-Door



leader ouvre la porte dans laquelle il n'y a définitivement pas de chèvre et qui n'a pas été choisie par le joueur, et dans Invoke-UserAction



nous simulons différents comportements d'utilisateurs.



L'objet décrivant la porte se déplace de gauche à droite, se transformant progressivement.



Cette méthode d'écriture du code permet de le garder en morceaux avec une répartition claire des responsabilités.



Powershell a ses propres conventions. Y compris les conventions sur la dénomination correcte des fonctions , elles doivent également être respectées et nous les respectons presque.



Faire des portes



Puisque nous allons simuler la situation, nous décrirons également les portes en détail.



La porte contient soit une chèvre, soit une voiture. La porte peut être choisie par le joueur ou ouverte par l'hôte.



class Door {
    <#
     ,    . 
            .
    #>
    [string]$Contains = "Goat"
    [bool]$Selected = $false
    [bool]$Opened = $false
}

      
      





Nous placerons chacune des portes dans un champ séparé dans une classe distincte.



class Doors {
    <#
     ,   3 
    #>
    [Door]$DoorOne 
    [Door]$DoorTwo 
    [Door]$DoorThree
}
      
      





Il était possible de mettre toutes les portes dans un tableau, mais plus tout est détaillé, mieux c'est. À propos, dans Powershell 7, les classes, leurs constructeurs, leurs méthodes et tout le reste sont de la POO, qui fonctionne presque comme il se doit, mais plus à ce sujet une autre fois. 



Le générateur de porte aléatoire ressemble à ceci. Tout d'abord, pour chaque montant de porte, sa propre porte est générée, puis le générateur choisit lequel d'entre eux la voiture se tiendra derrière.



function New-Doors {
    <#
      .
    #>
    $i = [Doors]::new()
 
    $i.DoorOne = [Door]::new()
    $i.DoorTwo = [Door]::new()
    $i.DoorThree = [Door]::new()
 
    switch ( Get-Random -Maximum 3 -Minimum 0 ) {
        0 { 
            $i.DoorOne.Contains = "Car"
        }
        1 { 
            $i.DoorTwo.Contains = "Car"
        }
        2 { 
            $i.DoorThree.Contains = "Car"
        }
        Default {
            Write-Error "Something in door generator went wrong"
            break
        }
    }
    
    return $i

      
      





Notre pipe ressemble à ceci:



New-Doors
      
      





Le joueur choisit la porte



Décrivons maintenant le choix initial. Le joueur peut choisir l'une des trois portes. Pour simuler davantage de situations, laissez le joueur choisir uniquement la première, la deuxième, la troisième et la porte au hasard à chaque fois. 



[Parameter(Mandatory)]
[ValidateSet("First", "Second", "Third", "Random")]
$Principle
      
      





Pour accepter les arguments du pipeline, vous devez spécifier une variable dans le bloc de paramètres qui fera cela. Ceci est fait comme ceci:



[parameter(ValueFromPipeline)]
[Doors]$i
      
      





Vous pouvez écrire ValueFromPipeline



sans True



.



Voici à quoi ressemble le bloc de sélection de porte fini:



function Select-Door {
    <#
      .
    #>
    Param (
        [parameter(ValueFromPipeline)]
        [Doors]$i,
        [Parameter(Mandatory)]
        [ValidateSet("First", "Second", "Third", "Random")]
        $Principle
    )
    
    switch ($Principle) {
        "First" {
            $i.DoorOne.Selected = $true
        }
        "Second" {
            $i.DoorTwo.Selected = $true
        }
        "Third" {
            $i.DoorThree.Selected = $true
        }
        "Random" {
            switch ( Get-Random -Maximum 3 -Minimum 0 ) {
                0 { 
                    $i.DoorOne.Selected = $true
                }
                1 { 
                    $i.DoorTwo.Selected = $true
                }
                2 { 
                    $i.DoorThree.Selected = $true
                }
                Default {
                    Write-Error "Something in door selector went wrong"
                    break
                }
            }
        }
        Default {
            Write-Error "Something in door selector went wrong"
            break
        }
    }
 
    return $i 

      
      





Notre pipe ressemble à ceci:



New-Doors | Select-Door -Principle Random
      
      





Diriger ouvre la porte



Tout est très simple ici. Si la porte n'a pas été choisie par le joueur et s'il y a une chèvre derrière, changez le champ Opened



en True



. Plus précisément, dans ce cas, il n'est Open



pas correct de nommer la commande avec un mot , la ressource appelée n'est pas lue, mais modifiée. Dans de tels cas, utilisez Set



, mais Open



laissez pour plus de clarté.



function Open-Door {
    <#
        ,   ,   .
    #>
    Param (
        [parameter(ValueFromPipeline)]
        [Doors]$i
    )
    switch ($false) {
        $i.DoorOne.Selected {
            if ($i.DoorOne.Contains -eq "Goat") {
                $i.DoorOne.Opened = $true
                continue
            }
           
        }
        $i.DoorTwo.Selected { 
            if ($i.DoorTwo.Contains -eq "Goat") {
                $i.DoorTwo.Opened = $true
                continue
            }
           
        }
        $i.DoorThree.Selected { 
            if ($i.DoorThree.Contains -eq "Goat") {
                $i.DoorThree.Opened = $true
                continue
            }
            
        }
    }
    return $i

      
      





Pour rendre notre simulation plus convaincante, nous «ouvrons» cette porte en remplaçant le champ .opened par au $true



lieu de supprimer l'objet du tableau de portes.



N'oubliez pas les continue



interrupteurs, la comparaison ne s'arrête pas après le premier match. Coninue



quitte le commutateur et continue d'exécuter le script, et l'opérateur break



du commutateur met fin au script.



Ajoutez une autre fonction au tube, cela ressemble maintenant à ceci:



New-Doors | Select-Door -Principle Random | Open-Door
      
      





Le joueur change son choix 



Le joueur change la porte ou ne la change pas. Dans le bloc de paramètres, nous n'avons qu'une variable du tube et un argument booléen. 



Utilisez le mot Invoke



dans les noms de ces fonctions, car cela Invoke



signifie appeler une opération synchrone, et Start



asynchrone, suivez les conventions et recommandations.



function Invoke-UserAction {
    <#
    ,        .
    #>
    Param (
        [parameter(ValueFromPipeline)]
        [Doors]$i,
        [Parameter(Mandatory)]
        [bool]$SwitchDoor
    )
 
    if ($true -eq $SwitchDoor) {
        switch ($false) {
            $i.DoorOne.Opened {  
                if ( $i.DoorOne.Selected ) {
                    $i.DoorOne.Selected = $false
                }
                else {
                    $i.DoorOne.Selected = $true
                }
            }
            $i.DoorTwo.Opened {
                if ( $i.DoorTwo.Selected ) {
                    $i.DoorTwo.Selected = $false
                }
                else {
                    $i.DoorTwo.Selected = $true
                }
            }
            $i.DoorThree.Opened {
                if ( $i.DoorThree.Selected ) {
                    $i.DoorThree.Selected = $false
                }
                else {
                    $i.DoorThree.Selected = $true
                }
            }
        }  
    }
 
    return $i

      
      





Dans les opérateurs de branchement et de comparaison, le système et les variables statiques doivent être précisés en premier. Probablement, il peut y avoir des difficultés à convertir un objet en un autre, mais l'auteur n'a pas rencontré de telles difficultés lorsqu'il écrivait d'une manière différente auparavant.



Une autre fonction dans le pipeline.



New-Doors | Select-Door -Principle Random | Open-Door | Invoke-UserAction -SwitchDoor $True
      
      





L'avantage de cette approche d'écriture est clair, car il n'a jamais été aussi pratique de diviser le code en parties avec une séparation claire des fonctions.



Comportement du joueur



À quelle fréquence le joueur change la porte. Il y a 5 lignes de comportement:



  1. Never



    - le joueur ne change jamais son choix
  2. Fifty-Fifty



    - 50 à 50. Le nombre de simulations est divisé en deux passes. La première passe le joueur ne change pas la porte, la seconde passe.
  3. Random



    - à chaque nouvelle simulation, le joueur lance une pièce
  4. Always



    - le joueur change toujours son choix.
  5. Ration



    - le joueur change son choix dans N% des cas.


switch ($SwitchDoors) {
        "Never" { 
            0..$Count | ForEach-Object {
                $Win += Invoke-Simulation -Door $Door -SwitchDoors $false
            }
            continue
        }
        "FiftyFifty" {
            $Fifty = [math]::Round($Count / 2)
 
            0..$Fifty | ForEach-Object {
                $Win += Invoke-Simulation -Door $Door -SwitchDoors $false
            }
 
            0..$Fifty | ForEach-Object {
                $Win += Invoke-Simulation -Door $Door -SwitchDoors $true
            }
            continue
        }
        "Random" {
            0..$Count | ForEach-Object {
                [bool]$Random = Get-Random -Maximum 2 -Minimum 0
                $Win += Invoke-Simulation -Door $Door -SwitchDoors $Random
            }
            continue
        }
        "Always" {
            0..$Count | ForEach-Object {
                $Win += Invoke-Simulation -Door $Door -SwitchDoors $true
            }
            continue
        }
        "Ratio" {
            $TrueRatio = $Ratio / 100 * $Count 
            $FalseRatio = $Count - $TrueRatio
 
            0..$TrueRatio | ForEach-Object {
                $Win += Invoke-Simulation -Door $Door -SwitchDoors $true
            }
 
            0..$FalseRatio | ForEach-Object {
                $Win += Invoke-Simulation -Door $Door -SwitchDoors $false
            }
            continue
        }
    }

      
      





ForEach-Object



dans Powershell 7, cela fonctionne beaucoup plus rapidement qu'une boucle for



, en plus il peut être parallélisé, donc il est utilisé ici au lieu d'une boucle for



.



Styliser l'applet de commande



Vous devez maintenant corriger l'applet de commande. Tout d'abord, vous devez faire la validation des arguments entrants. Le bonus n'est pas seulement qu'une personne ne peut pas entrer un argument non valide dans le champ, mais une liste de tous les arguments disponibles apparaît dans les invites.



Voici à quoi ressemble le code du bloc de paramètres:



param (
        [Parameter(Mandatory = $false,
            HelpMessage = "How often the player changes his choice.")]
        [ValidateSet("Never", "FiftyFifty", "Random", "Always", "Ratio")]
        $SwitchDoors = "Random"
    )

      
      





Voici le conseil:





Avant que le bloc de paramètres puisse être fait comment based help



. Voici à quoi ressemble le code avant le bloc de paramètres:




  <#
      .SYNOPSIS
   
      Performs monty hall paradox simulation.
   
      .DESCRIPTION
   
      The Invoke-MontyHallParadox.ps1 script invoke monty hall paradox simulation.
   
      .PARAMETER Door
      Specifies door the player will choose during the entire simulation
   
      .PARAMETER SwitchDoors
      Specifies principle how the player changes his choice.
   
      .PARAMETER Count
      Specifies how many times to run the simulation.
   
      .PARAMETER Ratio
      If -SwitchDoors Ratio, specifies how often the player changes his choice. As a percentage."
   
      .INPUTS
   
      None. You cannot pipe objects to Update-Month.ps1.
   
      .OUTPUTS
   
      None. Update-Month.ps1 does not generate any output.
   
      .EXAMPLE
   
      PS> Invoke-MontyHallParadox -SwitchDoors Always -Count 10000
   
      #>

      
      





Voici à quoi ressemble l'invite:





Lancer la simulation



Résultats de la simulation:





Si une personne ne change jamais son choix, elle gagne 33,37% du temps.



Dans le cas de deux passes, dont la moitié nous refusons de modifier notre choix, les chances de gagner sont de 49,9134%, ce qui est très proche d'exactement 50%.



Dans le cas d'un tirage au sort, rien ne change, les chances de gagner restent autour de 50,131%.



Eh bien, si le joueur change toujours son choix, les chances de gagner s'élèvent à 66,6184%, c'est-à-dire ennuyeuses et rien de nouveau.



Performance:



en termes de performances. Le script ne semble pas optimal. String



au lieu de cela Bool



, de nombreuses fonctions différentes avec un interrupteur à l'intérieur, se passant un objet entre elles, mais néanmoins, voici les résultats Measure-Command



pour ce script et un script d'un autre auteur .



La comparaison a été effectuée sur deux systèmes, pwsh 7.1 était partout, 100 000 passes.



▍I5-5200u



Cet algorithme:



Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 4
Milliseconds      : 581
Ticks             : 45811819
TotalDays         : 5,30229386574074E-05
TotalHours        : 0,00127255052777778
TotalMinutes      : 0,0763530316666667
TotalSeconds      : 4,5811819
TotalMilliseconds : 4581,1819
      
      





Cet algorithme:



Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 5
Milliseconds      : 104
Ticks             : 51048392
TotalDays         : 5,9083787037037E-05
TotalHours        : 0,00141801088888889
TotalMinutes      : 0,0850806533333333
TotalSeconds      : 5,1048392
TotalMilliseconds : 5104,8392
      
      





▍I9-9900K



Cet algorithme:



Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 1
Milliseconds      : 891
Ticks             : 18917629
TotalDays         : 2,18954039351852E-05
TotalHours        : 0,000525489694444444
TotalMinutes      : 0,0315293816666667  
TotalSeconds      : 1,8917629
TotalMilliseconds : 1891,7629
      
      





Cet algorithme:



Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 1
Milliseconds      : 954
Ticks             : 19543236
TotalDays         : 2,26194861111111E-05
TotalHours        : 0,000542867666666667
TotalMinutes      : 0,03257206
TotalSeconds      : 1,9543236
TotalMilliseconds : 1954,3236
      
      





Avantage de 63 ms, mais les résultats sont toujours très étranges compte tenu du nombre de fois que le script compare des chaînes.



L'auteur espère que cet article servira d'exemple convaincant pour ceux qui croient que les chances sont toujours de 50 à 50, mais vous pouvez lire le code sous ce spoiler.



Le code entier
class Doors {

<#

, 3

#>

[Door]$DoorOne

[Door]$DoorTwo

[Door]$DoorThree

}



class Door {

<#

, .

.

#>

[string]$Contains = «Goat»

[bool]$Selected = $false

[bool]$Opened = $false

}



function New-Doors {

<#

.

#>

$i = [Doors]::new()



$i.DoorOne = [Door]::new()

$i.DoorTwo = [Door]::new()

$i.DoorThree = [Door]::new()



switch ( Get-Random -Maximum 3 -Minimum 0 ) {

0 {

$i.DoorOne.Contains = «Car»

}

1 {

$i.DoorTwo.Contains = «Car»

}

2 {

$i.DoorThree.Contains = «Car»

}

Default {

Write-Error «Something in door generator went wrong»

break

}

}



return $i

}



function Select-Door {

<#

.

#>

Param (

[parameter(ValueFromPipeline)]

[Doors]$i,

[Parameter(Mandatory)]

[ValidateSet(«First», «Second», «Third», «Random»)]

$Principle

)



switch ($Principle) {

«First» {

$i.DoorOne.Selected = $true

continue

}

«Second» {

$i.DoorTwo.Selected = $true

continue

}

«Third» {

$i.DoorThree.Selected = $true

continue

}

«Random» {

switch ( Get-Random -Maximum 3 -Minimum 0 ) {

0 {

$i.DoorOne.Selected = $true

continue

}

1 {

$i.DoorTwo.Selected = $true

continue

}

2 {

$i.DoorThree.Selected = $true

continue

}

Default {

Write-Error «Something in selector generator went wrong»

break

}

}

continue

}

Default {

Write-Error «Something in door selector went wrong»

break

}

}



return $i

}



function Open-Door {

<#

, , .

#>

Param (

[parameter(ValueFromPipeline)]

[Doors]$i

)

switch ($false) {

$i.DoorOne.Selected {

if ($i.DoorOne.Contains -eq «Goat») {

$i.DoorOne.Opened = $true

continue

}

}

$i.DoorTwo.Selected {

if ($i.DoorTwo.Contains -eq «Goat») {

$i.DoorTwo.Opened = $true

continue

}

}

$i.DoorThree.Selected {

if ($i.DoorThree.Contains -eq «Goat») {

$i.DoorThree.Opened = $true

continue

}

}

}

return $i

}



function Invoke-UserAction {

<#

, .

#>

Param (

[parameter(ValueFromPipeline)]

[Doors]$i,

[Parameter(Mandatory)]

[bool]$SwitchDoor

)



if ($true -eq $SwitchDoor) {

switch ($false) {

$i.DoorOne.Opened {

if ( $i.DoorOne.Selected ) {

$i.DoorOne.Selected = $false

}

else {

$i.DoorOne.Selected = $true

}

}

$i.DoorTwo.Opened {

if ( $i.DoorTwo.Selected ) {

$i.DoorTwo.Selected = $false

}

else {

$i.DoorTwo.Selected = $true

}

}

$i.DoorThree.Opened {

if ( $i.DoorThree.Selected ) {

$i.DoorThree.Selected = $false

}

else {

$i.DoorThree.Selected = $true

}

}

}

}



return $i

}



function Get-Win {

Param (

[parameter(ValueFromPipeline)]

[Doors]$i

)

switch ($true) {

($i.DoorOne.Selected -and $i.DoorOne.Contains -eq «Car») {

return $true

}

($i.DoorTwo.Selected -and $i.DoorTwo.Contains -eq «Car») {

return $true

}

($i.DoorThree.Selected -and $i.DoorThree.Contains -eq «Car») {

return $true

}

default {

return $false

}

}

}



function Invoke-Simulation {

param (

[Parameter(Mandatory = $false,

HelpMessage = «Which door the player will choose during the entire simulation.»)]

[ValidateSet(«First», «Second», «Third», «Random»)]

$Door = «Random»,



[bool]$SwitchDoors

)

return New-Doors | Select-Door -Principle $Door | Open-Door | Invoke-UserAction -SwitchDoor $SwitchDoors | Get-Win

}



function Invoke-MontyHallParadox {

<#

.SYNOPSIS



Performs monty hall paradox simulation.



.DESCRIPTION



The Invoke-MontyHallParadox.ps1 script invoke monty hall paradox simulation.



.PARAMETER Door

Specifies door the player will choose during the entire simulation



.PARAMETER SwitchDoors

Specifies principle how the player changes his choice.



.PARAMETER Count

Specifies how many times to run the simulation.



.PARAMETER Ratio

If -SwitchDoors Ratio, specifies how often the player changes his choice. As a percentage."



.INPUTS



None. You cannot pipe objects to Update-Month.ps1.



.OUTPUTS



None. Update-Month.ps1 does not generate any output.



.EXAMPLE



PS> Invoke-MontyHallParadox -SwitchDoors Always -Count 10000



#>

param (

[Parameter(Mandatory = $false,

HelpMessage = «Which door the player will choose during the entire simulation.»)]

[ValidateSet(«First», «Second», «Third», «Random»)]

$Door = «Random»,



[Parameter(Mandatory = $false,

HelpMessage = «How often the player changes his choice.»)]

[ValidateSet(«Never», «FiftyFifty», «Random», «Always», «Ratio»)]

$SwitchDoors = «Random»,



[Parameter(Mandatory = $false,

HelpMessage = «How many times to run the simulation.»)]

[uint32]$Count = 10000,



[Parameter(Mandatory = $false,

HelpMessage = «How often the player changes his choice. As a percentage.»)]

[uint32]$Ratio = 30

)



[uint32]$Win = 0



switch ($SwitchDoors) {

«Never» {

0..$Count | ForEach-Object {

$Win += Invoke-Simulation -Door $Door -SwitchDoors $false

}

continue

}

«FiftyFifty» {

$Fifty = [math]::Round($Count / 2)



0..$Fifty | ForEach-Object {

$Win += Invoke-Simulation -Door $Door -SwitchDoors $false

}



0..$Fifty | ForEach-Object {

$Win += Invoke-Simulation -Door $Door -SwitchDoors $true

}

continue

}

«Random» {

0..$Count | ForEach-Object {

[bool]$Random = Get-Random -Maximum 2 -Minimum 0

$Win += Invoke-Simulation -Door $Door -SwitchDoors $Random

}

continue

}

«Always» {

0..$Count | ForEach-Object {

$Win += Invoke-Simulation -Door $Door -SwitchDoors $true

}

continue

}

«Ratio» {

$TrueRatio = $Ratio / 100 * $Count

$FalseRatio = $Count — $TrueRatio



0..$TrueRatio | ForEach-Object {

$Win += Invoke-Simulation -Door $Door -SwitchDoors $true

}



0..$FalseRatio | ForEach-Object {

$Win += Invoke-Simulation -Door $Door -SwitchDoors $false

}

continue

}

}



Write-Output («Player won in » + $Win + " times out of " + $Count)

Write-Output («Whitch is » + ($Win / $Count * 100) + "%")



return $Win

}



#Invoke-MontyHallParadox -SwitchDoors Always -Count 500000












All Articles