Color Guessing Game in Powershell [closed] - powershell

Closed. This question is not reproducible or was caused by typos. It is not currently accepting answers.
This question was caused by a typo or a problem that can no longer be reproduced. While similar questions may be on-topic here, this one was resolved in a way less likely to help future readers.
Closed last year.
Improve this question
I'm working on writing a script in PowerShell for a Color guessing game. The computer randomly picks a color then the player tries to guess the color. I had it working up until I switched some lines of code in the script in an attempt to get these two variables to display correctly. Now, I can't get the code to run past the point where a player declares if they want to play the game or not. My current error is with a do loop, where the console doesn't see that I have a while loop, and so throws an error and won't run the rest of the code.
I managed to get the formatting fixed with Visual Studio Code's format document feature, but I still can't get this one while loop problem figured out.
Write-Host ''; 'Hello again my friend!'; ''
$name = Read-Host "What is your name?"
Write-Host ''; "It's good to see you again, $name! Would you like to guess my favorite color?"; ''
$command = Read-Host #'
"How do you answer? (Yes or No?)"
1. Yes (y)
2. No (n)
3. Quit (q)
Enter Choice
'#
switch -Wildcard ($command) {
'Y' { 'Yes!'; '' }
{ $_ -eq 'y' -or $_ -like 'Ye*' } {
Write-Host "This will be fun! Let us begin!"
Break
}
'N' { 'No!'; '' }
{ $_ -eq 'n' -or $_ -like 'No*' } {
Write-Host "That's too bad, perhaps another day!"
Exit
}
'Q' { 'Quit'; '' }
{ $_ -eq 'q' -or $_ -like 'qu*' } {
Write-Host 'So long!'
Exit
}
default {
'Invalid Command, Please start over.'
Exit
}
}
[string]$playagain = 'y'
[int]$playerwins = 0
[int]$compwins = 0
[int]$totalguesses = 0
[int]$playergames = 0
[int]$compgames = 0
[int]$round = 1
[int]$game = 1
$cpuchoice = $color
while ($playagain -eq 'y') {
Write-Host ''; "Game $game!"; ''
$cpuchoice = #([System.Enum]::GetValues([System.ConsoleColor])) | Get-Random -Count 1
do {
Write-Host "Round $round! What is my favorite color?"; ''
$listcolor = Read-Host "Would you like to see a list of available colors? Choose 'y' for yes, and 'n' for no."
if ($listcolor -eq 'y') {
[System.Enum]::GetValues([System.ConsoleColor])
}
elseif ($listcolor -eq 'n') {
Write-Host "Suit yourself, let's start."
}
else {
Write-Host "Your choice was invalid. Please choose 'y' for yes, or 'n' for no."
}
do {
$playerchoice = Read-host "Enter your guess"
} while (([System.Enum]::GetValues([System.ConsoleColor])) -notcontains $playerchoice) {
if ($playerchoice -eq $cpuchoice ) {
Write-Host "You win, my favorite color is $cpuchoice." -ForegroundColor $cpuchoice; ''
$playerwins = $playerwins + 1
$totalguesses = $totalguesses + 1
}
elseif ($playerchoice -ne $cpuchoice ) {
Write-Host "You lose, try again."; ''
$playerguesses += $playerchoice
$playerguesses = $playerguesses.Split(',')
$totalguesses = $totalguesses + 1
Write-Host "Here are your guesses so far: "
$playerguesses
''
}
$round = $round + 1
}
until($playerwins -eq 1) {
$playergames = $playergames + 1
Write-Host "You've won this round and have won $playergames games." -ForegroundColor Green
Write-Host "Your total guesses: $totalguesses."
Write-Host "Your wins - $playergames" -ForegroundColor Yellow
Write-Host "Computer wins - $compgames" -ForegroundColor Yellow
''
}
$playagain = Read-Host "I enjoyed this game. Would you like to challenge again, $name? Y or N"
while (("y", "n") -notcontains $playagain) {
if ($playagain -eq "y") {
Write-Host "I look forward to our next battle!"; ''
$playerwins = 0
$compwins = 0
$game = $game + 1
}
elseif ($playagain -eq "n") {
Write-Host "Thank you for playing!"
exit
}
}
}
}
The do loop that causes the error is the one that starts with "Write-Host "Round $round!" after the first while statement.
Any help is appreciated! Thanks!

Your program is being parsed like this:
while ($playagain -eq 'y')
{
# do loop #1
do
{
# do loop #2
do {
}
while (([System.Enum]::GetValues([System.ConsoleColor])) -notcontains $playerchoice)
# floating script block #1
# (doesn't execute the scriptblock, but it gets sent to the output stream instead)
{
if ($playerchoice -eq $cpuchoice ) {
... etc ...
}
# try to invoke a cmdlet "until" with 2 parameters
# i.e. ($playerwins -eq 1) and { ... }
until ($playerwins -eq 1) {
$playergames = $playergames + 1
... etc ...
}
# while loop #1
while (("y", "n") -notcontains $playagain) {
...
}
}
}
The error is telling you the first do (do loop #1) doesn't have a trailing while or until.
There's no clear and simple fix I can offer to make your code run short of a significant rework because there's a number of issues (e.g. the floating script block #1, the dangling until and the while \ until-less do), but that's what the current error is saying anyway...

Related

ArrayList not displaying when first referenced in function

Facing a couple logistical issues in PowerShell - clearly I'm missing a basic concept:
Setup: Create the menu.ps1 file (shown below), launch PowerShell 7.2.2 and call the file locally.
Issues:
The first time you choose option 1 for the ArrayList ($psArrayList), it does not display (although we see from the initial screen load that the items are populated). If you return to the menu and choose option 1 again, it will display on the second pass. ($psArray does load fine on first try, so is this is a type issue.?)
When the script ends, $psArrayList and $psArray are still in the current session variables, as indicated by: Get-Variable psArray*. Even if I instantiate them with $script:psArrayList = [System.Collections.ArrayList]#() and $script:psArray = #() they seem to stay within the session scope. Is there a "right" way to clear them when the ps1 ends?
menu.ps1 contents:
$psArrayList = [System.Collections.ArrayList]#()
# example of populating later in function etc...
$psArrayList.Add([pscustomobject]#{name="bird";color="blue"})
$psArrayList.Add([pscustomobject]#{name="cat";color="orange"})
$psArrayList.Add([pscustomobject]#{name="bear";color="brown"})
$psArray = #()
# example of populating later in function etc...
$psArray += "dog"
$psArray += "fish"
$psArray += "squirrel"
function End-Script {
Remove-Variable psArray*
Exit
}
function Display-Menu {
[int]$choice=-1
Write-Host "This is a menu..." -ForegroundColor Green
Write-Host "Here are your options:"
Write-Host
Write-Host "`t1 - ArrayList"
Write-Host "`t2 - Array"
Write-Host "`t0 - quit (do nothing)"
Write-Host
while ($choice -lt 0) { $choice= Read-Host -Prompt "Choose 1-2 (or 0 to quit)" }
Process-Menu($choice)
}
function Process-Menu([int]$choice) {
switch($choice) {
1 { Write-Host "You chose ArrayList:"; Write-Output $psArrayList }
2 { Write-Host "You chose Array:"; Write-Output $psArray }
0 { Write-Host "You chose to quit. Exiting."; End-Script }
}
$yn=""
while ($yn -eq "") { $yn= Read-Host -Prompt "Return to main menu? (y/n)" }
if ($yn -eq "y") { Display-Menu } else { Write-Host "Ending..."; End-Script }
}
Display-Menu
Regarding the first issue, you would need to use Out-Host or Out-Default so that both outputs (Write-Host together with the arrays) are correctly displayed to the console. See these helpful answers for in depth details on this:
https://stackoverflow.com/a/50416448/15339544
https://stackoverflow.com/a/34858911/15339544
Regarding the second issue, your End-Script function would have a scope issue, Remove-Variable is trying to remove variables defined inside the function's scope (Local), if you want to target the variables defined outside it (Script), you would need to use the -Scope parameter, for example:
function End-Script {
Get-Variable psArray* | Remove-Variable -Scope Script
# `Remove-Variable psArray* -Scope Script` would be valid too
}
From the cmdlet's Parameters section we can read the following for the -Scope parameter:
A number relative to the current scope (0 through the number of scopes, where 0 is the current scope and 1 is its parent)
In that sense, -Scope 1 would also work.
Below you can see an example of your script with some improvements as well as input validation:
$psArrayList = [System.Collections.ArrayList]#()
$psArrayList.AddRange(#(
[pscustomobject]#{name="bird";color="blue"}
[pscustomobject]#{name="cat";color="orange"}
[pscustomobject]#{name="bear";color="brown"}
))
$psArray = "dog", "fish", "squirrel"
function End-Script {
Get-Variable psArray* | Remove-Variable -Scope Script
}
function Display-Menu {
Write-Host "This is a menu..." -ForegroundColor Green
Write-Host "Here are your options:"
Write-Host
Write-Host "`t1 - ArrayList"
Write-Host "`t2 - Array"
Write-Host "`t0 - quit (do nothing)"
Write-Host
# one of many methods for input validation is a Recursive Script Block:
$tryInput = {
try {
[ValidateSet(0, 1, 2)] $choice = Read-Host "Choose 1-2 (or 0 to quit)"
$choice
}
catch {
Write-Warning 'Invalid choice!'
& $tryInput
}
}
Process-Menu (& $tryInput)
}
function Process-Menu([int] $choice) {
switch($choice) {
1 {
Write-Host "You chose ArrayList:"
$psArrayList | Out-Host
}
2 {
Write-Host "You chose Array:"
$psArray | Out-Host
}
0 {
Write-Host "You chose to quit. Exiting."
End-Script
Return # => Exit this function
}
}
$tryInput = {
try {
[ValidateSet('y', 'n')] $choice = Read-Host "Return to main menu? (y/n)"
$choice
}
catch {
Write-Warning 'Invalid choice!'
& $tryInput
}
}
# No need to check for `N`
if((& $tryInput) -eq 'y') { Display-Menu }
}
Display-Menu

PowerShell Show Menu

I have the next code written in PowerShell.
When I run the code, the Menu is shown, I enter the option 1..9 and the selected option is not called, the menu is shown again and again(see the screenshot).
When I enter an option I want to be called that function related to each option entered then to display the message "The function has been called" and also to display the menu to enter a new option. (see the scrennshot - code in C++)
Any idea ?
function PORII
{
Write-Host " PORII was called"
}
function DXD-BODY
{
Write-Host " DXD BODY was called"
}
function DXD-PAINT
{
Write-Host " DXD PAINT was called"
}
function DXD-PTO
{
Write-Host " DXD PTO was called"
}
function DXD-TCF
{
Write-Host " DXD TCF was called"
}
function FIS-SERVERS
{
Write-Host " FIS SERVERS was called"
}
function SERVERS
{
Write-Host " SERVERS was called"
}
function Acronis
{
Write-Host " Acronis was called"
}
function Menu
{
param([string]$Title = 'Menu')
Write-Host " ==================== $Title ==================== "
while (1)
{
Clear-Host
Write-Host " Press 1 for PORII: "
Write-Host " Press 2 for DXD BODY: "
Write-Host " Press 3 for DXD PAINT: "
Write-Host " Press 4 for DXD PTO: "
Write-Host " Press 5 for DXD TCF: "
Write-Host " Press 6 for FIS SERVERS: "
Write-Host " Press 7 for SERVERS: "
Write-Host " Press 8 for Acronis Images: "
Write-Host " Press 9 for Exit: "
$a = Read-Host -Prompt "`n Enter your option "
if (($a -eq 1) -or ($a -eq 2) -or ($a -eq 3) -or ($a -eq 4) -or ($a -eq 5) -or ($a -eq 6) -or ($a -eq 7) -or ($a -eq 8) -or ($a -eq 9))
{
switch($a)
{
1{PORII}
2{DXD-BODY}
3{DXD-PAINT}
4{DXD-PTO}
5{DXD-TCF}
6{FIS-SERVERS}
7{SERVERS}
8{Acronis-Images}
9{Exit}
}
}
else
{
continue
}
}
}
Menu
Your functions are being called, but that isn't obvious because Clear-Host is called right after, discarding the called function's output.
Apart from that, your code can be streamlined:
Instead of the if (($a -eq 1) -or ($a -eq 2) ... conditional, you can add a default branch to your switch statement.
Also note that Read-Host always returns a string, whereas your conditionals operate on numbers; thanks to PowerShell's automatic type conversions, this isn't a problem in your particular case (with explicit or implied equality comparison), but it's something to keep in mind.
Through the use of a hash table you can simplify your function while making it easier to maintain.
Function Menu {
param([string]$Title = 'Menu')
while ($TRUE) {
$OptionHT = #{
1="PORII"
2="DXD-BODY"
3="DXD-PAINT"
4="DXD-PTO"
5="DXD-TCF"
6="FIS-SERVERS"
7="SERVERS"
8="Acronis-Images"
9="Exit"
}
Write-Host " ==================== $Title ==================== "
For ($Cntr = 1 ; $Cntr -lt $($OptionHT.Count) + 1; $Cntr++) {
Write-Host "Press $Cntr for $($OptionHT.$($Cntr)):"
}
$a = Read-Host -Prompt "`n Enter your option "
If (($a.length) -eq 1 -and ([byte][char]$a) -ge 49 -and
([byte][char]$a) -le 57) {
& ($OptionHT.[int]$a)
}
} # End While ($True)
} # End Function Menu
By placing your options in the hash table you only have a single point to make changes for called function names.
The if statement vs switch eliminates any value other than a single single number from 1 to 9 (note use of ASCII values to verify number input) from being processed. And since we have eliminated invalid inputs a single statement can be used to search the hash table for the function to execute.
UPDATE: Per the comments below you'll have to either trap the EXIT (9) with an If statement and exit or Create another Function and call it something like Exit-Program and place the Exit command there, I tested a function and it works.
Note: I didn't clear the console between writes of the menu so you could see your selection as mentioned in the comments you can add it where you deem necessary.
HTH

How can I allow only Y/N to be input in Read-Host without having to press Enter in Powershell?

I'm trying to find a way to have something like a Read-Host to ask the user if they want to output to the file listed or not. With this I want them to either press y or n and then the code continues rather than then pressing y/n then pressing enter as well. At the moment this all works well but again it's not quite what I'm wanting.
I've tried looking into Readkey and SendKeys (to push Enter for the user) but neither work as they seem to only execute after the user has pushed Enter on the Read-Host. I'm still very new to Powershell so I'm not entirely sure whether it's actually possible or not and I've spent too much time googling/testing to find an answer that works. If I was to use Write-Host or something to do this, it needs to not show up in the log.
I've included the necessary part of my script below. It basically asks the user if the file location is correct. If it is they press y and it uses it for the output, otherwise if they push n then it loads the FolderBrowserDialog for them to select the folder they want.
I should also note this is all within a Tee-object as this code is what determines where the Tee-object output goes to.
$OutputYN = Read-Host "Do you want the output file generated to $startDirectory\FolderList.txt? (Y/N)"
If (“y”,”n” -notcontains $OutputYN) {
Do {
$OutputYN = Read-Host "Please input either a 'Y' for yes or a 'N' for no"
} While (“y”,”n” -notcontains $OutputYN)
}
if ($OutputYN -eq "Y") {
$OutputLoc = $startDirectory
}
elseif ($OutputYN -eq "N") {
$OutputLocDir = New-Object System.Windows.Forms.FolderBrowserDialog
$OutputLocDir.Description = "Select a folder for the output"
$OutputLocDir.SelectedPath = "$StartDirectory"
if ($OutputLocDir.ShowDialog() -eq "OK") {
$OutputLoc = $OutputLocDir.SelectedPath
$OutputLoc = $OutputLoc.TrimEnd('\')
}
}
EDIT:
I should have been a little more clear. I had already tried message box type stuff as well but I'd really prefer if there is a way that the user types in a y or a n. I'm not really interested in a popup box that the user has to click. If it's not possible then so be it.
Readkey is the right way.
Use the following as template.
:prompt while ($true) {
switch ([console]::ReadKey($true).Key) {
{ $_ -eq [System.ConsoleKey]::Y } { break prompt }
{ $_ -eq [System.ConsoleKey]::N } { return }
default { Write-Error "Only 'Y' or 'N' allowed!" }
}
}
write-host 'do it' -ForegroundColor Green
:prompt gives the outer loop (while) a name which can be used in the switch statement to directly break out entirely via break prompt (and not within the switch statement).
Alternative (for Windows):
Use a MessageBox.
Add-Type -AssemblyName PresentationFramework
$messageBoxResult = [System.Windows.MessageBox]::Show("Do you want the output file generated to $startDirectory\FolderList.txt?" , 'Question' , [System.Windows.MessageBoxButton]::YesNo , [System.Windows.MessageBoxImage]::Question)
switch ($messageBoxResult) {
{ $_ -eq [System.Windows.MessageBoxResult]::Yes } {
'do this'
break
}
{ $_ -eq [System.Windows.MessageBoxResult]::No } {
'do that'
break
}
default {
# stop
return # or EXIT
}
}
Not sure if this is possible in the console. But when I need the user to write one answer of a specified set, I use a do-until-loop like:
Do {
$a = Read-Host "Y / N"
} until ( 'y', 'n' - contains $a )
try this:
$title = 'Question'
$question = 'Do you want the output file generated to $startDirectory\FolderList.txt?'
$choices = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes'))
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No'))
$decision = $Host.UI.PromptForChoice($title, $question, $choices, 1)
if ($decision -eq 0) {
Write-Host 'Yes'
} else {
Write-Host 'No'
}
If you are on Windows, you can do it :
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
$result = [System.Windows.Forms.MessageBox]::Show('Do you want the output file generated to $startDirectory\FolderList.txt?' , "Question" , [System.Windows.Forms.MessageBoxButtons]::YesNo, [System.Windows.Forms.MessageBoxIcon]::Question)
if ($result -eq 'Yes') {
"Yes"
}
else
{
"No"
}

Problem with switch statement in a while loop in PowerShell

For whatever reason the While loop works by itself, the Switch statement works by itself, when I combine them.. the While loop works ok, the Switch statement though.. not so much.
y or n are only values the While loop accepts, the problem is that when I give it y or n, none of the code gets executed, the script just finishes.
PowerShell version is 5.1.
While (($UserInput = Read-Host -Prompt "Are you sure? (y/n)") -notmatch '^n$|^y$') {
Switch ($UserInput) {
'y' {
Try {
Write-Output "Success."
}
Catch {
Write-Output "Error."
}
}
'n' {
Write-Output "Cancelled."
}
}
}
here's a rather more robust method to do what you seem to want. it sets up the valid choices, asks for input, detects invalid input, warns about that, displays the "success" or "failure" messages - all without twisty logic. [grin]
$Choice = ''
$ValidChoiceList = #(
'n'
'y'
)
while ([string]::IsNullOrEmpty($Choice))
{
$Choice = Read-Host 'Are you sure? [n/y] '
if ($Choice -notin $ValidChoiceList)
{
[console]::Beep(1000, 300)
Write-Warning ('Your choice [ {0} ] is not valid.' -f $Choice)
Write-Warning ' Please try again & choose "n" or "y".'
$Choice = ''
pause
}
switch ($Choice)
{
'y' {Write-Host 'Success!'; break}
'n' {Write-Warning ' Failure!'; break}
}
}
on screen output ...
Are you sure? [n/y] : t
WARNING: Your choice [ t ] is not valid.
WARNING: Please try again & choose "n" or "y".
Press Enter to continue...:
Are you sure? [n/y] : y
Success!
You are using -notmatch. Hence While loop is resulting in false and loop is not getting executed. As you want to execute the script until you get 'y' or 'n' as input, just use ! which will execute the script until it receives 'y' or 'n' as input.
Use below code:
While (!($UserInput = Read-Host -Prompt "Are you sure? (y/n)") -notmatch '^n$|^y$') {
Switch ($UserInput) {
'y' {
Try {
Write-Output "Success."
}
Catch {
Write-Output "Error."
}
}
'n' {
Write-Output "Cancelled."
}
}
}

How to concatenate 2 Write-Output on the same line?

I wrote a script that can return you if a software is installed or not.
I would like to give it a bit of colors but I don't know how to concatenate 2 Write-Output on the same line as -NoNewline only works for Write-Host... which in this case I cannot use:
# The function we use to give color
function Positive {
process { Write-Host $_ -ForegroundColor Green }
}
function Negative {
process { Write-Host $_ -ForegroundColor Red }
}
# Is the software installed?
# '0' = NO, is not installed
# '1' = YES, is installed
$Check = '1'
function Check_Installation($Check){
if ($Check -eq '0') {return $response = "No, is not installed" | Negative}
elseif ($Check -eq '1') {return $response = "Yes, is installed" | Positive}
}
$First_Phrase = "Let's check if the software is installed: "
Write-Output "$First_Phrase", "$response"
Check_Installation($Check)
I know I can concatenate with
[string]::Concat("$First_Phrase", "$response")
but is not working.
This works in console only as changing the foreground colour in ISE changes it for every line:
# The function we use to give color
function Set-Output {
param ($colour, $str1, $str2)
$t = $host.ui.RawUI.ForegroundColor
$host.ui.RawUI.ForegroundColor = "$colour"
$text = "$str1 $str2"
Write-Output "$text"
$host.ui.RawUI.ForegroundColor = $t
}
# Is the software installed?
# '0' = NO, is not installed
# '1' = YES, is installed
$Check = '1'
$First_Phrase = "Let's check if the software is installed: "
Switch ($check) {
( 0 ) {Set-Output -colour RED -str1 $First_Phrase -str2 "No, is not installed" }
( 1 ) {Set-Output -colour GREEN -str1 $First_Phrase -str2 "Yes, is installed" }
}
Check_Installation($Check)
All that it is doing is concatenating the two strings and changing the foreground colour.