I'm learning PowerShell for professional development, but decided to do a bit of a silly project to help learn scripting in more depth, and chose Conway's Game of Life. This is the first PowerShell script I've written besides a small toy that I wrote for an Active Directory assignment previously.
An example start:
PS C:\Users\user\PowershellScripts> . .\gol.ps1
PS C:\Users\user\PowershellScripts> Start-Main 15 15 0.3
--------------------------------------------------
█ █ █ █ █ █ █ █ █
█ █ █ █ █ █ █
█ █ █
█ █ █
█ █
█ █ █ █
█ █ █
█ █ █
█ █ █ █ █ █
█ █ █ █
█ █ █ █ █ █ █ █
█ █ █ █ █
█ █ █ █ █ █ █
█ █ █ █ █ █ █ █
█ █ █
Primarily, I'd like comments on anything stylistic or off that I'm doing so I don't develop bad habits.
Also, I'd like to know how I could make it perform better. Obviously I wasn't expecting speed here, but it's surprisingly slow. I was originally abusing range notation to make the looping a little nicer looking, and doing things like $NNeighbors -in 2..3
to do range checks, but ended switching those over to more traditional for
loops and comparison checks.
Also, I wanted to play around with defining constants using New-Variable
, but it ended up being awkward. Whenever I reloaded the script, I'd get an error because a constant with the same name already existed. I ended up using a C-style ifndef
-like check at the start, but this feels off.
There's no tag for it, but in case it matters, this was written with for Powershell 7.
if ($null -eq $MOORE_NEIGHBORHOOD_DEPTH) { # Since reloading script in a shell will cause errors otherwise
Set-Variable -Name MOORE_NEIGHBORHOOD_DEPTH -Value 1 -Option Constant
Set-Variable -Name ALIVE_CELL_REPR -Value "█" -Option Constant
Set-Variable -Name DEAD_CELL_REPR -Value " " -Option Constant
}
# The state holds an array of cells to read from, and an array of cells to write to.
# Each cell array holds a boolean value indicating if it's alive or not.
function New-State {
Param($Width, $Height)
$Total = $Width * $Height
[PSCustomObject]@{
ReadCells = (,$false) * $Total;
WriteCells = (,$false) * $Total;
Width = $Width;
Height = $Height;
}
}
function New-RandomState {
Param($Width, $Height, $AliveChance)
$State = New-State $Width $Height
for ($I = 0; $I -lt $State.ReadCells.Length; $I++) {
if ((Get-Random -Minimum 0.0 -Maximum 1.0) -lt $AliveChance) {
$State.ReadCells[$I] = $true
}
}
$State
}
# Swaps the read and write cell arrays so the read array is written to, and the write array is read from.
function Switch-Cells {
Param($State)
$Temp = $State.ReadCells
$State.ReadCells = $State.WriteCells
$State.WriteCells = $Temp
}
# A 1D-array is being used to emulate a 2D-array.
# This calculates the index into the array to simulate X,Y coordinate access.
function Get-Index {
Param($State, $X, $Y)
$State.Width * $Y + $X
}
function Confirm-Alive {
Param($State, $X, $Y)
$State.ReadCells[(Get-Index $State $X $Y)]
}
# Counts how many neighbors surrounding the given cell are alive.
# Depth is how many squares in each direction from the given cell to search (1 cooresponds to a Moore neighborhood)
function Find-NeighborCount {
Param($State, $X, $Y, $Depth)
$XMinBound = [math]::max(0, $X-$Depth)
$XMaxBound = [math]::min($X+$Depth, $State.Width-1)
$YMinBound = [math]::max(0, $Y-$Depth)
$YMaxBound = [math]::min($Y+$Depth, $State.Height-1)
$Count = 0
for ($FY = $YMinBound; $FY -le $YMaxBound; $FY++) {
for ($FX = $XMinBound; $FX -le $XMaxBound; $FX++) {
if ((-not ($X -eq $FX -and $Y -eq $FY)) -and (Confirm-Alive $State $FX $FY)) {
$Count++
}
}
}
$Count
}
# Updates the given cell according to how many of its neighbors are found to be alive.
function Update-Cell {
Param($State, $X, $Y)
$CurrentlyAlive = Confirm-Alive $State $X $Y
$NNeighbors = Find-NeighborCount $State $X $Y $MOORE_NEIGHBORHOOD_DEPTH
$NewState = $CurrentlyAlive -and (2 -le $NNeighbors -and $NNeighbors -le 3) -or
((-not $CurrentlyAlive) -and $NNeighbors -eq 3)
$State.WriteCells[(Get-Index $State $X $Y)] = $NewState
}
function Update-Cells {
Param($State)
for ($Y = 0; $Y -lt $State.Height; $Y++) {
for ($X = 0; $X -lt $State.Width; $X++) {
Update-Cell $State $X $Y
}
}
}
# Writes the state of the read cells to the screen.
function Show-State {
Param($State)
Write-Host ("-" * 50)
for ($Y = 0; $Y -lt $State.Height; $Y++) {
for ($X = 0; $X -lt $State.Width; $X++) {
Write-Host "$($State.ReadCells[(Get-Index $State $X $Y)] ? $ALIVE_CELL_REPR : $DEAD_CELL_REPR) " -NoNewline
}
Write-Host ""
}
}
function Write-DebugGlider {
Param($State)
function cell {
Param($X, $Y)
$State.WriteCells[(Get-Index $State $X $Y)] = $true
}
cell 1 0
cell 2 1
cell 0 2
cell 1 2
cell 2 2
}
function Start-Main {
Param($Width, $Height, $AlivePercChance)
$State = New-RandomState $Width $Height $AlivePercChance
for ($N = 0; $N -lt 100; $N++) {
Update-Cells $State
Switch-Cells $State
Show-State $State
#Start-Sleep -Milliseconds 250
}
}
1 Answer 1
This is a fun project. Here are a few things I noticed:
$AlivePercChance
implies that it's a percentage (30%) rather than a fraction (0.3). You might consider renaming it to$AliveChance
- Why did you flatten the 2D array to 1D? It adds substantial overhead (2 extra function calls * $Width * $Height per screen write, way more for checking neighbors) and reduces readability compared to
$State.ReadCells[$x,$y]
. - The program works in Powershell 5 except for the ternary operator on line 102, which you can change to:
Write-Host "$(If($State.ReadCells[(Get-Index $State $X $Y)]){$ALIVE_CELL_REPR}Else{$DEAD_CELL_REPR}) " -NoNewline
- To make it behave more like the Life implementations I've seen, you can clear the screen (
cls
) or move the cursor to the top of the console ([Console]::CursorTop = 0
) between writes. The cursor method is faster.
-
\$\begingroup\$ Thank you. Honestly for 2., at the time, I couldn't figure out how to create a 2D array. Every attempt seemed to automatically flatten itself to 1D when I checked it, so I figured I'd just roll with it. And thanks for 4. I'll check that out. \$\endgroup\$Carcigenicate– Carcigenicate2021年12月03日 23:02:40 +00:00Commented Dec 3, 2021 at 23:02