Last active
July 16, 2023 08:54
-
-
Save zett42/eb892cdaeba8dcb1bd723828dbce2204 to your computer and use it in GitHub Desktop.
Guess the password - minigame inspired by Fallout
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# | |
.SYNOPSIS | |
A PowerShell game inspired by the Fallout hacking minigame. | |
.DESCRIPTION | |
This is a PowerShell game inspired by the Fallout hacking minigame. | |
The game is played by guessing the password of a computer terminal. | |
The player has a limited number of attempts to guess the password. | |
After each guess, the game will display the number of correct letters. | |
The player can use this information to narrow down the list of possible passwords. | |
The game has multiple difficulty levels. The higher the level, the longer the passwords to guess become. | |
.LINK | |
https://gist.github.com/zett42/eb892cdaeba8dcb1bd723828dbce2204 | |
#> | |
# Constants | |
$MAX_ATTEMPTS = 5 | |
# The available passwords grouped by difficulty level | |
$PASSWORDS_BY_LEVEL = @{ | |
1 = @( | |
'BLINK', 'BLURB', 'BRAVE', 'BRISK', 'BROOD', 'BRUTE', 'BUNCH', 'BURST', 'CHALK', 'CLAMP' | |
'CLOAK', 'CLONE', 'CRAZY', 'CRISP', 'CROWN', 'CRUDE', 'CRUMB', 'CRUSH', 'DRAKE', 'FLANK' | |
'FLING', 'FLIRT', 'FLOCK', 'FLOOD', 'FLUFF', 'FLUKE', 'FLUTE', 'FREAK', 'GHOUL', 'GLADE' | |
'GLINT', 'GRIME', 'GRIND', 'GROAN', 'GROOM', 'GRUMP', 'GRUNT', 'GUARD', 'PLUSH', 'PRUNE' | |
'QUEST', 'ROBOT', 'SKULL', 'SLACK', 'SNAKE', 'SPLIT', 'SPRIG', 'SPURT', 'STACK', 'VAULT' | |
) | |
2 = @( | |
'ARMOUR', 'BOSSES', 'BOTTLE', 'BRAINS', 'BUNKER', 'CANNON', 'CRATER', 'DANGER', 'DEALER', 'DEBRIS' | |
'DUSTER', 'ESCAPE', 'FATHER', 'FUSION', 'GAMBIT', 'GHOULS', 'GIZMOS', 'GLITCH', 'GUNNER', 'HACKER' | |
'HAMMER', 'HAROLD', 'HARPER', 'HAZARD', 'HIDDEN', 'HUNTER', 'INSECT', 'JACKAL', 'JUNKER', 'KNIGHT' | |
'LONELY', 'MADDOG', 'MADMAN', 'MIDGET', 'MOTHER', 'MUTANT', 'NOVICE', 'NUKING', 'OUTFIT', 'PATROL' | |
'PERKUP', 'PIPBOY', 'PLASMA', 'RAIDER', 'PURIFY', 'REAPER', 'REPAIR', 'TACTIC', 'TUNNEL', 'TURRET' | |
) | |
3 = @( | |
'ARMORED', 'BANDITS', 'BIGTOWN', 'BLAZING', 'BRAVERY', 'BROTHER', 'CALIBER', 'CANNONS', 'CANTINA', 'CITADEL' | |
'CLARITY', 'DESTINY', 'FALLOUT', 'FORTIFY', 'FORTUNE', 'GIRDERS', 'HACKING', 'HARVEST', 'HUNTERS', 'INVADED' | |
'LAMPOON', 'MADNESS', 'MIDTOWN', 'MONSTER', 'MUTANTS', 'NEUTRON', 'NUCLEAR', 'PALADIN', 'POWERUP', 'PROJECT' | |
'RAIDERS', 'SALVAGE', 'SCIENCE', 'SETTLER', 'SHACKLE', 'SPECIAL', 'STIMPAK', 'STRANGE', 'SURVIVE', 'TUMBLER' | |
'TACTICS', 'TERRIFY', 'VILLAGE', 'THERMAL', 'TROUBLE', 'TUNNELS', 'TRAITOR', 'VAULT11', 'WARRIOR', 'VICTORY' | |
) | |
} | |
#-------------------------------------------------------------------------------------------- | |
<# | |
.SYNOPSIS | |
Starts the game and handles the game loop. | |
.PARAMETER Level | |
The difficulty level of the game. | |
.OUTPUTS | |
Outputs $true if the user won the game, $false otherwise. | |
#> | |
Function Start-HackerGame { | |
param ( | |
[Parameter(Mandatory)] | |
[ValidateRange(1, 3)] | |
[int] $Level | |
) | |
$passwords = $PASSWORDS_BY_LEVEL[ $Level ] | |
# Pick random words from the list of available passwords. | |
$words = $passwords | Get-Random -Count 20 | |
# Pick one random word as the password. | |
$password = Get-Random -InputObject $words | |
$attempts = $MAX_ATTEMPTS | |
$history = [Collections.Generic.Queue[string]]::new() | |
# Calculating a random seed once and passing it on to Write-GameScreen makes sure the "memory dump" will be the same for each attempt. | |
$randomSeed = Get-Random | |
# Start the game loop | |
while( $attempts -gt 0 ) { | |
Write-GameScreen -Attempts $attempts -Words $words -History $history -RandomSeed $randomSeed -Level $Level | |
# Prompt the user to enter a guess | |
$guess = (Read-HostAtPos -X 42 -Y 21).ToUpper() | |
if( Test-ValidGuess -Guess $guess -Passwd $password -History $history ) { | |
# Success - show the secret information and exit the loop | |
Enter-TerminalAccessGranted | |
return $true | |
} | |
else { | |
# Failure - decrement the number of attempts left | |
$attempts-- | |
} | |
} | |
# Check if the user ran out of attempts | |
if( $attempts -eq 0 ) { | |
Write-TerminalLocked -Level $Level | |
} | |
$false # Return false to indicate that the user lost the game | |
} | |
#-------------------------------------------------------------------------------------------- | |
<# | |
.SYNOPSIS | |
Displays the game screen. | |
.PARAMETER Attempts | |
The number of attempts left. | |
.PARAMETER Words | |
The list of words to display. | |
.PARAMETER History | |
The list of guesses made by the user. | |
.PARAMETER RandomSeed | |
The random seed that determines how the memory dump is randomized. | |
#> | |
Function Write-GameScreen { | |
param ( | |
[Parameter(Mandatory)] | |
[int] $Attempts, | |
[Parameter(Mandatory)] | |
[array] $Words, | |
[Parameter(Mandatory)] | |
[Collections.Generic.Queue[string]] $History, | |
[Parameter(Mandatory)] | |
[int] $RandomSeed, | |
[Parameter(Mandatory)] | |
[int] $Level | |
) | |
# Clear the screen and display the header | |
Clear-Host | |
# Display the header | |
Write-Host (Format-GameTitle -Level $level) | |
if( $Attempts -gt 1 ) { | |
Write-Host "ENTER PASSWORD NOW" | |
} | |
else { | |
Write-HostBlinking "!!! WARNING: LOCKOUT IMMINENT !!!" | |
} | |
Write-Host | |
# Display the number of attempts left and a box for each attempt left | |
$progressBar = @([char]0x25A0) * $Attempts | |
Write-Host "$Attempts ATTEMPT(S) LEFT: $progressBar" | |
Write-Host | |
# By setting a random seed we make sure that the output of Format-WordList always looks the same, for the current game loop. | |
$null = Get-Random -SetSeed $randomSeed | |
# Display the list of words in a grid | |
Format-WordList -Words $Words | |
# Remove old entries from the history | |
while( $History.Count -gt 15 ) { | |
$null = $History.Dequeue() | |
} | |
# Display the history of guesses | |
Write-HostAtPos -Text $History -X 42 -Y (20 - $History.Count) | |
} | |
#-------------------------------------------------------------------------------------------- | |
<# | |
.SYNOPSIS | |
Checks if the guess is valid and gives feedback to the user. | |
.PARAMETER Guess | |
The guess made by the user. | |
.PARAMETER Passwd | |
The password to guess. | |
.PARAMETER History | |
The list of guesses made by the user. | |
.OUTPUTS | |
True if the guess is correct, false otherwise. | |
#> | |
Function Test-ValidGuess { | |
param( | |
[Parameter()] | |
[string] $Guess, | |
[Parameter(Mandatory)] | |
[string] $Passwd, | |
[Parameter(Mandatory)] | |
[Collections.Generic.Queue[string]] $History | |
) | |
# Check if the guess is valid. | |
if( $words -contains $guess ) { | |
# Check if the guess is correct | |
if( $guess -eq $Passwd ) { | |
return $true | |
} else { | |
# Calculate how many letters match the password (same position and value) | |
$matched = 0 | |
foreach( $i in 0..($Passwd.Length - 1) ) { | |
if( $guess[ $i ] -eq $Passwd[ $i ] ) { | |
$matched++ | |
} | |
} | |
# Give feedback to the user | |
$history.Enqueue(">$guess") | |
$history.Enqueue(">Entry denied.") | |
$history.Enqueue(">$matched/$($Passwd.Length) correct.") | |
} | |
} else { | |
# The guess is not in the list of words. | |
$history.Enqueue(">$guess") | |
$history.Enqueue(">Entry denied.") | |
} | |
$false # Output | |
} | |
#-------------------------------------------------------------------------------------------- | |
<# | |
.SYNOPSIS | |
Displays the secret information. | |
#> | |
function Enter-TerminalAccessGranted { | |
Clear-Host | |
Write-Host | |
Write-Host "ACCESS GRANTED" | |
Write-Host | |
Write-Host "Please enter a number between 1 and 3 to access the secret terminal data. For security reasons, you can only access one of the informations at this time." | |
Write-Host "> " -NoNewline | |
# Loop until the user enters 0 | |
do { | |
$invalidInput = $false | |
# Prompt the user for a number | |
$number = Read-Host | |
# Switch on the number entered by the user | |
switch( $number ) { | |
# If the user enters 1, show the first text | |
1 { | |
Write-Host "Vault-Tec Corporation was a company contracted by the United States government before the Great War to design and produce the vault system, a vast network of complex bomb and research shelters." | |
Write-Host "Secret: Vault-Tec was aware of the impending nuclear war and used the vaults as part of a social experiment to study human behavior under extreme conditions." | |
break | |
} | |
# If the user enters 2, show the second text | |
2 { | |
Write-Host "The Brotherhood of Steel is a quasi-religious technological organization operating across the ruins of post-war North America, with its roots stemming from the American military and the government-sponsored scientific community from before the Great War." | |
Write-Host "Secret: The Brotherhood of Steel was founded by a group of rebellious soldiers who defected from the U.S. Army when they discovered that their superiors were conducting unethical experiments with the Forced Evolutionary Virus (FEV)." | |
break | |
} | |
# If the user enters 3, show the third text | |
3 { | |
Write-Host "The Enclave is a secretive political, scientific, and militaristic organization that is descended directly from members of the pre-War United States government, and claims to be the legally-sanctioned continuation of it." | |
Write-Host "Secret: The Enclave's ultimate goal is to eradicate all mutated life forms (including most humans) from the wasteland using a modified FEV strain, and to rebuild America according to their own vision." | |
break | |
} | |
# If the user enters anything else, show an error message | |
default { | |
Write-Host "Invalid input. Please enter a number between 1 and 3." | |
$invalidInput = $true | |
break | |
} | |
} | |
} | |
while( $invalidInput ) | |
Write-Host | |
Write-Host "Press <RETURN> to continue..." -NoNewline | |
Read-Host | |
} | |
#-------------------------------------------------------------------------------------------- | |
<# | |
.SYNOPSIS | |
Displays the terminal locked message. | |
#> | |
Function Write-TerminalLocked { | |
param( | |
[Parameter(Mandatory)] | |
[int] $Level | |
) | |
Clear-Host | |
Write-Host (Format-GameTitle -Level $level) | |
Write-Host | |
Write-Host "!!! TERMINAL LOCKED !!!" | |
Write-Host "PLEASE CONTACT AN ADMINISTRATOR" | |
} | |
#-------------------------------------------------------------------------------------------- | |
<# | |
.SYNOPSIS | |
Formats a list of words like a pseudo memory dump, mixed with random characters. | |
#> | |
Function Format-WordList { | |
param( | |
[Parameter(Mandatory)] | |
[string[]] $Words | |
) | |
$columns = 2 | |
$rows = 17 | |
$charsPerColumn = 12 | |
$memory = New-RandomStringWithWords -Words $Words -ResultLength ($columns * $rows * $charsPerColumn) | |
$dump = Format-PseudoMemoryDump -InputObject $memory -Rows $rows -Columns $columns -CharsPerColumn $charsPerColumn | |
Write-Host ($dump -join "`n") | |
} | |
#-------------------------------------------------------------------------------------------- | |
<# | |
.SYNOPSIS | |
This function generates a random string by shuffling the input words and adding random separators. | |
.DESCRIPTION | |
This function takes an array of words and a desired length and generates a random string by shuffling the | |
words and adding random separators between them. The resulting string will have the specified length. | |
.PARAMETER Words | |
An array of strings representing the words to be shuffled. | |
.PARAMETER ResultLength | |
An integer representing the desired length of the resulting string. | |
.EXAMPLE | |
$words = 'foo', 'bar', 'baz', 'qux', 'quux', 'corge' | |
New-RandomStringWithWords $words 50 | |
Possible output: | |
'|<foo\%!=|=#quux)*)^;^qux}<&`bar@{~'corge^~&{baz^ | |
#> | |
function New-RandomStringWithWords { | |
param( | |
[Parameter(Mandatory)] [string[]] $Words, | |
[Parameter(Mandatory)] [int] $ResultLength | |
) | |
$totalWordsLength = ($Words | Measure-Object Length -Sum).Sum | |
# Check if the parameters are valid | |
$minResultLength = $totalWordsLength + $Words.Count + 1 | |
if( $ResultLength -lt $minResultLength ) { | |
throw "Argument for ResultLength ($ResultLength) is too small, it must be at least $minResultLength" | |
} | |
# Shuffle the Words | |
$shuffledWords = $Words | Get-Random -Count $Words.Count | |
# Create a char array for use with Get-Random | |
$randomChars = '!"#$%&''()*+,-./:;<=>?@[\]^_`{|}~'.ToCharArray() | |
# Initialize an array of random separators with one random char each to ensure that there is at least one | |
# separator char between each word. | |
$separators = foreach( $i in 1..($Words.Count + 1) ) { | |
[Text.StringBuilder]::new().Append(( Get-Random -InputObject $randomChars )) | |
} | |
# Calculate the remaining length to be distributed randomly to all separators | |
$remaining = $ResultLength - $totalWordsLength - $separators.Count | |
# Loop until the remaining length is zero | |
while( $remaining-- ) { | |
# Pick a random index from the separators array | |
$index = Get-Random -Maximum $separators.Count | |
# Append a random char to the separator at that index | |
$null = $separators[ $index ].Append(( Get-Random -InputObject $randomChars )) | |
} | |
# Join the shuffled words with the random separators | |
-join @( | |
foreach( $i in 0..($shuffledWords.Count - 1) ) { | |
$separators[ $i ]; $shuffledWords[ $i ] | |
} | |
$separators[ $i + 1 ] | |
) | |
} | |
#-------------------------------------------------------------------------------------------- | |
<# | |
.SYNOPSIS | |
Formats a string as a Fallout-style memory dump. | |
.DESCRIPTION | |
This function takes a string and formats it as a Fallout-style memory dump. The string is split into chunks of a specified | |
size and each chunk is assigned a random address. The addresses are then formatted as hexadecimal numbers and the chunks are | |
written to the output in rows and columns. | |
.PARAMETER InputObject | |
The string to format. | |
.PARAMETER Rows | |
The number of rows to use in the output. | |
.PARAMETER Columns | |
The number of columns to use in the output. | |
.PARAMETER CharsPerColumn | |
The number of characters to use per column. | |
.EXAMPLE | |
Format-PseudoMemoryDump -InputObject 'abcdefghijklmnoprstuvwxyz0123456890foobarbazbamm' -Rows 2 -Columns 2 -CharsPerColumn 12 | |
Output: | |
0x7964 abcdefghijkl 0x797C z0123456890f | |
0x7970 mnoprstuvwxy 0x7988 oobarbazbamm | |
.OUTPUTS | |
[string] | |
#> | |
Function Format-PseudoMemoryDump { | |
param( | |
[Parameter(Mandatory)] [string] $InputObject, | |
[Parameter(Mandatory)] [int] $Rows, | |
[Parameter(Mandatory)] [int] $Columns, | |
[Parameter(Mandatory)] [int] $CharsPerColumn | |
) | |
# Validate the arguments | |
if ($Rows -le 0 -or $Columns -le 0 -or $CharsPerColumn -le 0) { | |
throw "Rows, Columns and CharsPerColumn must be positive integers" | |
} | |
$expectedLength = $Rows * $Columns * $CharsPerColumn | |
if ($InputObject.Length -ne $expectedLength) { | |
throw "InputObject length must match Rows * Columns * CharsPerColumn (expected length: $expectedLength actual length: $($InputObject.Length))" | |
} | |
# Split the input string into chunks of CharsPerColumn size | |
$chunks = $InputObject -split "(?<=\G.{$CharsPerColumn})" | |
# Get a random start address | |
$startAddress = Get-Random -Maximum (0x10000 - $InputObject.Length) | |
# Loop through the rows and columns and format the output | |
foreach( $i in 0..($Rows - 1) ) { | |
$line = "" | |
foreach( $j in 0..($Columns - 1) ) { | |
# Get the index of the current chunk | |
$index = $i + $j * $Rows | |
# Get the hex address of the current chunk | |
$address = "0x{0:X4}" -f ($index * $CharsPerColumn + $startAddress) | |
# Append the address and the chunk to the line | |
$line += "$address $($chunks[$index]) " | |
} | |
# Write the line to the output | |
$line | |
} | |
} | |
#-------------------------------------------------------------------------------------------- | |
function Format-GameTitle { | |
param( | |
[Parameter(Mandatory)] [int] $Level | |
) | |
'ROBCO INDUSTRIES (TM) TERMLINK PROTOCOL [SEC LVL {0}]' -f $Level | |
} | |
#-------------------------------------------------------------------------------------------- | |
<# | |
.SYNOPSIS | |
Write text to the console at a specific position. | |
.PARAMETER Text | |
The text to write to the console. | |
.PARAMETER X | |
The X coordinate of the position to write to. | |
.PARAMETER Y | |
The Y coordinate of the position to write to. | |
.PARAMETER NoNewline | |
If this switch is specified, the cursor will not be moved to the next line after writing the text. | |
.EXAMPLE | |
Write-HostAtPos -Text 'Hello', 'World' -x 10 -y 5 | |
#> | |
function Write-HostAtPos { | |
[CmdletBinding()] | |
param ( | |
[Parameter(ValueFromPipeline)] | |
[string[]] $Text, | |
[Parameter(Mandatory, Position=0)] | |
[int] $X, | |
[Parameter(Mandatory, Position=1)] | |
[int] $Y, | |
[switch] $NoNewline | |
) | |
process { | |
foreach( $line in ($Text -split '\r?\n') ) { | |
$host.ui.RawUI.CursorPosition = [Management.Automation.Host.Coordinates]::new( $X, $Y ) | |
Write-Host $line -NoNewline:$NoNewline | |
$Y++ | |
} | |
} | |
} | |
#-------------------------------------------------------------------------------------------- | |
<# | |
.SYNOPSIS | |
Write text to the console with a blinking effect, if supported. | |
#> | |
function Write-HostBlinking { | |
param( | |
[Parameter()] [string] $Text | |
) | |
if( $Host.UI.SupportsVirtualTerminal -and $PSStyle ) { | |
Write-Host ($PSStyle.Blink + $Text + $PSStyle.Reset) | |
} | |
else { | |
Write-Host $Text | |
} | |
} | |
#-------------------------------------------------------------------------------------------- | |
<# | |
.SYNOPSIS | |
Read input at a specific position. | |
.DESCRIPTION | |
This function writes a prompt to the console at a specific position. Then it reads input using Read-Host. | |
.PARAMETER X | |
The X coordinate of the position to write the prompt to. | |
.PARAMETER Y | |
The Y coordinate of the position to write the prompt to. | |
.PARAMETER Prompt | |
The prompt to write to the console. Defaults to '> '. | |
.EXAMPLE | |
$value = Read-HostAtPos 50 10 | |
$value = Read-HostAtPos -Prompt '>> ' -X 50 -Y 10 | |
#> | |
function Read-HostAtPos { | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory)] | |
[int] $X, | |
[Parameter(Mandatory)] | |
[int] $Y, | |
[Parameter()] | |
[string] $Prompt = '> ' | |
) | |
$host.ui.RawUI.CursorPosition = [Management.Automation.Host.Coordinates]::new( $X, $Y ) | |
Write-Host $Prompt -NoNewline | |
Read-Host | |
} | |
#-------------------------------------------------------------------------------------------- | |
# The script execution starts here. | |
$originalForegroundColor = $Host.UI.RawUI.ForegroundColor | |
$originalBackgroundColor = $Host.UI.RawUI.BackgroundColor | |
$Host.UI.RawUI.ForegroundColor = "Green" | |
$Host.UI.RawUI.BackgroundColor = "Black" | |
$level = 1 | |
while( $level -le 3 ) { | |
if( Start-HackerGame -Level $level ) { | |
$level++ | |
continue | |
} | |
if( $level -le 3 ) { | |
Write-Host | |
Write-Host "Administrators may reset the terminal by pressing the <RETURN> key." | |
Read-Host | |
$level = 1 | |
} | |
} | |
if( $level -gt 3 ) { | |
Write-Host | |
Write-Host "You beat the system, you are a true hacking genius. Have a nice day." | |
Read-Host | |
} | |
$Host.UI.RawUI.ForegroundColor = $originalForegroundColor | |
$Host.UI.RawUI.BackgroundColor = $originalBackgroundColor |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment