Skip to content

Instantly share code, notes, and snippets.

@dfinke
Last active March 10, 2026 16:31
Show Gist options
  • Select an option

  • Save dfinke/c222b5995a3136c4fdeb4df400f0ba0c to your computer and use it in GitHub Desktop.

Select an option

Save dfinke/c222b5995a3136c4fdeb4df400f0ba0c to your computer and use it in GitHub Desktop.
Using AI - built a standalone PowerShell arcade game at space-invaders.ps1
param(
[int]$DemoFrames = 0,
[int]$FrameDelayMs = 55,
[switch]$NoColor,
[switch]$Sound
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$script:Rng = [System.Random]::new()
$script:Escape = [char]27
$script:ResetStyle = if ($NoColor) { '' } else { "$($script:Escape)[0m" }
function Get-FgStyle {
param(
[int]$Red,
[int]$Green,
[int]$Blue
)
if ($NoColor) {
return ''
}
return "$($script:Escape)[38;2;${Red};${Green};${Blue}m"
}
function New-Buffer {
param(
[int]$Width,
[int]$Height
)
$chars = New-Object 'object[]' $Height
$styles = New-Object 'object[]' $Height
for ($y = 0; $y -lt $Height; $y++) {
$charRow = New-Object 'char[]' $Width
[System.Array]::Fill($charRow, [char]' ')
$chars[$y] = $charRow
$styleRow = New-Object 'string[]' $Width
[System.Array]::Fill($styleRow, '')
$styles[$y] = $styleRow
}
return [pscustomobject]@{
Width = $Width
Height = $Height
Chars = $chars
Styles = $styles
}
}
function Set-Cell {
param(
$Buffer,
[int]$X,
[int]$Y,
[string]$Glyph,
[string]$Style = ''
)
if ($X -lt 0 -or $X -ge $Buffer.Width -or $Y -lt 0 -or $Y -ge $Buffer.Height) {
return
}
$Buffer.Chars[$Y][$X] = [char]$Glyph[0]
$Buffer.Styles[$Y][$X] = $Style
}
function Draw-Text {
param(
$Buffer,
[int]$X,
[int]$Y,
[string]$Text,
[string]$Style = ''
)
for ($index = 0; $index -lt $Text.Length; $index++) {
Set-Cell -Buffer $Buffer -X ($X + $index) -Y $Y -Glyph $Text[$index] -Style $Style
}
}
function Draw-NeonText {
param(
$Buffer,
[int]$X,
[int]$Y,
[string]$Text,
[object[]]$Palette,
[int]$Offset = 0
)
for ($index = 0; $index -lt $Text.Length; $index++) {
$style = $Palette[($index + $Offset) % $Palette.Count]
Set-Cell -Buffer $Buffer -X ($X + $index) -Y $Y -Glyph $Text[$index] -Style $style
}
}
function Draw-Box {
param(
$Buffer,
[int]$X,
[int]$Y,
[int]$Width,
[int]$Height,
[string]$Style
)
for ($column = 0; $column -lt $Width; $column++) {
$edgeGlyph = if ($column -eq 0 -or $column -eq ($Width - 1)) { '+' } else { '-' }
Set-Cell -Buffer $Buffer -X ($X + $column) -Y $Y -Glyph $edgeGlyph -Style $Style
Set-Cell -Buffer $Buffer -X ($X + $column) -Y ($Y + $Height - 1) -Glyph $edgeGlyph -Style $Style
}
for ($row = 1; $row -lt ($Height - 1); $row++) {
Set-Cell -Buffer $Buffer -X $X -Y ($Y + $row) -Glyph '|' -Style $Style
Set-Cell -Buffer $Buffer -X ($X + $Width - 1) -Y ($Y + $row) -Glyph '|' -Style $Style
}
}
function Render-Buffer {
param($Buffer)
$builder = [System.Text.StringBuilder]::new()
for ($y = 0; $y -lt $Buffer.Height; $y++) {
$currentStyle = ''
for ($x = 0; $x -lt $Buffer.Width; $x++) {
$style = $Buffer.Styles[$y][$x]
if ($style -ne $currentStyle) {
[void]$builder.Append($style)
$currentStyle = $style
}
[void]$builder.Append($Buffer.Chars[$y][$x])
}
if ($currentStyle -ne '') {
[void]$builder.Append($script:ResetStyle)
}
if ($y -lt ($Buffer.Height - 1)) {
[void]$builder.Append("`n")
}
}
return $builder.ToString()
}
function New-Particle {
param(
[double]$X,
[double]$Y,
[string]$Style
)
$chars = @('.', '*', '+', '.', ':')
return [pscustomobject]@{
X = $X
Y = $Y
VX = (($script:Rng.NextDouble() * 2.0) - 1.0) * 18.0
VY = (($script:Rng.NextDouble() * 2.0) - 1.0) * 11.0
Life = 0.65 + ($script:Rng.NextDouble() * 0.55)
MaxLife = 1.20
Style = $Style
Glyph = $chars[$script:Rng.Next(0, $chars.Count)]
}
}
function Add-Explosion {
param(
$State,
[double]$X,
[double]$Y,
[string]$Style,
[int]$Count = 12
)
for ($index = 0; $index -lt $Count; $index++) {
[void]$State.Particles.Add((New-Particle -X $X -Y $Y -Style $Style))
}
if ($Sound) {
try {
[console]::Beep(560 + ($script:Rng.Next(0, 100)), 35)
}
catch {
}
}
}
function New-Star {
param($State)
$depth = $script:Rng.Next(1, 4)
$glyph = switch ($depth) {
1 { '.' }
2 { '*' }
default { '+' }
}
$style = switch ($depth) {
1 { $State.Styles.StarDim }
2 { $State.Styles.StarBright }
default { $State.Styles.StarHot }
}
return [pscustomobject]@{
X = [double]$script:Rng.Next(($State.PlayLeft + 1), $State.PlayRight)
Y = [double]$script:Rng.Next(($State.PlayTop + 1), $State.PlayBottom)
Speed = 1.2 + ($depth * 2.2)
Style = $style
Glyph = $glyph
}
}
function Initialize-Stars {
param($State)
$State.Stars = [System.Collections.ArrayList]::new()
$count = [Math]::Max(28, [Math]::Floor($State.Width / 2))
for ($index = 0; $index -lt $count; $index++) {
[void]$State.Stars.Add((New-Star -State $State))
}
}
function Initialize-Shields {
param($State)
$State.Shields = [System.Collections.ArrayList]::new()
$patterns = @(
' ##### ',
'#######',
'### ###',
'## ##'
)
$shieldCount = 4
$shieldSpacing = [Math]::Floor(($State.PlayWidth - ($shieldCount * 7)) / ($shieldCount + 1))
$shieldY = $State.PlayerY - 5
for ($shieldIndex = 0; $shieldIndex -lt $shieldCount; $shieldIndex++) {
$baseX = $State.PlayLeft + $shieldSpacing + ($shieldIndex * (7 + $shieldSpacing))
for ($row = 0; $row -lt $patterns.Count; $row++) {
for ($column = 0; $column -lt $patterns[$row].Length; $column++) {
if ($patterns[$row][$column] -eq '#') {
[void]$State.Shields.Add([pscustomobject]@{
X = $baseX + $column
Y = $shieldY + $row
HP = 3
})
}
}
}
}
}
function Initialize-Wave {
param($State)
$State.Enemies = [System.Collections.ArrayList]::new()
$rows = 4
$columns = 8
$enemyWidth = 5
$gap = 3
$totalWidth = ($columns * $enemyWidth) + (($columns - 1) * $gap)
$startX = $State.PlayLeft + [Math]::Floor(($State.PlayWidth - $totalWidth) / 2)
$startY = $State.PlayTop + 3
for ($row = 0; $row -lt $rows; $row++) {
for ($column = 0; $column -lt $columns; $column++) {
[void]$State.Enemies.Add([pscustomobject]@{
X = $startX + ($column * ($enemyWidth + $gap))
Y = $startY + ($row * 2)
Width = $enemyWidth
Row = $row
Score = (4 - $row) * 10
})
}
}
$State.EnemyDirection = 1
$State.EnemyMoveTimer = 0.32
$State.EnemyFireTimer = 0.75
$State.BannerText = "WAVE $($State.Level)"
$State.BannerSubText = if ($State.Level -eq 1) { 'Defend Earth.' } else { 'They are getting meaner.' }
$State.BannerTime = 2.2
}
function Reset-GameState {
param($State)
$State.Score = 0
$State.Level = 1
$State.Lives = 3
$State.Ticks = 0.0
$State.ExitRequested = $false
$State.GameOver = $false
$State.Paused = $false
$State.PlayerCooldown = 0.0
$State.PlayerInvulnerable = 0.0
$State.NextSaucerAt = 12.0 + ($script:Rng.NextDouble() * 10.0)
$State.Saucer = $null
$State.PlayerBullets = [System.Collections.ArrayList]::new()
$State.EnemyBullets = [System.Collections.ArrayList]::new()
$State.Particles = [System.Collections.ArrayList]::new()
$State.PlayerX = [Math]::Floor(($State.PlayLeft + $State.PlayRight - 4) / 2)
Initialize-Stars -State $State
Initialize-Shields -State $State
Initialize-Wave -State $State
}
function New-GameState {
param(
[int]$Width,
[int]$Height
)
$styles = [pscustomobject]@{
Border = Get-FgStyle -Red 55 -Green 155 -Blue 255
TitleA = Get-FgStyle -Red 0 -Green 255 -Blue 214
TitleB = Get-FgStyle -Red 68 -Green 164 -Blue 255
TitleC = Get-FgStyle -Red 255 -Green 80 -Blue 170
Player = Get-FgStyle -Red 84 -Green 255 -Blue 198
PlayerWarn = Get-FgStyle -Red 255 -Green 240 -Blue 100
Bullet = Get-FgStyle -Red 255 -Green 255 -Blue 255
EnemyA = Get-FgStyle -Red 255 -Green 120 -Blue 120
EnemyB = Get-FgStyle -Red 255 -Green 190 -Blue 85
EnemyC = Get-FgStyle -Red 255 -Green 90 -Blue 230
EnemyD = Get-FgStyle -Red 255 -Green 70 -Blue 90
EnemyShot = Get-FgStyle -Red 255 -Green 180 -Blue 50
ShieldHigh = Get-FgStyle -Red 98 -Green 255 -Blue 109
ShieldMid = Get-FgStyle -Red 255 -Green 216 -Blue 76
ShieldLow = Get-FgStyle -Red 255 -Green 120 -Blue 120
StarDim = Get-FgStyle -Red 70 -Green 110 -Blue 165
StarBright = Get-FgStyle -Red 120 -Green 195 -Blue 255
StarHot = Get-FgStyle -Red 255 -Green 255 -Blue 255
Saucer = Get-FgStyle -Red 255 -Green 70 -Blue 190
Alert = Get-FgStyle -Red 255 -Green 90 -Blue 90
Overlay = Get-FgStyle -Red 160 -Green 220 -Blue 255
Accent = Get-FgStyle -Red 120 -Green 255 -Blue 255
}
$state = [pscustomobject]@{
Width = $Width
Height = $Height
PlayTop = 3
PlayBottom = $Height - 2
PlayLeft = 1
PlayRight = $Width - 2
PlayWidth = $Width - 2
PlayHeight = $Height - 5
PlayerY = $Height - 4
Styles = $styles
Score = 0
Level = 1
Lives = 3
Ticks = 0.0
ExitRequested = $false
GameOver = $false
Paused = $false
PlayerCooldown = 0.0
PlayerInvulnerable = 0.0
EnemyDirection = 1
EnemyMoveTimer = 0.32
EnemyFireTimer = 0.75
NextSaucerAt = 12.0
Saucer = $null
BannerText = ''
BannerSubText = ''
BannerTime = 0.0
Stars = [System.Collections.ArrayList]::new()
Shields = [System.Collections.ArrayList]::new()
Enemies = [System.Collections.ArrayList]::new()
PlayerBullets = [System.Collections.ArrayList]::new()
EnemyBullets = [System.Collections.ArrayList]::new()
Particles = [System.Collections.ArrayList]::new()
PlayerX = 0
}
Reset-GameState -State $state
return $state
}
function Get-ShieldStyle {
param(
$State,
[int]$HP
)
switch ($HP) {
3 { return $State.Styles.ShieldHigh }
2 { return $State.Styles.ShieldMid }
default { return $State.Styles.ShieldLow }
}
}
function Get-EnemyStyle {
param(
$State,
[int]$Row
)
switch ($Row) {
0 { return $State.Styles.EnemyA }
1 { return $State.Styles.EnemyB }
2 { return $State.Styles.EnemyC }
default { return $State.Styles.EnemyD }
}
}
function Get-EnemySprite {
param(
[int]$Row,
[int]$Frame
)
switch ($Row) {
0 {
if ($Frame -eq 0) { return '[-^-]' }
return '[^_^]'
}
1 {
if ($Frame -eq 0) { return '<o_o>' }
return '<O_O>'
}
2 {
if ($Frame -eq 0) { return '{x_x}' }
return '{X_X}'
}
default {
if ($Frame -eq 0) { return '/MMM\' }
return '\MMM/'
}
}
}
function Try-SpawnSaucer {
param($State)
if ($State.Saucer -or $State.Ticks -lt $State.NextSaucerAt) {
return
}
$direction = if ($script:Rng.Next(0, 2) -eq 0) { 1 } else { -1 }
$x = if ($direction -eq 1) { $State.PlayLeft - 6 } else { $State.PlayRight + 1 }
$State.Saucer = [pscustomobject]@{
X = [double]$x
Y = [double]($State.PlayTop + 1)
Width = 5
Height = 1
VX = 10.0 * $direction
Score = 125
}
$State.NextSaucerAt = $State.Ticks + 13.0 + ($script:Rng.NextDouble() * 12.0)
}
function Fire-PlayerShot {
param($State)
if ($State.PlayerCooldown -gt 0.0 -or $State.GameOver) {
return
}
$spread = if ($State.Level -ge 4) { @(-1, 1) } else { @(0) }
foreach ($offset in $spread) {
[void]$State.PlayerBullets.Add([pscustomobject]@{
X = [double]($State.PlayerX + 2 + $offset)
Y = [double]($State.PlayerY - 1)
Speed = 26.0
})
}
$State.PlayerCooldown = if ($State.Level -ge 5) { 0.19 } else { 0.28 }
if ($Sound) {
try {
[console]::Beep(900, 20)
}
catch {
}
}
}
function Get-LowestEnemies {
param($State)
$lowestByColumn = @{}
foreach ($enemy in $State.Enemies) {
$key = [string]$enemy.X
if (-not $lowestByColumn.ContainsKey($key) -or $enemy.Y -gt $lowestByColumn[$key].Y) {
$lowestByColumn[$key] = $enemy
}
}
return @($lowestByColumn.Values)
}
function Fire-EnemyShot {
param($State)
if ($State.Enemies.Count -eq 0) {
return
}
$shooters = Get-LowestEnemies -State $State
if ($shooters.Count -eq 0) {
return
}
$shooter = $shooters[$script:Rng.Next(0, $shooters.Count)]
[void]$State.EnemyBullets.Add([pscustomobject]@{
X = [double]($shooter.X + 2)
Y = [double]($shooter.Y + 1)
Speed = 13.0 + ([Math]::Min($State.Level, 6) * 0.8)
})
}
function Test-ShieldHit {
param(
$State,
[int]$X,
[int]$Y
)
for ($index = $State.Shields.Count - 1; $index -ge 0; $index--) {
$cell = $State.Shields[$index]
if ($cell.X -eq $X -and $cell.Y -eq $Y) {
$cell.HP--
if ($cell.HP -le 0) {
$State.Shields.RemoveAt($index)
}
return $true
}
}
return $false
}
function Test-PointInRect {
param(
[int]$X,
[int]$Y,
[double]$Left,
[double]$Top,
[int]$Width,
[int]$Height
)
return ($X -ge [Math]::Floor($Left) -and $X -lt ([Math]::Floor($Left) + $Width) -and $Y -ge [Math]::Floor($Top) -and $Y -lt ([Math]::Floor($Top) + $Height))
}
function Advance-Wave {
param($State)
$State.Level++
Initialize-Shields -State $State
Initialize-Wave -State $State
}
function Update-Stars {
param(
$State,
[double]$Delta
)
foreach ($star in $State.Stars) {
$star.Y += ($star.Speed * $Delta)
if ($star.Y -gt ($State.PlayBottom - 1)) {
$star.Y = $State.PlayTop + 1
$star.X = [double]$script:Rng.Next(($State.PlayLeft + 1), $State.PlayRight)
}
}
}
function Update-Particles {
param(
$State,
[double]$Delta
)
for ($index = $State.Particles.Count - 1; $index -ge 0; $index--) {
$particle = $State.Particles[$index]
$particle.Life -= $Delta
if ($particle.Life -le 0.0) {
$State.Particles.RemoveAt($index)
continue
}
$particle.X += ($particle.VX * $Delta)
$particle.Y += ($particle.VY * $Delta)
$particle.VY += (8.0 * $Delta)
}
}
function Update-EnemyFormation {
param(
$State,
[double]$Delta
)
$State.EnemyMoveTimer -= $Delta
if ($State.EnemyMoveTimer -gt 0.0) {
return
}
$moveStep = 1
$needsDrop = $false
foreach ($enemy in $State.Enemies) {
$nextX = $enemy.X + ($State.EnemyDirection * $moveStep)
if ($nextX -le ($State.PlayLeft + 1) -or ($nextX + $enemy.Width - 1) -ge ($State.PlayRight - 1)) {
$needsDrop = $true
break
}
}
if ($needsDrop) {
$State.EnemyDirection *= -1
foreach ($enemy in $State.Enemies) {
$enemy.Y += 1
}
}
else {
foreach ($enemy in $State.Enemies) {
$enemy.X += ($State.EnemyDirection * $moveStep)
}
}
foreach ($enemy in $State.Enemies) {
if ($enemy.Y -ge ($State.PlayerY - 1)) {
$State.GameOver = $true
$State.BannerText = 'EARTH OVERRUN'
$State.BannerSubText = 'Press R to restart or Q to quit.'
$State.BannerTime = 999.0
return
}
}
$speedBoost = [Math]::Min($State.Level * 0.02, 0.12)
$densityBoost = [Math]::Min($State.Enemies.Count * 0.0035, 0.14)
$State.EnemyMoveTimer = [Math]::Max(0.08, 0.34 - $speedBoost - $densityBoost)
}
function Update-Saucer {
param(
$State,
[double]$Delta
)
Try-SpawnSaucer -State $State
if (-not $State.Saucer) {
return
}
$State.Saucer.X += ($State.Saucer.VX * $Delta)
if (($State.Saucer.VX -gt 0 -and $State.Saucer.X -gt $State.PlayRight + 5) -or ($State.Saucer.VX -lt 0 -and $State.Saucer.X -lt $State.PlayLeft - 7)) {
$State.Saucer = $null
}
}
function Update-Bullets {
param(
$State,
[double]$Delta
)
for ($index = $State.PlayerBullets.Count - 1; $index -ge 0; $index--) {
$bullet = $State.PlayerBullets[$index]
$bullet.Y -= ($bullet.Speed * $Delta)
$hitX = [int][Math]::Round($bullet.X)
$hitY = [int][Math]::Round($bullet.Y)
if ($bullet.Y -lt ($State.PlayTop + 1)) {
$State.PlayerBullets.RemoveAt($index)
continue
}
if (Test-ShieldHit -State $State -X $hitX -Y $hitY) {
$State.PlayerBullets.RemoveAt($index)
continue
}
if ($State.Saucer -and (Test-PointInRect -X $hitX -Y $hitY -Left $State.Saucer.X -Top $State.Saucer.Y -Width $State.Saucer.Width -Height $State.Saucer.Height)) {
$State.Score += $State.Saucer.Score
Add-Explosion -State $State -X $bullet.X -Y $bullet.Y -Style $State.Styles.Saucer -Count 18
$State.Saucer = $null
$State.PlayerBullets.RemoveAt($index)
continue
}
$enemyHit = $false
for ($enemyIndex = $State.Enemies.Count - 1; $enemyIndex -ge 0; $enemyIndex--) {
$enemy = $State.Enemies[$enemyIndex]
if (Test-PointInRect -X $hitX -Y $hitY -Left $enemy.X -Top $enemy.Y -Width $enemy.Width -Height 1) {
$State.Score += $enemy.Score
Add-Explosion -State $State -X $bullet.X -Y $bullet.Y -Style (Get-EnemyStyle -State $State -Row $enemy.Row)
$State.Enemies.RemoveAt($enemyIndex)
$State.PlayerBullets.RemoveAt($index)
$enemyHit = $true
break
}
}
if ($enemyHit) {
continue
}
}
for ($index = $State.EnemyBullets.Count - 1; $index -ge 0; $index--) {
$bullet = $State.EnemyBullets[$index]
$bullet.Y += ($bullet.Speed * $Delta)
$hitX = [int][Math]::Round($bullet.X)
$hitY = [int][Math]::Round($bullet.Y)
if ($bullet.Y -gt ($State.PlayBottom - 1)) {
$State.EnemyBullets.RemoveAt($index)
continue
}
if (Test-ShieldHit -State $State -X $hitX -Y $hitY) {
$State.EnemyBullets.RemoveAt($index)
continue
}
if ((Test-PointInRect -X $hitX -Y $hitY -Left $State.PlayerX -Top $State.PlayerY -Width 5 -Height 1) -and $State.PlayerInvulnerable -le 0.0) {
$State.Lives--
$State.PlayerInvulnerable = 2.0
Add-Explosion -State $State -X $bullet.X -Y $bullet.Y -Style $State.Styles.PlayerWarn -Count 16
$State.EnemyBullets.RemoveAt($index)
if ($State.Lives -le 0) {
$State.GameOver = $true
$State.BannerText = 'MISSION FAILED'
$State.BannerSubText = 'Press R to restart or Q to quit.'
$State.BannerTime = 999.0
}
continue
}
}
}
function Update-Game {
param(
$State,
[double]$Delta
)
if ($State.Paused -or $State.GameOver) {
Update-Stars -State $State -Delta ($Delta * 0.15)
Update-Particles -State $State -Delta ($Delta * 0.35)
return
}
$State.Ticks += $Delta
$State.PlayerCooldown = [Math]::Max(0.0, $State.PlayerCooldown - $Delta)
$State.PlayerInvulnerable = [Math]::Max(0.0, $State.PlayerInvulnerable - $Delta)
$State.BannerTime = [Math]::Max(0.0, $State.BannerTime - $Delta)
if ($script:Rng.NextDouble() -lt 0.45) {
[void]$State.Particles.Add([pscustomobject]@{
X = [double]($State.PlayerX + 2)
Y = [double]($State.PlayerY + 1)
VX = (($script:Rng.NextDouble() * 2.0) - 1.0) * 2.0
VY = 4.0 + ($script:Rng.NextDouble() * 2.0)
Life = 0.25 + ($script:Rng.NextDouble() * 0.15)
MaxLife = 0.40
Style = $State.Styles.TitleA
Glyph = '.'
})
}
Update-Stars -State $State -Delta $Delta
Update-Particles -State $State -Delta $Delta
Update-EnemyFormation -State $State -Delta $Delta
Update-Saucer -State $State -Delta $Delta
$State.EnemyFireTimer -= $Delta
if ($State.EnemyFireTimer -le 0.0) {
Fire-EnemyShot -State $State
$State.EnemyFireTimer = [Math]::Max(0.18, 0.85 - ([Math]::Min($State.Level, 7) * 0.06) - ($script:Rng.NextDouble() * 0.2))
}
Update-Bullets -State $State -Delta $Delta
if ($State.Enemies.Count -eq 0 -and -not $State.GameOver) {
Advance-Wave -State $State
}
}
function Process-Input {
param($State)
if ($DemoFrames -gt 0) {
return
}
while ([Console]::KeyAvailable) {
$key = [Console]::ReadKey($true)
switch ($key.Key) {
'LeftArrow' {
$State.PlayerX = [Math]::Max($State.PlayLeft + 1, $State.PlayerX - 2)
}
'RightArrow' {
$State.PlayerX = [Math]::Min($State.PlayRight - 5, $State.PlayerX + 2)
}
'A' {
$State.PlayerX = [Math]::Max($State.PlayLeft + 1, $State.PlayerX - 2)
}
'D' {
$State.PlayerX = [Math]::Min($State.PlayRight - 5, $State.PlayerX + 2)
}
'Spacebar' {
Fire-PlayerShot -State $State
}
'P' {
$State.Paused = -not $State.Paused
}
'R' {
if ($State.GameOver) {
Reset-GameState -State $State
}
}
'Escape' {
$State.ExitRequested = $true
}
'Q' {
$State.ExitRequested = $true
}
}
}
}
function Draw-Hud {
param($State, $Buffer)
$titlePalette = @($State.Styles.TitleA, $State.Styles.TitleB, $State.Styles.TitleC)
Draw-NeonText -Buffer $Buffer -X 2 -Y 0 -Text 'SPACE INVADERS // POWERSHELL EDITION' -Palette $titlePalette -Offset ([int]($State.Ticks * 8))
$scoreText = "SCORE {0:D5} LIVES {1} WAVE {2}" -f $State.Score, $State.Lives, $State.Level
Draw-Text -Buffer $Buffer -X 2 -Y 1 -Text $scoreText -Style $State.Styles.Accent
$controls = 'A/D or arrows move SPACE fires P pause R restart after defeat Q quit'
Draw-Text -Buffer $Buffer -X 2 -Y 2 -Text $controls.Substring(0, [Math]::Min($controls.Length, $State.Width - 4)) -Style $State.Styles.Border
}
function Draw-Playfield {
param($State, $Buffer)
for ($x = $State.PlayLeft; $x -le $State.PlayRight; $x++) {
Set-Cell -Buffer $Buffer -X $x -Y $State.PlayTop -Glyph '-' -Style $State.Styles.Border
Set-Cell -Buffer $Buffer -X $x -Y $State.PlayBottom -Glyph '-' -Style $State.Styles.Border
}
for ($y = $State.PlayTop; $y -le $State.PlayBottom; $y++) {
Set-Cell -Buffer $Buffer -X $State.PlayLeft -Y $y -Glyph '|' -Style $State.Styles.Border
Set-Cell -Buffer $Buffer -X $State.PlayRight -Y $y -Glyph '|' -Style $State.Styles.Border
}
Set-Cell -Buffer $Buffer -X $State.PlayLeft -Y $State.PlayTop -Glyph '+' -Style $State.Styles.Border
Set-Cell -Buffer $Buffer -X $State.PlayRight -Y $State.PlayTop -Glyph '+' -Style $State.Styles.Border
Set-Cell -Buffer $Buffer -X $State.PlayLeft -Y $State.PlayBottom -Glyph '+' -Style $State.Styles.Border
Set-Cell -Buffer $Buffer -X $State.PlayRight -Y $State.PlayBottom -Glyph '+' -Style $State.Styles.Border
foreach ($star in $State.Stars) {
Set-Cell -Buffer $Buffer -X ([int][Math]::Round($star.X)) -Y ([int][Math]::Round($star.Y)) -Glyph $star.Glyph -Style $star.Style
}
foreach ($shieldCell in $State.Shields) {
$glyph = switch ($shieldCell.HP) {
3 { '#' }
2 { '=' }
default { '.' }
}
Set-Cell -Buffer $Buffer -X $shieldCell.X -Y $shieldCell.Y -Glyph $glyph -Style (Get-ShieldStyle -State $State -HP $shieldCell.HP)
}
$enemyFrame = if ((([int]($State.Ticks * 5)) % 2) -eq 0) { 0 } else { 1 }
foreach ($enemy in $State.Enemies) {
Draw-Text -Buffer $Buffer -X $enemy.X -Y $enemy.Y -Text (Get-EnemySprite -Row $enemy.Row -Frame $enemyFrame) -Style (Get-EnemyStyle -State $State -Row $enemy.Row)
}
if ($State.Saucer) {
Draw-Text -Buffer $Buffer -X ([int][Math]::Round($State.Saucer.X)) -Y ([int][Math]::Round($State.Saucer.Y)) -Text '<==>' -Style $State.Styles.Saucer
}
foreach ($bullet in $State.PlayerBullets) {
Set-Cell -Buffer $Buffer -X ([int][Math]::Round($bullet.X)) -Y ([int][Math]::Round($bullet.Y)) -Glyph '|' -Style $State.Styles.Bullet
}
foreach ($bullet in $State.EnemyBullets) {
Set-Cell -Buffer $Buffer -X ([int][Math]::Round($bullet.X)) -Y ([int][Math]::Round($bullet.Y)) -Glyph '!' -Style $State.Styles.EnemyShot
}
foreach ($particle in $State.Particles) {
$x = [int][Math]::Round($particle.X)
$y = [int][Math]::Round($particle.Y)
if ($x -gt $State.PlayLeft -and $x -lt $State.PlayRight -and $y -gt $State.PlayTop -and $y -lt $State.PlayBottom) {
Set-Cell -Buffer $Buffer -X $x -Y $y -Glyph $particle.Glyph -Style $particle.Style
}
}
if (($State.PlayerInvulnerable -le 0.0) -or ((([int]($State.Ticks * 18)) % 2) -eq 0)) {
$playerSprite = if ((([int]($State.Ticks * 10)) % 2) -eq 0) { '/_A_\' } else { '\_^_/' }
$playerStyle = if ($State.PlayerInvulnerable -gt 0.0) { $State.Styles.PlayerWarn } else { $State.Styles.Player }
Draw-Text -Buffer $Buffer -X $State.PlayerX -Y $State.PlayerY -Text $playerSprite -Style $playerStyle
}
}
function Draw-Overlay {
param($State, $Buffer)
if ($State.BannerTime -le 0.0 -and -not $State.Paused -and -not $State.GameOver) {
return
}
$boxWidth = 38
$boxHeight = 6
$boxX = [Math]::Floor(($State.Width - $boxWidth) / 2)
$boxY = [Math]::Floor(($State.Height - $boxHeight) / 2)
Draw-Box -Buffer $Buffer -X $boxX -Y $boxY -Width $boxWidth -Height $boxHeight -Style $State.Styles.Overlay
$headline = if ($State.Paused) { 'PAUSED' } else { $State.BannerText }
$subline = if ($State.Paused) { 'Press P to continue.' } else { $State.BannerSubText }
$headlineX = $boxX + [Math]::Floor(($boxWidth - $headline.Length) / 2)
$sublineX = $boxX + [Math]::Floor(($boxWidth - $subline.Length) / 2)
Draw-NeonText -Buffer $Buffer -X $headlineX -Y ($boxY + 2) -Text $headline -Palette @($State.Styles.TitleA, $State.Styles.TitleB, $State.Styles.TitleC) -Offset ([int]($State.Ticks * 7))
Draw-Text -Buffer $Buffer -X $sublineX -Y ($boxY + 3) -Text $subline -Style $State.Styles.Accent
}
function Draw-Frame {
param($State)
$buffer = New-Buffer -Width $State.Width -Height $State.Height
Draw-Hud -State $State -Buffer $buffer
Draw-Playfield -State $State -Buffer $buffer
Draw-Overlay -State $State -Buffer $buffer
return (Render-Buffer -Buffer $buffer)
}
function Start-SpaceInvaders {
$windowWidth = 0
$windowHeight = 0
try {
$windowWidth = [Console]::WindowWidth
$windowHeight = [Console]::WindowHeight
}
catch {
if ($DemoFrames -gt 0) {
$windowWidth = 92
$windowHeight = 32
}
else {
Write-Host "Unable to access the interactive console. Run the game in a regular PowerShell terminal window."
return
}
}
if ($windowWidth -lt 78 -or $windowHeight -lt 30) {
Write-Host "Please enlarge the terminal to at least 78x30, then run the game again."
return
}
$width = [Math]::Min($windowWidth, 100)
$height = [Math]::Min($windowHeight, 36)
$state = New-GameState -Width $width -Height $height
$frameCounter = 0
$clock = [System.Diagnostics.Stopwatch]::StartNew()
$last = $clock.Elapsed
try {
try {
[Console]::CursorVisible = $false
}
catch {
}
Write-Host -NoNewline "$($script:Escape)[2J$($script:Escape)[H"
while (-not $state.ExitRequested) {
$now = $clock.Elapsed
$delta = ($now - $last).TotalSeconds
if ($delta -gt 0.12) {
$delta = 0.12
}
$last = $now
Process-Input -State $state
Update-Game -State $state -Delta $delta
$frame = Draw-Frame -State $state
Write-Host -NoNewline "$($script:Escape)[H$frame"
if ($DemoFrames -gt 0) {
$frameCounter++
if ($frameCounter -ge $DemoFrames) {
break
}
}
Start-Sleep -Milliseconds $FrameDelayMs
}
}
finally {
Write-Host -NoNewline "$($script:ResetStyle)$($script:Escape)[?25h`n"
try {
[Console]::CursorVisible = $true
}
catch {
}
}
}
Start-SpaceInvaders
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment