3
\$\begingroup\$

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
    }
}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

This is a fun project. Here are a few things I noticed:

  1. $AlivePercChance implies that it's a percentage (30%) rather than a fraction (0.3). You might consider renaming it to $AliveChance
  2. 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].
  3. 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
  4. 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.
\$\endgroup\$
1
  • \$\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\$ Commented Dec 3, 2021 at 23:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.