Skip to content

Instantly share code, notes, and snippets.

@zett42
Last active July 16, 2023 08:54
Show Gist options
  • Save zett42/eb892cdaeba8dcb1bd723828dbce2204 to your computer and use it in GitHub Desktop.
Save zett42/eb892cdaeba8dcb1bd723828dbce2204 to your computer and use it in GitHub Desktop.
Guess the password - minigame inspired by Fallout
<#
.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