Skip to content

Instantly share code, notes, and snippets.

@mklement0
Created October 23, 2018 14:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mklement0/b797cf35efda8acceed6857ec7e79cb5 to your computer and use it in GitHub Desktop.
Save mklement0/b797cf35efda8acceed6857ec7e79cb5 to your computer and use it in GitHub Desktop.
PowerShell script that implements a terminal-based version of Conway's Game of Life (see https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life)
<#
.SYNOPSIS
A terminal-based implementation of Conway's Game of Life.
.DESCRIPTION
Conway's Game of Life is a zero-player game that simulates a cellular automaton.
See https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life for background.
The initial state can be seeded explicitly or, by default, randomly.
The game ends automatically once all cells have died or when a "still life"
state is reached, i.e., when the grid no longer changes.
No attempt is made to detect periods (cyclic states).
.PARAMETER GridSize
Specifies how many cells make up the square grid along each axis, 20 by default,
with a maxium of 255.
In other words: The grid will be made up of GridSize x GridSize cells.
Choose a value small enough so that the grid fits into your terminal window
and note that the computation effort increases quadratically; in short:
large grids are computation-intensive and slow to update.
.PARAMETER RandomSeedCount
Specifies how many cells to seed randomly, i.e., how many cells should
initially be "alive".
The default is a quarter (0.25) of all cells.
.PARAMETER SeedCoordinates
As an alternative to random seeding, this parameter allows you to specify
the exact cells to initially mark as alive, which allows deterministic replays
of games.
You may specify the coordinates either:
* as a string of coordinate pairs (pairs can also be separated by commas):
'x1, y1 x2, y2 [...]'
* or as a flat array of coordinates (unsinged integers):
x1, y1, x2, y2 [...]
.PARAMETER GenerationCount
Specifies the count of generations after which to stop the game automatically.
The default value, -1, keeps the game going indefinitely, or until a
"still life" state is reached, where generations no longer change.
.PARAMETER List
Specifies that successive generations of the grid should be printed one
after the other, whereas by default the grid is updated in-place.
.PARAMETER PauseAtLeastMilliseconds
Specifies how long, in milliseconds, to pause at a minimum between displaying
successive generations, so that play progress can be followed by a human
observer. Note that, due to computational effort, the actual interval be
longer.
The default is 500 msecs.
.EXAMPLE
Invoke-Life
Starts a game with the default grid size and random seeding.
.EXAMPLE
Invoke-Life -GridSize 40 -RandomSeedCount (40 * 40 / 3)
Starts a game in a 40 x 40 grid with a third of randomly chosen cells
initially alive.
.EXAMPLE
Invoke-Life -GridSize 6 -SeedCoordinates '0,5 1,2 3,5 4,1 4,3 4,5 5,2 5,3 5,5'
Starts a game with a 6 x 6 grid and explicit seed coordinates.
This particular eventually game exhibits cyclic patterns with a period of 4.
#>
#Requires -version 3
[cmdletbinding(PositionalBinding=$false, DefaultParameterSetName='Random')]
param(
[parameter(Position=0)]
[ValidateRange(1, 80)]
[int] $GridSize = 20, # The number of cells along each axis of the grid.
[parameter(ParameterSetName='Random', Position=1)]
[int] $RandomSeedCount = $GridSize * $GridSize * 0.25, # how many live cells, randomly selected, to seed the grid with
[parameter(Mandatory, ParameterSetName='ExplicitSeed', Position=1)]
[ValidateNotNull()]
[object] $SeedCoordinates, # Note: we accept both an [int[]] array or a single string representing one
[ValidateRange(-1, [int]::MaxValue)]
[int] $GenerationCount = -1, # How many generations to show (0 means: just the seed (initial state)); omit to keep going indefinitely.
[switch] $List, # By default, generations are updated in-place; whether to print subsequent generations sequentially.
[ValidateRange(0, [int]::MaxValue)]
[Alias('ms')]
[int] $PauseAtLeastMilliseconds = 500 # For slowing down updates: the minimum time to pause between ticks (generations).
)
$ErrorActionPreference = 'Stop' # Abort on all unhandled errors.
Set-StrictMode -version 1 # Prevent use of uninitialized variables.
if ($RandomSeedCount -lt 0 -or $RandomSeedCount -gt $GridSize * $GridSize) { Throw "Invalid seed count: $_`nYou must specify a number between 0 and $($GridSize * $GridSize)" }
# Given a grid as $grid_ref, calculates the next generation and assigns it
# back to $grid_ref.
function update-generation {
param(
[ref] [byte[,]]$grid_ref # the by-reference grid variable
)
# Create a new, all-zero clone of the current grid to
# receive the new generation.
$grid_new = New-Object 'byte[,]' ($grid_ref.Value.GetLength(0), $grid_ref.Value.GetLength(1))
$aliveCount = 0
For($x=0; $x -le $grid_new.GetUpperBound(0); $x++ ){
For($y=0; $y -le $grid_new.GetUpperBound(1); $y++){
# Get the count of live neighbors.
# Note that the *original* matrix must be used to:
# - determine the live neighbors
# - inspect the current state
# because the game rules must be applied *simultaneously*.
$neighborCount = get-LiveNeighborCount $grid_ref.Value $x $y
if ($grid_ref.Value[$x,$y]) { # currently LIVE cell
# A live cell with 2 or 3 neighbors lives, all others die.
$grid_new[$x,$y] = [int] ($neighborCount -eq 2 -or $neighborCount -eq 3)
} else { # curently DEAD cell
# A currently dead cell is resurrected if it has 3 live neighbors.
$grid_new[$x,$y] = [int] ($neighborCount -eq 3)
}
if ($grid_new[$x,$y]) { ++$aliveCount }
}
}
# Assign the new generation to the by-reference grid variable.
$grid_ref.Value = $grid_new
# Output the number of alive cells
$aliveCount
}
# Get the count of live neighbors for grid position $x, $y.
function get-LiveNeighborCount{
param(
[byte[,]]$grid, # the grid
[Int]$x,
[Int]$y
)
$xLength = $grid.GetLength(0)
$yLength = $grid.GetLength(1)
$count = 0
for($xOffset = -1; $xOffset -le 1; $xOffset++) {
for($yOffset = -1; $yOffset -le 1; $yOffset++) {
if (-not ($xOffset -eq 0 -and $yOffset -eq 0)) { # skip the position at hand itself
if($grid[(get-wrappedIndex $xLength ($x + $xOffset)),(get-wrappedIndex $yLength ($y + $yOffset))]) {
$count++
}
}
}
}
# Output the count.
$count
}
# Given a potentially out-of-bounds index along a dimension of a given length,
# return the wrapped-around-the-edges value.
function get-wrappedIndex{
param(
[Int]$length,
[Int]$index
)
If($index -lt 0){
$index += $length
}
ElseIf($index -ge $length){
$index -= $length
}
# Output the potentially wrapped index.
$index
}
# Initialize a grid with a specified number of randomly selected live cells.
function initialize-WithRandomSeed {
param(
[byte[,]] $grid,
[int] $RandomSeedCount # how many cells to make live randomly
)
$i = 0
while ($i -lt $RandomSeedCount){
$randomX, $randomY = (Get-Random $GridSize), (Get-Random $GridSize)
# Avoid seeding the same cell twice.
if ($grid[$randomX, $randomY] -eq 0) {
$grid[$randomX, $randomY] = 1
++$i
}
}
# Output the number of live cells that were seeded.
$RandomSeedCount
}
# Initialize a grid from a *string* containing a list of coordinates indicating live cells
function initialize-FromLiveCoordinatesString {
param(
[byte[,]] $grid,
[uint32[]] $coordinates
)
$gridSize = $grid.GetLength(0)
Write-Verbose "Coordinate indices: $coordinates"
for ([int] $i = 0; $i -lt $coordinates.Length - 1 ; $i += 2) {
if ($coordinates[$i] -ge $gridSize -or $coordinates[$i+1] -ge $gridSize) { Throw "Invalid coordinate pair specified. Coordinate values must be between 0 and $gridSize`: $i, $($i+1)" }
$grid[$coordinates[$i], $coordinates[$i+1]] = 1
}
# Output the number of live cells that were seeded.
$coordinates.Length / 2
}
# Initialize a grid from a *[byte[]]* array containing a sequence of coordinates indicating live cells
function initialize-FromLiveCoordinates {
param(
[byte[,]] $grid,
[byte[]] $liveCoordinates
)
# Make sure that the array contains an even number of numbers.
if ($liveCoordinates % 2) { Throw "Coordinates array is invalid, because it has an uneven count of elements: $($liveCoordinates.Count)" }
$gridSize = $grid.GetLength(0)
for ([int] $i = 0; $i -lt $liveCoordinates.Length - 1 ; $i += 2) {
$x, $y = $liveCoordinates[$i], $liveCoordinates[$i+1]
if ($x -ge $gridSize -or $y -ge $gridSize) { Throw "Invalid coordinate pair specified. Coordinate values must be between 0 and $gridSize`: $x, $y" }
$grid[$x, $y] = 1
}
# Output the number of live cells that were seeded.
$liveCoordinates.Length / 2
}
# Get the live cells as a flat list of consecutive coordinate pairs as a [byte[]] array
function convertTo-LiveCoordinates {
param(
[int] $aliveCellCount,
[byte[,]] $grid
)
$barr = New-Object 'byte[]' (2 * $aliveCellCount)
$i = 0
for($row=0; $row -lt $grid.GetLength(0); ++$row) {
for ($col=0; $col -lt $grid.GetLength(1); ++$col) {
if ($grid[$row, $col]) {
$barr[$i++] = $row
$barr[$i++] = $col
}
}
}
$barr
}
# Get the live cells as a list of coordinates in human-friendly string format.
function convertTo-LiveCoordinatesString {
param(
[byte[,]] $grid
)
$coords = $sep = ''
for($row=0; $row -lt $grid.GetLength(0); ++$row) {
for ($col=0; $col -lt $grid.GetLength(1); ++$col) {
if ($grid[$row, $col]) {
# $coords += $sep + "[$row,$col]"
$coords += $sep + "$row,$col"
$sep = ' '
}
}
}
$coords
}
# Test if any cells are alive in a grid.
function test-anyGridCellAlive {
param(
[byte[,]] $grid
)
$anyAlive=$false
for($row=0; $row -lt $grid.GetLength(0); ++$row) {
for ($col=0; $col -lt $grid.GetLength(1); ++$col) {
if ($grid[$row, $col]) { $anyAlive = $true; break }
}
}
$anyAlive
}
# Test if two grids - assumed to be the same size - are equal.
function test-gridsEqual {
param(
[byte[,]] $grid1,
[byte[,]] $grid2
)
$equal = $true
for($row=0; $row -lt $grid1.GetLength(0); ++$row) {
for ($col=0; $col -lt $grid1.GetLength(1); ++$col) {
if ($grid1[$row, $col] -ne $grid2[$row, $col]) { $equal = $false; break }
}
}
$equal
}
# Print a (single generation's) grid.
function show-grid {
param(
[byte[,]] $grid,
[string] $legend
)
for($row=0; $row -lt $grid.GetLength(0); ++$row) {
$line = $sep = ''
for ($col=0; $col -lt $grid.GetLength(1); ++$col) {
$line += $sep + (' ', '■')[$grid[$row, $col]]
$sep = ' '
}
Write-Host $line
}
if ($legend) { Write-Host -ForegroundColor Gray "`n$legend" }
}
# Returns information about a pending keypress, if any, and consumes it.
# - If one has been pressed, a [System.ConsoleKeyInfo] instance representing that key is returned
# - Otherwise, nothing is returned.
function waitingKeyPress {
if ($host.Name -eq 'ConsoleHost') {
# If a key is waiting, read and return it.
# Note: The $True argument is meant to suppress echoing of the key pressed to the terminal,
# but that only works with *synchronous* use of [Console]::ReadKey()
if ([Console]::KeyAvailable) { [Console]::ReadKey($True) }
}
}
# Write a full line to the console, right-padding with spaces to the full
# window width to ensure that any previous content is overwritten.
function write-FullHostLine {
param([string] $txt)
Write-Host $txt.PadRight($host.UI.RawUI.WindowSize.Width - $txt.Length)
}
# Turn console output (echoing) on or off.
function show-output {
param([bool]$Show = $true)
if ($Host.Name -eq 'ConsoleHost') {
$thisFunc = $MyInvocation.MyCommand; $customPropName = 'net_same2_orgStdout'
# try {
if ($Show) {
# Restore the cached [Console]::Out stream.
# !! This assumes that show-output $False was previously called at least once.
$outStream = $thisFunc.$customPropName
# !! If there's no cached stream, do nothing.
if (-not $outStream) { return }
} else {
# !! Restoring [Console]::Out to New-Object System.IO.StreamWriter ([Console]::OpenStandardOutput())
# !! as follows works on Windows, but not macOS.
# $outStream = New-Object System.IO.StreamWriter ([Console]::OpenStandardOutput())
# $outStream.AutoFlush = true
# !! By contrast, saving and restoring the original [Console]::Out works on all platforms.
# !! We use a custom property on this function object to
if (-not $thisFunc.$customPropName) {
$thisFunc | Add-Member -MemberType NoteProperty -Name $customPropName -Value ([Console]::Out)
}
$outStream = New-Object System.IO.StreamWriter ([System.IO.Stream]::Null)
}
[Console]::SetOut($outStream)
# } catch {}
}
}
# Turn the cursor in the console on or off.
function show-cursor {
param([bool]$Show = $true)
# !! The console on macOS doesn't support toggling cursor visibility, so
# !! must trap exceptions (and quietly ignore them).
if ($host.Name -eq 'ConsoleHost') {
try {
[Console]::CursorVisible = $Show
} catch {}
}
}
# Show successive generations.
function show-generations {
$instructions = "`nVisit https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life to learn more."
# Print the specified number of generations or until all cells have died,
# which may never happen; press Ctrl+C to interrupt anytime.
$gridSize = $grid.GetLength(0)
$cellCount = $gridSize * $gridSize
$fillRatio = $seedCount / $cellCount
$gridDescr = "$seedCount-seed $gridSize x $gridSize grid " + ('({0:0}% seeded)' -f ($fillRatio * 100))
if ($host.Name -eq 'ConsoleHost') {
show-cursor $False # Hide the cursor, if possible - !! Trying to hide the cursor fails on macOS as of v6-alpha18 - simply ignore the failure.
} else { # Not a console, default to -List display (no in-place updating)
$List = $true
Write-Warning "Forcing list mode, because the host is not a console."
Write-Warning "Can only check for keypresses in a console host. Use Ctrl+C to terminate."
}
if (-not $List) {
Clear-Host # !! On macOS, this is currently necessary for in-place updating to work.
$startCursorPos = $host.UI.RawUI.CursorPosition
}
$sw = New-Object System.Diagnostics.Stopwatch
$paused = $false
$belowGridCursorPos = $null
# Initialize the list of states, each element of which stores a flat [byte[]]
# array containing consecutive pairs of the live cells in each generation.
$stateHistory = [System.Collections.Generic.List[byte[]]]::New(100)
try {
for ([int] $i = 0; $i -le $GenerationCount -or $GenerationCount -eq -1; ++$i) { # generation loop
$sw.Start()
# See if the user pressed a key.
$keyPressed = waitingKeyPress
if (-not $keyPressed) {
# -- Calculate this generation
if ($i -eq 0) { # initial state (seed)
$aliveCellCount = $seedCount
} else { # calculate the next generation.
$prevGrid = $grid.Clone()
$aliveCellCount = update-generation ([ref] $grid)
}
# Add this generation to the state history
$stateHistory.Add((convertTo-LiveCoordinates $aliveCellCount $grid))
# If a minimum delay is to be enforced, sleep accordingly.
do {
# Break, if a key was pressed.
if ($keyPressed = waitingKeyPress) { break }
}
while ($sw.Elapsed.TotalMilliseconds -lt $PauseAtLeastMilliseconds -and $i -gt 0 -and $(Start-Sleep -Milliseconds 10; $true))
}
$sw.Stop(); $sw.Reset()
if ($i -gt 0 -and ($paused -or $keyPressed)) {
:interact do {
if (-not $keyPressed) { # Show the menu prompt and wait for a keypress
$host.ui.RawUI.CursorPosition = $belowGridCursorPos
show-output
write-FullHostLine "(Q)uit, (N)ew game, (<-/->) step back/forward, (<space>) resume?"
show-output $False
show-cursor
$keyPressed = [Console]::ReadKey($true)
show-cursor $False
}
$valid = $false
if ($keyPressed.Modifiers -eq 0 -or $keyPressed.Modifiers -eq [System.ConsoleModifiers]::Shift) { # ignore keypresses that involve modifiers other than Shift
$valid = $true
switch ($keyPressed.Key) { # !! be sure to (...)-enclose all non-script-block branch conditions - otherwise they'll be treated as *strings*
# Note: On macOS, attempts to use Write-Host here - whose output should render *below* the menu hint/prompt - do not work.
([System.ConsoleKey]::Q) { exit } # Write-Host -ForegroundColor Yellow 'Terminated by user request.'; exit }
{ $_ -in [System.ConsoleKey]::Escape, [System.ConsoleKey]::Spacebar } { if ($paused) { $paused = $false; break interact } else { $paused = $true; $keyPressed = $null; continue interact } }
([System.ConsoleKey]::LeftArrow) { } # Write-Host -ForegroundColor Yellow 'Generation back' }
([System.ConsoleKey]::RightArrow) { } # Write-Host -ForegroundColor Yellow 'Generation forward' }
default { $valid = $false }
}
}
# if (-not $valid) { Write-Warning "Unrecognized key: $($keyPressed.KeyChar)" }
$keyPressed = $null
} while (-not $valid)
}
# -- Display this generation.
if ($List) {
Write-Host '' # Output empty line before the next grid is printed.
} else {
# Resume the original cursor position to "paint over" the original grid.
# !! [As of v6-alpha18] On macOS, this doesn't work reliably; After having run
# !! Clear-Host, it seems to work predictably.
$host.UI.RawUI.CursorPosition = $startCursorPos
}
$descr = $gridDescr + ' - '
if ($i -eq 0 ) {
$descr += "initial state"
} else {
$descr += "generation #$i " + $(if ($GenerationCount -gt 0) { "of max. $GenerationCount "}) + ('({0:0}% filled)' -f ($aliveCellCount / $cellCount * 100))
}
$descr += $instructions
# Print this generation.
show-output
show-grid $grid $descr
if (-not $belowGridCursorPos) {
$belowGridCursorPos = $host.UI.RawUI.CursorPosition
}
if (-not $paused) {
write-FullHostLine "Press Q to quit, <space> to pause and show menu."
}
show-output $False
# Stop:
# * if all cells died.
$sbEndReasonMessage = $null
if ($aliveCellCount -eq 0) {
$sbEndReasonMessage = { Write-Host -ForegroundColor Red "`nLife ended in generation $i." }
break
# * if the grid became a "still life", i.e., if the previous generation
# was exactly the same and all future generations would therefore be
# as well.
} elseif ($i -gt 0 -and (test-gridsEqual $prevGrid $grid)) {
# Write-Host -ForegroundColor Yellow "`nLife froze in generation $($i-1)."
$sbEndReasonMessage = { Write-Host -ForegroundColor Yellow "`nLife became a still life in generation $($i-1)." }
break
}
# Note: We make no attempt to detect *periods* (cycles of repeating)
# generations.
} # for
} finally {
# Turn echoing and the cursor back on.
show-output
show-cursor
if ($sbEndReasonMessage) {
& $sbEndReasonMessage
} else {
Write-Host -ForegroundColor Yellow "Life halted by request."
}
# Write the command that allows replaying the very same game on demand.
Write-Host -ForegroundColor Yellow "To replay this exact game on demand:`n $replayCmd"
}
}
# The grid is always a square
$grid = New-Object 'byte[,]' $GridSize, $GridSize
# Seed the grid
if ($SeedCoordinates) { # Explicit seed coordinates given.
try {
if ($SeedCoordinates -is [string]) {
[uint32[]] $coordinates = $SeedCoordinates -split '[, ]' -ne ''
} else {
[uint32[]] $coordinates = $SeedCoordinates
}
} catch {
Throw "Invalid seed coordinates given: $_"
}
$seedCount = initialize-FromLiveCoordinatesString $grid $coordinates
} else { # Random initialization
$seedCount = initialize-WithRandomSeed $grid $RandomSeedCount
}
# Construct a command line for replaying this very invocation, with incidental parameters omitted.
$replayCmd = "$($MyInvocation.MyCommand.Name) -GridSize $GridSize"
# if ($null -ne $GenerationCount) {
# $replayCmd += " -GenerationCount $GenerationCount"
# }
$replayCmd += " -SeedCoordinates '$(convertTo-LiveCoordinatesString $grid)'"
# Start showing the generations.
show-generations
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment