├── Add-Sprite.ps1 ├── Assets ├── Blackjack.png ├── Nibbles2020.1.gif ├── Nibbles2020.2.gif ├── Nibbles2020.3.gif ├── Nibbles2020.4.gif └── Nibbles2020.png ├── Find-Game.ps1 ├── Find-Sprite.ps1 ├── Formatting ├── PowerArcade.Box.format.ps1 ├── PowerArcade.GameInfo.format.ps1 ├── PowerArcade.Level.format.ps1 ├── PowerArcade.MessageBox.format.ps1 ├── PowerArcade.PlayingCard.format.ps1 └── PowerArcade.Sprite.format.ps1 ├── Get-Game.ps1 ├── Initialize-Game.ps1 ├── Initialize-Level.ps1 ├── Initialize-Sprite.ps1 ├── Install-Game.ps1 ├── LICENSE ├── Move-Sprite.ps1 ├── New-Sprite.ps1 ├── Parts └── GetScriptMembers.ps1 ├── PowerArcade-Azure-Pipeline.yml ├── PowerArcade.ezout.ps1 ├── PowerArcade.format.ps1xml ├── PowerArcade.psd1 ├── PowerArcade.psm1 ├── PowerArcade.tests.ps1 ├── PowerArcade.types.ps1xml ├── README.md ├── ROM ├── Nibbles2020 │ ├── Game │ │ ├── Game.ps1 │ │ ├── Game.psd1 │ │ ├── OnKey_Down.ps1 │ │ ├── OnKey_Esc.ps1 │ │ ├── OnKey_Left.ps1 │ │ ├── OnKey_P.ps1 │ │ ├── OnKey_Right.ps1 │ │ ├── OnKey_Up.ps1 │ │ └── Over.ps1 │ ├── Levels │ │ ├── 1 │ │ │ └── 1.ps1 │ │ ├── 2 │ │ │ └── 2.ps1 │ │ ├── 3 │ │ │ └── 3.ps1 │ │ ├── 4 │ │ │ └── 4.ps1 │ │ ├── 5 │ │ │ └── 5.ps1 │ │ ├── 6 │ │ │ └── 6.ps1 │ │ ├── 7 │ │ │ └── 7.ps1 │ │ ├── 8 │ │ │ └── 8.ps1 │ │ ├── 9 │ │ │ └── 9.ps1 │ │ ├── GameOver │ │ │ ├── GameOver.ps1 │ │ │ └── OnKey_All.ps1 │ │ ├── Levels.ps1 │ │ ├── Menu │ │ │ ├── Menu.ps1 │ │ │ └── OnKey_All.ps1 │ │ └── Pause │ │ │ └── Pause.ps1 │ ├── Nibbles2020.psd1 │ └── Sprites │ │ ├── Number │ │ └── Number.psd1 │ │ ├── Snake │ │ ├── +.ps1 │ │ ├── +Number.ps1 │ │ ├── +Wall,Tail,Snake.ps1 │ │ ├── Dies.ps1 │ │ ├── OnTick.ps1 │ │ ├── Snake.psd1 │ │ └── SwitchDirection.ps1 │ │ └── Wall │ │ └── Wall.psd1 └── SumGame │ ├── Game │ ├── Game.psd1 │ ├── OnKey_Down.ps1 │ ├── OnKey_Esc.ps1 │ ├── OnKey_Left.ps1 │ ├── OnKey_Right.ps1 │ ├── OnKey_Up.ps1 │ ├── OnTick.ps1 │ └── README.md │ ├── Levels │ ├── Levels.ps1 │ ├── Main │ │ ├── Main.ps1 │ │ ├── Main.psd1 │ │ └── README.md │ ├── Menu │ │ ├── Menu.ps1 │ │ ├── OnKey_Space.ps1 │ │ └── README.md │ └── README.md │ ├── README.md │ ├── Sprites │ ├── Number │ │ ├── Number.ps1 │ │ ├── Number.psd1 │ │ └── README.md │ ├── Player │ │ ├── +Number.ps1 │ │ ├── Player.psd1 │ │ └── README.md │ ├── README.md │ └── Wall │ │ ├── README.md │ │ └── Wall.psd1 │ └── SumGame.psd1 ├── Remove-Sprite.ps1 ├── Restart-Level.ps1 ├── Resume-Game.ps1 ├── Resume-Level.ps1 ├── Show-Game.ps1 ├── Start-Game.ps1 ├── Suspend-Level.ps1 ├── Switch-Level.ps1 ├── Types ├── PowerArcade.Game │ └── GetSpatialHash.ps1 ├── PowerArcade.Level │ └── Draw.ps1 ├── PowerArcade.Point │ └── ToString.ps1 ├── PowerArcade.Sprite.Reference │ └── ToString.ps1 └── PowerArcade.Sprite │ ├── Clear.ps1 │ ├── Draw.ps1 │ ├── Hide.ps1 │ ├── MeasureBounds.ps1 │ ├── Move.ps1 │ ├── get_Bounds.ps1 │ └── get_SpatialHash.ps1 ├── Watch-Game.ps1 ├── Watch-Keyboard.ps1 └── en-us └── About_PowerArcade.help.txt /Add-Sprite.ps1: -------------------------------------------------------------------------------- 1 | function Add-Sprite 2 | { 3 | <# 4 | .Synopsis 5 | Adds a sprite 6 | .Description 7 | Adds a Sprite to the current level, display it for the first time, and register it for collision detection. 8 | .Example 9 | # Adds walls surrounding the screen. 10 | Add-Sprite -X 0 -Y 0 -Width $game.Width -Height 1 -Type Wall # Top 11 | Add-Sprite -X 0 -Y $game.Height -Width $game.Width -Height 1 -Type Wall # Bottom 12 | Add-Sprite -X 0 -Y 1 -Width 1 -Height ($game.Height -1) -Type Wall # Left 13 | Add-Sprite -X $game.Width -Y 1 -Width 1 -Height ($game.Height - 1) -Type Wall #Right 14 | .Link 15 | Move-Sprite 16 | .Link 17 | New-Sprite 18 | .Link 19 | Remove-Sprite 20 | #> 21 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification="Games Must Use the Host")] 22 | [OutputType([Nullable],[PSObject])] 23 | param( 24 | # The type of the sprite. The sprite type is used to group sprites and handle specific collisions. 25 | [Parameter(Position=0,ValueFromPipelineByPropertyName)] 26 | [string] 27 | $Type, 28 | 29 | # The X coordinate of the sprite. 30 | # If the X coordinate would not be visible, the sprite will not be rendered 31 | [Parameter(ValueFromPipelineByPropertyName)] 32 | [int] 33 | $X, 34 | 35 | # The X coordinate of the sprite. 36 | # If the Y coordinate would not be visible, the sprite will not be rendered 37 | [Parameter(ValueFromPipelineByPropertyName)] 38 | [int] 39 | $Y, 40 | 41 | # The name of the sprite. 42 | # Giving a sprite a name will declare it as a global variable. 43 | [Parameter(ValueFromPipelineByPropertyName)] 44 | [string] 45 | $Name, 46 | 47 | # The sprite content. This can be used for sprites that are a single line. 48 | [Parameter(ValueFromPipelineByPropertyName)] 49 | [string] 50 | $Content, 51 | 52 | # The sprite color. 53 | [Parameter(ValueFromPipelineByPropertyName)] 54 | [string] 55 | $Color, 56 | 57 | # The sprite background color. 58 | [Parameter(ValueFromPipelineByPropertyName)] 59 | [string] 60 | $BackgroundColor, 61 | 62 | # The width of the sprite. 63 | # Supplying this and -Height will make the sprite a rectangle. 64 | [Parameter(ValueFromPipelineByPropertyName)] 65 | [int] 66 | $Width, 67 | 68 | # The height of the sprite 69 | # Supplying this and -Width will make the sprite a rectangle. 70 | [Parameter(ValueFromPipelineByPropertyName)] 71 | [int] 72 | $Height, 73 | 74 | # Additional properties of the sprite. 75 | # This can contain any custom information. 76 | [Parameter(ValueFromPipelineByPropertyName)] 77 | [Alias('Properties')] 78 | [Collections.IDictionary] 79 | $Property, 80 | 81 | # Additional methods for the sprite 82 | # These can be used to dynamically create sprite behavior. 83 | [Parameter(ValueFromPipelineByPropertyName)] 84 | [Collections.IDictionary] 85 | $Method, 86 | 87 | # If set, this will find randomized empty space to add the sprite. 88 | [Parameter(ValueFromPipelineByPropertyName)] 89 | [Alias('WhereEver')] 90 | [switch] 91 | $Anywhere, 92 | 93 | # If set, returns the variable of the sprite. 94 | [Parameter(ValueFromPipelineByPropertyName)] 95 | [switch] 96 | $PassThru 97 | ) 98 | 99 | 100 | process { 101 | #region Prepare to Splat 102 | # Most of the sprite creation is handled by New-Sprite 103 | $toSplat = @{} + $PSBoundParameters # so copy all parameters 104 | foreach ($NotToSplat in @('PassThru', 'Anywhere')) { # and remove -PassThru and -Anywhere 105 | $toSplat.Remove($NotToSplat) # (which are not parameters of New-Sprite) 106 | } 107 | #endregion Prepare to Splat 108 | 109 | #region Determine Where anywhere Is 110 | if ($Anywhere) { # If we want to put a sprite -Anywhere 111 | $randomizer = [random]::new() # Create a random number 112 | if ($game.CurrentLevel.SpatialMap.Count) { # If the spatial map has been initialized 113 | $tryCount = 100 # we'll give it a hundred tries 114 | $foundASpot = $false # to see if we find a spot. 115 | :NextSpot do { 116 | $X = $randomizer.Next(3,$game.Width - 3) # Pick a random X,Y 117 | $Y = $randomizer.Next(3, $game.Height - 3) 118 | 119 | $coordinates = @(if ($Width -and $Height) { # If provided a -Width and -Height, make a of coordinates 120 | for ($ex = $x; $ex -lt ($x + $Width); $ex++) { 121 | for ($ey = $y; $ey -lt ($y + $Height); $ey++) { 122 | [PSCustomObject]@{X=$ex;Y=$ey} 123 | } 124 | 125 | } 126 | } else { 127 | [PSCustomObject]@{X=$x;Y=$y} 128 | }) 129 | $tryCount-- 130 | if ($tryCount -le 0) { 131 | break 132 | } 133 | foreach ($c in $coordinates) { # Walk over each coordinate 134 | if (Find-Sprite -X $c.X -Y $c.Y) { # if any sprites were found 135 | continue NextSpot # move to the next spot 136 | # (this way if the map is cluttered near x,y, 137 | # we pick a new spot as soon as we know it's bad) 138 | } 139 | } 140 | $foundASpot = $true # If we made it to here, we've found a spot 141 | } while (-not $foundASpot) 142 | if ($tryCount -le 0) {return } # If the try count is depleted, return. 143 | } else 144 | { 145 | # If we didn't have a spatial map yet, just pick a random spot a bit from the edge. 146 | $X = $randomizer.Next(3, $game.Width - 3) 147 | $Y = $randomizer.Next(3, $game.Height - 3) 148 | } 149 | 150 | $toSplat.X = $X 151 | $toSplat.Y = $y 152 | } 153 | #endregion Determine Where anywhere Is 154 | 155 | # Now, create our sprite. 156 | $newSprite = New-Sprite @toSplat 157 | 158 | if ($Name) # If the sprite was named 159 | { 160 | $ExecutionContext.SessionState.PSVariable.Set("Global:$name",$newSprite) # set a global variable 161 | } 162 | 163 | # If the sprite would be within the field of view, and we're not initializing a level 164 | if ($newSprite.X -ge 0 -and $newSprite.X -le $Host.UI.RawUI.WindowSize.Width -and 165 | $newSprite.Y -ge 0 -and $newSprite.Y -le $Host.UI.RawUI.WindowSize.Height -and 166 | -not $game.CurrentLevel.Initializing 167 | ) { 168 | # Write the sprite to the screen. 169 | [Console]::Write('' + [char]0x1b + '[25l' + (Out-String -InputObject $newSprite -Width 1kb).Trim()) 170 | # (this way, a lot of initial sprite draws can be buffered to improve performance) 171 | } 172 | 173 | #region Put the Sprite in its Place 174 | if ($game.CurrentLevel.Sprites.Add) { # Assuming we have a sprite collection 175 | $newSpriteSpatialHash = $newSprite.SpatialHash # get the spatial hash of the sprite 176 | if ($game.CurrentLevel.SpatialMap -and # If there is a spatial map 177 | $newSpriteSpatialHash # and we have at least one spatial hash 178 | ) 179 | { 180 | foreach ($sh in $newSpriteSpatialHash) { # Walk the spatial hashes 181 | # (sprites could be in more than one) 182 | 183 | # If the spatial hash wasn't already in the map 184 | if (-not $game.CurrentLevel.SpatialMap.ContainsKey($sh)) { 185 | # they may be off the gamespace and into the ether, 186 | # but they may want a world bigger than their screen, 187 | # so create a new spatial map bucket. 188 | $game.CurrentLevel.SpatialMap[$sh] = [Collections.Generic.List[PSObject]]::new() 189 | } 190 | # Create a sprite reference. 191 | $newSpriteRef = 192 | [PSCustomObject]@{PSTypeName='PowerArcade.Sprite.Reference';Type=$type;SpriteID=$newSprite.SpriteID} 193 | 194 | # Sprite References allow us to safely see the map in terms of a type and ID, 195 | # and minimize the data we store. 196 | 197 | # So we add the sprite reference to the spatial map, so it can be hit. 198 | $game.CurrentLevel.SpatialMap[$sh].Add($newSpriteRef) 199 | } 200 | } 201 | 202 | $game.CurrentLevel.SpritesById[$newSprite.SpriteID] = $newSprite 203 | $game.CurrentLevel.Sprites.Add($newSprite) 204 | } 205 | #endregion Put the Sprite in its Place 206 | 207 | if (-not $game.CurrentLevel.Sprites.Add -or $PassThru) { 208 | $newSprite 209 | } 210 | } 211 | } -------------------------------------------------------------------------------- /Assets/Blackjack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StartAutomating/PowerArcade/2f45408db51c55c44c93d363eec551b29eb74f64/Assets/Blackjack.png -------------------------------------------------------------------------------- /Assets/Nibbles2020.1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StartAutomating/PowerArcade/2f45408db51c55c44c93d363eec551b29eb74f64/Assets/Nibbles2020.1.gif -------------------------------------------------------------------------------- /Assets/Nibbles2020.2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StartAutomating/PowerArcade/2f45408db51c55c44c93d363eec551b29eb74f64/Assets/Nibbles2020.2.gif -------------------------------------------------------------------------------- /Assets/Nibbles2020.3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StartAutomating/PowerArcade/2f45408db51c55c44c93d363eec551b29eb74f64/Assets/Nibbles2020.3.gif -------------------------------------------------------------------------------- /Assets/Nibbles2020.4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StartAutomating/PowerArcade/2f45408db51c55c44c93d363eec551b29eb74f64/Assets/Nibbles2020.4.gif -------------------------------------------------------------------------------- /Assets/Nibbles2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StartAutomating/PowerArcade/2f45408db51c55c44c93d363eec551b29eb74f64/Assets/Nibbles2020.png -------------------------------------------------------------------------------- /Find-Game.ps1: -------------------------------------------------------------------------------- 1 | function Find-Game 2 | { 3 | <# 4 | .Synopsis 5 | Finds games 6 | .Description 7 | Finds games in the PowerShell Gallery 8 | .Example 9 | Find-Game 10 | .Link 11 | Get-Game 12 | .Link 13 | Install-Game 14 | #> 15 | [OutputType([PSObject])] 16 | param( 17 | # If provided, will only return games like this -Name. 18 | [Parameter(ValueFromPipelineByPropertyName)] 19 | [string] 20 | $Name, 21 | 22 | # If provided, will look for modules in a given -Repository. 23 | # If not provided, all registered repositories will be searched 24 | # Use Register-PSRepository to register a repository. 25 | [Parameter(ValueFromPipelineByPropertyName)] 26 | [string[]] 27 | $Repository 28 | ) 29 | 30 | process { 31 | #region Find Find-Module 32 | $findModuleCommand = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Find-Module','All') 33 | if (-not $findModuleCommand) { 34 | Write-Error "Find-Module must exist in order to Find-Game" 35 | return 36 | } 37 | #region Find Find-Module 38 | 39 | #region Splatting Find-Module 40 | $findModuleSplat = @{} 41 | if ($Repository) { 42 | $findModuleSplat.Repository = $Repository 43 | } 44 | if ($name) { 45 | $findModuleSplat.Name = $name 46 | } else { 47 | $findModuleSplat.Tag = 'PowerArcade' 48 | } 49 | 50 | $foundModules = & $findModuleCommand @findModuleSplat 51 | #region Splatting Find-Module 52 | 53 | #region Make a Game out of you 54 | foreach ($fm in $foundModules) { 55 | [PSCustomOBject]([Ordered]@{ 56 | Name=$fm.Name; 57 | Version = $fm.Version 58 | Description = $fm.Description 59 | Category = @( 60 | $fm.Tags -like 'GameCategory:*' | Foreach-Object { @($_ -split ':', 2)[-1] } 61 | $fm.Tags -eq 'Screensaver' 62 | ) 63 | PSTypeName = 'PowerArcade.GameInfo' 64 | }) 65 | } 66 | #endregion Make a Game out of you 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Find-Sprite.ps1: -------------------------------------------------------------------------------- 1 | function Find-Sprite 2 | { 3 | <# 4 | .Synopsis 5 | Finds a Sprite 6 | .Description 7 | Finds a Sprite. 8 | 9 | This can find a sprite at a given -X,-Y coordinate, 10 | or find a sprite within a bounding box (defined by -X,-Width,-Y, and -Height) 11 | or find a srptie within a given -Radius from -X,-Y 12 | .Link 13 | Add-Sprite 14 | .Link 15 | Move-Sprite 16 | .Link 17 | Remove-Sprite 18 | .Example 19 | # Finds sprites in the current game map at 10,20 20 | Find-Sprite -X 10 -Y 20 21 | .Example 22 | # Finds sprites within the current game map at 10,20, or within a 3 pixel radius 23 | Find-Sprite -X 10 -Y 20 -Radius 3 24 | #> 25 | param( 26 | # The X coordinate to look for a sprite. 27 | [Parameter(ValueFromPipelineByPropertyName)] 28 | [int] 29 | $X, 30 | 31 | # The Y coordinate to look for a sprite. 32 | [Parameter(ValueFromPipelineByPropertyName)] 33 | [int] 34 | $Y, 35 | 36 | # The width of an area in which to look for a sprite. 37 | # If -Width and -Height are passed, -X and -Y will be treated as the upper left. 38 | [Parameter(ValueFromPipelineByPropertyName)] 39 | [int] 40 | $Width, 41 | 42 | 43 | # The height of an area in which to look for a sprite. 44 | # If -Width and -Height are passed, -X and -Y will be treated as the upper right. 45 | [Parameter(ValueFromPipelineByPropertyName)] 46 | [int] 47 | $Height, 48 | 49 | # The radius of the search area. 50 | # If provided, the -X and -Y will be the center of a virtual square of coordinates 51 | [Parameter(ValueFromPipelineByPropertyName)] 52 | [int] 53 | $Radius, 54 | 55 | # If provided, will only find sprites of one or more given types. 56 | [Parameter(ValueFromPipelineByPropertyName)] 57 | [string[]] 58 | $Type 59 | ) 60 | 61 | begin { 62 | $collided = @() 63 | } 64 | 65 | process { 66 | if (-not $game) { return } 67 | 68 | $bounds = @( 69 | if ($x -ge 0 -and $y -ge 0) { 70 | if ($Width -and $Height) { 71 | # Block 72 | for ($ox =0; $ox -lt $Width; $ox++) { 73 | for ($oy =0; $oy -lt $Height; $oy++) { 74 | [PSCustomObject]@{ 75 | X = $x + $ox 76 | Y = $y + $oy 77 | SpatialHash = $game.GetSpatialHash($x + $ox,$y + $oy) 78 | PSTypeName='PowerArcade.Point' 79 | } 80 | } 81 | } 82 | } elseif ($Width) { 83 | # Row 84 | for ($ox=0;$ox -lt $Width;$ox++) { 85 | [PSCustomObject]@{ 86 | X = $x + $ox 87 | Y = $y 88 | SpatialHash = $game.GetSpatialHash($x + $ox,$y) 89 | PSTypeName='PowerArcade.Point' 90 | } 91 | } 92 | } elseif ($Height) { 93 | # Column 94 | for ($oy=0;$ox -lt $Height;$ox++) { 95 | [PSCustomObject]@{ 96 | X = $x 97 | Y = $y + $oy 98 | SpatialHash = $game.GetSpatialHash($x,$y + $oy) 99 | PSTypeName='PowerArcade.Point' 100 | } 101 | } 102 | } 103 | elseif ($Radius) { 104 | # Radius 105 | for ($ox = $x - $Radius; $ox -lt ($x + $Radius); $ox++) { 106 | for ($oy =$y - $Radius; $oy -lt ($y + $radius); $oy++) { 107 | [PSCustomObject]@{ 108 | X = $ox 109 | Y = $oy 110 | SpatialHash = $game.GetSpatialHash($ox,$oy) 111 | PSTypeName='PowerArcade.Point' 112 | } 113 | } 114 | } 115 | } else { 116 | # Point 117 | [PSCustomObject]@{ 118 | X = $x 119 | Y = $y 120 | SpatialHash = $game.GetSpatialHash($x,$y) 121 | PSTypeName='PowerArcade.Point' 122 | } 123 | 124 | } 125 | } 126 | ) 127 | 128 | 129 | :NextBoundsGroup foreach ($boundsGroup in $bounds | 130 | Group-Object SpatialHash | 131 | Sort-Object Count -Descending) { 132 | 133 | $spriteList = $game.CurrentLevel.SpatialMap[$boundsGroup.Name] 134 | :NextBound foreach ($b in $boundsGroup.Group) { 135 | $boundString = "$($b.X),$($b.Y)" 136 | :NextSprite foreach ($spriteRef in $spriteList) { 137 | $sprite = $game.CurrentLevel.SpritesById[$spriteRef.SpriteID] 138 | if ($type) { 139 | $typeMatch = $false 140 | foreach ($t in $type) { 141 | $typeMatch = $typeMatch -bor ($sprite.Type -like $t) 142 | } 143 | if (-not $typeMatch) { continue NextSprite } 144 | } 145 | if ($sprite.Bounds -contains $boundString) { 146 | # It's a hit! 147 | $sprite 148 | $collided+=$sprite 149 | } 150 | } 151 | } 152 | } 153 | 154 | 155 | 156 | if (-not $bounds) { 157 | if ($type -and $game.CurrentLevel.Sprites.Count) { 158 | :NextSprite foreach ($sprite in $game.CurrentLevel.Sprites) { 159 | $typeMatch = $false 160 | foreach ($t in $type) { 161 | $typeMatch = $typeMatch -bor ($sprite.Type -like $t) 162 | } 163 | if (-not $typeMatch) { continue NextSprite } 164 | $sprite 165 | } 166 | } 167 | } 168 | 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Formatting/PowerArcade.Box.format.ps1: -------------------------------------------------------------------------------- 1 | @( 2 | $box = $_ 3 | $boxWidth = 4 | if ($_.Width) { $_.Width} 5 | else {$host.UI.RawUI.WindowSize.Width} 6 | $boxHeight = 7 | if ($_.Height) { $_.Height} 8 | else {$host.UI.RawUI.WindowSize.Height} 9 | $boxBackgroundColor = $box.BackgroundColor 10 | $boxColor = $box.Color 11 | 12 | $boxFill = 13 | if ($box.Fill) { $box.Fill } 14 | else { '█'; $boxColor = $boxBackgroundColor } 15 | 16 | 17 | 18 | 19 | 20 | $colorStart = 21 | @( 22 | if ($boxColor) { 23 | $intColor = [int]($boxColor -replace '#', '0x') 24 | $r,$g,$b = 25 | [byte](($intColor -band 0xff0000) -shr 16), 26 | [byte](($intColor -band 0x00ff00) -shr 8), 27 | [byte]($intColor -band 0x0000ff) 28 | [char]0x1b+"[38;2;$r;$g;${b}m" 29 | } 30 | 31 | if ($box.BackgroundColor) { 32 | $intColor = [int]($box.BackgroundColor -replace '#', '0x') 33 | $r,$g,$b = 34 | [byte](($intColor -band 0xff0000) -shr 16), 35 | [byte](($intColor -band 0x00ff00) -shr 8), 36 | [byte]($intColor -band 0x0000ff) 37 | [char]0x1b+"[48;2;$r;$g;${b}m" 38 | } 39 | ) -join '' 40 | 41 | 42 | $colorEnd = 43 | @( 44 | if ($boxColor) { 45 | [char]0x1b + '[39m' 46 | } 47 | 48 | if ($box.BackgroundColor) { 49 | [char]0x1b + '[49m' 50 | } 51 | ) -join '' 52 | $boxChar = [string]"$boxFill".Substring(0,1) 53 | 54 | if ($null -ne $box.X -and $null -ne $box.Y) { 55 | @(for ($l =0 ;$l -lt $boxHeight; $l++) { 56 | $colorStart 57 | '' + [char]0x1b + "[$($box.Y + $l);$($box.X)H" 58 | $boxChar * $boxWidth 59 | $colorEnd 60 | }) -join '' 61 | } else { 62 | $colorStart 63 | @( 64 | for ($l = 0; $l -lt $boxHeight; $l++) { 65 | $boxChar * $boxWidth 66 | } 67 | ) -join [Environment]::NewLine 68 | $colorEnd 69 | } 70 | '' 71 | 72 | 73 | ) -join '' 74 | -------------------------------------------------------------------------------- /Formatting/PowerArcade.GameInfo.format.ps1: -------------------------------------------------------------------------------- 1 | Write-FormatView -TypeName PowerArcade.GameInfo -Property Name, Version, Description -Wrap 2 | -------------------------------------------------------------------------------- /Formatting/PowerArcade.Level.format.ps1: -------------------------------------------------------------------------------- 1 | Write-FormatView -Property Name, IsCurrentLevel -TypeName PowerArcade.Level 2 | -------------------------------------------------------------------------------- /Formatting/PowerArcade.MessageBox.format.ps1: -------------------------------------------------------------------------------- 1 | $messageData = $_ 2 | $Messages = 3 | if ($_.Messages){ 4 | $_.Messages 5 | } else { 6 | $_.Message 7 | } 8 | 9 | $y = 10 | if ($messageData.Y) { 11 | $messageData.Y 12 | } else { 13 | $GAME.Height * .33 14 | } 15 | 16 | if ($messageData.Border -eq $true) { 17 | 18 | } 19 | 20 | $y-- 21 | 22 | $colorSplat = @{ 23 | Color= 24 | $( 25 | if ($messageData.Color) { 26 | $messageData.Color 27 | } else { 28 | $game.TextColor 29 | } 30 | ) 31 | BackgroundColor= 32 | $( 33 | if ($messageData.BackgroundColor) { 34 | $messageData.BackgroundColor 35 | } else { 36 | $game.BackgroundColor 37 | } 38 | ) 39 | } 40 | @(foreach ($Message in $Messages) { 41 | if (-not $Message) { continue } 42 | $MessageLines = @($Message -split '(?>\r\n|\n)') 43 | 44 | 45 | 46 | $MaxLength = $MessageLines | 47 | Measure-Object -Property Length -Maximum | 48 | Select-Object -ExpandProperty Maximum 49 | 50 | $TextLineStart = 51 | if ($messageData.X) { 52 | $messageData.X 53 | } else { 54 | ($game.Width - $MaxLength) / 2 55 | } 56 | 57 | if ($messageData.Border -eq $true) { 58 | $Y++ 59 | New-Sprite -Type Message -X $TextLineStart -Y $y -Content ('┌' + $('─' * $MaxLength) + '┐') @colorSplat 60 | } 61 | foreach ($MessageLine in $messageLines) { 62 | $y++ 63 | if ($messageData.Border -eq $true) { 64 | 65 | New-Sprite -Type Message -X $TextLineStart -Y $y -Content $( 66 | 67 | '│' + "$MessageLine".PadRight($MaxLength) + '│' 68 | 69 | ) @colorSplat 70 | } else { 71 | New-Sprite -Type Message -X $TextLineStart -Y $y -Content $MessageLine @colorSplat 72 | } 73 | } 74 | if ($messageData.Border -eq $true) { 75 | $Y++ 76 | New-Sprite -Type Message -X $TextLineStart -Y $y -Content ('└' + $('─' * $MaxLength) + '┘') @colorSplat 77 | } 78 | 79 | }) | Out-String -Width 1kb -------------------------------------------------------------------------------- /Formatting/PowerArcade.PlayingCard.format.ps1: -------------------------------------------------------------------------------- 1 |  2 | $designs = @{ 3 | 0 = @' 4 | ┌─────────┐ 5 | │▒▒▒▒▒▒▒▒▒│ 6 | │▒▒▒▒▒▒▒▒▒│ 7 | │▒▒▒▒▒▒▒▒▒│ 8 | │▒▒▒▒▒▒▒▒▒│ 9 | │▒▒▒▒▒▒▒▒▒│ 10 | │▒▒▒▒▒▒▒▒▒│ 11 | │▒▒▒▒▒▒▒▒▒│ 12 | └─────────┘ 13 | '@ 14 | 1 = @' 15 | ┌─────────┐ 16 | │A♣ │ 17 | │ │ 18 | │ │ 19 | │ ♣ │ 20 | │ │ 21 | │ │ 22 | │ ♣A│ 23 | └─────────┘ 24 | '@ 25 | 2= @' 26 | ┌─────────┐ 27 | │2♣ │ 28 | │ ♣ │ 29 | │ │ 30 | │ │ 31 | │ │ 32 | │ ♣ │ 33 | │ ♣2│ 34 | └─────────┘ 35 | '@ 36 | 3= @' 37 | ┌─────────┐ 38 | │3♣ │ 39 | │ ♣ │ 40 | │ │ 41 | │ ♣ │ 42 | │ │ 43 | │ ♣ │ 44 | │ ♣3│ 45 | └─────────┘ 46 | '@ 47 | 4 = @' 48 | ┌─────────┐ 49 | │4♣ │ 50 | │ ♣ ♣ │ 51 | │ │ 52 | │ │ 53 | │ │ 54 | │ ♣ ♣ │ 55 | │ ♣4│ 56 | └─────────┘ 57 | '@ 58 | 5 = @' 59 | ┌─────────┐ 60 | │5♣ │ 61 | │ ♣ ♣ │ 62 | │ │ 63 | │ ♣ │ 64 | │ │ 65 | │ ♣ ♣ │ 66 | │ ♣5│ 67 | └─────────┘ 68 | '@ 69 | 6 = @' 70 | ┌─────────┐ 71 | │6♣ │ 72 | │ ♣ ♣ │ 73 | │ │ 74 | │ ♣ ♣ │ 75 | │ │ 76 | │ ♣ ♣ │ 77 | │ ♣6│ 78 | └─────────┘ 79 | '@ 80 | 81 | 7 = @' 82 | ┌─────────┐ 83 | │7♣ │ 84 | │ ♣ ♣ │ 85 | │ │ 86 | │ ♣ ♣ ♣ │ 87 | │ │ 88 | │ ♣ ♣ │ 89 | │ ♣7│ 90 | └─────────┘ 91 | '@ 92 | 8 = @' 93 | ┌─────────┐ 94 | │8♣ │ 95 | │ ♣ ♣ ♣ │ 96 | │ │ 97 | │ ♣ ♣ │ 98 | │ │ 99 | │ ♣ ♣ ♣ │ 100 | │ ♣8│ 101 | └─────────┘ 102 | '@ 103 | 9= @' 104 | ┌─────────┐ 105 | │9♣ │ 106 | │ ♣ ♣ ♣ │ 107 | │ │ 108 | │ ♣ ♣ ♣ │ 109 | │ │ 110 | │ ♣ ♣ ♣ │ 111 | │ ♣9│ 112 | └─────────┘ 113 | '@ 114 | 10 = @' 115 | ┌─────────┐ 116 | │10♣ │ 117 | │ ♣ ♣ ♣ │ 118 | │ ♣ │ 119 | │ ♣ ♣ │ 120 | │ ♣ │ 121 | │ ♣ ♣ ♣ │ 122 | │ ♣10│ 123 | └─────────┘ 124 | '@ 125 | 11= @' 126 | ┌─────────┐ 127 | │J♣ │ 128 | │ ♣ │ 129 | │ ♣ │ 130 | │ ♣ │ 131 | │ ♣ ♣ │ 132 | │ ♣ │ 133 | │ ♣J│ 134 | └─────────┘ 135 | '@ 136 | 12= @' 137 | ┌─────────┐ 138 | │Q♣ │ 139 | │ ♣♣♣ │ 140 | │ ♣ ♣ │ 141 | │ ♣ ♣ │ 142 | │ ♣ ♣ │ 143 | │ ♣♣♣ │ 144 | │ ♣♣Q│ 145 | └─────────┘ 146 | '@ 147 | 13= @' 148 | ┌─────────┐ 149 | │K♣ │ 150 | │ ♣ ♣ │ 151 | │ ♣ ♣ │ 152 | │ ♣♣♣ │ 153 | │ ♣ ♣ │ 154 | │ ♣ ♣ │ 155 | │ ♣K│ 156 | └─────────┘ 157 | '@ 158 | 159 | } 160 | 161 | 162 | $card = $_ 163 | $realSuite = 164 | if ($card.Suite -eq '♣' -or $card.Suite -eq 'Clubs' -or $card.Suite -eq 1) 165 | { 166 | '♣' 167 | } 168 | elseif ($card.Suite -eq '♦' -or $card.Suite -eq 'Diamonds' -or $card.Suite -eq 2) 169 | { 170 | '♦' 171 | } 172 | elseif ($card.Suite -eq '♥' -or $card.Suite -eq 'Hearts' -or $card.Suite -eq 3) 173 | { 174 | '♥' 175 | } 176 | elseif ($card.Suite -eq '♠' -or $card.Suite -eq 'Spades' -or $card.Suite -eq 4) 177 | { 178 | '♠' 179 | } 180 | 181 | $cardNumber = $card.Number -as [int] 182 | if (-not $cardNumber) { 183 | if ($card.Number -eq 'Ace') { 184 | $cardNumber = 1 185 | } elseif ($card.Number -eq 'Jack') { 186 | $cardNumber = 11 187 | } elseif ($card.Number -eq 'Queen') { 188 | $cardNumber = 12 189 | } elseif ($card.Number -eq 'King') { 190 | $cardNumber = 13 191 | } 192 | } 193 | if (-not $designs[$cardNumber]) { 194 | throw "$($card.Number) not found" 195 | } 196 | if (-not $Host.UI.SupportsVirtualTerminal) { 197 | return $designs[$cardNumber] -replace '♣', $realSuite 198 | } else { 199 | @( 200 | 201 | '' + [char]0x1b+"[48;2;255;255;255m" 202 | if ('♣', '♠' -contains $realSuite) { 203 | '' + [char]0x1b+"[38;2;0;0;0m" 204 | } else { 205 | '' + [char]0x1b+"[38;2;255;0;0m" 206 | } 207 | if ($card.Selected) { 208 | '' + [char]0x1b + '[7m' 209 | } 210 | if ($card.X -ge 0 -and $card.Y -ge 0) { 211 | $designLines = $designs[$cardNumber] -replace '♣', $realSuite -split '(?>\r\n|\n)' 212 | $y = $card.Y 213 | foreach ($dl in $designLines) { 214 | '' + [char]0x1b + "[$($Y);$($card.X)H" 215 | $dl.Trim() 216 | $y++ 217 | } 218 | } else { 219 | $designs[$cardNumber] -replace '♣', $realSuite 220 | } 221 | if ($card.Selected) { 222 | '' + [char]0x1b + '[27m' 223 | } 224 | '' + [char]0x1b +'[39m' 225 | '' + [char]0x1b +'[49m' 226 | ) -join '' 227 | } 228 | -------------------------------------------------------------------------------- /Formatting/PowerArcade.Sprite.format.ps1: -------------------------------------------------------------------------------- 1 | $_.Draw() -------------------------------------------------------------------------------- /Get-Game.ps1: -------------------------------------------------------------------------------- 1 | function Get-Game 2 | { 3 | <# 4 | .Synopsis 5 | Gets installed games 6 | .Description 7 | Gets installed PowerArcade games. 8 | 9 | PowerArcade games are installed to a ROM folder in the PowerArcade directory 10 | The PowerArcade directory is located in the same folder as the user's $profile 11 | .Example 12 | Get-Game 13 | .Link 14 | Find-Game 15 | .Link 16 | Install-Game 17 | .Link 18 | Start-Game 19 | #> 20 | param( 21 | # If provided, will only return games like this -Name. 22 | [string] 23 | $Name, 24 | 25 | # If provided, will only return games like this -Cateogry. 26 | [string] 27 | $Category 28 | ) 29 | 30 | 31 | $gameRomDirectories = @( 32 | $profile | 33 | Split-Path | 34 | Join-Path -ChildPath PowerArcade | 35 | Join-Path -ChildPath ROM 36 | 37 | $MyInvocation.MyCommand.ScriptBlock.File | 38 | Split-Path | 39 | Join-Path -ChildPath ROM 40 | ) 41 | 42 | $gameDirectories = $gameRomDirectories | 43 | Get-Item -ErrorAction Ignore | 44 | Get-ChildItem -Directory 45 | 46 | foreach ($gameDirectory in $gameDirectories) { 47 | $realDirectory =$gameDirectory 48 | $gamePsd1 = @{FileName = $gameDirectory.Name + '.psd1';ErrorAction='Ignore'} 49 | $psd1 = try { Import-LocalizedData -BaseDirectory $gameDirectory.FullName @gamePsd1 } catch {$null } 50 | if (-not $psd1) { 51 | $mostRecentVersionDirectory = 52 | $gameDirectory | 53 | Get-ChildItem -Directory | 54 | Where-Object {$_.Name -as [Version] } | 55 | Sort-Object { $_.Name -as [Version] } -Descending | 56 | Select-Object -First 1 57 | $psd1 = 58 | try { Import-LocalizedData -BaseDirectory $mostRecentVersionDirectory.FullName @gamePsd1 } 59 | catch {$null } 60 | if (-not $psd1) { 61 | Write-Verbose "Could not find $($gamePSD1.FileName) game in $($gameDirectory.Name)" 62 | continue 63 | } else { 64 | $realDirectory = $mostRecentVersionDirectory 65 | } 66 | } 67 | 68 | $tags = @() + $psd1.PrivateData.PSData.Tags 69 | $gotGame = [PSCustomOBject]([Ordered]@{ 70 | Name=$gameDirectory.Name 71 | Version = $psd1.ModuleVersion -as [Version] 72 | Description = $psd1.Description 73 | Category = @( 74 | $tags -like 'GameCategory:*' | Foreach-Object { @($_ -split ':', 2)[-1] } 75 | $tags -eq 'Screensaver' 76 | ) 77 | ModuleManifest = $psd1 78 | GamePath = $realDirectory.Fullname 79 | PSTypeName = 'PowerArcade.GameInfo' 80 | }) 81 | 82 | if ($name -and ($gotGame.Name -notlike $name)) { continue } 83 | if ($Category -and -not ($gotGame.Category -like $Category)) { continue } 84 | $gotGame 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Initialize-Game.ps1: -------------------------------------------------------------------------------- 1 | function Initialize-Game 2 | { 3 | <# 4 | .Synopsis 5 | Initializes a game 6 | .Description 7 | Initializes the global variable $game with the game logic contains within a -GamePath. 8 | .Link 9 | Start-Game 10 | .Link 11 | Initialize-Level 12 | .Link 13 | Initialize-Sprite 14 | .Example 15 | # Initializes Nibbles2020, when run from PowerArcade's module root. 16 | Initialize-Game -GamePath .\ROM\Nibbles2020 17 | #> 18 | [CmdletBinding(DefaultParameterSetName='GameModule')] 19 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "", Justification="Games are Global")] 20 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectComparisonWithNull", "", Justification="Using Null against a list")] 21 | param( 22 | # The path to the game. This path should contain a module, and 'Game','Levels',and 'Sprites' subdirectories. 23 | [Parameter(Mandatory,Position=0,ValueFromPipelineByPropertyName,ParameterSetName='GamePath')] 24 | [Alias('ROM','FullName')] 25 | [string] 26 | $GamePath, 27 | 28 | # The game module 29 | [Parameter(Mandatory,Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName,ParameterSetName='GameModule')] 30 | [Management.Automation.PSModuleInfo] 31 | $Module, 32 | 33 | # The level to the game will start. 34 | # If not provided, will use the default from the game.psd1. 35 | # If no default is found, will start at the first defined level. 36 | [Alias('Level')] 37 | [string] 38 | $StartLevel, 39 | 40 | # If set, will not clear the screen, clear the spritemap, and clear the sprite list 41 | [Alias('DoNotClear')] 42 | [switch] 43 | $NoClear 44 | ) 45 | 46 | process { 47 | if ($PSCmdlet.ParameterSetName -eq 'GamePath') { 48 | 49 | $resolvedGamePath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($gamePath) 50 | if (-not $resolvedGamePath) { return } 51 | if ($resolvedGamePath -like '*.ps1') { 52 | 53 | return 54 | } 55 | 56 | $gamePathItem = Get-Item -LiteralPath $resolvedGamePath 57 | if ($resolvedGamePath -notlike '*.psd1') { 58 | if ($gamePathItem -is [IO.DirectoryInfo]) { 59 | $psd1Name = 60 | if ($gamePathItem.Name -as [Version]) { 61 | $gamePathItem.Parent.Name 62 | } else { 63 | $gamePathItem.Name 64 | } 65 | foreach ($file in $gamePathItem.GetFileSystemInfos()) { 66 | if ($file.Name -eq "$psd1Name.psd1") { 67 | $gamePathItem = $file 68 | break 69 | } 70 | } 71 | if ($gamePathItem -is [IO.DirectoryInfo]) { 72 | return 73 | } 74 | } else { 75 | return 76 | } 77 | } 78 | 79 | if ($GamePathItem.Name -notlike '*.psd1') { 80 | Write-Verbose "Skipping $GamePath because it is not a manifest file" 81 | return 82 | } 83 | 84 | $toSplat = @{} + $PSBoundParameters 85 | $toSplat.Remove('GamePath') 86 | Import-Module $gamePathItem.Fullname -Force -PassThru | Initialize-Game @toSplat 87 | 88 | return 89 | } 90 | 91 | $gameModuleDirectory = $Module | Split-Path | Get-Item 92 | if (-not $gameModuleDirectory) { return } 93 | 94 | foreach ($fsi in $GameModuleDirectory.GetFileSystemInfos()) { 95 | if ($fsi -isnot [IO.DirectoryInfo]) { 96 | continue 97 | } 98 | 99 | 100 | if ($fsi.Name -eq 'Game') { 101 | # Game-wide content 102 | 103 | 104 | 105 | $global:Game = [PSCustomObject]@{ 106 | Name = $Module.Name 107 | Version = $module.Version 108 | Root = $Module | Split-Path 109 | CurrentLevel = $null 110 | CurrentLevelName = '' 111 | Clock = '00:00:00.01' 112 | Levels = [Ordered]@{} 113 | SpriteTypes = [Ordered]@{} 114 | } 115 | foreach ($member in & $GetScriptMembers $fsi) { 116 | $game.psobject.members.add($member) 117 | } 118 | if ($game.Default) { 119 | if ($game.Default -is [Collections.IDictionary]) { 120 | foreach ($kv in $game.Default.GetEnumerator()) { 121 | $game.psobject.members.add( 122 | [PSNoteProperty]::new($kv.Key, $kv.Value), 123 | $true 124 | ) 125 | } 126 | } 127 | } 128 | 129 | if (-not $game.Width) { 130 | $Game | Add-Member NoteProperty Width $host.UI.RawUI.WindowSize.Width -Force 131 | } 132 | 133 | if (-not $game.Height) { 134 | $Game | Add-Member NoteProperty Height $host.UI.RawUI.WindowSize.Height -Force 135 | } 136 | 137 | if (-not $game.CellWidth) { 138 | $game | Add-Member NoteProperty CellWidth ([Math]::Ceiling([Math]::Sqrt($game.Width))) 139 | } 140 | if (-not $game.CellHeight) { 141 | 142 | $game | Add-Member NoteProperty CellHeight ([Math]::Ceiling([Math]::Sqrt($game.Height))) 143 | } 144 | 145 | $game.pstypenames.clear() 146 | $game.pstypenames.add('PowerArcade.Game') 147 | 148 | } 149 | } 150 | 151 | $game | Initialize-Level 152 | $game | Initialize-Sprite 153 | 154 | if ($game.Initialize.Invoke) { 155 | $game.Initialize() 156 | } 157 | 158 | if (-not $StartLevel) { 159 | $startLevel = @($game.StartLevel, $game.StartingLevel, $game.DefaultLevel -ne $null -ne '')[0] 160 | } 161 | if (-not $StartLevel) { 162 | $StartLevel = @($game.Levels.GetEnumerator())[0].Key 163 | } 164 | 165 | 166 | 167 | $global:Game = $game 168 | 169 | if ($StartLevel) { 170 | Switch-Level -Level $startLevel -NoClear:$NoClear 171 | } 172 | $game 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Initialize-Level.ps1: -------------------------------------------------------------------------------- 1 | function Initialize-Level 2 | { 3 | <# 4 | .Synopsis 5 | Initializes Game Levels 6 | .Description 7 | Initializes Game Levels. 8 | 9 | Game Levels can be located in subdirectory named 'Leve' or 'Levels'. 10 | 11 | Any game level 12 | .Link 13 | Initialize-Level 14 | .Link 15 | Initialize-Sprite 16 | #> 17 | [CmdletBinding(DefaultParameterSetName='Game')] 18 | param( 19 | # The path to a specific level 20 | [Parameter(Mandatory,Position=0,ValueFromPipelineByPropertyName,ParameterSetName='LevelPath')] 21 | [Alias('FullName')] 22 | [string] 23 | $LevelPath, 24 | 25 | # The path to a game. 26 | [Parameter(Mandatory,Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName,ParameterSetName='Game')] 27 | [Parameter(Position=1,ParameterSetName='LevelPath')] 28 | [PSTypename('PowerArcade.Game')] 29 | [PSObject] 30 | $Game 31 | ) 32 | 33 | begin { 34 | $levelDirectories = [Collections.Generic.List[IO.DirectoryInfo]]::new() 35 | } 36 | 37 | process { 38 | if ($PSCmdlet.ParameterSetName -eq 'Game') { 39 | $levelRoot = $Game.Root | 40 | Get-ChildItem -Directory | 41 | Where-Object Name -In 'Level', 'Levels' | 42 | Select-Object -First 1 43 | 44 | if (-not $levelRoot) { return } 45 | $allLevels =[PSCustomObject]@{} 46 | foreach ($member in & $GetScriptMembers $levelRoot) { 47 | $allLevels.psobject.Members.Add($member, $true) 48 | } 49 | $game | 50 | Add-Member NoteProperty LevelBaseObject $allLevels -Force 51 | 52 | 53 | 54 | $gameLevels = $levelRoot | 55 | Get-ChildItem -Directory | 56 | Initialize-Level -Game $game 57 | 58 | $gameLevelsByName = [Ordered]@{} 59 | foreach ($gl in $gameLevels) { 60 | $gameLevelsByName[$gl.Name] = $gl 61 | } 62 | $game | 63 | Add-Member NoteProperty Levels $gameLevelsByName -Force 64 | 65 | return 66 | } 67 | 68 | $resolvedLevelPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($LevelPath) 69 | if (-not $resolvedLevelPath) { return} 70 | $resolvedLevelItem = Get-Item -LiteralPath $resolvedLevelPath 71 | if (-not $resolvedLevelItem) { return } 72 | if ($resolvedLevelItem -is [IO.DirectoryInfo]) { 73 | $levelDirectories.Add($resolvedLevelItem) 74 | } 75 | } 76 | 77 | end { 78 | $c, $t, $id = 0, $levelDirectories.Count, [Random]::new().Next() 79 | foreach ($levelDir in $levelDirectories) { 80 | Write-Progress "Loading Levels" "$($levelDir.Name)" -Id $id -PercentComplete ($c * 100 / $t) 81 | $c++ 82 | 83 | $levelObject = [PSCustomObject]@{ 84 | PSTypeName='PowerArcade.Level'; 85 | Name=$levelDir.Name; 86 | Sprites=[Collections.Generic.List[PSObject]]::new() 87 | SpatialMap = 88 | [Collections.Generic.Dictionary[ 89 | string, 90 | [Collections.Generic.List[PSObject]] 91 | ]]::new([StringComparer]::OrdinalIgnoreCase) 92 | SpritesByID = 93 | [Collections.Generic.Dictionary[ 94 | string, 95 | [PSObject] 96 | ]]::new([StringComparer]::OrdinalIgnoreCase) 97 | 98 | SpriteMap=[Collections.Generic.Dictionary[string,[Collections.Queue]]]::new([StringComparer]::OrdinalIgnoreCase) 99 | } 100 | foreach ($X in 0..$game.CellWidth) { 101 | foreach ($Y in 0..$game.CellHeight) { 102 | $levelObject.SpatialMap["$X,$y"] = [Collections.Generic.List[PSObject]]::new() 103 | } 104 | } 105 | 106 | foreach ($member in & $GetScriptMembers $levelDir) { 107 | $levelObject.psobject.Members.Add($member, $true) 108 | } 109 | 110 | if ($levelObject.Default) { 111 | if ($levelObject.Default -is [Collections.IDictionary]) { 112 | foreach ($kv in $levelObject.Default.GetEnumerator()) { 113 | $levelObject.psobject.members.add( 114 | [PSNoteProperty]::new($kv.Key, $kv.Value), 115 | $true 116 | ) 117 | } 118 | } 119 | } 120 | 121 | $levelObject 122 | } 123 | 124 | Write-Progress "Loading Levels" "Complete" -Id $id -Completed 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Initialize-Sprite.ps1: -------------------------------------------------------------------------------- 1 | function Initialize-Sprite 2 | { 3 | <# 4 | .Synopsis 5 | Initializes Sprites 6 | .Description 7 | Initializes Sprites for a Game. 8 | 9 | This will preload sprite behaviors and content for any sprite located beneath a Sprite(s) directory 10 | .Link 11 | Initialize-Game 12 | .Link 13 | Initialize-Level 14 | #> 15 | [CmdletBinding(DefaultParameterSetName='Game')] 16 | param( 17 | # The path to a specific sprite. 18 | [Parameter(Mandatory,Position=0,ValueFromPipelineByPropertyName,ParameterSetName='SpritePath')] 19 | [Alias('FullName')] 20 | [string] 21 | $SpritePath, 22 | 23 | # The path to a game. 24 | [Parameter(Mandatory,Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName,ParameterSetName='Game')] 25 | [Parameter(Position=1,ParameterSetName='SpritePath')] 26 | [PSTypename('PowerArcade.Game')] 27 | [PSObject] 28 | $Game 29 | ) 30 | 31 | begin { 32 | $SpriteDirectories = [Collections.Generic.List[IO.DirectoryInfo]]::new() 33 | } 34 | 35 | process { 36 | if ($PSCmdlet.ParameterSetName -eq 'Game') { 37 | $SpriteRoot = $Game.Root | 38 | Get-ChildItem -Directory | 39 | Where-Object Name -In 'Sprite', 'Sprites' | 40 | Select-Object -First 1 41 | 42 | if (-not $SpriteRoot) { return } 43 | $allSprites =[PSCustomObject]@{} 44 | foreach ($member in & $GetScriptMembers $SpriteRoot) { 45 | $allSprites.psobject.Members.Add($member, $true) 46 | } 47 | $game | 48 | Add-Member NoteProperty SpriteBaseObject $allSprites -Force 49 | 50 | 51 | 52 | $gameSprites = $SpriteRoot | 53 | Get-ChildItem -Directory | 54 | Initialize-Sprite -Game $game 55 | 56 | $gameSpritesByType = [Ordered]@{} 57 | foreach ($gs in $gameSprites) { 58 | $gameSpritesByType[$gs.Type] = $gs 59 | } 60 | $game | 61 | Add-Member NoteProperty SpriteTypes $gameSpritesByType -Force 62 | 63 | return 64 | } 65 | 66 | $resolvedSpritePath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($SpritePath) 67 | if (-not $resolvedSpritePath) { return} 68 | $resolvedSpriteItem = Get-Item -LiteralPath $resolvedSpritePath 69 | if (-not $resolvedSpriteItem) { return } 70 | if ($resolvedSpriteItem -is [IO.DirectoryInfo]) { 71 | $SpriteDirectories.Add($resolvedSpriteItem) 72 | } 73 | } 74 | 75 | end { 76 | $c, $t, $id = 0, $SpriteDirectories.Count, [Random]::new().Next() 77 | foreach ($SpriteDir in $SpriteDirectories) { 78 | Write-Progress "Loading Sprites" "$($SpriteDir.Name)" -Id $id -PercentComplete ($c * 100 / $t) 79 | $c++ 80 | 81 | $SpriteObject = [PSCustomObject]@{PSTypeName='PowerArcade.Sprite';Type=$SpriteDir.Name} 82 | foreach ($member in & $GetScriptMembers $SpriteDir) { 83 | $SpriteObject.psobject.Members.Add($member, $true) 84 | } 85 | $SpriteObject 86 | } 87 | 88 | Write-Progress "Loading Sprites" "Complete" -Id $id -Completed 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Install-Game.ps1: -------------------------------------------------------------------------------- 1 | function Install-Game 2 | { 3 | <# 4 | .Synopsis 5 | Installs a Game from the PowerShell Gallery 6 | .Description 7 | Installs a Game from the PowerShell Gallery to the PowerArcade ROM directory. 8 | .Link 9 | Get-Game 10 | .Example 11 | Install-Game -Name Blackjack 12 | .Example 13 | Find-Game -Name ShuffleScreen | Install-Game 14 | #> 15 | [OutputType([Nullable])] 16 | param( 17 | # The name of the game module. 18 | [Parameter(Mandatory,ValueFromPipelineByPropertyName,Position=0)] 19 | [string] 20 | $Name, 21 | 22 | # The repository. If this is not provided, all default registered repositories will be contacted. 23 | [Parameter(ValueFromPipelineByPropertyName,Position=0)] 24 | [string] 25 | $Repository 26 | ) 27 | 28 | process { 29 | #region Where to? 30 | $saveTo = $profile | 31 | Split-Path | 32 | Join-Path -ChildPath PowerArcade | 33 | Join-Path -ChildPath ROM 34 | #endregion Where to? 35 | 36 | #region Is it Safe? 37 | if (-not (Test-Path $saveTo)) { 38 | $createdDirectory = New-Item -ItemType Directory -Path $saveTo -Force 39 | if (-not $createdDirectory) { 40 | return 41 | } 42 | } 43 | #endregion Is it Safe? 44 | 45 | #region Save the Module, Save the World 46 | $saveModuleSplat = @{Path=$saveTo} + $PSBoundParameters 47 | Save-Module @saveModuleSplat 48 | #endregion Save the Module, Save the World 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Start-Automating 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Move-Sprite.ps1: -------------------------------------------------------------------------------- 1 | function Move-Sprite 2 | { 3 | <# 4 | .Synopsis 5 | Moves sprites 6 | .Description 7 | Moves sprites around the screen 8 | .Example 9 | $dot | Move-Sprite -X 10 -Y 20 10 | .Link 11 | Add-Sprite 12 | .Link 13 | Find-Sprite 14 | .Link 15 | New-Sprite 16 | .Link 17 | Remove-Sprite 18 | #> 19 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification="Games Must Use the Host")] 20 | [OutputType([Nullable],[PSObject])] 21 | param( 22 | # The Sprite 23 | [Parameter(Mandatory,ValueFromPipeline)] 24 | [PSTypeName('PowerArcade.Sprite')] 25 | [PSObject] 26 | $Sprite, 27 | 28 | # The target X coordinate 29 | [Parameter(Mandatory,Position=0,ValueFromPipelineByPropertyName)] 30 | [int] 31 | $X, 32 | 33 | # The target Y coordinate 34 | [Parameter(Mandatory,Position=1,ValueFromPipelineByPropertyName)] 35 | [int] 36 | $Y, 37 | 38 | # If set, will not clear the content at the old sprite position 39 | [Alias('DoNotClear')] 40 | [switch] 41 | $NoClear, 42 | 43 | # If set, will force an overwrite in the case of a collision. 44 | [switch] 45 | $Force 46 | ) 47 | 48 | process { 49 | # It's possible the sprite doesn't have an X/Y yet, so add it if they don't. 50 | if (-not $sprite.psobject.properties['X']) { 51 | $sprite.psobject.properties.Add([PSNoteProperty]::new('X',$X)) 52 | } 53 | if (-not $sprite.psobject.properties['Y']) { 54 | $sprite.psobject.properties.Add([PSNoteProperty]::new('Y',$Y)) 55 | } 56 | 57 | #region Where am I going, and who will I run into there? 58 | $newBounds = $Sprite.MeasureBounds($x, $y) 59 | 60 | $collision = $false 61 | $foundSprites = $newBounds |Find-Sprite 62 | if ($foundSprites) { 63 | $null = $null 64 | foreach ($ThingIHit in $foundSprites) { 65 | if ($ThingIHit -eq $Sprite) { continue } 66 | $collision = $true 67 | if ($sprite.'+'.Invoke) { 68 | $sprite.'+'($ThingIHit) 69 | } 70 | if ($sprite."+$($ThingIHit.Type)".Invoke) { 71 | $sprite."+$($ThingIHit.Type)"($ThingIHit) 72 | } 73 | } 74 | } 75 | #region Where am I going, and who will I run into there? 76 | 77 | if ($collision -and -not $Force) { 78 | return 79 | } 80 | 81 | 82 | try {[Console]::CursorVisible = $false} catch {$PSCmdlet.WriteVerbose("$_")} 83 | if (-not $NoClear -and -not $this.Hidden) { 84 | [Console]::Write("$($Sprite.Clear())".Trim()) 85 | } 86 | 87 | $oldBounds = $Sprite.MeasureBounds() 88 | 89 | $sprite.X, $sprite.Y = $X, $Y 90 | 91 | $newSpatialHashes = $newBounds | Select-Object -ExpandProperty SpatialHash -Unique 92 | $oldSpatialHashes = $oldBounds | Select-Object -ExpandProperty SpatialHash -Unique 93 | 94 | foreach ($nsh in $newSpatialHashes) { 95 | if ($oldSpatialHashes -notcontains $nsh) { # Moving into a new spatial hash 96 | if ($game.CurrentLevel.SpatialMap.ContainsKey($nsh)) { 97 | $game.CurrentLevel.SpatialMap[$nsh].Add( 98 | [PSCustomObject]@{PSTypeName='PowerArcade.Sprite.Reference';Type=$sprite.type;SpriteID=$sprite.SpriteID} 99 | ) 100 | 101 | $game.CurrentLevel.SpritesById[$sprite.SpriteID] = $sprite 102 | } 103 | } 104 | } 105 | 106 | foreach ($osh in $oldSpatialHashes) { 107 | if ($newSpatialHashes -notcontains $osh) { # Moving out of an old spatial hash 108 | if ($game.CurrentLevel.SpatialMap.ContainsKey($osh)) { 109 | $toRemove = 110 | for ($in =0 ; $in -lt $game.CurrentLevel.SpatialMap[$osh].Count; $in++) { 111 | if ($game.CurrentLevel.SpatialMap[$osh][$in].SpriteID -eq $Sprite.SpriteID) { 112 | $game.CurrentLevel.SpatialMap[$osh][$in] 113 | break 114 | } 115 | } 116 | 117 | foreach ($tr in $toRemove) { 118 | $null = $game.CurrentLevel.SpatialMap[$osh].Remove($tr) 119 | } 120 | } 121 | } 122 | } 123 | if (-not $this.Hidden -and 124 | $game.CurrentLevel.SpriteMap.ContainsKey) { 125 | 126 | } 127 | 128 | if (-not $this.Hidden) { 129 | [Console]::Write("$($Sprite.Draw())") 130 | try {[Console]::CursorVisible = $false} catch {$PSCmdlet.WriteVerbose("$_")} 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /New-Sprite.ps1: -------------------------------------------------------------------------------- 1 | function New-Sprite 2 | { 3 | <# 4 | .Synopsis 5 | Creates Sprites 6 | .Description 7 | Creates a new Sprite. 8 | .Example 9 | New-Sprite -X 10 -Y 20 -Content '!' -Color "#ff0000" 10 | .Link 11 | Add-Sprite 12 | .Link 13 | Find-Sprite 14 | .Link 15 | Move-Sprite 16 | .Link 17 | Remove-Sprite 18 | #> 19 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="This does not change state, it creates an object")] 20 | param( 21 | # The type of the sprite. The sprite type is used to group sprites and handle specific collisions. 22 | [Parameter(Position=0,ValueFromPipelineByPropertyName)] 23 | [string] 24 | $Type, 25 | 26 | # The X coordinate of the sprite. 27 | # If the X coordinate would not be visible, the sprite will not be rendered 28 | [Parameter(ValueFromPipelineByPropertyName)] 29 | [int] 30 | $X, 31 | 32 | # The X coordinate of the sprite. 33 | # If the Y coordinate would not be visible, the sprite will not be rendered 34 | [Parameter(ValueFromPipelineByPropertyName)] 35 | [int] 36 | $Y, 37 | 38 | # The name of the sprite. 39 | # Giving a sprite a name will declare it as a global variable. 40 | [Parameter(ValueFromPipelineByPropertyName)] 41 | [string] 42 | $Name, 43 | 44 | # The sprite content. This can be used for sprites that are a single line. 45 | [Parameter(ValueFromPipelineByPropertyName)] 46 | [string] 47 | $Content, 48 | 49 | # The sprite color. 50 | [Parameter(ValueFromPipelineByPropertyName)] 51 | [string] 52 | $Color, 53 | 54 | # The sprite background color. 55 | [Parameter(ValueFromPipelineByPropertyName)] 56 | [string] 57 | $BackgroundColor, 58 | 59 | # The width of the sprite. 60 | # Supplying this and -Height will make the sprite a rectangle. 61 | [Parameter(ValueFromPipelineByPropertyName)] 62 | [int] 63 | $Width, 64 | 65 | # The height of the sprite 66 | # Supplying this and -Width will make the sprite a rectangle. 67 | [Parameter(ValueFromPipelineByPropertyName)] 68 | [int] 69 | $Height, 70 | 71 | # Additional properties of the sprite. 72 | # This can contain any custom information. 73 | [Parameter(ValueFromPipelineByPropertyName)] 74 | [Alias('Properties')] 75 | [Collections.IDictionary] 76 | $Property, 77 | 78 | # Additional methods for the sprite 79 | # These can be used to dynamically create sprite behavior. 80 | [Parameter(ValueFromPipelineByPropertyName)] 81 | [Collections.IDictionary] 82 | $Method) 83 | 84 | process { 85 | $out = [PSCustomObject]([Ordered]@{ 86 | SpriteID = [BitConverter]::ToString([GUID]::NewGuid().ToByteArray()).Replace('-','').ToLower() 87 | } + $PSBoundParameters) 88 | $out.pstypenames.clear() 89 | $out.pstypenames.add("PowerArcade.Sprite") 90 | if ($out.Width -and $out.Height) { 91 | $out.psobject.Members.Add([PSNoteProperty]::new('Shapes',@( 92 | $shape = [PSCustomObject]@{PSTypeName='PowerArcade.Box';Width=$out.Width;Height=$out.Height} 93 | $shape.psobject.Members.Add([PSNoteProperty]::new('Sprite',$out)) 94 | $shape.psobject.Members.Add([PSScriptProperty]::new('X', { $this.Sprite.X})) 95 | $shape.psobject.Members.Add([PSScriptProperty]::new('Y', { $this.Sprite.Y})) 96 | $shape.psobject.Members.Add([PSScriptProperty]::new('BackgroundColor', { $this.Sprite.BackgroundColor})) 97 | $shape.psobject.Members.Add([PSScriptProperty]::new('Color', { $this.Sprite.Color})) 98 | $shape 99 | ))) 100 | } 101 | if ($Type) { 102 | if ($Game.SpriteTypes.$Type) { 103 | foreach ($member in $game.SpriteTypes.$type.psobject.Members) { 104 | $out.psobject.Members.Add($member, $true) 105 | } 106 | } 107 | 108 | if ($out.Default -is [Collections.IDictionary]) { 109 | foreach ($kv in $out.Default.GetEnumerator()) { 110 | if (-not $out.($kv.Key)) { 111 | $out.psobject.members.Add([PSNoteProperty]::new($kv.Key,$kv.Value), $true) 112 | } 113 | } 114 | } 115 | $out.pstypenames.add("$Type.Sprite") 116 | } 117 | if ($Property) { 118 | foreach ($kv in $Property.GetEnumerator()) { 119 | $out.psobject.members.add([PSNoteProperty]::new($kv.Key,$kv.Value)) 120 | } 121 | } 122 | if ($Method) { 123 | foreach ($kv in $Method.GetEnumerator()) { 124 | $out.psobject.members.add([PSScriptMethod]::new($kv.Key, $kv.Value)) 125 | } 126 | } 127 | if ($out.Initialize.Invoke) { 128 | $out.Initialize() 129 | } 130 | $out 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Parts/GetScriptMembers.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory)] 3 | [IO.DirectoryInfo] 4 | $Directory 5 | ) 6 | 7 | $properties = [Ordered]@{} 8 | foreach ($file in $Directory.GetFileSystemInfos()) { 9 | if ('.ps1', '.psd1','.json','.xml','.clixml','.csv' -notcontains $file.Extension) { continue } 10 | $fileName = $file.Name.Substring(0, $file.Name.Length - $file.Extension.Length) 11 | if ($file.Extension -eq '.ps1') { 12 | $methodScript = $ExecutionContext.SessionState.InvokeCommand.GetCommand($file.Fullname, 'ExternalScript').ScriptBlock 13 | if ($fileName -match '(?get|set)_') { 14 | $properties[$fileName] = $methodScript 15 | continue 16 | } 17 | $methodName = $fileName 18 | if ($methodName -eq $Directory.Name) { 19 | $methodName = 'Initialize' 20 | } 21 | 22 | [PSScriptMethod]::new($methodName, $methodScript) 23 | if ($methodName.StartsWith('+') -and $methodName.Contains(',')) { 24 | foreach ($aliasName in $methodName.TrimStart('+') -split ',') { 25 | [PSScriptMethod]::new("+$aliasName", $methodScript) 26 | } 27 | } 28 | continue 29 | } 30 | if ($fileName -eq $Directory.Name) { 31 | $fileName = 'Default' 32 | } 33 | [PSNoteProperty]::new($fileName, $( 34 | switch ($file.Extension) 35 | { 36 | '.psd1' { 37 | Import-LocalizedData -BaseDirectory $Directory.FullName -FileName $file.Name 38 | } 39 | '.csv' { 40 | Import-Csv -LiteralPath $file.Fullname 41 | } 42 | '.json' { 43 | [IO.File]::ReadAllText($file.Fullname) | ConvertFrom-Json 44 | } 45 | '.xml' { 46 | [xml][IO.File]::ReadAllText($file.Fullname) 47 | } 48 | '.clixml' { 49 | Import-Clixml -LiteralPath $file.Fullname 50 | } 51 | } 52 | )) 53 | } 54 | 55 | $alreadyGotIt = @() 56 | foreach ($pv in $properties.GetEnumerator()) { 57 | if ($alreadyGotIt -contains $pv.Key) { continue } 58 | $propName = "$($pv.Key)".Substring(4) 59 | $alreadyGotIt += $pv.Key 60 | if ($pv.Key -like 'get_*') { 61 | if ($properties["set_$propName"]) { 62 | $alreadyGotIt += "set_$propName" 63 | [PSScriptProperty]::new($propName, $pv.Value, $properties["set_$propName"]) 64 | } else { 65 | [PSScriptProperty]::new($propName, $pv.Value) 66 | } 67 | } else { 68 | [PSScriptProperty]::new($propName, {}, $pv.Value) 69 | } 70 | } -------------------------------------------------------------------------------- /PowerArcade-Azure-Pipeline.yml: -------------------------------------------------------------------------------- 1 | 2 | stages: 3 | - stage: PowerShellStaticAnalysis 4 | displayName: Static Analysis 5 | condition: succeeded() 6 | jobs: 7 | - job: PSScriptAnalyzer 8 | displayName: PSScriptAnalyzer 9 | pool: 10 | vmImage: windows-latest 11 | steps: 12 | - powershell: | 13 | Install-Module -Name PSDevOps -Repository PSGallery -Force -Scope CurrentUser 14 | Import-Module PSDevOps -Force -PassThru 15 | displayName: InstallPSDevOps 16 | - powershell: | 17 | Install-Module -Name PSScriptAnalyzer -Repository PSGallery -Force -Scope CurrentUser 18 | Import-Module PSScriptAnalyzer -Force -PassThru 19 | displayName: InstallPSScriptAnalyzer 20 | - powershell: | 21 | Import-Module PSScriptAnalyzer, PSDevOps 22 | $invokeScriptAnalyzerSplat = @{Path='.\'} 23 | if ($ENV:PSScriptAnalyzer_Recurse) { 24 | $invokeScriptAnalyzerSplat.Recurse = $true 25 | } 26 | $result = Invoke-ScriptAnalyzer @invokeScriptAnalyzerSplat 27 | 28 | foreach ($r in $result) { 29 | if ('information', 'warning' -contains $r.Severity) { 30 | Write-ADOWarning -Message $r.Message -SourcePath $r.ScriptPath -LineNumber $r.Line -ColumnNumber $r.Column 31 | } 32 | elseif ($r.Severity -eq 'Error') { 33 | Write-ADOError -Message $r.Message -SourcePath $r.ScriptPath -LineNumber $r.Line -ColumnNumber $r.Column 34 | } 35 | } 36 | displayName: RunPSScriptAnalyzer 37 | 38 | - stage: TestPowerShellCrossPlatform 39 | displayName: Test 40 | jobs: 41 | - job: Windows 42 | displayName: Windows 43 | pool: 44 | vmImage: windows-latest 45 | steps: 46 | - powershell: | 47 | Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser 48 | Import-Module Pester -Force -PassThru 49 | displayName: InstallPester 50 | - powershell: | 51 | $orgName, $moduleName = $env:BUILD_REPOSITORY_ID -split "/" 52 | Import-Module ".\$moduleName.psd1" -Force -PassThru | Out-Host 53 | $result = 54 | Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml ` 55 | -CodeCoverage "$(Build.SourcesDirectory)\*-*.ps1" -CodeCoverageOutputFile ".\$moduleName.Coverage.xml" 56 | 57 | $psDevOpsImported = Import-Module PSDevOps -Force -PassThru -ErrorAction SilentlyContinue 58 | 59 | if ($psDevOpsImported) { 60 | foreach ($pesterTestResult in $pesterResults.TestResult) { 61 | if ($pesterTestResult.Result -eq 'Failed') { 62 | $foundLineNumber = [Regex]::Match($pesterTestResult.StackTrace, ':\s{0,}(?\d+)\s{0,}\w{1,}\s{0,}(?.+)$', 'Multiline') 63 | $errSplat = @{ 64 | Message = $pesterTestResult.ErrorRecord.Exception.Message 65 | Line = $foundLineNumber.Groups["Line"].Value 66 | SourcePath = $foundLineNumber.Groups["File"].Value 67 | } 68 | 69 | Write-ADOError @errSplat 70 | } 71 | } 72 | } else { 73 | if ($result.FailedCount -gt 0) { 74 | throw "$($result.FailedCount) tests failed." 75 | } 76 | } 77 | displayName: RunPester 78 | - task: PublishTestResults@2 79 | inputs: 80 | testResultsFormat: NUnit 81 | testResultsFiles: '**/*.TestResults.xml' 82 | mergeTestResults: true 83 | - task: PublishCodeCoverageResults@1 84 | inputs: 85 | codeCoverageTool: JaCoCo 86 | summaryFileLocation: '**/*.Coverage.xml' 87 | reportDirectory: $(System.DefaultWorkingDirectory) 88 | - job: Linux 89 | displayName: Linux 90 | pool: 91 | vmImage: ubuntu-latest 92 | steps: 93 | - script: | 94 | 95 | curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - 96 | curl https://packages.microsoft.com/config/ubuntu/16.04/prod.list | sudo tee /etc/apt/sources.list.d/microsoft.list 97 | sudo apt-get update 98 | sudo apt-get install -y powershell 99 | 100 | displayName: Install PowerShell Core 101 | - pwsh: | 102 | Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser 103 | Import-Module Pester -Force -PassThru 104 | displayName: InstallPester 105 | - pwsh: | 106 | $orgName, $moduleName = $env:BUILD_REPOSITORY_ID -split "/" 107 | Import-Module ".\$moduleName.psd1" -Force -PassThru | Out-Host 108 | $result = 109 | Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml ` 110 | -CodeCoverage "$(Build.SourcesDirectory)\*-*.ps1" -CodeCoverageOutputFile ".\$moduleName.Coverage.xml" 111 | 112 | $psDevOpsImported = Import-Module PSDevOps -Force -PassThru -ErrorAction SilentlyContinue 113 | 114 | if ($psDevOpsImported) { 115 | foreach ($pesterTestResult in $pesterResults.TestResult) { 116 | if ($pesterTestResult.Result -eq 'Failed') { 117 | $foundLineNumber = [Regex]::Match($pesterTestResult.StackTrace, ':\s{0,}(?\d+)\s{0,}\w{1,}\s{0,}(?.+)$', 'Multiline') 118 | $errSplat = @{ 119 | Message = $pesterTestResult.ErrorRecord.Exception.Message 120 | Line = $foundLineNumber.Groups["Line"].Value 121 | SourcePath = $foundLineNumber.Groups["File"].Value 122 | } 123 | 124 | Write-ADOError @errSplat 125 | } 126 | } 127 | } else { 128 | if ($result.FailedCount -gt 0) { 129 | throw "$($result.FailedCount) tests failed." 130 | } 131 | } 132 | displayName: RunPester 133 | - task: PublishTestResults@2 134 | inputs: 135 | testResultsFormat: NUnit 136 | testResultsFiles: '**/*.TestResults.xml' 137 | mergeTestResults: true 138 | - task: PublishCodeCoverageResults@1 139 | inputs: 140 | codeCoverageTool: JaCoCo 141 | summaryFileLocation: '**/*.Coverage.xml' 142 | reportDirectory: $(System.DefaultWorkingDirectory) 143 | - job: MacOS 144 | displayName: MacOS 145 | pool: 146 | vmImage: macos-latest 147 | steps: 148 | - script: | 149 | brew update 150 | brew tap caskroom/cask 151 | brew cask install powershell 152 | displayName: Install PowerShell Core 153 | - pwsh: | 154 | Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser 155 | Import-Module Pester -Force -PassThru 156 | displayName: InstallPester 157 | - pwsh: | 158 | $orgName, $moduleName = $env:BUILD_REPOSITORY_ID -split "/" 159 | Import-Module ".\$moduleName.psd1" -Force -PassThru | Out-Host 160 | $result = 161 | Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml ` 162 | -CodeCoverage "$(Build.SourcesDirectory)\*-*.ps1" -CodeCoverageOutputFile ".\$moduleName.Coverage.xml" 163 | 164 | $psDevOpsImported = Import-Module PSDevOps -Force -PassThru -ErrorAction SilentlyContinue 165 | 166 | if ($psDevOpsImported) { 167 | foreach ($pesterTestResult in $pesterResults.TestResult) { 168 | if ($pesterTestResult.Result -eq 'Failed') { 169 | $foundLineNumber = [Regex]::Match($pesterTestResult.StackTrace, ':\s{0,}(?\d+)\s{0,}\w{1,}\s{0,}(?.+)$', 'Multiline') 170 | $errSplat = @{ 171 | Message = $pesterTestResult.ErrorRecord.Exception.Message 172 | Line = $foundLineNumber.Groups["Line"].Value 173 | SourcePath = $foundLineNumber.Groups["File"].Value 174 | } 175 | 176 | Write-ADOError @errSplat 177 | } 178 | } 179 | } else { 180 | if ($result.FailedCount -gt 0) { 181 | throw "$($result.FailedCount) tests failed." 182 | } 183 | } 184 | displayName: RunPester 185 | - task: PublishTestResults@2 186 | inputs: 187 | testResultsFormat: NUnit 188 | testResultsFiles: '**/*.TestResults.xml' 189 | mergeTestResults: true 190 | - task: PublishCodeCoverageResults@1 191 | inputs: 192 | codeCoverageTool: JaCoCo 193 | summaryFileLocation: '**/*.Coverage.xml' 194 | reportDirectory: $(System.DefaultWorkingDirectory) 195 | 196 | condition: succeeded() 197 | - stage: UpdatePowerShellGallery 198 | displayName: Update 199 | condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master')) 200 | variables: 201 | - group: Gallery 202 | jobs: 203 | - job: Publish 204 | displayName: PowerShell Gallery 205 | pool: 206 | vmImage: windows-latest 207 | steps: 208 | - powershell: | 209 | $orgName, $moduleName = $env:BUILD_REPOSITORY_ID -split "/" 210 | $imported = Import-Module ".\$moduleName.psd1" -Force -PassThru 211 | $foundModule = Find-Module -Name $ModuleName 212 | if ($foundModule.Version -ge $imported.Version) { 213 | Write-Warning "##vso[task.logissue type=warning]Gallery Version of $moduleName is more recent ($($foundModule.Version) >= $($imported.Version))" 214 | } else { 215 | $gk = '$(GalleryKey)' 216 | $stagingDir = '$(Build.ArtifactStagingDirectory)' 217 | $moduleTempPath = Join-Path $stagingDir $moduleName 218 | 219 | Write-Host "Staging Directory: $ModuleTempPath" 220 | 221 | $imported | Split-Path | Copy-Item -Destination $moduleTempPath -Recurse 222 | $moduleGitPath = Join-Path $moduleTempPath '.git' 223 | Write-Host "Removing .git directory" 224 | Remove-Item -Recurse -Force $moduleGitPath 225 | Write-Host "Module Files:" 226 | Get-ChildItem $moduleTempPath -Recurse 227 | Write-Host "Publishing $moduleName [$($imported.Version)] to Gallery" 228 | Publish-Module -Path $moduleTempPath -NuGetApiKey $gk 229 | if ($?) { 230 | Write-Host "Published to Gallery" 231 | } else { 232 | Write-Host "Gallery Publish Failed" 233 | exit 1 234 | } 235 | } 236 | displayName: PublishPowerShellGallery 237 | 238 | 239 | -------------------------------------------------------------------------------- /PowerArcade.ezout.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module EZOut 2 | # Install-Module EZOut or https://github.com/StartAutomating/EZOut 3 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "", Justification="Games are Global")] 4 | param() 5 | $myFile = $MyInvocation.MyCommand.ScriptBlock.File 6 | $myModuleName = $($myFile | Split-Path -Leaf) -replace '\.ezout\.ps1', '' 7 | $myRoot = $myFile | Split-Path 8 | Push-Location $myRoot 9 | $formatting = @( 10 | # Add your own Write-FormatView here, or put them in a Formatting or Views directory 11 | 12 | foreach ($potentialDirectory in 'Formatting','Views') { 13 | Join-Path $myRoot $potentialDirectory | 14 | Get-ChildItem -ea ignore | 15 | Import-FormatView -FilePath {$_.Fullname} 16 | } 17 | ) 18 | 19 | if ($formatting) { 20 | $myFormatFile = Join-Path $myRoot "$myModuleName.format.ps1xml" 21 | $formatting | Out-FormatData -Module $MyModuleName | Set-Content $myFormatFile -Encoding UTF8 22 | } 23 | 24 | $types = @( 25 | # Add your own Write-TypeView statements here 26 | Join-Path $myRoot 'Types' | 27 | Get-Item -ErrorAction Ignore | 28 | Import-TypeView 29 | 30 | Write-TypeView -TypeName PowerArcade.CurrentLevel -HideProperty SpriteMap -DefaultDisplay Name, IsCurrentLevel -ScriptProperty @{ 31 | IsCurrentLevel = { 32 | if (-not $Global:Game) { return $false } 33 | return $Global:Game.CurrentLevel -eq $this 34 | } 35 | } 36 | ) 37 | 38 | if ($types) { 39 | $myTypesFile = Join-Path $myRoot "$myModuleName.types.ps1xml" 40 | $types | Out-TypeData | Set-Content $myTypesFile -Encoding UTF8 41 | } 42 | Pop-Location 43 | -------------------------------------------------------------------------------- /PowerArcade.format.ps1xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | PowerArcade.Box 7 | 8 | PowerArcade.Box 9 | 10 | 11 | 12 | 13 | 14 | 15 | @( 16 | $box = $_ 17 | $boxWidth = 18 | if ($_.Width) { $_.Width} 19 | else {$host.UI.RawUI.WindowSize.Width} 20 | $boxHeight = 21 | if ($_.Height) { $_.Height} 22 | else {$host.UI.RawUI.WindowSize.Height} 23 | $boxBackgroundColor = $box.BackgroundColor 24 | $boxColor = $box.Color 25 | 26 | $boxFill = 27 | if ($box.Fill) { $box.Fill } 28 | else { '█'; $boxColor = $boxBackgroundColor } 29 | 30 | 31 | 32 | 33 | 34 | $colorStart = 35 | @( 36 | if ($boxColor) { 37 | $intColor = [int]($boxColor -replace '#', '0x') 38 | $r,$g,$b = 39 | [byte](($intColor -band 0xff0000) -shr 16), 40 | [byte](($intColor -band 0x00ff00) -shr 8), 41 | [byte]($intColor -band 0x0000ff) 42 | [char]0x1b+"[38;2;$r;$g;${b}m" 43 | } 44 | 45 | if ($box.BackgroundColor) { 46 | $intColor = [int]($box.BackgroundColor -replace '#', '0x') 47 | $r,$g,$b = 48 | [byte](($intColor -band 0xff0000) -shr 16), 49 | [byte](($intColor -band 0x00ff00) -shr 8), 50 | [byte]($intColor -band 0x0000ff) 51 | [char]0x1b+"[48;2;$r;$g;${b}m" 52 | } 53 | ) -join '' 54 | 55 | 56 | $colorEnd = 57 | @( 58 | if ($boxColor) { 59 | [char]0x1b + '[39m' 60 | } 61 | 62 | if ($box.BackgroundColor) { 63 | [char]0x1b + '[49m' 64 | } 65 | ) -join '' 66 | $boxChar = [string]"$boxFill".Substring(0,1) 67 | 68 | if ($null -ne $box.X -and $null -ne $box.Y) { 69 | @(for ($l =0 ;$l -lt $boxHeight; $l++) { 70 | $colorStart 71 | '' + [char]0x1b + "[$($box.Y + $l);$($box.X)H" 72 | $boxChar * $boxWidth 73 | $colorEnd 74 | }) -join '' 75 | } else { 76 | $colorStart 77 | @( 78 | for ($l = 0; $l -lt $boxHeight; $l++) { 79 | $boxChar * $boxWidth 80 | } 81 | ) -join [Environment]::NewLine 82 | $colorEnd 83 | } 84 | '' 85 | 86 | 87 | ) -join '' 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | PowerArcade.GameInfo 97 | 98 | PowerArcade.GameInfo 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | Name 115 | 116 | 117 | Version 118 | 119 | 120 | Description 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | PowerArcade.Level 129 | 130 | PowerArcade.Level 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | Name 144 | 145 | 146 | IsCurrentLevel 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | PowerArcade.MessageBox 155 | 156 | PowerArcade.MessageBox 157 | 158 | 159 | 160 | 161 | 162 | 163 | $messageData = $_ 164 | $Messages = 165 | if ($_.Messages){ 166 | $_.Messages 167 | } else { 168 | $_.Message 169 | } 170 | 171 | $y = 172 | if ($messageData.Y) { 173 | $messageData.Y 174 | } else { 175 | $GAME.Height * .33 176 | } 177 | 178 | if ($messageData.Border -eq $true) { 179 | 180 | } 181 | 182 | $y-- 183 | 184 | $colorSplat = @{ 185 | Color= 186 | $( 187 | if ($messageData.Color) { 188 | $messageData.Color 189 | } else { 190 | $game.TextColor 191 | } 192 | ) 193 | BackgroundColor= 194 | $( 195 | if ($messageData.BackgroundColor) { 196 | $messageData.BackgroundColor 197 | } else { 198 | $game.BackgroundColor 199 | } 200 | ) 201 | } 202 | @(foreach ($Message in $Messages) { 203 | if (-not $Message) { continue } 204 | $MessageLines = @($Message -split '(?>\r\n|\n)') 205 | 206 | 207 | 208 | $MaxLength = $MessageLines | 209 | Measure-Object -Property Length -Maximum | 210 | Select-Object -ExpandProperty Maximum 211 | 212 | $TextLineStart = 213 | if ($messageData.X) { 214 | $messageData.X 215 | } else { 216 | ($game.Width - $MaxLength) / 2 217 | } 218 | 219 | if ($messageData.Border -eq $true) { 220 | $Y++ 221 | New-Sprite -Type Message -X $TextLineStart -Y $y -Content ('┌' + $('─' * $MaxLength) + '┐') @colorSplat 222 | } 223 | foreach ($MessageLine in $messageLines) { 224 | $y++ 225 | if ($messageData.Border -eq $true) { 226 | 227 | New-Sprite -Type Message -X $TextLineStart -Y $y -Content $( 228 | 229 | '│' + "$MessageLine".PadRight($MaxLength) + '│' 230 | 231 | ) @colorSplat 232 | } else { 233 | New-Sprite -Type Message -X $TextLineStart -Y $y -Content $MessageLine @colorSplat 234 | } 235 | } 236 | if ($messageData.Border -eq $true) { 237 | $Y++ 238 | New-Sprite -Type Message -X $TextLineStart -Y $y -Content ('└' + $('─' * $MaxLength) + '┘') @colorSplat 239 | } 240 | 241 | }) | Out-String -Width 1kb 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | PowerArcade.PlayingCard 250 | 251 | PowerArcade.PlayingCard 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | $designs = @{ 260 | 0 = @' 261 | ┌─────────┐ 262 | │▒▒▒▒▒▒▒▒▒│ 263 | │▒▒▒▒▒▒▒▒▒│ 264 | │▒▒▒▒▒▒▒▒▒│ 265 | │▒▒▒▒▒▒▒▒▒│ 266 | │▒▒▒▒▒▒▒▒▒│ 267 | │▒▒▒▒▒▒▒▒▒│ 268 | │▒▒▒▒▒▒▒▒▒│ 269 | └─────────┘ 270 | '@ 271 | 1 = @' 272 | ┌─────────┐ 273 | │A♣ │ 274 | │ │ 275 | │ │ 276 | │ ♣ │ 277 | │ │ 278 | │ │ 279 | │ ♣A│ 280 | └─────────┘ 281 | '@ 282 | 2= @' 283 | ┌─────────┐ 284 | │2♣ │ 285 | │ ♣ │ 286 | │ │ 287 | │ │ 288 | │ │ 289 | │ ♣ │ 290 | │ ♣2│ 291 | └─────────┘ 292 | '@ 293 | 3= @' 294 | ┌─────────┐ 295 | │3♣ │ 296 | │ ♣ │ 297 | │ │ 298 | │ ♣ │ 299 | │ │ 300 | │ ♣ │ 301 | │ ♣3│ 302 | └─────────┘ 303 | '@ 304 | 4 = @' 305 | ┌─────────┐ 306 | │4♣ │ 307 | │ ♣ ♣ │ 308 | │ │ 309 | │ │ 310 | │ │ 311 | │ ♣ ♣ │ 312 | │ ♣4│ 313 | └─────────┘ 314 | '@ 315 | 5 = @' 316 | ┌─────────┐ 317 | │5♣ │ 318 | │ ♣ ♣ │ 319 | │ │ 320 | │ ♣ │ 321 | │ │ 322 | │ ♣ ♣ │ 323 | │ ♣5│ 324 | └─────────┘ 325 | '@ 326 | 6 = @' 327 | ┌─────────┐ 328 | │6♣ │ 329 | │ ♣ ♣ │ 330 | │ │ 331 | │ ♣ ♣ │ 332 | │ │ 333 | │ ♣ ♣ │ 334 | │ ♣6│ 335 | └─────────┘ 336 | '@ 337 | 338 | 7 = @' 339 | ┌─────────┐ 340 | │7♣ │ 341 | │ ♣ ♣ │ 342 | │ │ 343 | │ ♣ ♣ ♣ │ 344 | │ │ 345 | │ ♣ ♣ │ 346 | │ ♣7│ 347 | └─────────┘ 348 | '@ 349 | 8 = @' 350 | ┌─────────┐ 351 | │8♣ │ 352 | │ ♣ ♣ ♣ │ 353 | │ │ 354 | │ ♣ ♣ │ 355 | │ │ 356 | │ ♣ ♣ ♣ │ 357 | │ ♣8│ 358 | └─────────┘ 359 | '@ 360 | 9= @' 361 | ┌─────────┐ 362 | │9♣ │ 363 | │ ♣ ♣ ♣ │ 364 | │ │ 365 | │ ♣ ♣ ♣ │ 366 | │ │ 367 | │ ♣ ♣ ♣ │ 368 | │ ♣9│ 369 | └─────────┘ 370 | '@ 371 | 10 = @' 372 | ┌─────────┐ 373 | │10♣ │ 374 | │ ♣ ♣ ♣ │ 375 | │ ♣ │ 376 | │ ♣ ♣ │ 377 | │ ♣ │ 378 | │ ♣ ♣ ♣ │ 379 | │ ♣10│ 380 | └─────────┘ 381 | '@ 382 | 11= @' 383 | ┌─────────┐ 384 | │J♣ │ 385 | │ ♣ │ 386 | │ ♣ │ 387 | │ ♣ │ 388 | │ ♣ ♣ │ 389 | │ ♣ │ 390 | │ ♣J│ 391 | └─────────┘ 392 | '@ 393 | 12= @' 394 | ┌─────────┐ 395 | │Q♣ │ 396 | │ ♣♣♣ │ 397 | │ ♣ ♣ │ 398 | │ ♣ ♣ │ 399 | │ ♣ ♣ │ 400 | │ ♣♣♣ │ 401 | │ ♣♣Q│ 402 | └─────────┘ 403 | '@ 404 | 13= @' 405 | ┌─────────┐ 406 | │K♣ │ 407 | │ ♣ ♣ │ 408 | │ ♣ ♣ │ 409 | │ ♣♣♣ │ 410 | │ ♣ ♣ │ 411 | │ ♣ ♣ │ 412 | │ ♣K│ 413 | └─────────┘ 414 | '@ 415 | 416 | } 417 | 418 | 419 | $card = $_ 420 | $realSuite = 421 | if ($card.Suite -eq '♣' -or $card.Suite -eq 'Clubs' -or $card.Suite -eq 1) 422 | { 423 | '♣' 424 | } 425 | elseif ($card.Suite -eq '♦' -or $card.Suite -eq 'Diamonds' -or $card.Suite -eq 2) 426 | { 427 | '♦' 428 | } 429 | elseif ($card.Suite -eq '♥' -or $card.Suite -eq 'Hearts' -or $card.Suite -eq 3) 430 | { 431 | '♥' 432 | } 433 | elseif ($card.Suite -eq '♠' -or $card.Suite -eq 'Spades' -or $card.Suite -eq 4) 434 | { 435 | '♠' 436 | } 437 | 438 | $cardNumber = $card.Number -as [int] 439 | if (-not $cardNumber) { 440 | if ($card.Number -eq 'Ace') { 441 | $cardNumber = 1 442 | } elseif ($card.Number -eq 'Jack') { 443 | $cardNumber = 11 444 | } elseif ($card.Number -eq 'Queen') { 445 | $cardNumber = 12 446 | } elseif ($card.Number -eq 'King') { 447 | $cardNumber = 13 448 | } 449 | } 450 | if (-not $designs[$cardNumber]) { 451 | throw "$($card.Number) not found" 452 | } 453 | if (-not $Host.UI.SupportsVirtualTerminal) { 454 | return $designs[$cardNumber] -replace '♣', $realSuite 455 | } else { 456 | @( 457 | 458 | '' + [char]0x1b+"[48;2;255;255;255m" 459 | if ('♣', '♠' -contains $realSuite) { 460 | '' + [char]0x1b+"[38;2;0;0;0m" 461 | } else { 462 | '' + [char]0x1b+"[38;2;255;0;0m" 463 | } 464 | if ($card.Selected) { 465 | '' + [char]0x1b + '[7m' 466 | } 467 | if ($card.X -ge 0 -and $card.Y -ge 0) { 468 | $designLines = $designs[$cardNumber] -replace '♣', $realSuite -split '(?>\r\n|\n)' 469 | $y = $card.Y 470 | foreach ($dl in $designLines) { 471 | '' + [char]0x1b + "[$($Y);$($card.X)H" 472 | $dl.Trim() 473 | $y++ 474 | } 475 | } else { 476 | $designs[$cardNumber] -replace '♣', $realSuite 477 | } 478 | if ($card.Selected) { 479 | '' + [char]0x1b + '[27m' 480 | } 481 | '' + [char]0x1b +'[39m' 482 | '' + [char]0x1b +'[49m' 483 | ) -join '' 484 | } 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | PowerArcade.Sprite 494 | 495 | PowerArcade.Sprite 496 | 497 | 498 | 499 | 500 | 501 | 502 | $_.Draw() 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | -------------------------------------------------------------------------------- /PowerArcade.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | ModuleVersion = '0.4.2' 3 | RootModule = 'PowerArcade.psm1' 4 | Description = 'A Retro Arcade Game Console in PowerShell' 5 | FormatsToProcess = 'PowerArcade.format.ps1xml' 6 | TypesToProcess = 'PowerArcade.types.ps1xml' 7 | Author = 'James Brundage' 8 | Copyright = '2020 Start-Automating' 9 | Guid = '0c583666-bf61-49dd-abdd-8ebb270f4eb3' 10 | PrivateData = @{ 11 | PSData = @{ 12 | Tags = 'PowerArcade', 'Games', 'Formatting', 'Fun' 13 | ProjectURI = 'https://github.com/StartAutomating/PowerArcade' 14 | LicenseURI = 'https://github.com/StartAutomating/PowerArcade/blob/master/LICENSE' 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /PowerArcade.psm1: -------------------------------------------------------------------------------- 1 | foreach ($file in Get-ChildItem $PSScriptRoot -Filter *-*.ps1 -Recurse) { 2 | . $file.Fullname 3 | } 4 | 5 | 6 | # Parts are simple .ps1 files beneath a /Parts directory that can be used throughout the module. 7 | $partsDirectory = $( # Because we want to be case-insensitive, and because it's fast 8 | foreach ($dir in [IO.Directory]::GetDirectories($psScriptRoot)) { # [IO.Directory]::GetDirectories() 9 | if ($dir -imatch "\$([IO.Path]::DirectorySeparatorChar)Parts$") { # and some Regex 10 | [IO.DirectoryInfo]$dir;break # to find our parts directory. 11 | } 12 | }) 13 | 14 | if ($partsDirectory) { # If we have parts directory 15 | foreach ($partFile in $partsDirectory.EnumerateFileSystemInfos()) { # enumerate all of the files. 16 | if ($partFile.Extension -eq '.ps1') { # If it's a PowerShell script, 17 | $partName = # get the name of the script. 18 | $partFile.Name.Substring(0, $partFile.Name.Length - $partFile.Extension.Length) 19 | $ExecutionContext.SessionState.PSVariable.Set( # and set a variable 20 | $partName, # named the script that points to the script (e.g. $foo = gcm .\Parts\foo.ps1) 21 | $ExecutionContext.SessionState.InvokeCommand.GetCommand($partFile.Fullname, 'ExternalScript').ScriptBlock 22 | ) 23 | } 24 | } 25 | } 26 | 27 | Set-Alias -Name _/\+ -Value Add-Sprite 28 | Set-Alias -Name _/\- -Value Remove-Sprite 29 | Set-Alias -Name _/\-> -Value Move-Sprite 30 | 31 | 32 | Export-ModuleMember -Alias * -Function *-* 33 | 34 | -------------------------------------------------------------------------------- /PowerArcade.tests.ps1: -------------------------------------------------------------------------------- 1 | $TestGame = @{ 2 | Game = @{ 3 | "Game.psd1" = {@{BackgroundColor = '#0000ff'}} 4 | 'OnKey_Left.ps1' = { 5 | $dot | Move-Sprite -X ($dot.X - 1) -Y ($dot.Y) 6 | } 7 | 'OnKey_Right.ps1' = { 8 | $dot | Move-Sprite -X ($dot.X + 1) -Y ($dot.Y) 9 | } 10 | 'OnKey_Up.ps1' = { 11 | $dot | Move-Sprite -X ($dot.X) -Y ($dot.Y - 1) 12 | } 13 | 'OnKey_Down.ps1' = { 14 | $dot | Move-Sprite -X ($dot.X) -Y ($dot.Y - 1) 15 | } 16 | 'Game.ps1' = { 17 | $global:GameInitialized = $true 18 | } 19 | } 20 | Levels = @{ 21 | 1 = @{ 22 | '1.ps1' = { 23 | $Global:FirstLevelInitializedAt = [DateTime]::Now 24 | Add-Sprite -X ($game.Width / 2) -Y ($game.Height / 2) -Name Dot -Type Dot 25 | 26 | Add-Sprite -X ($game.Width * .45) -Y ($game.Height / 2) -Height 3 -Width 1 -Name LeftObstacle -Type Wall 27 | 28 | Add-Sprite -X ($game.Width * .55) -Y ($game.Height / 2) -Name RightObstacle -Type Wall 29 | 30 | } 31 | } 32 | 'Levels.ps1' = { 33 | $Global:AllLevelsInitializedAt = [DateTime]::Now 34 | } 35 | } 36 | Sprites = @{ 37 | Dot = @{ 38 | 'Dot.psd1' = {@{Content = 'O'}} 39 | } 40 | Wall = @{ 41 | 'Wall.psd1' = {@{Content = '█'}} 42 | } 43 | } 44 | } 45 | 46 | 47 | $popDir = { 48 | param( 49 | [Parameter(Mandatory)][string]$root, 50 | [Parameter(Mandatory,ValueFromPipelineByPropertyName)] 51 | [string]$key, 52 | [Parameter(Mandatory,ValueFromPipelineByPropertyName)] 53 | $value 54 | ) 55 | 56 | process { 57 | $dest = Join-Path $root $key 58 | if ($value -is [Collections.IDictionary]) { 59 | if (-not (Test-Path $dest)) { 60 | $newDir = New-Item -ItemType Directory -Path $dest 61 | } 62 | $value.GetEnumerator() | & $MyInvocation.MyCommand.ScriptBlock -Root $dest 63 | } else { 64 | [IO.File]::WriteAllText($dest, "$value") 65 | } 66 | } 67 | } 68 | 69 | $tempDir = 70 | if ($PSVersionTable.Platform -eq 'Windows' -or -not $PSVersionTable.Platform) { 71 | Join-Path $env:TEMP "TempGame$(Get-Random)" 72 | } else { 73 | Join-Path '/tmp' "TempGame$(Get-Random)" 74 | } 75 | 76 | 77 | 78 | $newDir = New-Item -ItemType Directory -Path $tempDir -Force 79 | $psd1Path = Join-Path $newDir "$($newDir.Name).psd1" 80 | "@{ModuleVersion='0.1'}" | Set-Content $psd1Path 81 | $TestGame.GetEnumerator() | & $popDir -Root $newDir 82 | 83 | $global:TheTestGame = $null 84 | $global:GameInitialized = $false 85 | $Global:AllLevelsInitializedAt = $null 86 | $Global:FirstLevelInitializedAt = $null 87 | 88 | 89 | 90 | describe PowerArcade { 91 | it 'Is a Game Engine Written in PowerShell' { 92 | Get-Module PowerArcade | 93 | ForEach-Object { $_.ExportedCommands.Values } | 94 | Where-Object { $_.Noun -eq 'Game' } | 95 | should belike '*-Game*' 96 | } 97 | 98 | it "Lets you define games with a trio of directories: 'Game', 'Level(s)', and 'Sprites'" { 99 | $directoryNames = $newdir | 100 | Get-ChildItem -Directory | 101 | Select-Object -ExpandProperty Name 102 | 103 | if ($directoryNames -notcontains 'Game') { 104 | throw "'Game' directory not found beneath '$newDir'" 105 | } 106 | if (-not ($directoryNames -match '^Level[s]?')) { 107 | throw "'Levels' directory not found beneath '$newdir'" 108 | } 109 | if (-not ($directoryNames -match '^Sprite[s]?')) { 110 | throw "'Sprites' directory not found beneath '$newDir'" 111 | } 112 | } 113 | 114 | context Game { 115 | 116 | it 'Can Initialize-Game' { 117 | $global:TheTestGame = Initialize-Game -GamePath $newDir -NoClear 118 | 119 | $global:TheTestGame.pstypenames | should be 'PowerArcade.Game' 120 | } 121 | 122 | it 'Initializing a Game will call the Game.ps1 script in the Game directory' { 123 | $global:GameInitialized | should be $true 124 | } 125 | } 126 | 127 | context Levels { 128 | it 'Will default to the first level (alphabetically)' { 129 | $global:TheTestGame.currentlevelname | should be "1" 130 | } 131 | it 'Will run the Initialize method for all levels' { 132 | $Global:AllLevelsInitializedAt | should -BeLessOrEqual $Global:FirstLevelInitializedAt 133 | } 134 | it 'Will run the Initialize method for the currently selected level' { 135 | $Global:FirstLevelInitializedAt | should -BeGreaterOrEqual $Global:AllLevelsInitializedAt 136 | } 137 | } 138 | 139 | it 'Can Watch for keyboard input' { 140 | $noKeys = Watch-Keyboard 141 | $noKeys | should be $null 142 | } 143 | } 144 | 145 | describe Games { 146 | it 'Got Game' { 147 | $games = Get-Game 148 | $games | 149 | Select-Object -ExpandProperty PSTypeNames | 150 | Select-Object -First 1 | 151 | should be PowerArcade.GameInfo 152 | } 153 | it 'Can find games' { 154 | $foundGames = Find-Game 155 | $foundGames | Select-Object -ExpandProperty PSTypeNames 156 | } 157 | } 158 | 159 | describe Sprites { 160 | context Add-Sprite { 161 | it 'Can Add-Sprite to add content to the screen' { 162 | Add-Sprite -X ($dot.X + 3) -Y ($dot.Y + 3) -Content '!' 163 | } 164 | it 'Will not corrupt the spatial map to add a sprite right next to another one' { 165 | Add-Sprite -X ($LeftObstacle.X - 1) -Y ($dot.Y) -Type Dot 166 | $lo = $LeftObstacle 167 | $global:foundSomething = $LeftObstacle | Find-Sprite # # | should be $LeftObstacle 168 | if (-not $foundSomething) { throw "Found nothing" } 169 | } 170 | it 'Can add a sprite anywhere' { 171 | $addedSprite = Add-Sprite -Anywhere -Type Dot -Color "#ff0000" -PassThru 172 | $addedOtherSprite = Add-Sprite -Type Wall -Anywhere -Width 3 -Height 3 -PassThru 173 | } 174 | } 175 | 176 | context Find-Sprite { 177 | it 'Can Find-Sprite to see what is there' { 178 | Find-Sprite -X $dot.X -Y $dot.Y | should be $Global:Dot 179 | } 180 | it 'Can Find-Sprite within a -Radius' { 181 | $dot | Move-Sprite -X ($game.Width / 2) -Y ($game.Height / 2) 182 | Find-Sprite -X ($dot.x + 1) -Y ($DOT.y + 1) -Radius 2 | 183 | Measure-Object | 184 | Select-Object -ExpandProperty Count | 185 | should be 1 186 | } 187 | } 188 | 189 | context Move-Sprite { 190 | it 'Can Move a Sprite' { 191 | $ox = $dot.x 192 | $dot | Move-Sprite -X ($dot.X - 1) -Y $dot.Y 193 | $dot.X | should be ($ox - 1) 194 | } 195 | it 'Can Move next to a Sprite, and still find it (and the adjacent sprite)' { 196 | $dot | Move-Sprite -X ($RightObstacle.X - 1) -Y ($dot.Y) 197 | $RightObstacle | Find-Sprite | should be $RightObstacle 198 | $dot | Find-Sprite | should be $dot 199 | } 200 | it 'Can Move next to a Sprite with -NoClear, and still find it (and the adjacent sprite)' { 201 | $dot | Move-Sprite -X ($RightObstacle.X + 1) -Y ($RightObstacle.Y + 1) -NoClear 202 | $RightObstacle | Find-Sprite | should be $RightObstacle 203 | $dot | Find-Sprite | should be $dot 204 | } 205 | } 206 | 207 | context Remove-Sprite { 208 | it 'Can Remove-Sprite when we are done with it' { 209 | $leftX, $leftY = $LeftObstacle.X, $LeftObstacle.Y 210 | $spatialHash = $LeftObstacle.SpatialHash 211 | $spriteId = $LeftObstacle.SpriteID 212 | $lo = $LeftObstacle 213 | $LeftObstacle | Remove-Sprite 214 | $LeftObstacle | should be $null 215 | Find-Sprite -X $leftX -Y $leftY | should be $null 216 | $game.CurrentLevel.SpatialMap[$spatialHash] | 217 | Where-Object { $_.SpriteID -eq $spriteId } | 218 | should be $null 219 | $game.CurrentLevel.SpritesByID.ContainsKey($spriteId) | should be $false 220 | if ($game.CurrentLevel.Sprites -contains $lo) { 221 | throw "Sprite still in CurrentLevel.Sprites" 222 | } 223 | } 224 | 225 | it 'Will not corrupt up the spatial map during a Remove' { 226 | $dot | Move-Sprite -X ($RightObstacle.X - 1) -Y ($RightObstacle.Y) 227 | $RightObstacle | Remove-Sprite 228 | $dot | Find-Sprite | should be $dot 229 | 230 | } 231 | } 232 | } 233 | 234 | Remove-Item $newdir -Recurse -Force 235 | 236 | 237 | -------------------------------------------------------------------------------- /PowerArcade.types.ps1xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | PowerArcade.Level 6 | 7 | 8 | Draw 9 | 18 | 19 | 20 | 21 | 22 | PowerArcade.Point 23 | 24 | 25 | ToString 26 | 30 | 31 | 32 | 33 | 34 | PowerArcade.Game 35 | 36 | 37 | GetSpatialHash 38 | 54 | 55 | 56 | 57 | 58 | PowerArcade.Sprite.Reference 59 | 60 | 61 | ToString 62 | 71 | 72 | 73 | 74 | 75 | PowerArcade.Sprite 76 | 77 | 78 | Move 79 | 84 | 85 | 86 | Hide 87 | 91 | 92 | 93 | MeasureBounds 94 | 154 | 155 | 156 | Clear 157 | 200 | 201 | 202 | Draw 203 | 244 | 245 | 246 | Bounds 247 | 248 | $x = $this.X -as [int] 249 | $y = $this.Y -as [int] 250 | @(if ($this.Width -and $this.Height) { 251 | for ($oy = 0; $oy -lt $this.Height; $oy++) { 252 | for ($ox = 0; $ox -lt $this.Width; $ox++) { 253 | "$($x + $ox),$($y + $oy)" 254 | } 255 | } 256 | 257 | } elseif ($this.Content) 258 | { 259 | $cl = 260 | if ($this.ContentLength) { 261 | $this.ContentLength 262 | } else { 263 | $this.Content.ToString().Length 264 | } 265 | for ($ox =0; $ox -lt $cl; $ox++) { 266 | "$($x + $ox),$y" 267 | } 268 | } elseif ($x -ge 0 -and $y -ge 0) { 269 | "$x,$y" 270 | } 271 | ) 272 | 273 | 274 | 275 | 276 | SpatialHash 277 | 278 | @(foreach ($xy in $this.MeasureBounds()) { 279 | $x, $y = "$xy".Split(',') 280 | "$( 281 | [int][Math]::Floor($x / ($game.Width / $game.CellWidth)) 282 | ),$( 283 | [int][Math]::Floor($y / ($game.Height / $game.CellHeight)) 284 | )" 285 | }) | Select-Object -Unique 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | PowerArcade.CurrentLevel 294 | 295 | 296 | PSStandardMembers 297 | 298 | 299 | DefaultDisplayPropertySet 300 | 301 | Name 302 | IsCurrentLevel 303 | 304 | 305 | 306 | 307 | 308 | IsCurrentLevel 309 | 310 | 311 | if (-not $Global:Game) { return $false } 312 | return $Global:Game.CurrentLevel -eq $this 313 | 314 | 315 | 316 | 317 | 318 | 319 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerArcade| Retro Gaming in PowerShell 2 | 3 | ## April 1, 2020: 4 | 5 | At Start-Automating, we believe PowerShell can power anything. 6 | PowerShell has helped almost every area of automation, and has been used to build Winforms, WPF, and web applications. 7 | Certain members of the PowerShell team made [legendary HTML5 prototypes](https://www.leeholmes.com/blog/2011/04/01/powershell-and-html5/). 8 | 9 | This year, we set out to build a Game Console and Development Kit in PowerShell 10 | 11 | 12 | ### Introducing PowerArcade 13 | 14 | 15 | 16 | PowerArcade is a Game Console for your System Console. 17 | 18 | You can quickly and easily build build cross-platform games and share them on the PowerShell Gallery. 19 | 20 | 21 | Compared to their old Console counterparts, PowerArcade is HD. Old Console games used a resolution of 80x50. 22 | PowerArcade can use any resolution your terminal can handle, which nowadays can be almost 200 characters wide! 23 | Plus we _could_ add a nicer rendering engine to it, if we ever get around to it. 24 | 25 | Like all Consoles, PowerArcade ships with a game. 26 | 27 | We decided to do a retro update of the classic Nibbles.bas. Check it out: 28 | 29 | ![Nibbles2020](Assets/Nibbles2020.png) 30 | ![Nibbles2020](Assets/Nibbles2020.1.gif) 31 | ![Nibbles2020](Assets/Nibbles2020.2.gif) 32 | ![Nibbles2020](Assets/Nibbles2020.3.gif) 33 | ![Nibbles2020EndlessMode](Assets/Nibbles2020.4.gif) 34 | 35 | ### Installing and Playing 36 | 37 | You can Install PowerArcade from the PowerShell Gallery: 38 | 39 | ~~~ 40 | Install-Module PowerArcade -Scope CurrentUser 41 | ~~~ 42 | 43 | 44 | Then you can start playing Nibbles right away with: 45 | 46 | 47 | ~~~ 48 | Start-Game Nibbles2020 49 | ~~~ 50 | 51 | Want more games? You can use Find-Game to find them and Install-Game to install them. 52 | 53 | ~~~ 54 | Find-Game # see what's out there 55 | ~~~ 56 | 57 | You can pipe Find-Game to Install-Game, then just Start-Game 58 | ~~~ 59 | Find-Game Blackjack | Install-Game 60 | Start-Game Blackjack 61 | ~~~ 62 | ![Blackjack](Assets/Blackjack.png) 63 | 64 | ### How does it work? 65 | 66 | Nibbles.bas is a fitting choice, as Nibbles was a demostration of building games in QBASIC. 67 | While PowerShell is not a direct ancestor of QBASIC, it is a successor to Visual Basic. 68 | A few attempted to create games using Visual Basic, but [no developers were known to survive the process](https://youtu.be/WGqD-J_pRvs). 69 | 70 | Anyhow, developing games in PowerArcade is considerably less likely to be lethal. 71 | 72 | Let's take a look at how Nibbles works. Here's it's file tree (courtesy of [EZOut](https://github.com/StartAutomating/EZOut)): 73 | 74 | ~~~ 75 | ├──Game 76 | ├──Game.ps1 77 | ├──Game.psd1 78 | ├──OnKey_Down.ps1 79 | ├──OnKey_Esc.ps1 80 | ├──OnKey_Left.ps1 81 | ├──OnKey_P.ps1 82 | ├──OnKey_Right.ps1 83 | ├──OnKey_Up.ps1 84 | ├──Over.ps1 85 | ├──Levels 86 | ├──Levels.ps1 87 | ├──1 88 | ├──1.ps1 89 | ├──2 90 | ├──2.ps1 91 | ├──3 92 | ├──3.ps1 93 | ├──4 94 | ├──4.ps1 95 | ├──5 96 | ├──5.ps1 97 | ├──6 98 | ├──6.ps1 99 | ├──7 100 | ├──7.ps1 101 | ├──8 102 | ├──8.ps1 103 | ├──9 104 | ├──9.ps1 105 | ├──GameOver 106 | ├──GameOver.ps1 107 | ├──OnKey_All.ps1 108 | ├──Menu 109 | ├──Menu.ps1 110 | ├──OnKey_All.ps1 111 | ├──Pause 112 | ├──Pause.ps1 113 | ├──Sprites 114 | ├──Number 115 | ├──Number.psd1 116 | ├──Snake 117 | ├──+.ps1 118 | ├──+Number.ps1 119 | ├──+Wall,Tail,Snake.ps1 120 | ├──Dies.ps1 121 | ├──OnTick.ps1 122 | ├──Snake.psd1 123 | ├──SwitchDirection.ps1 124 | ├──Wall 125 | ├──Wall.psd1 126 | ├──Nibbles2020.psd1 127 | ~~~ 128 | 129 | 130 | A game is made of a module and up to three subdirectories: 131 | 132 | #### The Game Directory 133 | 134 | The Game directory contains a Game.psd1 which has initial settings for the game. 135 | 136 | A Game.ps1 file, if found, will be run when the game starts to initialize the game. 137 | 138 | Any other .ps1 files become methods of the Game, which can be accessed with running in a global variable called $game (duh). 139 | 140 | Any methods named On* denote an event handler. OnTick will be called when the game clock ticks. 141 | Files named OnKey_KeyName.ps1 will handle specific key presses. 142 | 143 | #### The Levels directory 144 | 145 | The Levels directory defines game levels. Pretty easy to navigate, right? 146 | 147 | Each named subdirectory is the name of a level. A script sharing the directory name will initialize the level. 148 | 149 | These named subdirectories work like the game directory, and can also handle keys. 150 | 151 | If there is Levels.ps1 beneath levels, it will be called before the level initializes. 152 | 153 | #### The Sprites directory 154 | 155 | Sprites are magical game creatures that move about in the imaginary world of the game. 156 | 157 | Just like each Levels directory, a Sprite can have an initializer (e.g. Unicorn\Unicorn.ps1) 158 | and can also have default properties (e.g. Unicorn\Unicorn.psd1) 159 | 160 | Sprites cannot respond to key presses, but they can respond to game ticks. 161 | 162 | Additionally, Sprites can have interaction methods. These describe what happens with two Sprites meet. 163 | 164 | In Nibbles, Sprites\Snake\+Number.ps1 descibes what happens when a snake hits a number, 165 | and \Sprites\Snake\+Wall,Tail,Snake.ps1 describes what happens when a snake hits a wall, tail, or a snake. 166 | 167 | That's about it. Inside your methods you can do whatever your game logic needs, 168 | and you can use Add-Sprite, Find-Sprite, Move-Sprite, and Remove-Sprite to control the sprites on the screen. 169 | 170 | 171 | ### Playing around and contributing 172 | 173 | This is mostly in good fun, but feedback, contributions, and game submissions are welcome. 174 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Game/Game.ps1: -------------------------------------------------------------------------------- 1 |  2 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Game/Game.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | 3 | Logo = ' 4 | ███╗ ██╗██╗██████╗ ██████╗ ██╗ ███████╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗ 5 | ████╗ ██║██║██╔══██╗██╔══██╗██║ ██╔════╝██╔════╝ ╚════██╗██╔═████╗╚════██╗██╔═████╗ 6 | ██╔██╗ ██║██║██████╔╝██████╔╝██║ █████╗ ███████╗ █████╔╝██║██╔██║ █████╔╝██║██╔██║ 7 | ██║╚██╗██║██║██╔══██╗██╔══██╗██║ ██╔══╝ ╚════██║ ██╔═══╝ ████╔╝██║██╔═══╝ ████╔╝██║ 8 | ██║ ╚████║██║██████╔╝██████╔╝███████╗███████╗███████║ ███████╗╚██████╔╝███████╗╚██████╔╝ 9 | ╚═╝ ╚═══╝╚═╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ 10 | 11 | ' 12 | Instructions = 13 | '1. Avoid the Walls!', 14 | ' 2. Eat your numbers!', 15 | ' 3. Watch your tail grow!' 16 | 17 | Controls = 18 | 'Arrows - Control Direction', 19 | 'Space - Start', 20 | ' P - Pause', 21 | ' 1-9 - Classic Levels [1-9]', 22 | ' 0 - Endless Mode' 23 | BackgroundColor = '#000a92' 24 | TextColor = '#11ff33' 25 | Clock = '00:00:00.1' 26 | StartLevel = 'Menu' 27 | Player1Lives = 5 28 | Player1Score = 0 29 | GameOverMessage = " 30 | 31 | 32 | ██████╗ █████╗ ███╗ ███╗███████╗ ██████╗ ██╗ ██╗███████╗██████╗ 33 | ██╔════╝ ██╔══██╗████╗ ████║██╔════╝ ██╔═══██╗██║ ██║██╔════╝██╔══██╗ 34 | ██║ ███╗███████║██╔████╔██║█████╗ ██║ ██║██║ ██║█████╗ ██████╔╝ 35 | ██║ ██║██╔══██║██║╚██╔╝██║██╔══╝ ██║ ██║╚██╗ ██╔╝██╔══╝ ██╔══██╗ 36 | ╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗ ╚██████╔╝ ╚████╔╝ ███████╗██║ ██║ 37 | ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ 38 | 39 | 40 | " 41 | } 42 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Game/OnKey_Down.ps1: -------------------------------------------------------------------------------- 1 | $snake1.SwitchDirection('Down') 2 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Game/OnKey_Esc.ps1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StartAutomating/PowerArcade/2f45408db51c55c44c93d363eec551b29eb74f64/ROM/Nibbles2020/Game/OnKey_Esc.ps1 -------------------------------------------------------------------------------- /ROM/Nibbles2020/Game/OnKey_Left.ps1: -------------------------------------------------------------------------------- 1 | $snake1.SwitchDirection('Left') 2 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Game/OnKey_P.ps1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StartAutomating/PowerArcade/2f45408db51c55c44c93d363eec551b29eb74f64/ROM/Nibbles2020/Game/OnKey_P.ps1 -------------------------------------------------------------------------------- /ROM/Nibbles2020/Game/OnKey_Right.ps1: -------------------------------------------------------------------------------- 1 | $snake1.SwitchDirection('Right') 2 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Game/OnKey_Up.ps1: -------------------------------------------------------------------------------- 1 | $snake1.SwitchDirection('Up') 2 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Game/Over.ps1: -------------------------------------------------------------------------------- 1 | $game | Add-Member NoteProperty LastLevelName $game.CurrentLevelName 2 | Switch-Level -Name GameOver 3 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/1/1.ps1: -------------------------------------------------------------------------------- 1 | Add-Sprite -Type Snake -X ($game.Width * (5/8)) -Y ($game.Height /2) -Property @{ 2 | Direction = 4 3 | } -Name Snake1 4 | 5 | Add-Sprite -Type Number -Content 1 -Name TargetNumber -Anywhere 6 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/2/2.ps1: -------------------------------------------------------------------------------- 1 | Add-Sprite -X ($game.Width * .25) -Y ($game.Height * .5) -Width ($game.Width * .5) -Height 1 -Type Wall # Middle Wall 2 | Add-Sprite -X ($game.Width * .75) -Y ($game.Height * .1) -Type Snake -Name Snake1 -Property @{Direction=3} 3 | Add-Sprite -Type Number -Content 1 -AnyWhere -Name TargetNumber -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/3/3.ps1: -------------------------------------------------------------------------------- 1 | foreach ($x in ($game.Width * .25), ($game.Width * .75)) { 2 | _/\+ -Type Wall -X $x -Y ($game.Height * .25) -Width 1 -Height ($game.Height * .5) 3 | } 4 | Add-Sprite -X ($game.Width * .75) -Y ($game.Height * .1) -Type Snake -Name Snake1 -Property @{Direction=3} 5 | Add-Sprite -Type Number -Content 1 -AnyWhere -Name TargetNumber -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/4/4.ps1: -------------------------------------------------------------------------------- 1 | $wallHeight = ($game.Height * .6) 2 | $verticalWall = @{Width=1;Height=$wallHeight;Type='Wall'} 3 | 4 | _/\+ @verticalWall -X ($game.Width * .25) -Y 2 5 | _/\+ @verticalWall -X ($game.Width * .75) -Y ($game.Height - $wallHeight) 6 | 7 | $wallWidth = ($game.Width * .6) 8 | $horizontalWall = @{Height=1;Width=$wallWidth;Type='Wall'} 9 | 10 | _/\+ @horizontalWall -X 2 -Y ($game.Height * .76) 11 | _/\+ @horizontalWall -X ($game.Width - $wallWidth) -Y ($game.Height * .3) 12 | 13 | 14 | Add-Sprite -X ($game.Width * .75) -Y ($game.Height * .1) -Type Snake -Name Snake1 -Property @{Direction=3} 15 | 16 | Add-Sprite -Type Number -Content 1 -AnyWhere -Name TargetNumber -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/5/5.ps1: -------------------------------------------------------------------------------- 1 | $wallHeight = ($game.Height * .52) 2 | $verticalWall = @{Width=1;Height=$wallHeight;Type='Wall'} 3 | 4 | _/\+ @verticalWall -X ($game.Width * .2625) -Y ($game.Height * .26) 5 | _/\+ @verticalWall -X ($game.Width * .7375) -Y ($game.Height * .26) 6 | 7 | $wallWidth = ($game.Width * .425) 8 | $horizontalWall = @{Height=1;Width=$wallWidth;Type='Wall'} 9 | 10 | _/\+ @horizontalWall -X ($game.Width * .2875) -Y ($game.Height * .22) 11 | _/\+ @horizontalWall -X ($game.Width * .2875) -Y ($game.Height * .82) 12 | 13 | Add-Sprite -X ($game.Width * .625) -Y ($game.Height * .5) -Type Snake -Name Snake1 -Property @{Direction=1} 14 | 15 | Add-Sprite -Type Number -Content 1 -Anywhere -Name TargetNumber -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/6/6.ps1: -------------------------------------------------------------------------------- 1 | $UpperWall = @{Width=1;Y=1;Height=$game.Height * .46;Type='Wall'} 2 | $LowerWall = @{Width=1;Y=($game.Height * .6);Height=$game.Height * .4;Type='Wall'} 3 | $Gap = $game.Width / 8 4 | for ($x =0; $x -lt $game.Width; $x+=$gap) { 5 | _/\+ @UpperWall -X $x 6 | _/\+ @LowerWall -X $x 7 | } 8 | 9 | Add-Sprite -X ($game.Width * .8125) -Y ($game.Height * .14) -Type Snake -Name Snake1 -Property @{Direction=2} 10 | 11 | Add-Sprite -Type Number -Content 1 -Anywhere -Name TargetNumber -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/7/7.ps1: -------------------------------------------------------------------------------- 1 | $wallHeight = ($game.Height * .52) 2 | $verticalWall = @{Width=1;Height=1;X=$game.Width*.5;Type='Wall'} 3 | for ($y =4; $y -lt ($game.Height - 4); $y+= 2) { 4 | _/\+ @verticalWall -Y $y 5 | } 6 | 7 | Add-Sprite -X ($game.Width * .8125) -Y ($game.Height * .14) -Type Snake -Name Snake1 -Property @{Direction=2} 8 | 9 | Add-Sprite -Type Number -Content 1 -AnyWhere -Name TargetNumber -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/8/8.ps1: -------------------------------------------------------------------------------- 1 | $UpperWall = @{Width=1;Y=1;Height=$game.Height * .72;Type='Wall'} 2 | $LowerWall = @{Width=1;Y=($game.Height * .28);Height=$game.Height * .72;Type='Wall'} 3 | $Gap = $game.Width / 8 4 | for ($x =0; $x -lt $game.Width; $x+=$gap) { 5 | if (($x % ($gap * 2))) { 6 | _/\+ @UpperWall -X $x 7 | } else { 8 | _/\+ @LowerWall -X $x 9 | } 10 | } 11 | 12 | Add-Sprite -X ($game.Width * .8125) -Y ($game.Height * .14) -Type Snake -Name Snake1 -Property @{Direction=2} 13 | Add-Sprite -Type Number -Content 1 -Anywhere -Name TargetNumber -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/9/9.ps1: -------------------------------------------------------------------------------- 1 | $MiniWall = @{Width=1;Height=1;Type='Wall'} 2 | 3 | $startHeight = [int]($game.Height * .15) 4 | $endHeight = [int]($game.Height * .85) 5 | 6 | for ($y = $startHeight; $y -lt $endHeight; $y++) { 7 | _/\+ @MiniWall -Y $y -X $Y 8 | _/\+ @MiniWall -Y $y -X ($Y + ($Game.Width * .45)) 9 | } 10 | 11 | Add-Sprite -X ($game.Width * .8125) -Y ($game.Height * .14) -Type Snake -Name Snake1 -Property @{Direction=2} 12 | 13 | Add-Sprite -Type Number -Content 1 -AnyWhere -Name TargetNumber -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/GameOver/GameOver.ps1: -------------------------------------------------------------------------------- 1 | @( 2 | [PSCustomObject]@{PSTypeName='PowerArcade.MessageBox';Message=$game.GameOverMessage;Y=$game.Height * .3;Border=$true} 3 | 4 | [PSCustomObject]@{PSTypeName='PowerArcade.MessageBox';Messages=@" 5 | Level - $($game.LastLevelName) 6 | Score - $($game.Player1Score) 7 | "@, $($game.Controls -join [Environment]::NewLine);Y = $game.Height * .55;Border=$true 8 | } 9 | ) |Out-String -Width 1kb | Write-Host -NoNewline 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/GameOver/OnKey_All.ps1: -------------------------------------------------------------------------------- 1 | $key = $args 2 | 3 | if ($key.VirtualKeyCode -eq 32) { # Space, start the game 4 | Switch-Level -Name 1 5 | } 6 | 7 | $keyChar = [char]$key.VirtualKeyCode 8 | if ($keyChar -match '\d') { 9 | 10 | if ($game.Levels["$keyChar"]) { 11 | Switch-Level $keyChar 12 | } else { 13 | Switch-Level -Name 10 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/Levels.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | 4 | .Description 5 | 6 | #> 7 | 8 | Add-Sprite -X 0 -Y 0 -Width $game.Width -Height 1 -Type Wall # Top 9 | Add-Sprite -X 0 -Y $game.Height -Width $game.Width -Height 1 -Type Wall # Bottom 10 | Add-Sprite -X 0 -Y 1 -Width 1 -Height ($game.Height -1) -Type Wall # Left 11 | Add-Sprite -X $game.Width -Y 1 -Width 1 -Height ($game.Height - 1) -Type Wall #Right 12 | 13 | $levelNumber = $game.CurrentLevelName -as [int] 14 | if ($levelNumber -ge 10) { 15 | foreach ($blockNumber in 1..$levelNumber) { 16 | Add-Sprite -Type Wall -X $x -Y $y -Anywhere -Width (1, 3 | Get-Random) -Height (1,3 | Get-Random) 17 | } 18 | 19 | Add-Sprite -Type Number -Content 1 -AnyWhere -Name TargetNumber 20 | 21 | Add-Sprite -Anywhere -Type Snake -Name Snake1 -Property @{Direction=1..4|Get-Random} 22 | } 23 | 24 | $host.UI.RawUI.WindowTitle = @( 25 | $game.Name 26 | 27 | if ($game.CurrentLevelName -ne 'Menu') { 28 | "Level $($game.CurrentLevelName)" 29 | $game.Player1Score 30 | "$($game.Player1Lives)/$($game.Default.Player1Lives) Lives" 31 | } 32 | ) -join ' - ' -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/Menu/Menu.ps1: -------------------------------------------------------------------------------- 1 |  2 | @([PSCustomObject]@{PSTypeName='PowerArcade.MessageBox';Message=$game.Logo -join [Environment]::NewLine;} 3 | 4 | 5 | $startHeight = $game.Height * .55 6 | [PSCustomObject]@{PSTypeName='PowerArcade.MessageBox';Message=$game.Instructions -join [Environment]::NewLine;Y=$startHeight;Border=$true} 7 | $startHeight += $game.Instructions.Count 8 | $startHeight += 4 9 | [PSCustomObject]@{PSTypeName='PowerArcade.MessageBox';Message=$game.Controls -join [Environment]::NewLine;Y=$startHeight;Border=$true} 10 | ) | Out-String -Width 1kb | Write-Host -NoNewline 11 | 12 | return 13 | 14 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/Menu/OnKey_All.ps1: -------------------------------------------------------------------------------- 1 | $key = $args 2 | 3 | if ($key.VirtualKeyCode -eq 32) { # Space, start the game 4 | Switch-Level -Name 1 5 | } 6 | 7 | $keyChar = [char]$key.VirtualKeyCode 8 | if ($keyChar -match '\d') { 9 | 10 | if ($game.Levels["$keyChar"]) { 11 | Switch-Level $keyChar 12 | } else { 13 | Switch-Level -Name 10 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Levels/Pause/Pause.ps1: -------------------------------------------------------------------------------- 1 | ([PSCustomObject]@{PSTypeName='PowerArcade.MessageBox';Message=@' 2 | GAME PAUSED 3 | 4 | Press P to resume 5 | '@;Border=$true}) |Out-String -Width 1kb | Write-Host -NoNewline 6 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Nibbles2020.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RequiredModules = 'PowerArcade' 3 | ModuleVersion = 0.1 4 | Description = 'A New and Improved Nibbles Game, Written in PowerShell' 5 | } 6 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Sprites/Number/Number.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Color = '#00ffff' 3 | # N = 1 4 | } -------------------------------------------------------------------------------- /ROM/Nibbles2020/Sprites/Snake/+.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | What happens when a hits anything else? 4 | .Description 5 | When a Snake Hits anything else, it Dies. 6 | #> 7 | # $snake.Dies() 8 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Sprites/Snake/+Number.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory)] 3 | $Number 4 | ) 5 | 6 | 7 | $intNumber = ([int]$number.Content) 8 | $levelNumber = [int]$game.CurrentLevelName 9 | $this.MaxLength+=($intNumber * 4) 10 | $nextNumber = $intNumber + 1 11 | if ($nextNumber -lt 10) { 12 | $Number.Content = $nextNumber 13 | $this | Add-Member NoteProperty NextNumber $nextNumber -Force 14 | $foundSpot = $false 15 | 16 | 17 | if ($levelNumber -ge 10) { 18 | foreach ($n in 1..(($levelNumber - 10) + $nextNumber)) { 19 | Add-Sprite -Type Wall -X $x -Y $y -Anywhere -Width (1, 3 | Get-Random) -Height (1,3 | Get-Random) 20 | } 21 | } 22 | do { 23 | $nx = Get-random -Minimum 2 -Maximum ($game.Width - 2) 24 | $ny = Get-random -Minimum 2 -Maximum ($game.Height - 2) 25 | if (-not (Find-Sprite -X $nx -Y $ny)) { 26 | $foundSpot = $true 27 | } 28 | } while (-not $foundSpot) 29 | 30 | $Number | Move-Sprite -X $nx -Y $ny 31 | } elseif ($game.CurrentLevelName -match '^\d+$') { 32 | $LevelNumber = [int]($Matches.0) 33 | $nextLevelNumber = $levelNumber + 1 34 | $this | Remove-Sprite 35 | Switch-Level -Name $nextLevelNumber 36 | } 37 | if ($this.Name -eq 'Snake1') { 38 | $game.Player1Score += ($intNumber * $levelNumber) 39 | $host.UI.RawUI.WindowTitle = @( 40 | $game.Name 41 | 42 | if ($game.CurrentLevelName -ne 'Menu') { 43 | "Level $($game.CurrentLevelName)" 44 | $game.Player1Score 45 | "$($game.Player1Lives)/$($game.Default.Player1Lives) Lives" 46 | } 47 | ) -join ' - ' 48 | } 49 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Sprites/Snake/+Wall,Tail,Snake.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | What happens when a hits anything else? 4 | .Description 5 | When a Snake Hits anything else, it Dies. 6 | #> 7 | 8 | 9 | $this.Dies() 10 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Sprites/Snake/Dies.ps1: -------------------------------------------------------------------------------- 1 | if ($this.Name -eq 'Snake1') { 2 | $game.Player1Lives-- 3 | if (-not $game.Player1Lives) { 4 | $game.Over() 5 | } else { 6 | Restart-Level 7 | } 8 | } -------------------------------------------------------------------------------- /ROM/Nibbles2020/Sprites/Snake/OnTick.ps1: -------------------------------------------------------------------------------- 1 | $xy = 2 | @(switch ($this.Direction) { 3 | 1 { 4 | $this.X, ($this.Y - 1) 5 | } 6 | 2 { 7 | $this.X, ($this.Y + 1) 8 | } 9 | 3 { 10 | ($this.X -1), $this.Y 11 | } 12 | 4 { 13 | ($this.X+ 1), $this.Y 14 | } 15 | }) 16 | $oldX, $oldy = $this.X, $this.Y 17 | $this | Move-Sprite @xy -NoClear 18 | $intColor = [int]($this.Color) 19 | $r,$g,$b = 20 | [byte](($intColor -band 0xff0000) -shr 16), 21 | [byte](($intColor -band 0x00ff00) -shr 8), 22 | [byte]($intColor -band 0x0000ff) 23 | $r=[byte]($r* .75) 24 | $g=[byte]($g *.75) 25 | $b=[byte]($b * .75) 26 | 27 | 28 | $MoreTail = Add-Sprite -Type Tail -Content $this.Content -X $oldX -Y $oldy -Property @{ 29 | TimeStamp = [DateTime]::Now 30 | } -Color ("#{0:x2}{1:x2}{2:x2}" -f $r,$g,$b) -PassThru 31 | 32 | $snakeTail = @($MoreTail) + $snake1.Tail 33 | $snakeTail = @($snakeTail | Sort-Object TimeStamp -Descending) 34 | 35 | $StillTail = $snakeTail[0..($snake1.MaxLength)] 36 | $TailToRemove =$snakeTail[($snake1.MaxLength + 1)..($snakeTail.Length)] 37 | 38 | if ($TailToRemove) { 39 | @($TailToRemove -ne $null) | Remove-Sprite 40 | } 41 | 42 | 43 | $snake1 | Add-Member NoteProperty Tail $StillTail -Force 44 | 45 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Sprites/Snake/Snake.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Color = '#0affa0' 3 | Content = '█' 4 | Direction = 1 5 | MaxLength = 3 6 | Tail = @() 7 | } 8 | -------------------------------------------------------------------------------- /ROM/Nibbles2020/Sprites/Snake/SwitchDirection.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Changes the snake direction 4 | .Description 5 | Changes the snake direction 6 | #> 7 | param( 8 | [Parameter(Mandatory)] 9 | [ValidateSet('Up','Down','Left','Right')] 10 | [string] 11 | $Direction 12 | ) 13 | 14 | if ($Direction -eq 'UP' -and $this.Direction -ne 2) { 15 | $this.Direction = 1 16 | } 17 | elseif ($Direction -eq 'DOWN' -and $this.Direction -ne 1) { 18 | $this.Direction = 2 19 | } 20 | elseif ($Direction -eq 'LEFT' -and $this.Direction -ne 4) { 21 | $this.Direction = 3 22 | } 23 | elseif ($Direction -eq 'RIGHT' -and $this.Direction -ne 3) { 24 | $this.Direction = 4 25 | } -------------------------------------------------------------------------------- /ROM/Nibbles2020/Sprites/Wall/Wall.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Color = '#a81300' 3 | Content = '█' 4 | } -------------------------------------------------------------------------------- /ROM/SumGame/Game/Game.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | # The Background Color of the game (most games should set this) 3 | BackgroundColor = '#000000' 4 | 5 | # The Text Color of the game (most games should set this, it's used in MessageBoxes by default) 6 | TextColor = '#00ff00' 7 | 8 | # The Level the game starts on (most games should set this. If they don't, the first alphabetically named Level will be used. 9 | StartLevel = 'Menu' 10 | 11 | # An ASCII art logo. This is not required, but having one makes implementing a Menu easy 12 | Logo = @' 13 | 14 | _________ ________ 15 | / _____/__ __ _____ / _____/_____ _____ ____ 16 | \_____ \| | \/ \/ \ ___\__ \ / \_/ __ \ 17 | / \ | / Y Y \ \_\ \/ __ \| Y Y \ ___/ 18 | /_______ /____/|__|_| /\______ (____ /__|_| /\___ > 19 | \/ \/ \/ \/ \/ \/ 20 | '@ 21 | 22 | # Instructions on how to play the game. 23 | # This is not required, but having them makes implementing a Menu easy. 24 | Instructions = 25 | 'Eat Numbers until you reach the total', 26 | 'If you eat too much,', 27 | ' eat a negative number to subtract' 28 | 29 | # The game controls. 30 | # This is not required, but having them makes implementing a Menu easy. 31 | Controls = 32 | 'Space - Start', 33 | 'Arrows - Move', 34 | 'Escape - Quit' 35 | } -------------------------------------------------------------------------------- /ROM/SumGame/Game/OnKey_Down.ps1: -------------------------------------------------------------------------------- 1 | param($key) # Any OnKey_ script is passed the key press as a parameter. 2 | # If you don't ignore either key up or key down, it will behave unexpectedly. 3 | # If you want the key to respond to being held down, you'll want to ignore KeyUp 4 | if (-not $key.KeyDown) { return } # (which we do here) 5 | 6 | if ($Player) { # If there was a player on the screen 7 | $Player | # Move it DOWN 8 | # (this works by piping the current player in, and using a [ScriptBlock] to alter the value for Y) 9 | Move-Sprite -Y { $_.Y + 1 } 10 | } 11 | -------------------------------------------------------------------------------- /ROM/SumGame/Game/OnKey_Esc.ps1: -------------------------------------------------------------------------------- 1 | $game.IsRunning = $false 2 | Clear-Host -------------------------------------------------------------------------------- /ROM/SumGame/Game/OnKey_Left.ps1: -------------------------------------------------------------------------------- 1 | param($key) # Any OnKey_ script is passed the key press as a parameter. 2 | # If you don't ignore either key up or key down, it will behave unexpectedly. 3 | # If you want the key to respond to being held down, you'll want to ignore KeyUp 4 | if (-not $key.KeyDown) { return } # (which we do here) 5 | 6 | if ($Player) { # If there was a player on the screen 7 | $Player | # Move it LEFT 8 | # (this works by piping the current player in, and using a [ScriptBlock] to alter the value for X) 9 | Move-Sprite -X { $_.X - 1 } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /ROM/SumGame/Game/OnKey_Right.ps1: -------------------------------------------------------------------------------- 1 | param($key) # Any OnKey_ script is passed the key press as a parameter. 2 | # If you don't ignore either key up or key down, it will behave unexpectedly. 3 | # If you want the key to respond to being held down, you'll want to ignore KeyUp 4 | if (-not $key.KeyDown) { return } # (which we do here) 5 | 6 | if ($Player) { # If there was a player on the screen 7 | $Player | # Move it RIGHT 8 | # (this works by piping the current player in, and using a [ScriptBlock] to alter the value for X) 9 | Move-Sprite -X { $_.X + 1 } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /ROM/SumGame/Game/OnKey_Up.ps1: -------------------------------------------------------------------------------- 1 | param($key) # Any OnKey_ script is passed the key press as a parameter. 2 | # If you don't ignore either key up or key down, it will behave unexpectedly. 3 | # If you want the key to respond to being held down, you'll want to ignore KeyUp 4 | if (-not $key.KeyDown) { return } # (which we do here) 5 | 6 | if ($Player) { # If there was a player on the screen 7 | $Player | # Move it UP 8 | # (this works by piping the current player in, and using a [ScriptBlock] to alter the value for Y) 9 | Move-Sprite -Y { $_.Y -1 } 10 | } 11 | -------------------------------------------------------------------------------- /ROM/SumGame/Game/OnTick.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | The Game's OnTick Handler 4 | .Description 5 | This script is called whenever the game clock ticks 6 | #> 7 | 8 | if ($game.CurrentLevel.StartTime) { # If the level is timed 9 | # Update the window title 10 | $host.UI.RawUI.WindowTitle = @("$($game.Name)" 11 | "{0} / {1}" -f $player.Score, $game.CurrentLevel.TargetScore 12 | "$([DateTime]::Now - $game.CurrentLevel.StartTime)".Substring(0,8) 13 | ) -join ' - ' 14 | } -------------------------------------------------------------------------------- /ROM/SumGame/Game/README.md: -------------------------------------------------------------------------------- 1 | ## Understanding the Game Directory 2 | 3 | The Game Directory contains methods and data used throughout the game. 4 | 5 | Any .PS1 file located in this directory will become a ScriptMethod on the game. 6 | 7 | Any .PSD1 file located in this directory will become a Property on the game object. 8 | 9 | 10 | ## Special Files 11 | 12 | 13 | ### Initialize and Default 14 | 15 | A Script Named Game.PS1 will become a method named Initialize(), 16 | which will be called when the game is initialized. 17 | 18 | A Data File Named Game.PSD1 will become a property named Default, 19 | and this will set the default properties for the game object. 20 | 21 | ### OnTick 22 | 23 | A Script Named OnTick.ps1 will be called whenever the game clock ticks. 24 | 25 | 26 | ### OnKey 27 | 28 | Keys can be trapped anywhere in the Game by defining files named OnKey_NameOfKey.ps1 29 | 30 | A Script Named OnKey_Any.ps1 or OnKey_All.ps1 will be called whenever any key is pressed. 31 | 32 | If this script is provided, all key events must be handled within it. 33 | 34 | Otherwise, specific key named can be trapped with OnKey_NameOfKey.ps1 -------------------------------------------------------------------------------- /ROM/SumGame/Levels/Levels.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | All Levels Initialization Script 4 | .Description 5 | This Script is called When Any Levels Initialize. 6 | #> 7 | 8 | 9 | # In this game, we want to draw four walls. 10 | 11 | 12 | # The Top Wall 13 | Add-Sprite -X 0 -Y 0 -Width $game.Width -Height 1 -Type Wall 14 | # The Bottom Wall 15 | Add-Sprite -X 0 -Y $game.Height -Width $game.Width -Height 1 -Type Wall 16 | # The Left Wall 17 | Add-Sprite -X 0 -Y 1 -Width 1 -Height ($game.Height -1) -Type Wall 18 | # The Right Wall 19 | Add-Sprite -X $game.Width -Y 1 -Width 1 -Height ($game.Height - 1) -Type Wall 20 | -------------------------------------------------------------------------------- /ROM/SumGame/Levels/Main/Main.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Initializes the Main Level 4 | .Description 5 | Initializes the Main Level (the actual game) 6 | .Notes 7 | In this Game, A Player (represented by a +) will eat numbers until a total is reached. 8 | 9 | Numbers can be positive or negative, and can have a value between 1-9. 10 | 11 | When any number is eaten, two new numbers will be created. 12 | 13 | When the total is reached exactly, the level will restart. 14 | 15 | #> 16 | 17 | # First, create the player sprite, and put it anywhere. 18 | Add-Sprite -Type Player -Anywhere -Name Player # By using -Name Player, $Player will be available elsewhere in the game. 19 | 20 | foreach ($n in 1..5) { # Next, add 5 numbers, anywhere 21 | Add-Sprite -Type Number -Anywhere 22 | } 23 | 24 | # Then set, the level's target score 25 | $this.TargetScore = Get-Random -Minimum 10 -Maximum 100 26 | $this.StartTime = [DateTime]::Now 27 | 28 | # And update the window title. 29 | $Host.UI.RawUI.WindowTitle = 30 | @("$($game.Name)" 31 | "{0} / {1}" -f $player.Score, $this.TargetScore 32 | ) -join ' - ' 33 | -------------------------------------------------------------------------------- /ROM/SumGame/Levels/Main/Main.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | # This level has a target score (we'll initialize it in the .ps1) 3 | TargetScore = 0 4 | # this level has a Start Time (we'll initialize it in the .ps1) 5 | StartTime = $null 6 | } 7 | -------------------------------------------------------------------------------- /ROM/SumGame/Levels/Main/README.md: -------------------------------------------------------------------------------- 1 | This directory contains the main game level. 2 | 3 | * [Main.ps1](Main.ps1) is called to initialize the level 4 | * [Main.psd1](Main.psd1) contains the level's default properties -------------------------------------------------------------------------------- /ROM/SumGame/Levels/Menu/Menu.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Initializes the Menu 4 | .Description 5 | Initializes the Menu Level. 6 | This is used to display basic information about the game before playing. 7 | .Notes 8 | This implementation of a menu shows up to 3 messages: 9 | * The $Game.Logo 10 | * The $Game.Instructions 11 | * The $Game.Controls 12 | 13 | Because of the generic way this can be written, you can often reuse this Menu implementation. 14 | #> 15 | 16 | # Show the game logo at the top 1/3rd 17 | Show-Game -Message $game.Logo 18 | 19 | # Set a starting height for additional messages 20 | $startHeight = $game.Height * .55 21 | if ($game.Instructions) { # If the game had instructions 22 | Show-Game -Message $game.Instructions -Y $startHeight -Border # show them with a border 23 | $startHeight += $game.Instructions.Count # and update the starting height. 24 | $startHeight += 4 25 | } 26 | if ($game.Controls) { # If the game had controls 27 | Show-Game -Message $game.Controls -Y $startHeight -Border # show them with a border. 28 | } -------------------------------------------------------------------------------- /ROM/SumGame/Levels/Menu/OnKey_Space.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | On Space, Start the Game 4 | .Description 5 | Press space key to start. 6 | #> 7 | param( 8 | $Key 9 | ) 10 | # The game is started by switching to the main level. 11 | Switch-Level -Name Main -------------------------------------------------------------------------------- /ROM/SumGame/Levels/Menu/README.md: -------------------------------------------------------------------------------- 1 | This directory contains the game menu. 2 | 3 | This directory can often be copy/pasted into any game that needs a menu, with only minor modifications. 4 | 5 | [Menu.ps1](Menu.ps1) initalizes the menu, and it displays messages from a few properties of the $Game object: 6 | 7 | * Logo 8 | * Instructions 9 | * Controls 10 | 11 | Additionally, [OnKey_Space.ps1](OnKey_Space.ps1) switches to the main game level to start playing. -------------------------------------------------------------------------------- /ROM/SumGame/Levels/README.md: -------------------------------------------------------------------------------- 1 | ## About the Levels Directory 2 | 3 | The Levels directory contains the levels of the game. 4 | 5 | Individual levels can be defined in subdirectories of this directory. 6 | 7 | If there is a [Levels.ps1](Levels.ps1), it will be called whenever any level initializes. -------------------------------------------------------------------------------- /ROM/SumGame/README.md: -------------------------------------------------------------------------------- 1 | # SumGame 2 | 3 | This directory contains the implementation of SumGame. 4 | 5 | ## Gameplay 6 | 7 | In SumGame, you control a +, which eats numbers on the screen until it reaches an exact total. 8 | 9 | Positive Numbers are Green, Negative Numbers are red. 10 | 11 | ## Important Files / Directories 12 | 13 | |Item |Purpose | 14 | |----------------------------|-----------------------| 15 | |[SumGame.psd1](SumGame.psd1)|The Module Manifest | 16 | |[Game](Game) |Game Data and Methods | 17 | |[Levels](Levels) |Game Levels | 18 | |[Sprites](Sprites) |Game Sprites | -------------------------------------------------------------------------------- /ROM/SumGame/Sprites/Number/Number.ps1: -------------------------------------------------------------------------------- 1 | $value = 1..9 | Get-Random # Pick a number between 1/9 2 | $valueMultiplier = 1,1,-1 | Get-Random # Give it a 2/3rds chance of being positive 3 | $value *= $valueMultiplier 4 | $numberString = "$value" 5 | $this.Value = $value # Keep track of the value in the sprite, for when we're hit 6 | $this.Content = $numberString # update the display string 7 | if ($this.Value -lt 0) { # If the number is negative 8 | $this.Color = '#ff0000' # make it red instead. 9 | } -------------------------------------------------------------------------------- /ROM/SumGame/Sprites/Number/Number.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Color = '#00ff00' # The Color 3 | Content = '' # The Content, in this case, a blank (our initializer will handle it) 4 | Value = 1 # The value, defaulted to 1 (our initializer will reset it) 5 | } -------------------------------------------------------------------------------- /ROM/SumGame/Sprites/Number/README.md: -------------------------------------------------------------------------------- 1 | This directory contains the definition of a Number Sprite. 2 | 3 | * [Number.psd1](Number.psd1) contains the default properties of the number 4 | * [Number.ps1](Number.ps1) contains the initializer. 5 | 6 | -------------------------------------------------------------------------------- /ROM/SumGame/Sprites/Player/+Number.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Collision handler for Currency 4 | .Description 5 | This script is run whenever the Player sprite collides with the Currency Sprite 6 | 7 | The Currency Sprite is passed as a parameter 8 | #> 9 | param($currency) 10 | 11 | $player.Score += $currency.Value 12 | 13 | $currency | Remove-Sprite 14 | 15 | # Add two more currency symbols 16 | Add-Sprite -Type Number -Anywhere 17 | Add-Sprite -Type Number -Anywhere 18 | 19 | if ($player.Score -eq $game.CurrentLevel.TargetScore) { 20 | Restart-Level 21 | } else { 22 | 23 | $Host.UI.RawUI.WindowTitle = [String]::Format( 24 | "$($game.Name) - {0} / {1} - $("$([DateTime]::Now - $game.CurrentLevel.StartTime)".Substring(0,8))", 25 | $player.Score, 26 | $game.CurrentLevel.TargetScore) 27 | } 28 | -------------------------------------------------------------------------------- /ROM/SumGame/Sprites/Player/Player.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Content = '+' # The player is represented by a + 3 | Color = '#faaf00' # In this color 4 | Score = 0 # with a default score 0. 5 | } -------------------------------------------------------------------------------- /ROM/SumGame/Sprites/Player/README.md: -------------------------------------------------------------------------------- 1 | This directory contains the definition of the Player Sprite. 2 | 3 | * [Player.psd1](Player.psd1) contains the default properties of the player sprite 4 | * [+Number.ps1](+Number.ps1) describes what happens when a Player sprite hits a number sprite 5 | -------------------------------------------------------------------------------- /ROM/SumGame/Sprites/README.md: -------------------------------------------------------------------------------- 1 | ## Understanding the Sprites directory 2 | The Sprites directory defines the sprites used in the game. 3 | 4 | Each Sprite is in it's own subdirectory. 5 | 6 | Each subdirectory can have default properties (in SubDirectory/SubDirectory.psd1) and an initializer (in SubDirectory/SubDirectory.ps1) 7 | 8 | All other .ps1 files in the subdirectory will become methods of the Sprite. 9 | 10 | ### OnTick.ps1 11 | 12 | Any Sprite directory can have an OnTick.ps1, which will be called when the game clock ticks. 13 | 14 | ### Collision files (+Target.ps1) 15 | 16 | Any Sprite directory can also have one or more files starting with a +. 17 | 18 | These files describe what happens when the sprite collides with another sprite. 19 | 20 | For instance, a file named Player\+Number.ps1 would describe what happens when a Player sprite hits a Number sprite. 21 | 22 | Multiple collisions can be described in a single file by separating types with commas, 23 | for example: Player\+Number,Letter.ps1 would describe what happens when a Player sprite hits either a Number of a Letter. 24 | 25 | A file named +.ps1 will describe all sprite collisions. 26 | -------------------------------------------------------------------------------- /ROM/SumGame/Sprites/Wall/README.md: -------------------------------------------------------------------------------- 1 | This directory contains the definition of a Wall Sprite. 2 | 3 | This can be copied and pasted from game to game, with only minor modifications made to change the color of the wall. -------------------------------------------------------------------------------- /ROM/SumGame/Sprites/Wall/Wall.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Color = '#00318a' # The Wall Color 3 | Content = '█' # The Wall Content (a solid block) 4 | } -------------------------------------------------------------------------------- /ROM/SumGame/SumGame.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | RequiredModules = 'PowerArcade' 3 | ModuleVersion = '0.1' 4 | Description = 'An Educational Demo Game - Move Around and Collect Numbers until you have exactly the total' 5 | PrivateData = @{ 6 | PSData = @{ 7 | 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /Remove-Sprite.ps1: -------------------------------------------------------------------------------- 1 | function Remove-Sprite 2 | { 3 | <# 4 | .Synopsis 5 | Removes a Sprite 6 | .Description 7 | Removes a Sprite from the screen and the current level. 8 | .Example 9 | $byeByeSprite | Remove-Sprite 10 | .Link 11 | Add-Sprite 12 | .Link 13 | Find-Sprite 14 | .Link 15 | Move-Sprite 16 | #> 17 | [CmdletBinding(SupportsShouldProcess,ConfirmImpact='Low')] 18 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification="Games Must Use the Host")] 19 | [OutputType([Nullable])] 20 | param( 21 | # The Sprite. 22 | [Parameter(Mandatory,ValueFromPipeline)] 23 | [PSTypeName('PowerArcade.Sprite')] 24 | [PSObject] 25 | $Sprite 26 | ) 27 | 28 | process { 29 | if (-not $PSCmdlet.ShouldProcess("Remove $($sprite.Type) $($sprite.spriteID)")) {return } 30 | [Console]::Write("$($sprite.Clear())") 31 | 32 | if ($game.CurrentLevel -and $game.CurrentLevel.SpatialMap.ContainsKey) { 33 | 34 | foreach ($osh in $Sprite.SpatialHash) { 35 | if ($game.CurrentLevel.SpatialMap.ContainsKey($osh)) { 36 | $toRemove = 37 | @(for ($in =0 ; $in -lt $game.CurrentLevel.SpatialMap[$osh].Count; $in++) { 38 | if ($game.CurrentLevel.SpatialMap[$osh][$in].SpriteID -eq $Sprite.SpriteID) { 39 | $game.CurrentLevel.SpatialMap[$osh][$in] 40 | break 41 | } 42 | }) 43 | 44 | foreach ($tr in $toRemove) { 45 | $null = $game.CurrentLevel.SpatialMap[$osh].Remove($tr) 46 | } 47 | $null = $game.CurrentLevel.SpritesById.Remove($sprite.SpriteID) 48 | } 49 | } 50 | 51 | $null = $game.CurrentLevel.Sprites.Remove($sprite) 52 | $sprite.psobject.Properties.Remove('Level') 53 | } 54 | 55 | if ($sprite.Name) { 56 | $ExecutionContext.SessionState.PSVariable.Remove("global:$($Sprite.Name)") 57 | } 58 | 59 | $sprite | Add-Member NoteProperty Hidden $true -Force 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Restart-Level.ps1: -------------------------------------------------------------------------------- 1 | function Restart-Level 2 | { 3 | <# 4 | .Synopsis 5 | Restarts the current level 6 | .Description 7 | Used within a PowerArcade game to restart the current level. 8 | .Link 9 | Switch-Level 10 | .Link 11 | Suspend-Level 12 | .Link 13 | Resume-Level 14 | #> 15 | 16 | param( 17 | # If set, will not rerun the level initialize routines. 18 | [Alias('NoInit','DoNotInitialize')] 19 | [switch] 20 | $NoInitialize, 21 | 22 | # If set, will not clear the screen and sprite map. 23 | [Alias('DoNotClear')] 24 | [switch] 25 | $NoClear 26 | ) 27 | 28 | process { 29 | if (-not $Global:Game) { 30 | Write-Error "Cannot restart a level without initializing the game." 31 | return 32 | } 33 | 34 | if (-not $NoClear) { 35 | Clear-Host 36 | if ($game.BackgroundColor) { 37 | # [Console]::Write(('' + [char]0x1b + '[1049l')) 38 | [Console]::Write(([PSCustomObject]@{ 39 | PSTypeName='PowerArcade.Box' 40 | BackgroundColor = $game.BackgroundColor 41 | } | Out-String -Width 1kb).Trim()) 42 | try {[Console]::CursorVisible = $false} catch {$PSCmdlet.WriteVerbose("$_")} 43 | } 44 | if ($game.CurrentLevel.Sprites.Clear) { 45 | $game.CurrentLevel.Sprites.Clear() 46 | } 47 | if ($game.CurrentLevel.SpritesByID.Clear) { 48 | $game.CurrentLevel.SpritesByID.Clear() 49 | } 50 | if ($game.CurrentLevel.SpatialMap.Clear) { 51 | $game.CurrentLevel.SpatialMap.Clear() 52 | } 53 | 54 | foreach ($X in 0..$game.CellWidth) { 55 | foreach ($Y in 0..$game.CellHeight) { 56 | $game.CurrentLevel.SpatialMap["$X,$y"] = [Collections.Generic.List[PSObject]]::new() 57 | } 58 | } 59 | } 60 | 61 | if (-not $NoInitialize) { 62 | 63 | $Global:Game.CurrentLevel | Add-Member NoteProperty Initializing $true -Force 64 | if ($Global:Game.LevelBaseObject.Initialize.Invoke) { 65 | $Global:Game.LevelBaseObject.Initialize() 66 | } 67 | if ($Global:Game.CurrentLevel.Initialize.Invoke) { 68 | $Global:Game.CurrentLevel.Initialize() 69 | } 70 | if ($Global:Game.CurrentLevel.DynamicInitialize.Invoke) { 71 | $Global:Game.CurrentLevel.DynamicInitialize.Invoke() 72 | } 73 | 74 | $Global:Game.CurrentLevel.Initializing = $false 75 | $Global:Game.CurrentLevel.Draw() 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Resume-Game.ps1: -------------------------------------------------------------------------------- 1 | function Resume-Game 2 | { 3 | <# 4 | .Synopsis 5 | Resumes a Game 6 | .Description 7 | If a Game has been stopped but is still in memory, resumes the game from its current state 8 | .Example 9 | Resume-Game 10 | .Link 11 | Watch-Game 12 | #> 13 | param() 14 | 15 | if (-not $game) { # If there is no game, 16 | Write-Error "No Game to Resume" # error 17 | return # and return. 18 | } 19 | Show-Game -GameState # We show the current game state 20 | $game.IsRunning = $true # then mark the game as running 21 | $game | Watch-Game # then watch the game. 22 | } -------------------------------------------------------------------------------- /Resume-Level.ps1: -------------------------------------------------------------------------------- 1 | function Resume-Level 2 | { 3 | <# 4 | .Synopsis 5 | Resumes a level 6 | .Description 7 | Resumes a level suspended with Suspend-Level 8 | .Example 9 | Resume-Level 10 | .Link 11 | Suspend-Level 12 | #> 13 | param() 14 | if (-not $Global:Game) { return } 15 | 16 | 17 | if (-not $Global:Game.SuspendedLevel) { return } 18 | if (-not $Global:Game.SuspendedLevelName) { return } 19 | $Global:Game.CurrentLevel = $Global:Game.SuspendedLevel 20 | $Global:Game.CurrentLevelName = $Global:Game.SuspendedLevelName 21 | Clear-Host 22 | if ($game.BackgroundColor) { 23 | # [Console]::Write(('' + [char]0x1b + '[1049l')) 24 | ([PSCustomObject]@{ 25 | PSTypeName='PSArcade.Box' 26 | BackgroundColor = $game.BackgroundColor 27 | } | Out-String -Width 1kb).Trim() | Write-Host -NoNewline 28 | } 29 | if ($Global:Game.CurrentLevel.Draw) { 30 | $Global:Game.CurrentLevel.Draw() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Show-Game.ps1: -------------------------------------------------------------------------------- 1 | function Show-Game 2 | { 3 | <# 4 | .Synopsis 5 | Shows the game, or shows messages in the game. 6 | .Description 7 | By default, Shows the game (redrawing the current level and state). 8 | 9 | When -Message is provided, shows in-game messages. 10 | .Example 11 | Show-Game 12 | .Link 13 | Start-Game 14 | #> 15 | [CmdletBinding(DefaultParameterSetName='GameState')] 16 | param( 17 | # The Message to display 18 | [Parameter(Position=0,ParameterSetName='Message',ValueFromPipelineByPropertyName)] 19 | [string[]] 20 | $Message, 21 | 22 | # The X-coordinate of the message. If not provided, this will default to center the message on the game width. 23 | [Parameter(ParameterSetName='Message',ValueFromPipelineByPropertyName)] 24 | [int] 25 | $X, 26 | 27 | # The Y-coordinate of the message. If not provided, this will default to 1/3rd of the game height. 28 | [Parameter(ParameterSetName='Message',ValueFromPipelineByPropertyName)] 29 | [int] 30 | $y, 31 | 32 | 33 | # If provided, the message will have a border. 34 | [Parameter(ParameterSetName='Message',ValueFromPipelineByPropertyName)] 35 | [switch] 36 | $Border, 37 | 38 | # While the command shows game state by default, you can pass -GameState for clarity in code. 39 | [Parameter(ParameterSetName='GameState',ValueFromPipelineByPropertyName)] 40 | [switch] 41 | $GameState 42 | ) 43 | 44 | begin { 45 | $allMessages = [Collections.Generic.List[PSObject]]::new() 46 | } 47 | 48 | process { 49 | #region Showing a Message 50 | if ($PSCmdlet.ParameterSetName -eq 'Message') 51 | { 52 | # If we're showing a message, 53 | $PSBoundParameters.Message = # Join all message lines into a single string 54 | $PSBoundParameters.Message -split '(?>\r\n|\n)' -join [Environment]::NewLine 55 | $allMessages.Add([PSCustomObject]( # then create a MessageBox using the bound parameters 56 | [Ordered]@{PSTypeName='PowerArcade.MessageBox'} + $PSBoundParameters 57 | )) # and add it to all messages. 58 | return 59 | } 60 | #endregion Showing a Message 61 | if ($PSCmdlet.ParameterSetName -eq 'GameState') { 62 | Clear-Host 63 | if ($game.BackgroundColor) { 64 | # [Console]::Write(('' + [char]0x1b + '[1049l')) 65 | ([PSCustomObject]@{ 66 | PSTypeName='PowerArcade.Box' 67 | BackgroundColor = $game.BackgroundColor 68 | } | Out-String -Width 1kb).Trim() | Write-Host -NoNewline 69 | } 70 | 71 | if ($game.CurrentLevel.Draw) { 72 | $Game.CurrentLevel.Draw() 73 | } 74 | } 75 | } 76 | 77 | end { 78 | if ($allMessages) { # If we have any messages to show 79 | # Pipe them to out string and write them to the console 80 | [Console]::Write("$(($allMessages | Out-String -Width 1kb).Trim())") 81 | # (don't forget to hide the cursor, if we can) 82 | try {[Console]::CursorVisible = $false} catch {$PSCmdlet.WriteVerbose("$_")} 83 | } 84 | 85 | if ($passThru) { # If we're passing values thru 86 | if ($allMessages) { $allMessages.ToArray() } # return all messages 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /Start-Game.ps1: -------------------------------------------------------------------------------- 1 | function Start-Game 2 | { 3 | <# 4 | .Synopsis 5 | Starts a Game 6 | .Description 7 | Starts a Game. 8 | .Link 9 | Initialize-Game 10 | .Link 11 | Watch-Game 12 | #> 13 | [CmdletBinding(SupportsShouldProcess,ConfirmImpact='Low')] 14 | [OutputType([Nullable])] 15 | param( 16 | # The path to the game 17 | [Parameter(Mandatory,Position=0,ParameterSetName='GamePath',ValueFromPipelineByPropertyName)] 18 | [Alias('ROM','FullName')] 19 | [string] 20 | $GamePath, 21 | 22 | # The level 23 | [string] 24 | $Level 25 | ) 26 | 27 | 28 | 29 | process { 30 | #region Find that Game 31 | 32 | $resolvePathError = $null 33 | $resolvedGamePath = 34 | try { $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($gamePath) } 35 | catch { $resolvePathError = $_ } 36 | if (-not $resolvedGamePath) { # If we can't resolve the gamepath, 37 | # see if there's an installed game by that name. 38 | $resolvedGamePath = Get-Game -Name $GamePath | Select-Object -First 1 -ExpandProperty GamePath 39 | if (-not $resolvedGamePath) { # If we still couldn't resolve the game path, 40 | $PSCmdlet.WriteError($resolvePathError) # error out 41 | return 42 | } 43 | 44 | } 45 | if ($resolvedGamePath -like '*.ps1') { # If the gamepath was a specific ps1 file, run it and return 46 | # (this lets 'legacy' games, like PSInvaders, run) 47 | & $resolvedGamePath 48 | return 49 | } 50 | #endregion Find that Game 51 | 52 | # Make sure they want to run a game from there. 53 | if (-not $PSCmdlet.ShouldProcess("Running Game from $resolvedGamePath")) { return } 54 | 55 | #region ISE is not cool enough 56 | if ($host.Name -eq 'Windows PowerShell ISE Host') { 57 | Write-Warning "ISE may be cool, but it can't play. Launching the console. It will quit when done." 58 | $powerShellArgs = @( 59 | '-windowstyle' 60 | 'maximized' 61 | '-noexit' 62 | '-command' 63 | 'Import-Module PowerArcade -Force -PassThru; Start-Game ' + 64 | $(@( 65 | foreach ($kv in $PSBoundParameters.GetEnumerator()) { 66 | if ($kv.Value -is [string]) { 67 | "-$($kv.Key)" 68 | "'$($kv.Value.Replace("'","''"))'" 69 | } 70 | } 71 | ) -join ' ') + ';if ($?) { exit }' 72 | 73 | ) 74 | Start-Process powershell -ArgumentList $powerShellArgs -WorkingDirectory $pwd 75 | return 76 | } 77 | #endregion ISE is not cool enough 78 | 79 | #region Start it up 80 | $theGame = Initialize-Game -GamePath $resolvedGamePath -Level $level | 81 | Where-Object { $_.PSTypeNames -contains 'PowerArcade.Game' } | 82 | Select-Object -First 1 83 | $theGame | 84 | Add-Member NoteProperty IsRunning $true -Force -PassThru | 85 | Add-Member ScriptMethod Exit { $this.IsRunning = $false } -Force 86 | #endregion Start it up 87 | #region Watch the game 88 | if ($theGame) { 89 | $theGame | Watch-Game 90 | } 91 | #endregion Watch the game 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Suspend-Level.ps1: -------------------------------------------------------------------------------- 1 | function Suspend-Level 2 | { 3 | <# 4 | .Synopsis 5 | Suspends a level 6 | .Description 7 | Suspends a level data in $game.SuspendedLevel and $Game.SuspendedLevelName. 8 | 9 | This can be resumed with Resume-Level 10 | .Example 11 | Suspend-Level 12 | .Link 13 | Resume-Level 14 | #> 15 | [OutputType([Nullable])] 16 | param() 17 | 18 | #region Hold Up 19 | if (-not $Global:Game) { return } 20 | 21 | $game | Add-Member NoteProperty SuspendedLevel $game.CurrentLevel -Force 22 | $game | Add-Member NoteProperty SuspendedLevelName $game.CurrentLevelName -Force 23 | #endregion Hold Up 24 | } 25 | -------------------------------------------------------------------------------- /Switch-Level.ps1: -------------------------------------------------------------------------------- 1 | function Switch-Level 2 | { 3 | <# 4 | .Synopsis 5 | Switches the game level 6 | .Description 7 | Switches to a different level of the game. 8 | 9 | If the level does not exist, a message will be written to -Verbose, and the game will otherwise continue to run. 10 | This enables virtual levels of increasing difficulty. 11 | #> 12 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification="Games Must Use the Host")] 13 | [OutputType([Nullable])] 14 | param( 15 | # The name of the level 16 | [Parameter(Mandatory,Position=0)] 17 | [Alias('Level')] 18 | [string] 19 | $Name, 20 | 21 | # A script to run after switching the level. This can be used to provide dynamic level setup 22 | [ScriptBlock] 23 | $Then, 24 | 25 | # If set, will not run the Level's initialize routine 26 | [Alias('NoInit','DoNotInitialize')] 27 | [switch] 28 | $NoInitialize, 29 | 30 | # If set, will not clear the screen, clear the spritemap, and clear the sprite list 31 | [Alias('DoNotClear')] 32 | [switch] 33 | $NoClear, 34 | 35 | # If set, will clear the screen and redraw the current level. 36 | [switch] 37 | $Redraw 38 | ) 39 | 40 | process { 41 | if (-not $Global:Game) { 42 | Write-Error "Cannot switch a level without initializing the game." 43 | return 44 | } 45 | if ($Global:Game.CurrentLevelName -eq $Name) { return } 46 | if (-not $Global:Game.Levels[$Name]) { 47 | #region Initialize Dynamic Level 48 | Write-Verbose "Level name $name not found in $($game.Name)" 49 | $Global:Game.CurrentLevel = [PSCustomObject]@{ 50 | PSTypeName='PowerArcade.Level'; 51 | Name=$Name; 52 | Sprites=[Collections.Generic.List[PSObject]]::new() 53 | SpatialMap = 54 | [Collections.Generic.Dictionary[ 55 | string, 56 | [Collections.Generic.List[PSObject]] 57 | ]]::new([StringComparer]::OrdinalIgnoreCase) 58 | SpritesByID = 59 | [Collections.Generic.Dictionary[ 60 | string, 61 | [PSObject] 62 | ]]::new([StringComparer]::OrdinalIgnoreCase) 63 | SpriteMap=[Collections.Generic.Dictionary[string,[Collections.Queue]]]::new([StringComparer]::OrdinalIgnoreCase) 64 | } 65 | 66 | foreach ($X in 0..$game.CellWidth) { 67 | foreach ($Y in 0..$game.CellHeight) { 68 | $Game.CurrentLevel.SpatialMap["$X,$y"] = [Collections.Generic.List[PSObject]]::new() 69 | } 70 | } 71 | 72 | #endregion Initialize Dynamic Level 73 | } else { 74 | $Global:Game.CurrentLevel = $Global:Game.Levels[$Name] 75 | } 76 | $Global:Game.CurrentLevelName = $Name 77 | 78 | $OnKey = @{} 79 | foreach ($method in @($Global:Game.psobject.Methods; $Global:Game.CurrentLevel.psobject.Methods)) { 80 | if ($method.Name -like 'OnKey_*') { 81 | $onKey[$method.Name.Substring(6)] = $method.Script 82 | } 83 | } 84 | $Global:Game.PSObject.Members.Add([PSNoteProperty]::new('KeyHandlers', $OnKey)) 85 | 86 | 87 | if ($Redraw) { 88 | Clear-Host 89 | if ($game.BackgroundColor) { 90 | # [Console]::Write(('' + [char]0x1b + '[1049l')) 91 | ([PSCustomObject]@{ 92 | PSTypeName='PowerArcade.Box' 93 | BackgroundColor = $game.BackgroundColor 94 | } | Out-String -Width 1kb).Trim() | Write-Host -NoNewline 95 | } 96 | 97 | $Game.CurrentLevel.Draw() 98 | 99 | return 100 | } 101 | 102 | 103 | if (-not $NoClear) { 104 | Clear-Host 105 | if ($game.BackgroundColor) { 106 | # [Console]::Write(('' + [char]0x1b + '[1049l')) 107 | [Console]::Write(([PSCustomObject]@{ 108 | PSTypeName='PowerArcade.Box' 109 | BackgroundColor = $game.BackgroundColor 110 | } | Out-String -Width 1kb).Trim()) 111 | } 112 | try {[Console]::CursorVisible = $false} catch {$PSCmdlet.WriteVerbose("$_")} 113 | if ($game.CurrentLevel.Sprites.Clear) { 114 | $game.CurrentLevel.Sprites.Clear() 115 | } 116 | if ($game.CurrentLevel.SpriteMap.Clear) { 117 | $game.CurrentLevel.SpriteMap.Clear() 118 | } 119 | 120 | } 121 | 122 | if (-not $NoInitialize) { 123 | $Global:Game.CurrentLevel | Add-Member NoteProperty Initializing $true -Force 124 | if ($Global:Game.LevelBaseObject.Initialize.Invoke) { 125 | $Global:Game.LevelBaseObject.Initialize() 126 | } 127 | if ($Global:Game.CurrentLevel.Initialize.Invoke) { 128 | $Global:Game.CurrentLevel.Initialize() 129 | } 130 | if ($then) { 131 | $Global:Game.CurrentLevel | Add-Member NoteProperty DynamicInitialize $then -Force 132 | } 133 | if ($Global:Game.CurrentLevel.DynamicInitialize.Invoke) { 134 | $Global:Game.CurrentLevel.DynamicInitialize.Invoke() 135 | } 136 | 137 | $Global:Game.CurrentLevel.Initializing = $false 138 | } 139 | 140 | $Global:Game.CurrentLevel.Draw() 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Types/PowerArcade.Game/GetSpatialHash.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory)] 3 | $X, 4 | 5 | [Parameter(Mandatory)] 6 | $Y 7 | ) 8 | 9 | "$( 10 | [int][Math]::Floor($x / ($this.Width / $this.CellWidth)) 11 | ),$( 12 | [int][Math]::Floor($y / ($this.Height / $this.CellHeight)) 13 | )" 14 | -------------------------------------------------------------------------------- /Types/PowerArcade.Level/Draw.ps1: -------------------------------------------------------------------------------- 1 | [Console]::Write("$(@( 2 | foreach ($sprite in $this.Sprites) { 3 | [char]0x1b + '[25l' + ($sprite | Out-String -Width 1kb).Trim() 4 | } 5 | ) -join '')") 6 | try {[Console]::CursorVisible = $false} catch {Write-Verbose "$_"} 7 | -------------------------------------------------------------------------------- /Types/PowerArcade.Point/ToString.ps1: -------------------------------------------------------------------------------- 1 | "$($this.X),$($this.Y)" 2 | -------------------------------------------------------------------------------- /Types/PowerArcade.Sprite.Reference/ToString.ps1: -------------------------------------------------------------------------------- 1 | @( 2 | if ($this.Type) { 3 | $this.Type 4 | } 5 | $this.SpriteID 6 | ) -join ' ' 7 | -------------------------------------------------------------------------------- /Types/PowerArcade.Sprite/Clear.ps1: -------------------------------------------------------------------------------- 1 | @(if ($this.X -ge 0 -and $this.Y -ge 0) { 2 | '' + [char]0x1b + "[$($this.Y);$($this.X)H" 3 | } 4 | if ($this.Shapes) { 5 | @(foreach ($shape in $this.Shapes) { 6 | $newShape = [PSCustomObject]::new() 7 | foreach ($member in $shape.Members) { 8 | $newShape.psobject.members.add($member, $true) 9 | } 10 | foreach ($tn in $shape.pstypenames) { 11 | $newShape.pstypenames.add($tn) 12 | } 13 | 14 | $newShape.psobject.members.add([PSNoteProperty]::new('Color', $game.BackgroundColor), $true) 15 | $newShape.psobject.members.add([PSNoteProperty]::new('BackgroundColor', $game.BackgroundColor), $true) 16 | $newShape.psobject.members.add([PSNoteProperty]::new('Fill', ' '), $true) 17 | $newShape 18 | }) | Out-String -Width 1kb 19 | } elseif ($this.Content) { 20 | if ($this.BackgroundColor -or $game.BackgroundColor) { 21 | $bgColor = if ($this.BackgroundColor) { } elseif ($game.BackgroundColor) { $game.BackgroundColor } 22 | $intColor = [int]($bgColor -replace '#', '0x') 23 | $r,$g,$b = 24 | [byte](($intColor -band 0xff0000) -shr 16), 25 | [byte](($intColor -band 0x00ff00) -shr 8), 26 | [byte]($intColor -band 0x0000ff) 27 | 28 | '' + [char]0x1b+"[48;2;$r;$g;${b}m" 29 | 30 | } 31 | #$intColor = [int]($game.BackgroundColor -replace '#', '0x') 32 | if ($this.ContentLength) { 33 | ' ' * $this.ContentLength 34 | } else { 35 | ' ' * "$($this.Content)".Length 36 | } 37 | 38 | } 39 | '' + [char]0x1b + '[25l' 40 | ) -join '' 41 | -------------------------------------------------------------------------------- /Types/PowerArcade.Sprite/Draw.ps1: -------------------------------------------------------------------------------- 1 | @( 2 | '' + [char]0x1b + '[25l' 3 | if ($this.X -ge 0 -and $this.Y -ge 0) { 4 | '' + [char]0x1b + "[$($this.Y);$($this.X)H" 5 | } 6 | if ($this.Color) { 7 | $intColor = [int]($this.Color -replace '#', '0x') 8 | $r,$g,$b = 9 | [byte](($intColor -band 0xff0000) -shr 16), 10 | [byte](($intColor -band 0x00ff00) -shr 8), 11 | [byte]($intColor -band 0x0000ff) 12 | 13 | '' + [char]0x1b+"[38;2;$r;$g;${b}m" 14 | } 15 | if ($this.BackgroundColor -or $game.BackgroundColor) { 16 | $bgColor = if ($this.BackgroundColor) { $this.BackgroundColor } elseif ($game.BackgroundColor) { $game.BackgroundColor } 17 | $intColor = [int]($bgColor -replace '#', '0x') 18 | $r,$g,$b = 19 | [byte](($intColor -band 0xff0000) -shr 16), 20 | [byte](($intColor -band 0x00ff00) -shr 8), 21 | [byte]($intColor -band 0x0000ff) 22 | 23 | '' + [char]0x1b+"[48;2;$r;$g;${b}m" 24 | 25 | } 26 | if ($this.Shapes) { 27 | ($this.Shapes | Out-String -Width 1kb).Trim() 28 | } elseif ($this.Content) { 29 | "$($this.Content)" 30 | } 31 | if ($this.Color) { 32 | [char]0x1b +"[39m" 33 | } 34 | if ($this.BackgroundColor -or $game.BackgroundColor) { 35 | [char]0x1b +"[49m" 36 | } 37 | '' + [char]0x1b + '[25h' 38 | ) -join '' 39 | -------------------------------------------------------------------------------- /Types/PowerArcade.Sprite/Hide.ps1: -------------------------------------------------------------------------------- 1 | Add-Member NoteProperty Hidden $true -Force -InputObject $this 2 | -------------------------------------------------------------------------------- /Types/PowerArcade.Sprite/MeasureBounds.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [int] 3 | $X = $this.X, 4 | 5 | [int] 6 | $Y = $this.Y 7 | ) 8 | 9 | 10 | if ($this.Width -and $this.Height) { 11 | for ($oy = 0; $oy -lt $this.Height; $oy++) { 12 | for ($ox = 0; $ox -lt $this.Width; $ox++) { 13 | 14 | [PSCustomObject]@{ 15 | X = $x + $ox 16 | Y = $y + $oy 17 | SpatialHash = 18 | $(if ($game.GetSpatialHash) { 19 | $game.GetSpatialHash($x + $ox,$y + $oy) 20 | }) 21 | PSTypeName='PowerArcade.Point' 22 | } 23 | } 24 | } 25 | 26 | } elseif ($this.Content) 27 | { 28 | $cl = 29 | if ($this.ContentLength) { 30 | $this.ContentLength 31 | } else { 32 | $this.Content.ToString().Length 33 | } 34 | for ($ox =0; $ox -lt $cl; $ox++) { 35 | [PSCustomObject]@{ 36 | X = $x + $ox 37 | Y = $y 38 | SpatialHash = 39 | $(if ($game.GetSpatialHash) { 40 | $game.GetSpatialHash($x + $ox,$y) 41 | }) 42 | PSTypeName='PowerArcade.Point' 43 | } 44 | } 45 | } elseif ($x -ge 0 -and $y -ge 0) { 46 | [PSCustomObject]@{ 47 | X = $x 48 | Y = $y 49 | SpatialHash = 50 | $(if ($game.GetSpatialHash) { 51 | $game.GetSpatialHash($x,$y) 52 | }) 53 | PSTypeName='PowerArcade.Point' 54 | } 55 | } 56 | 57 | 58 | -------------------------------------------------------------------------------- /Types/PowerArcade.Sprite/Move.ps1: -------------------------------------------------------------------------------- 1 | param([Parameter(Mandatory)][int]$X,[Parameter(Mandatory)][int]$Y) 2 | $this | Move-Sprite -X $X -Y $Y 3 | return -------------------------------------------------------------------------------- /Types/PowerArcade.Sprite/get_Bounds.ps1: -------------------------------------------------------------------------------- 1 | $x = $this.X -as [int] 2 | $y = $this.Y -as [int] 3 | @(if ($this.Width -and $this.Height) { 4 | for ($oy = 0; $oy -lt $this.Height; $oy++) { 5 | for ($ox = 0; $ox -lt $this.Width; $ox++) { 6 | "$($x + $ox),$($y + $oy)" 7 | } 8 | } 9 | 10 | } elseif ($this.Content) 11 | { 12 | $cl = 13 | if ($this.ContentLength) { 14 | $this.ContentLength 15 | } else { 16 | $this.Content.ToString().Length 17 | } 18 | for ($ox =0; $ox -lt $cl; $ox++) { 19 | "$($x + $ox),$y" 20 | } 21 | } elseif ($x -ge 0 -and $y -ge 0) { 22 | "$x,$y" 23 | } 24 | ) 25 | -------------------------------------------------------------------------------- /Types/PowerArcade.Sprite/get_SpatialHash.ps1: -------------------------------------------------------------------------------- 1 | @(foreach ($xy in $this.MeasureBounds()) { 2 | $x, $y = "$xy".Split(',') 3 | "$( 4 | [int][Math]::Floor($x / ($game.Width / $game.CellWidth)) 5 | ),$( 6 | [int][Math]::Floor($y / ($game.Height / $game.CellHeight)) 7 | )" 8 | }) | Select-Object -Unique 9 | 10 | -------------------------------------------------------------------------------- /Watch-Game.ps1: -------------------------------------------------------------------------------- 1 | function Watch-Game 2 | { 3 | <# 4 | .Synopsis 5 | Watches the Game Loop 6 | .Description 7 | While the game is running, this function will: 8 | 9 | * Watch for keyboard input 10 | * Call any key handlers 11 | * Call any OnTick events found on the Game, CurrentLevel, or loaded sprites 12 | * Sleep until the next expected $game.Clock event 13 | .Notes 14 | Unless you are building a game, you won't run this command directly. 15 | This command will be run within Initialize-Game, which in turn is called 16 | .Example 17 | Initialize-Game -GamePath .\ROM\Nibbles2020 | Add-Member NoteProperty IsRunning $true -Force -PassThru | Watch-Game 18 | .Link 19 | Start-Game 20 | .Link 21 | Initialize-Game 22 | #> 23 | param( 24 | # The Game Object 25 | [Parameter(Mandatory,Position=0,ValueFromPipeline,ValueFromPipelineByPropertyName,ParameterSetName='Game')] 26 | [Parameter(Position=1,ParameterSetName='LevelPath')] 27 | [PSObject] 28 | $Game 29 | ) 30 | 31 | begin { 32 | $NotifyTick = { 33 | if ($game.OnTick.Invoke) { 34 | $game.OnTick() 35 | } 36 | 37 | if ($game.CurrentLevel.OnTick.Invoke) { 38 | $game.CurrentLevel.OnTick() 39 | } 40 | 41 | foreach ($gameSprite in @($game.CurrentLevel.Sprites)) { 42 | if ($gameSprite.OnTick.Invoke) { 43 | $gameSprite.OnTick() 44 | } 45 | } 46 | } 47 | 48 | } 49 | 50 | process { 51 | $lastTick = $null 52 | $game | Add-Member NoteProperty LoopCounter 0 -Force 53 | 54 | :GameLoop while ($game.IsRunning) { 55 | $game.LoopCounter++ 56 | & $NotifyTick 57 | $keyInput = @( 58 | $keySplat = @{} 59 | if ($Game.KeyHandlers) { 60 | $keySplat['OnKey'] = $Game.KeyHandlers 61 | } 62 | . Watch-Keyboard @keySplat 63 | ) 64 | if ($keyInput -and $keyInput.Key.VirtualKeyCode -ne 10 -and $keyInput.Key.VirtualKeyCode -ne 13) { 65 | $null = $null 66 | } 67 | $gameClock = $game.Clock -as [Timespan] 68 | if (-not $gameClock) { $gameClock = [Timespan]'00:00:00.02' } 69 | do { 70 | [Threading.Thread]::Sleep(1) 71 | # Start-Sleep -Milliseconds 1 72 | } while ($lastTick -and ([DateTime]::Now -lt $nextTick)) 73 | 74 | $lastTick = [DateTime]::Now 75 | $nextTick = $lastTick + $gameClock 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Watch-Keyboard.ps1: -------------------------------------------------------------------------------- 1 | function Watch-Keyboard 2 | { 3 | <# 4 | .Synopsis 5 | Watches for Keyboard Input 6 | .Description 7 | Watches for Keyboard Input without blocking the script. 8 | 9 | When input arrives, it can be caught be any handler in -OnKey, 10 | and the results of that handler will be returned. 11 | 12 | If no input handler catches the key, it will be returned from Watch-Keyboard. 13 | 14 | If no keys are currently pressed, nothing will be returned. 15 | .Example 16 | # Watches the keys until you hit CTRL+C 17 | do { Watch-Keyboard | Select-Object -ExpandProperty Key } while ($true) 18 | .Link 19 | Watch-Game 20 | #> 21 | [OutputType([PSObject])] 22 | param( 23 | # A dictionary of key handlers. 24 | # The key should be the name of a key, and the value should be a script block. 25 | [Parameter(ValueFromPipelineByPropertyName)] 26 | [Collections.IDictionary] 27 | $OnKey, 28 | 29 | # The read key options. 30 | [Parameter(ValueFromPipelineByPropertyName)] 31 | [Alias('ReadKeyOptions')] 32 | [Management.Automation.Host.ReadKeyOptions] 33 | $ReadKeyOption = 'NoEcho,IncludeKeyDown,IncludeKeyUp' 34 | ) 35 | 36 | process { 37 | :KeyLoop while ($Host.UI.RawUI.KeyAvailable) { 38 | #region Read the Keys 39 | $KeyRead = $Host.UI.RawUI.ReadKey($ReadKeyOption) 40 | $KeyReadAt = [DateTime]::Now 41 | if (-not $OnKey) { 42 | [PSCustomObject][Ordered]@{ 43 | PSTypeName='PowerArcade.Keypress' 44 | Key = $KeyRead 45 | TimeStamp = $KeyReadAt 46 | } 47 | continue 48 | } 49 | #endregion Read the Keys 50 | 51 | #region Deal with the Handlers 52 | :NextOnKey foreach ($kv in $(if ($OnKey) {$OnKey.GetEnumerator()})) { 53 | $key = "$($kv.Key)" 54 | if ($key.Contains('+')) 55 | { 56 | # check for modifiers, and continue if not found 57 | # if found, strip the modifiers from $key 58 | } 59 | $IsMatch = 60 | ($key -eq 'Any') -or 61 | ($key -eq 'All') -or 62 | ($key -eq 'left' -and $KeyRead.virtualkeyCode -eq 37) -or 63 | ($key -eq 'up' -and $KeyRead.virtualkeyCode -eq 38) -or 64 | ($key -eq 'right' -and $KeyRead.virtualkeyCode -eq 39) -or 65 | ($key -eq 'down' -and $KeyRead.virtualkeyCode -eq 40) -or 66 | ($key -eq 'space' -and $keyRead.virtualKeyCode -eq 32) -or 67 | (('esc', 'escape' -contains $key) -and $keyRead.virtualKeyCode -eq 27) -or 68 | (('enter', 'return' -contains $key) -and $keyRead.VirtualKeyCode -eq 13) -or 69 | (('back', 'backspace' -contains $key) -and $keyRead.VirtualKeyCode -eq 8) -or 70 | ($KeyRead.character -eq $key) 71 | 72 | 73 | if ($IsMatch) { 74 | if ($kv.Value -is [ScriptBlock]) { 75 | . $kv.Value $keyRead 76 | } 77 | break KeyLoop 78 | } 79 | } 80 | #endregion Deal with the Handlers 81 | 82 | [PSCustomObject][Ordered]@{ 83 | PSTypeName='PowerArcade.Keypress' 84 | Key = $KeyRead 85 | TimeStamp = $KeyReadAt 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /en-us/About_PowerArcade.help.txt: -------------------------------------------------------------------------------- 1 | # PowerArcade| Retro Gaming in PowerShell 2 | 3 | ## April 1, 2020: 4 | 5 | At Start-Automating, we believe PowerShell can power anything. 6 | PowerShell has helped almost every area of automation, and has been used to build Winforms, WPF, and web applications. 7 | Certain members of the PowerShell team made [legendary HTML5 prototypes](https://www.leeholmes.com/blog/2011/04/01/powershell-and-html5/). 8 | 9 | This year, we set out to build a Game Console and Development Kit in PowerShell 10 | 11 | 12 | ### Introducing PowerArcade 13 | 14 | 15 | 16 | PowerArcade is a Game Console for your System Console. 17 | 18 | You can quickly and easily build build cross-platform games and share them on the PowerShell Gallery. 19 | 20 | 21 | Compared to their old Console counterparts, PowerArcade is HD. Old Console games used a resolution of 80x50. 22 | PowerArcade can use any resolution your terminal can handle, which nowadays can be almost 200 characters wide! 23 | Plus we _could_ add a nicer rendering engine to it, if we ever get around to it. 24 | 25 | Like all Consoles, PowerArcade ships with a game. 26 | 27 | We decided to do a retro update of the classic Nibbles.bas. Check it out: 28 | 29 | ![Nibbles2020](Assets/Nibbles2020.png) 30 | ![Nibbles2020](Assets/Nibbles2020.1.gif) 31 | ![Nibbles2020](Assets/Nibbles2020.2.gif) 32 | ![Nibbles2020](Assets/Nibbles2020.3.gif) 33 | ![Nibbles2020EndlessMode](Assets/Nibbles2020.4.gif) 34 | 35 | ### Installing and Playing 36 | 37 | You can Install PowerArcade from the PowerShell Gallery: 38 | 39 | ~~~ 40 | Install-Module PowerArcade -Scope CurrentUser 41 | ~~~ 42 | 43 | 44 | Then you can start playing Nibbles right away with: 45 | 46 | 47 | ~~~ 48 | Start-Game Nibbles2020 49 | ~~~ 50 | 51 | Want more games? You can use Find-Game to find them and Install-Game to install them. 52 | 53 | ~~~ 54 | Find-Game # see what's out there 55 | ~~~ 56 | 57 | You can pipe Find-Game to Install-Game, then just Start-Game 58 | ~~~ 59 | Find-Game Blackjack | Install-Game 60 | Start-Game Blackjack 61 | ~~~ 62 | ![Blackjack](Assets/Blackjack.png) 63 | 64 | ### How does it work? 65 | 66 | Nibbles.bas is a fitting choice, as Nibbles was a demostration of building games in QBASIC. 67 | While PowerShell is not a direct ancestor of QBASIC, it is a successor to Visual Basic. 68 | A few attempted to create games using Visual Basic, but [no developers were known to survive the process](https://youtu.be/WGqD-J_pRvs). 69 | 70 | Anyhow, developing games in PowerArcade is considerably less likely to be lethal. 71 | 72 | Let's take a look at how Nibbles works. Here's it's file tree (courtesy of [EZOut](https://github.com/StartAutomating/EZOut)): 73 | 74 | ~~~ 75 | ├──Game 76 | ├──Game.ps1 77 | ├──Game.psd1 78 | ├──OnKey_Down.ps1 79 | ├──OnKey_Esc.ps1 80 | ├──OnKey_Left.ps1 81 | ├──OnKey_P.ps1 82 | ├──OnKey_Right.ps1 83 | ├──OnKey_Up.ps1 84 | ├──Over.ps1 85 | ├──Levels 86 | ├──Levels.ps1 87 | ├──1 88 | ├──1.ps1 89 | ├──2 90 | ├──2.ps1 91 | ├──3 92 | ├──3.ps1 93 | ├──4 94 | ├──4.ps1 95 | ├──5 96 | ├──5.ps1 97 | ├──6 98 | ├──6.ps1 99 | ├──7 100 | ├──7.ps1 101 | ├──8 102 | ├──8.ps1 103 | ├──9 104 | ├──9.ps1 105 | ├──GameOver 106 | ├──GameOver.ps1 107 | ├──OnKey_All.ps1 108 | ├──Menu 109 | ├──Menu.ps1 110 | ├──OnKey_All.ps1 111 | ├──Pause 112 | ├──Pause.ps1 113 | ├──Sprites 114 | ├──Number 115 | ├──Number.psd1 116 | ├──Snake 117 | ├──+.ps1 118 | ├──+Number.ps1 119 | ├──+Wall,Tail,Snake.ps1 120 | ├──Dies.ps1 121 | ├──OnTick.ps1 122 | ├──Snake.psd1 123 | ├──SwitchDirection.ps1 124 | ├──Wall 125 | ├──Wall.psd1 126 | ├──Nibbles2020.psd1 127 | ~~~ 128 | 129 | 130 | A game is made of a module and up to three subdirectories: 131 | 132 | #### The Game Directory 133 | 134 | The Game directory contains a Game.psd1 which has initial settings for the game. 135 | 136 | A Game.ps1 file, if found, will be run when the game starts to initialize the game. 137 | 138 | Any other .ps1 files become methods of the Game, which can be accessed with running in a global variable called $game (duh). 139 | 140 | Any methods named On* denote an event handler. OnTick will be called when the game clock ticks. 141 | Files named OnKey_KeyName.ps1 will handle specific key presses. 142 | 143 | #### The Levels directory 144 | 145 | The Levels directory defines game levels. Pretty easy to navigate, right? 146 | 147 | Each named subdirectory is the name of a level. A script sharing the directory name will initialize the level. 148 | 149 | These named subdirectories work like the game directory, and can also handle keys. 150 | 151 | If there is Levels.ps1 beneath levels, it will be called before the level initializes. 152 | 153 | #### The Sprites directory 154 | 155 | Sprites are magical game creatures that move about in the imaginary world of the game. 156 | 157 | Just like each Levels directory, a Sprite can have an initializer (e.g. Unicorn\Unicorn.ps1) 158 | and can also have default properties (e.g. Unicorn\Unicorn.psd1) 159 | 160 | Sprites cannot respond to key presses, but they can respond to game ticks. 161 | 162 | Additionally, Sprites can have interaction methods. These describe what happens with two Sprites meet. 163 | 164 | In Nibbles, Sprites\Snake\+Number.ps1 descibes what happens when a snake hits a number, 165 | and \Sprites\Snake\+Wall,Tail,Snake.ps1 describes what happens when a snake hits a wall, tail, or a snake. 166 | 167 | That's about it. Inside your methods you can do whatever your game logic needs, 168 | and you can use Add-Sprite, Find-Sprite, Move-Sprite, and Remove-Sprite to control the sprites on the screen. 169 | 170 | 171 | ### Playing around and contributing 172 | 173 | This is mostly in good fun, but feedback, contributions, and game submissions are welcome. 174 | --------------------------------------------------------------------------------