├── docs └── PowerSneks.gif ├── PowerSneks_GameSettings.ps1 ├── LICENSE ├── PowerSneks_BaseObjects.ps1 ├── Sockets.ps1 ├── ToBots.ps1 ├── entities ├── 3_GoldenSnitch.ps1 ├── 3_Bot.ps1 ├── 2_Snake.ps1 └── 1_Game.ps1 ├── PowerSneks.ps1 ├── README.md └── PowerSneks_Engine.ps1 /docs/PowerSneks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avdaredevil/PowerSneks/HEAD/docs/PowerSneks.gif -------------------------------------------------------------------------------- /PowerSneks_GameSettings.ps1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avdaredevil/PowerSneks/HEAD/PowerSneks_GameSettings.ps1 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 avdaredevil 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 | -------------------------------------------------------------------------------- /PowerSneks_BaseObjects.ps1: -------------------------------------------------------------------------------- 1 | #= GLOBAL-OBJECTS =============================================================================| 2 | enum Direction {Left;Up;Right;Down} 3 | enum LifeState {Alive;Dead} 4 | enum GameMode {SinglePlayer;MultiPlayer} 5 | 6 | #= INTERFACES =================================================================================| 7 | class Serializeable { 8 | [object]serializeState() {return @{}} 9 | deSerializeState([Object]$State) { 10 | $State.psobject.Properties | % { 11 | try { 12 | # Write-Host Importing $_.Name = ($_.Value | Out-String) 13 | $this.($_.Name) = $_.Value 14 | } catch {<#Write-Host Error $_#>} 15 | } 16 | if (!($State.Head -is [string])) {return} 17 | $this.Head = $State.Head.split(' ') 18 | } 19 | } 20 | class LiveObject : Serializeable { 21 | [int]$Id = -1 22 | [int]$Size = 1 23 | [int]$Score = 0 24 | [int[]]$Head = @(0,0) 25 | [LifeState]$State = [LifeState]::Alive 26 | 27 | 28 | tick() {throw 'Implement'} 29 | die() {$this.State = [LifeState]::Dead} 30 | draw() {$this.draw($false)} 31 | draw([bool]$Spawn) {throw 'Implement'} 32 | respawn() {$this.draw($true)} 33 | incrementScore() {$this.Score += $(gv GameKnobs | % Value).ScoreIncr} 34 | [bool] isAlive() {return $this.State -eq [LifeState]::Alive} 35 | } 36 | class Player : LiveObject { 37 | [bool]$canCollide = $true 38 | 39 | erase() {throw 'Implement'} 40 | checkSelfCollision() {throw 'Implement'} 41 | collided([string]$Reason) {throw 'Implement'} 42 | [bool]checkForCollision([int[]]$Coords) {return $this.checkForCollision(@($Coords))} 43 | [bool]checkForCollision([int[][]]$coords) {throw 'Implement'} 44 | [int[][]]getCollideablePoints() {throw 'Implement'} 45 | } 46 | -------------------------------------------------------------------------------- /Sockets.ps1: -------------------------------------------------------------------------------- 1 | function Host-Server { 2 | param( 3 | [String]$Server = 'localhost', 4 | [Int]$Port = '7777', 5 | [ScriptBlock]$OnData = {}, 6 | [ScriptBlock]$OnChunk = {}, 7 | [ScriptBlock]$OnDisconnect = {} 8 | ) 9 | 10 | $tcpConnection = New-Object System.Net.Sockets.TcpClient($Server, $Port) 11 | $tcpStream = $tcpConnection.GetStream() 12 | $reader = New-Object System.IO.StreamReader($tcpStream) 13 | $writer = New-Object System.IO.StreamWriter($tcpStream) 14 | $writer.AutoFlush = $true 15 | 16 | while ($tcpConnection.Connected) { 17 | [String[]]$Chunk = @() 18 | while ($tcpStream.DataAvailable) { 19 | $Chunk += $data = $reader.ReadLine() 20 | $OnData.invoke($data) 21 | } 22 | $OnChunk.invoke($Chunk) 23 | } 24 | $OnDisconnect.invoke() 25 | 26 | $reader.Close() 27 | $writer.Close() 28 | $tcpConnection.Close() 29 | } 30 | 31 | function Client-Server { 32 | param ( 33 | [String]$Server = 'localhost', 34 | [Int]$Port = '7777', 35 | [ScriptBlock]$OnData = {}, 36 | [ScriptBlock]$OnChunk = {}, 37 | [ScriptBlock]$OnDisconnect = {} 38 | ) 39 | [System.Net.Sockets.TcpClient] $tcpClient = [System.Net.Sockets.TcpClient]::new("localhost", "50001") 40 | 41 | $tcpStream = $tcpClient.GetStream() 42 | [System.IO.StreamReader] $reader = [System.IO.StreamReader]::new($tcpStream) 43 | [System.IO.StreamWriter] $writer = [System.IO.StreamWriter]::new($tcpStream) 44 | $writer.AutoFlush = $true 45 | 46 | return [PSCustomObject]@{ 47 | Listen = Value 48 | } 49 | while ($tcpClient.Connected) { 50 | [String[]]$Chunk = @() 51 | while ($tcpStream.DataAvailable) { 52 | $Chunk += $data = $reader.ReadLine() 53 | $OnData.invoke($data) 54 | } 55 | $OnChunk.invoke($Chunk) 56 | } 57 | $OnDisconnect.invoke() 58 | $reader.Close() 59 | $writer.Close() 60 | $tcpConnection.Close() 61 | } 62 | -------------------------------------------------------------------------------- /ToBots.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | |===============================================================>| 3 | AP-Snakes 2 [PowerSneks] by APoorv Verma [AP] on 10/25/2019 4 | |===============================================================>| 5 | $) Color Scheming 1 6 | $) Level Making 0 * 2 7 | $) MAP Saving and Loading 3 8 | $) Obstruction Creation 9 | $) Laser Beams To cut through walls 10 | $) Warping of Obstructions and Snake 11 | $) Game-Console with Laser and Score count 12 | $) CPU cycle and sleep time adjustment as game progresses 13 | $) Snake Grows upon eating food 14 | $) Restore Console Properties upon Close 15 | $) OO Design 16 | $) Multiobject support / Multiplayer! 17 | |===============================================================>| 18 | #> 19 | param( 20 | [int]$Snakes=1, 21 | [ValidatePattern("[A-z]?:?.?\\.*\..*|\/\*\\")][String]$MapFile='/*\', 22 | [Switch]$ShowPlayerLabels, 23 | [Switch]$LoadDefaultSave, 24 | [Switch]$Debug 25 | ) 26 | . $PSScriptRoot\PowerSneks_BaseObjects.ps1 27 | . $PSScriptRoot\PowerSneks_GameSettings.ps1 28 | . $PSScriptRoot\PowerSneks_Engine.ps1 29 | . $PSScriptRoot\BotClass.ps1 30 | 31 | $SN = $Snakes 32 | $Snakes = 0 33 | #= RUNTIME ====================================================================================| 34 | Start-Game {param($Game, $Tick) 35 | try { 36 | if ($Tick -eq 1) { 37 | if ($Game.Players.Length) { 38 | $bt = $Game.Players | % {$_.SerializeState()} 39 | $Game.Players.clear() 40 | foreach ($e in $bt) { 41 | $b = [SnakeBot]::new($c, $Game) 42 | $Game.attachPlayer($b) 43 | $b.DeserializeState(($e | ConvertTo-Json | ConvertFrom-Json)) 44 | $b.respawn() 45 | } 46 | return 47 | } 48 | 1..$SN | % { 49 | $c = Get-PlayerSpawn $Game 50 | $Game.attachPlayer([SnakeBot]::new($c, $Game)) 51 | } 52 | } 53 | if ($Game.isMultiPlayer() -and ($Tick -lt 100 -and !($Tick % 5)) -or !($Tick % 100)) { 54 | $Game.Players | % {$_.ScanForFoodCoords()} 55 | } 56 | } catch {Write-Host "BOT CRASHED, $_"} 57 | } -------------------------------------------------------------------------------- /entities/3_GoldenSnitch.ps1: -------------------------------------------------------------------------------- 1 | # using module ../PowerSneks_BaseObjects.ps1 2 | # using module ./1_Game.ps1 3 | 4 | class GoldenSnitch : LiveObject { 5 | [int]$Size = 2 6 | [String]$Class = 'GoldenSnitch' 7 | [Direction]$Direction 8 | [int]$Life 9 | [int]$Points 10 | [int[]]$OldHead 11 | [int[]]$Head 12 | [Game]$Game 13 | 14 | GoldenSnitch([int[]]$Coords, [Game]$Game) { 15 | $this.Direction = Get-Random -min 0 -max 4 16 | $this.Points = Get-Random -min 1 -max 5 17 | $this.Life = Get-Random -min 20 -max 130 18 | $this.Game = $Game 19 | $this.Head = $Coords 20 | } 21 | [object] serializeState() { 22 | return @{ 23 | Class = $this.Class 24 | Id = $this.Id 25 | Life = $this.Life 26 | Points = $this.Points 27 | Direction = $this.Direction 28 | Head = [int[]]$this.Head 29 | } 30 | } 31 | draw([bool]$Spawn) { 32 | $x, $y = $this.OldHead 33 | $this.Game.RenderPoint($x, $y) 34 | ($x-1),($x+1) | % { 35 | $nx, $y = [Game]::WarpCoords($_, $y) 36 | $this.Game.RenderPoint($nx, $y) 37 | } 38 | $x, $y = $this.Head 39 | $ch = '*' 40 | Write-ToPos $CH $x $y -fgc $( 41 | if($this.life -gt 90){'darkgreen'}elseif($this.life -gt 50){'green'}elseif($this.life -gt 25){'Yellow'}else{'Red'} 42 | ) 43 | ($x-1),($x+1) | % { 44 | $nx, $y = [Game]::WarpCoords($_, $y) 45 | Write-ToPos '~' $nx $y -fgc 'Blue' 46 | } 47 | } 48 | incrementScore() {} 49 | checkSelfCollision() {} 50 | [bool]checkForCollision([int[][]]$Coords) { 51 | foreach ($c in $Coords) { 52 | if ([Game]::CoordsDistance($c, $this.Head) -gt 1) {continue} 53 | return $True 54 | } 55 | return $False 56 | } 57 | [int[][]]getCollideablePoints() { 58 | return [int[][]]@(,$this.head) 59 | } 60 | turn([Direction]$Dir) { 61 | if ($this.BodyDirection -eq (($Dir+2)%4)) {return} # Cannot move into body 62 | $this.Direction = $Dir 63 | } 64 | tick() { 65 | if ($this.Life-- -le 0) {$this.die();$this.erase();return} 66 | $x, $y = $this.OldHead = $this.Head 67 | $this.Head = [Game]::WarpCoords((Modify-Coord $x $y $this.Direction)) 68 | } 69 | collided([Object]$Collider) { 70 | $this.die() 71 | $this.erase() 72 | 1..$this.Points | % { 73 | $Collider.incrementScore() 74 | $Collider.Game.incrementScore() 75 | } 76 | $Collider.draw($true) 77 | $Collider.Game.ScoreBoard.Write("You caught a snitch worth $($this.points) points") 78 | } 79 | erase() { 80 | $x,$y = $this.Head 81 | $this.Game.RenderPoint($x, $y) 82 | $this.Game.RenderPoint(([Game]::WarpCoords(($x-1), $y))) 83 | $this.Game.RenderPoint(([Game]::WarpCoords(($x+1), $y))) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /PowerSneks.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | |===============================================================>| 3 | AP-Snakes 2 [PowerSneks] by APoorv Verma [AP] on 10/25/2019 4 | |===============================================================>| 5 | $) Color Scheming 1 6 | $) Level Making 0 * 2 7 | $) MAP Saving and Loading 3 8 | $) Obstruction Creation 9 | $) Laser Beams To cut through walls 10 | $) Warping of Obstructions and Snake 11 | $) Game-Console with Laser and Score count 12 | $) CPU cycle and sleep time adjustment as game progresses 13 | $) Snake Grows upon eating food 14 | $) Restore Console Properties upon Close 15 | $) Bots to play against 16 | $) Golden Snitch to chase for extra points (and less walls) 17 | $) OO Design 18 | $) Multiobject support / Multiplayer! 19 | |===============================================================>| 20 | #> 21 | param( 22 | [int]$Snakes=1, 23 | [int]$Bots=0, 24 | [ValidatePattern("[A-z]?:?.?\\.*\..*|\/\*\\")][String]$MapFile='/*\', 25 | [Switch]$LoadDefaultSave, 26 | [Switch]$Debug, 27 | [Switch]$ShowPlayerLabels 28 | ) 29 | # =======================================START=OF=COMPILER==========================================================| 30 | # The Following Code was added by AP-Compiler 1.6 (APC: 1.2) To Make this program independent of AP-Core Engine 31 | # GitHub: https://github.com/avdaredevil/AP-Compiler 32 | # ==================================================================================================================| 33 | $Script:PSHell=$(if($PSHell){$PSHell}elseif($PSScriptRoot){$PSScriptRoot}else{"."}); 34 | $Script:AP_Console = @{version=[version]'1.2'; isShim = $true} 35 | function B64 {param([Parameter(ValueFromPipeline=$true)][String]$Text, [ValidateSet("UTF8","Unicode")][String]$Encoding = "UTF8") [System.Text.Encoding]::$Encoding.GetString([System.Convert]::FromBase64String($Text))} 36 | # This syntax is to prevent AV's from misclassifying this as anything but innocuous 37 | & (Get-Alias iex) (B64 "ZnVuY3Rpb24gR2V0LVBvd2VyU2hlbGxQcm9jZXNzUGF0aCB7DQogICAgaWYgKCRQU0NvbW1hbmRQYXRoKSB7DQogICAgICAgICMgSWYgcnVubmluZyBpbiBhIHNjcmlwdA0KICAgICAgICBpZiAoJFBTRWRpdGlvbiAtZXEgJ0NvcmUnKSB7DQogICAgICAgICAgICBHZXQtQ29tbWFuZCBwd3NoIHwgU2VsZWN0LU9iamVjdCAtRXhwYW5kUHJvcGVydHkgU291cmNlDQogICAgICAgIH0gZWxzZSB7DQogICAgICAgICAgICBHZXQtQ29tbWFuZCBwb3dlcnNoZWxsIHwgU2VsZWN0LU9iamVjdCAtRXhwYW5kUHJvcGVydHkgU291cmNlDQogICAgICAgIH0NCiAgICB9IGVsc2VpZiAoJGhvc3QuVmVyc2lvbi5NYWpvciAtZ2UgNikgew0KICAgICAgICAjIFBvd2VyU2hlbGwgNisgKENvcmUpDQogICAgICAgICRQU0hvbWUgKyAnL3B3c2gnICsgKCcuZXhlJywgJycpWyRJc0xpbnV4IC1vciAkSXNNYWNPU10NCiAgICB9IGVsc2Ugew0KICAgICAgICAjIFdpbmRvd3MgUG93ZXJTaGVsbA0KICAgICAgICAiJFBTSG9tZVxwb3dlcnNoZWxsLmV4ZSINCiAgICB9DQp9Cg==") 38 | # ========================================END=OF=COMPILER===========================================================| 39 | function bool($a) {if($a){"`$true"}else{"`$false"}} 40 | if ($args[0] -ne "in-frame") { 41 | return & (Get-PowerShellProcessPath) -ep bypass -noprofile $PSCommandPath -Snakes $Snakes -Bots $Bots -MapFile """$MapFile""" -LoadDefaultSave:$(bool($LoadDefaultSave)) -Debug:$(bool($Debug)) -ShowPlayerLabels:$(bool($ShowPlayerLabels)) 'in-frame' 42 | } 43 | . $PSScriptRoot\PowerSneks_BaseObjects.ps1 44 | . $PSScriptRoot\PowerSneks_GameSettings.ps1 45 | . $PSScriptRoot\PowerSneks_Engine.ps1 46 | #= RUNTIME ====================================================================================| 47 | Start-Game 48 | -------------------------------------------------------------------------------- /entities/3_Bot.ps1: -------------------------------------------------------------------------------- 1 | # using module ../PowerSneks_BaseObjects.ps1 2 | # using module ./1_Game.ps1 3 | # using module ./2_Snake.ps1 4 | 5 | class SnakeBot : Snake { 6 | [int]$ScanFreshness = 0 7 | [int[][]]$FoodCoords = $null 8 | [String]$Class = 'SnakeBot' 9 | SnakeBot([int[]]$Coords, [Game]$Game) : base($Coords, $Game) {} 10 | incrementScore(){ 11 | ([Snake]$this).incrementScore() 12 | $this.ScanForFoodCoords() 13 | } 14 | ScanForFoodCoords() { 15 | $this.ScanFreshness = 10 # How many ticks should I wait before considering this target expired 16 | $G = $this.Game.Grid 17 | $Food = $(gv SymbolMap | % Value).Food 18 | $this.FoodCoords = @() 19 | 0..($G.length-1) | % { 20 | $y = $_ 21 | 0..($G[$y].length-1) | % { 22 | $x = $_ 23 | if ($G[$y][$x] -eq $Food) { 24 | $this.FoodCoords += ,@($x, $y) 25 | } 26 | } 27 | } 28 | } 29 | [int[]] closestFood() { 30 | if (!$this.FoodCoords -or !$this.FoodCoords.length) {$this.ScanForFoodCoords()} 31 | $c = $this.FoodCoords | % { 32 | $x, $y = $_ 33 | [PSCustomObject]@{x=$x;y=$y;d=([Game]::CoordsDistance($_, $this.Head))} 34 | } | sort d | select -f 1 | % {$_.x,$_.y} 35 | return $c 36 | } 37 | [boolean]NavigateToFood([Direction[]]$GoodDir) { 38 | $tryTurn = { 39 | param($d) 40 | if ($d -in $GoodDir) {$this.turn($d);return $true} 41 | return $false 42 | } 43 | $cdir = $this.BodyDirection 44 | $x, $y = $this.Head 45 | $fx, $fy = $this.closestFood() 46 | # Write-ToPos $(gv CharMap | % Value).Symbol.Food -y $fy -x $fx -fgc $(gv ColorMap | % Value).Food -bgc ($this.Id+1) 47 | # Place-BufferedContent ("X: {0,3} -> {1,3}" -f ($x, $fx)) 2 2 48 | # Place-BufferedContent ("Y: {0,3} -> {1,3}" -f ($y, $fy)) 2 3 49 | if (($cdir -eq [Direction]::Left -or $cdir -eq [Direction]::Right) -and $fy -ne $y) { 50 | return $tryTurn.invoke($(if ($fy -gt $y) {[Direction]::Down} else {[Direction]::Up})) 51 | } 52 | if ($fx -eq $x) {return $true} 53 | return $tryTurn.invoke($(if ($fx -gt $x) {[Direction]::Right} else {[Direction]::Left})) 54 | } 55 | tick() { 56 | $G = $this.Game 57 | [Direction[]]$GoodDir = @() 58 | 59 | if (--$this.ScanFreshness -le 0) {$this.ScanForFoodCoords()} 60 | 61 | :A do { 62 | $BodyHash = @{} 63 | foreach ($c in $this.Body) { 64 | $BodyHash."$c" = 1 65 | } 66 | foreach ($dir in (0..3)) { 67 | if ($this.size -gt 1 -and $dir -eq [int](($this.BodyDirection+2)%4)) {continue} 68 | $x, $y = [Game]::WarpCoords((Modify-Coord $this.Head[0] $this.Head[1] $dir)) 69 | if ($G.Grid[$y][$x] -eq $(gv SymbolMap | % Value).Food) {$this.turn($dir);break A} 70 | if ($G.Grid[$y][$x] -eq $(gv SymbolMap | % Value).Space -and !$BodyHash."$x $y") { 71 | $GoodDir += $dir 72 | } 73 | } 74 | 75 | # If we continue 76 | $x, $y = [Game]::WarpCoords((Modify-Coord $this.Head[0] $this.Head[1] $this.BodyDirection)) 77 | if ($G.Grid[$y][$x] -ne $(gv SymbolMap | % Value).Wall) {if ($this.NavigateToFood($GoodDir)) {break A}} 78 | 79 | # We need to turn 80 | if (!$GoodDir.length) {$G.shootLaser($this);break} # No option but to laser 81 | $this.turn($GoodDir[(Get-Random $GoodDir.length)]) # Turn to any valid direction 82 | } while (0) 83 | ([Snake]$this).tick() 84 | } 85 | } -------------------------------------------------------------------------------- /entities/2_Snake.ps1: -------------------------------------------------------------------------------- 1 | # using module ../PowerSneks_BaseObjects.ps1 2 | # using module ./1_Game.ps1 3 | 4 | class Snake : Player { 5 | [String]$Class = 'Snake' 6 | [Int]$Size = 1 7 | [Int]$Consumption = 0 8 | [Int]$Lasers = $(gv GameKnobs | % Value).Laser.InitialCount 9 | [Int]$GrowFactor = $(gv GameKnobs | % Value).SnakeStartSz 10 | [Direction]$Direction = [Direction]::Up 11 | [Direction]$BodyDirection = [Direction]::Up 12 | [Collections.Queue]$Body = [Collections.Queue]::new() 13 | [Collections.Queue]$ErasePts = [Collections.Queue]::new() 14 | [Game]$Game 15 | 16 | Snake([int[]]$Coords, [Game]$Game) { 17 | $this.Head = $Coords 18 | $this.Game = $Game 19 | } 20 | [object] serializeState() { 21 | return @{ 22 | Class = $this.Class 23 | Id = $this.Id 24 | Size = 1 25 | Score = $this.Score 26 | Lasers = $this.Lasers 27 | Consumption = $this.Consumption 28 | GrowFactor = $this.GrowFactor + @($this.Body).length 29 | Direction = $this.Direction 30 | BodyDirection = $this.BodyDirection 31 | Head = [int[]]$this.Head 32 | } 33 | } 34 | draw([bool]$Spawn) { 35 | $y = -1 36 | while ($this.ErasePts.Count) { 37 | if ($y -eq -1) {$y = $this.ErasePts.Dequeue()} 38 | $this.Game.RenderPoint($this.ErasePts.Dequeue(), $y) 39 | } 40 | $x, $y = $this.Head 41 | $ch = if ($this.Game.isMultiPlayer() -and $(gv ShowPlayerLabels | % Value)) {$this.ID} else {$(gv CharMap | % Value).SnakeMoves[$this.Direction]} 42 | Write-ToPos $CH $x $y -fgc $(gv ColorMap | % Value).Snake.Head 43 | if ($Spawn) { 44 | $this.Body.GetEnumerator() | % { 45 | $x, $y = $_ 46 | Write-ToPos $(gv CharMap | % Value).SnakeMoves[$this.BodyDirection] $x $y -fgc $(gv ColorMap | % Value).Snake.Body 47 | } 48 | } else { 49 | $x, $y = @($this.Body)[-1] 50 | Write-ToPos $(gv CharMap | % Value).SnakeMoves[$this.BodyDirection] $x $y -fgc $(gv ColorMap | % Value).Snake.Body 51 | } 52 | if ($Spawn) {return} 53 | if ($this.GrowFactor) {$this.Size++;$this.GrowFactor--} 54 | else { 55 | $x, $y = $this.Body.Dequeue() 56 | $this.Game.RenderPoint($x, $y) 57 | } 58 | $this.checkSelfCollision() 59 | $this.Game.checkCollision($this, $this.Head) 60 | } 61 | respawn() { 62 | $this.erase() 63 | $this.GrowFactor += $this.size - 1 64 | $this.size = 1 65 | $this.Body.Clear() 66 | ([LiveObject]$this).respawn() 67 | $this.alert() 68 | } 69 | alert() { 70 | $this.ErasePts.Enqueue($this.Head[1]) 71 | ($this.Head[0]-2)..($this.Head[0]+2) | % { 72 | if ($_ -eq $This.Head[0]) {return} 73 | $nx, $ny = [Game]::WarpCoords($_, $this.Head[1]) 74 | Write-ToPos '!' $nx $ny -fgc 'Yellow' 75 | $this.ErasePts.Enqueue($nx) 76 | } 77 | } 78 | incrementScore() { 79 | ([LiveObject]$this).incrementScore() 80 | $this.GrowFactor++ 81 | $this.Consumption++ 82 | if (!($this.Consumption % $(gv GameKnobs | % Value).Laser.AquireCost)) { 83 | $this.Lasers++ 84 | } 85 | } 86 | checkSelfCollision() { 87 | foreach ($p in $this.Body) { 88 | if (![Game]::CoordsEqual($this.Head, $p)) {continue} 89 | $this.collided() 90 | return 91 | } 92 | } 93 | [bool]checkForCollision([int[][]]$Coords) { 94 | $Hash = @{} 95 | foreach ($c in $this.Body) { 96 | $Hash."$c" = 1 97 | } 98 | foreach ($c in $Coords) { 99 | if ($Hash."$c") {return $true} 100 | } 101 | return $false 102 | } 103 | [int[][]]getCollideablePoints() { 104 | return [int[][]]@(,$this.head) 105 | } 106 | turn([Direction]$Dir) { 107 | if ($this.size -gt 1 -and $this.BodyDirection -eq (($Dir+2)%4)) {return} # Cannot move into body 108 | $this.Direction = $Dir 109 | } 110 | tick() { 111 | $x, $y = $this.Head 112 | $this.Body.Enqueue(($x, $y)) 113 | $this.Head = [Game]::WarpCoords((Modify-Coord $x $y $this.Direction)) 114 | $this.BodyDirection = $this.Direction 115 | } 116 | collided() {$this.collided('Snake bit itself!')} 117 | collided([String]$Reason) { 118 | $this.die() 119 | $this.Game.ScoreBoard.write($Reason) 120 | } 121 | erase() { 122 | $this.Body.ToArray() | % { 123 | $x, $y = $_ 124 | $this.Game.RenderPoint($x, $y) 125 | } 126 | $this.Game.RenderPoint($this.Head) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerSneks 2 | 3 | > Snake game written in PowerShell which uses any windows console (`cmd.exe`, `powershell.exe`, even VSCode!) and draws out a custom native code based game. Useful and fun game for aspiring programmers and techies. Features: 4 | 5 | - Robust Game engine: 6 | - Capable of handling multiple snakes / objects 7 | - Mod capabilities by extending engine and providing your own Game Hook / Classes (as long as you implement the appropriate interfaces) 8 | - Saving / Restoring complete state (Snakes, directions, objects, Game Grid, Tick Speed, etc) 9 | - Level Mechanics: 10 | - Obstruction creation 11 | - Laser beams 12 | - Moving snitches (with random point values) 13 | - Customization Segment: 14 | - Allows you to Customize `$Script:GameKnobs` to adjust everything from Sleep Times, Obstruction sizes, Render sizes, Colors, Symbols, SaveMaps, Laser Logic, etc 15 | 16 | ## Screenshots 17 | 18 | Standard Single Player | Multiplayer (w/ Debug Console) | Scoreboard during gameplay 19 | --- | --- | --- 20 | ![Standard Single Player](https://user-images.githubusercontent.com/5303018/67819656-4fe37300-fa73-11e9-9539-bd7cf05c1f5a.png) | ![Multiplayer (w/ Debug Console)](https://user-images.githubusercontent.com/5303018/67819723-9d5fe000-fa73-11e9-8ade-a39dd99e91b4.png) | ![PowerSneks Scoreboard](https://user-images.githubusercontent.com/5303018/69590755-59de9000-0fa5-11ea-8393-259a8b1d06fb.png) 21 | 22 | Gameplay Gif | 23 | --- | 24 | ![PowerSneks Gameplay](./docs/PowerSneks.gif) 25 | 26 | ## Usage 27 | 28 | ```PowerShell 29 | .\PowerSneks.ps1 [[-Snakes] ] [[-Bots] ] [[-MapFile] ] [-LoadDefaultSave] [-ShowPlayerLabels] [-Debug] 30 | ``` 31 | 32 | - Will draw a game to match the dimensions of the console window 33 | - `Snake` is the number of human players (controls cut off after 2 players), `Bots` is the number of bots in the game 34 | - `Debug` mode is for debugging to see snake positions, object distances, active render, current block, tick 35 | - `LoadDefaultSave` loads game from a previous save [File: `%appdata%\AP-PowerSneks.Map.Save`] 36 | - `MapFile` is the save-file path you want to load the game from 37 | - *__Note__: If you've never run PowerShell Scripts before refer to __Setup PowerShell Section__ Below* 38 | 39 | ## Features 40 | 41 | - In house Game Engine, and View overlay + Grid System 42 | - Color Scheming 43 | - Level Making 44 | - MAP Saving and Loading 45 | - Obstruction Creation 46 | - Laser Beams To cut through walls 47 | - Warping of Obstructions and Snake 48 | - Game-Console with Laser and Score count 49 | - Debug console like Minecraft for the game Engine 50 | - CPU cycle and sleep time adjustment as game progresses 51 | - Bots to play against 52 | - Golden Snitch to chase for extra points (and less walls) 53 | 54 | ## Commands 55 | 56 | Key Code | Usage 57 | ---------------- | ----- 58 | `q`, `x`, `ESC` | Quit Game 59 | `←` , `→` , `↑` , `↓` | Move Snake (left/right/up/down) (Player 1) 60 | `A` , `D` , `W` , `S` | Move Snake (left/right/up/down) (Player 2) 61 | `J` , `L` , `I` , `K` | Move Snake (left/right/up/down) (Player 3) 62 | `num4`, `num6`, `num8`, `num5` | Move Snake (left/right/up/down) (Player 4) 63 | `end`, `r-ctrl`, `🔙` | Laser Beam (Player 1) 64 | `l-ctrl`, `c` | Laser Beam (Player 2) 65 | `space`, `{` | Laser Beam (Player 3) 66 | `0`, `num-enter` | Laser Beam (Player 4) 67 | `+` | Speed Up Game [*Increases Tick speed*] 68 | `-` | Slow Down Game [*Increases Tick speed*] 69 | `p` | Pause Game 70 | `f12` | Full Screen 71 | `Tab` | Save Current Game State 72 | `f5` | Refresh view [*fast*] 73 | ``` ` ``` | Enable dev console on game (also enable-able via the `-debug` cmdline flag) 74 | `.` | Add more food to the game [__Cheat Code__] 75 | 76 | ## Files 77 | 78 | #### Game Launchers 79 | 80 | File | Description 81 | --- | --- 82 | `PowerSneks.ps1` | Main Game launcher, use this to start the game 83 | `ToBots.ps1` | Use a saved game and convert all players to bots (*best use for this file, use `-LoadDefaultSave`*) 84 | 85 | #### Base Files 86 | 87 | File | Description 88 | --- | --- 89 | `PowerSneks_BaseObjects.ps1` | Contains enums and base object definitions that the game engine relies on 90 | `PowerSneks_GameSettings.ps1` | Contains all the tinkerable variables that can be modified before launching the game engine 91 | `PowerSneks_Engine.ps1` | Loads all entities mentioned below, and exposes functions that are needed to draw or start the game 92 | `Sockets.ps1` | *__TODO__: Add LAN based multiplayer support for the game* 93 | 94 | #### Game Entity Files 95 | 96 | *The Number before the file name is the order in which the files are loaded into the game engine (since they depend on each other)* 97 | 98 | File | Description 99 | --- | --- 100 | `entities/1_Game.ps1` | Contains the `Game` and `Scoreboard` class definitions 101 | `entities/2_Snake.ps1` | Contains the `Snake` class definition 102 | `entities/3_Bot.ps1` | Contains the `SnakeBot` class definition which can kinda play the game (super basic AI) 103 | `entities/3_GoldenSnitch.ps1` | Contains the `GoldenSnitch`, which is a special moving object that gives extra points (without creating walls) 104 | 105 | ## Set Up PowerShell [If you've never run a script in PowerShell] 106 | 107 | - Open PowerShell with Admin Access 108 | - Run `Set-ExecutionPolicy Bypass` 109 | - This allows scripts to be run in PowerShell 110 | - cd to the *Folder* where you downloaded/cloned [PowerSneks.ps1](PowerSneks.ps1) 111 | - `./PowerSneks.ps1`*``* 112 | -------------------------------------------------------------------------------- /PowerSneks_Engine.ps1: -------------------------------------------------------------------------------- 1 | param() 2 | # =======================================START=OF=COMPILER==========================================================| 3 | # The Following Code was added by AP-Compiler 1.6 (APC: 1.2) To Make this program independent of AP-Core Engine 4 | # GitHub: https://github.com/avdaredevil/AP-Compiler 5 | # ==================================================================================================================| 6 | $Script:PSHell=$(if($PSHell){$PSHell}elseif($PSScriptRoot){$PSScriptRoot}else{"."}); 7 | $Script:AP_Console = @{version=[version]'1.2'; isShim = $true} 8 | function B64 {param([Parameter(ValueFromPipeline=$true)][String]$Text, [ValidateSet("UTF8","Unicode")][String]$Encoding = "UTF8") [System.Text.Encoding]::$Encoding.GetString([System.Convert]::FromBase64String($Text))} 9 | # This syntax is to prevent AV's from misclassifying this as anything but innocuous 10 | & (Get-Alias iex) (B64 "ZnVuY3Rpb24gV3JpdGUtQVAgew0KICAgIFtDbWRsZXRCaW5kaW5nKCldDQogICAgcGFyYW0oW1BhcmFtZXRlcihWYWx1ZUZyb21QaXBlbGluZT0kdHJ1ZSwgTWFuZGF0b3J5PSRUcnVlKV0kVGV4dCxbU3dpdGNoXSROb1NpZ24sW1N3aXRjaF0kUGxhaW5UZXh0LFtWYWxpZGF0ZVNldCgiQ2VudGVyIiwiUmlnaHQiLCJMZWZ0IildW1N0cmluZ10kQWxpZ249J0xlZnQnLFtTd2l0Y2hdJFBhc3NUaHJ1KQ0KICAgIGJlZ2luIHskVFQgPSBAKCl9DQogICAgUHJvY2VzcyB7JFRUICs9ICwkVGV4dH0NCiAgICBFTkQgew0KICAgICAgICAkQmx1ZSA9ICQoaWYgKCRXUklURV9BUF9MRUdBQ1lfQ09MT1JTKXszfWVsc2V7J0JsdWUnfSkNCiAgICAgICAgaWYgKCRUVC5jb3VudCAtZXEgMSkgeyRUVCA9ICRUVFswXX07JFRleHQgPSAkVFQNCiAgICAgICAgaWYgKCR0ZXh0LmNvdW50IC1ndCAxIC1vciAkdGV4dC5HZXRUeXBlKCkuTmFtZSAtbWF0Y2ggIlxbXF0kIikgew0KICAgICAgICAgICAgcmV0dXJuICRUZXh0IHwgJSB7DQogICAgICAgICAgICAgICAgV3JpdGUtQVAgJF8gLU5vU2lnbjokTm9TaWduIC1QbGFpblRleHQ6JFBsYWluVGV4dCAtQWxpZ24gJEFsaWduIC1QYXNzVGhydTokUGFzc1RocnUNCiAgICAgICAgICAgIH0NCiAgICAgICAgfQ0KICAgICAgICBpZiAoISR0ZXh0IC1vciAkdGV4dCAtbm90bWF0Y2ggIig/c21pKV4oKD88Tk5MPngpfCg/PE5TPm5zPykpezAsMn0oPzx0Plw+KikoPzxzPltcK1wtXCFcKlwjXEBfXSkoPzx3Pi4qKSIpIHtyZXR1cm4gV3JpdGUtSG9zdCAkVGV4dH0NCiAgICAgICAgJHRiICA9ICIgICAgIiokTWF0Y2hlcy50Lmxlbmd0aA0KICAgICAgICAkQ29sID0gQHsnKyc9JzInOyctJz0nMTInOychJz0nMTQnOycqJz0kQmx1ZTsnIyc9J0RhcmtHcmF5JzsnQCc9J0dyYXknOydfJz0nd2hpdGUnfVsoJFNpZ24gPSAkTWF0Y2hlcy5TKV0NCiAgICAgICAgaWYgKCEkQ29sKSB7VGhyb3cgIkluY29ycmVjdCBTaWduIFskU2lnbl0gUGFzc2VkISJ9DQogICAgICAgICRTaWduID0gJChpZiAoJE5vU2lnbiAtb3IgJE1hdGNoZXMuTlMpIHsiIn0gZWxzZSB7IlskU2lnbl0gIn0pDQogICAgICAgICREYXRhID0gIiR0YiRTaWduJCgkTWF0Y2hlcy5XKSI7aWYgKCEkRGF0YSkge3JldHVybiBXcml0ZS1Ib3N0ICIifQ0KICAgICAgICBpZiAoQVAtUmVxdWlyZSAiZnVuY3Rpb246QWxpZ24tVGV4dCIgLXBhKSB7DQogICAgICAgICAgICAkRGF0YSA9IEFsaWduLVRleHQgLUFsaWduICRBbGlnbiAiJHRiJFNpZ24kKCRNYXRjaGVzLlcpIg0KICAgICAgICB9DQogICAgICAgIGlmICgkUGxhaW5UZXh0KSB7cmV0dXJuICREYXRhfQ0KICAgICAgICAkRGF0YUxpbmVzID0gJERhdGEgLXNwbGl0ICJgbiINCiAgICAgICAgMS4uJERhdGFMaW5lcy5Db3VudCB8ICUgew0KICAgICAgICAgICAgJElkeCA9ICRfIC0gMQ0KICAgICAgICAgICAgJE5OTCA9ICEkaWR4IC1hbmQgJE1hdGNoZXMuTk5MDQogICAgICAgICAgICBXcml0ZS1Ib3N0IC1Ob05ld0xpbmU6JE5OTCAtZiAkQ29sICREYXRhTGluZXNbJElkeF0NCiAgICAgICAgICAgIGlmICgkUGFzc1RocnUpIHtyZXR1cm4gJERhdGF9DQogICAgICAgIH0NCiAgICB9DQp9CgpmdW5jdGlvbiBLZXlQcmVzc2VkQ29kZSB7cGFyYW0oW1BhcmFtZXRlcihNYW5kYXRvcnk9JFRydWUpXVtJbnRdJEtleSwgJFN0b3JlPSJeXl4iKQ0KDQogICAgaWYgKCEkSG9zdC5VSS5SYXdVSS5LZXlBdmFpbGFibGUgLWFuZCAkU3RvcmUgLWVxICJeXl4iKSB7UmV0dXJuICRGYWxzZX0NCiAgICBpZiAoJFN0b3JlIC1lcSAiXl5eIikgeyRTdG9yZSA9ICRIb3N0LlVJLlJhd1VJLlJlYWRLZXkoIkluY2x1ZGVLZXlVcCxOb0VjaG8iKX0NCiAgICByZXR1cm4gKCRLZXkgLWluICRTdG9yZS5WaXJ0dWFsS2V5Q29kZSkNCn0KCmZ1bmN0aW9uIEpTLU9SIHtmb3JlYWNoICgkYSBpbiAkYXJncykgeyRhID0gSW52b2tlLU9yUmV0dXJuICRhO2lmICghJGEpe2NvbnRpbnVlfTtyZXR1cm4gJGF9O3JldHVybiAkYX0KCmZ1bmN0aW9uIFBsYWNlLUJ1ZmZlcmVkQ29udGVudCB7cGFyYW0oJFRleHQsICR4LCAkeSwgW0NvbnNvbGVDb2xvcl0kRm9yZWdyb3VuZENvbG9yPVtDb25zb2xlXTo6Rm9yZWdyb3VuZENvbG9yLCBbQ29uc29sZUNvbG9yXSRCYWNrZ3JvdW5kQ29sb3I9W0NvbnNvbGVdOjpCYWNrZ3JvdW5kQ29sb3IpDQoNCiAgICAkY3JkID0gW01hbmFnZW1lbnQuQXV0b21hdGlvbi5Ib3N0LkNvb3JkaW5hdGVzXTo6bmV3KCR4LCR5KQ0KICAgICRiID0gJEhvc3QuVUkuUmF3VUkNCiAgICAkYXJyID0gJGIuTmV3QnVmZmVyQ2VsbEFycmF5KEAoJFRleHQpLCAkRm9yZWdyb3VuZENvbG9yLCAkQmFja2dyb3VuZENvbG9yKQ0KICAgICR4ID0gW0NvbnNvbGVdOjpCdWZmZXJXaWR0aC0xLSRUZXh0Lmxlbmd0aA0KICAgICRiLlNldEJ1ZmZlckNvbnRlbnRzKCRjcmQsICRhcnIpDQp9CgpmdW5jdGlvbiBBUC1Db252ZXJ0UGF0aCB7cGFyYW0oW1BhcmFtZXRlcihNYW5kYXRvcnk9JFRydWUpXVtTdHJpbmddJFBhdGgpDQoNCiAgICAkUGF0aFNlcCA9IFtJTy5QYXRoXTo6RGlyZWN0b3J5U2VwYXJhdG9yQ2hhcg0KICAgIHJldHVybiAkUGF0aCAtcmVwbGFjZSANCiAgICAgICAgIjxEZXA+IiwiPExpYj4ke1BhdGhTZXB9RGVwZW5kZW5jaWVzIiAtcmVwbGFjZSANCiAgICAgICAgIjxMaWI+IiwiPEhvbWU+JHtQYXRoU2VwfUFQLUxpYnJhcmllcyIgLXJlcGxhY2UgDQogICAgICAgICI8Q29tcChvbmVudHMpPz4iLCI8SG9tZT4ke1BhdGhTZXB9QVAtQ29tcG9uZW50cyIgLXJlcGxhY2UgDQogICAgICAgICI8SG9tZT4iLCRQU0hlbGx9CgpmdW5jdGlvbiBHZXQtUGF0aCB7cGFyYW0oJG1hdGNoLCBbc3RyaW5nXSRQYXRoVmFyID0gIlBBVEgiKQ0KDQogICAgJFB0aCA9IFtFbnZpcm9ubWVudF06OkdldEVudmlyb25tZW50VmFyaWFibGUoJFBhdGhWYXIpDQogICAgJElzVW5peCA9ICRQU1ZlcnNpb25UYWJsZS5QbGF0Zm9ybSAtZXEgIlVuaXgiDQogICAgJFBhdGhTZXAgPSAkKGlmICgkSXNVbml4KSB7IjoifSBlbHNlIHsiOyJ9KQ0KICAgIGlmICghJFB0aCkge3JldHVybiBAKCl9DQogICAgU2V0LVBhdGggJFB0aCAtUGF0aFZhciAkUGF0aFZhcg0KICAgICRkID0gKCRQdGgpLnNwbGl0KCRQYXRoU2VwKQ0KICAgIGlmICgkbWF0Y2gpIHskZCAtbWF0Y2ggJG1hdGNofSBlbHNlIHskZH0NCn0KCmZ1bmN0aW9uIEludm9rZS1PclJldHVybiB7cGFyYW0oW1BhcmFtZXRlcihNYW5kYXRvcnk9JHRydWUsIFZhbHVlRnJvbVBpcGVsaW5lPSR0cnVlLCBQb3NpdGlvbj0wKV1bQWxsb3dOdWxsKCldJENvZGUsIFtQYXJhbWV0ZXIoVmFsdWVGcm9tUmVtYWluaW5nQXJndW1lbnRzPTEpXSRfX1Jlc3QpDQoNCiAgICBpZiAoJENvZGUgLWlzIFtTY3JpcHRCbG9ja10pIHsmICRDb2RlIEBfX1Jlc3R9IGVsc2UgeyRDb2RlfQ0KfQoKZnVuY3Rpb24gU3RyaXAtQ29sb3JDb2RlcyB7cGFyYW0oJFN0cikNCg0KICAgICRTdHIgfCAlIHskXyAtcmVwbGFjZSAiJChbcmVnZXhdOjplc2NhcGUoIiQoR2V0LUVzY2FwZSlbIikpXGQrKFw7XGQrKSptIiwiIn0NCn0KCmZ1bmN0aW9uIEFsaWduLVRleHQge3BhcmFtKFtQYXJhbWV0ZXIoTWFuZGF0b3J5PSRUcnVlKV1bU3RyaW5nW11dJFRleHQsIFtWYWxpZGF0ZVNldCgiQ2VudGVyIiwiUmlnaHQiLCJMZWZ0IildW1N0cmluZ10kQWxpZ249J0NlbnRlcicpDQoNCiAgICBpZiAoJEFsaWduIC1lcSAiTGVmdCIpIHtyZXR1cm4gJFRleHR9DQogICAgDQogICAgaWYgKCRUZXh0LmNvdW50IC1ndCAxKSB7DQogICAgICAgIHJldHVybiAkVGV4dCB8ICUge0FsaWduLVRleHQgJF8gJEFsaWdufSAgIA0KICAgIH0NCiAgICAkV2luU2l6ZSA9IFtjb25zb2xlXTo6QnVmZmVyV2lkdGgNCiAgICAkQ2xlYW5UZXh0U2l6ZSA9IChTdHJpcC1Db2xvckNvZGVzICgiIiskVGV4dCkpLkxlbmd0aA0KICAgIGlmICgkQ2xlYW5UZXh0U2l6ZSAtZ2UgJFdpblNpemUpIHsNCiAgICAgICAgJEFwcGVuZGVyID0gQCgiIik7DQogICAgICAgICRqID0gMA0KICAgICAgICBmb3JlYWNoICgkcCBpbiAwLi4oJENsZWFuVGV4dFNpemUtMSkpew0KICAgICAgICAgICAgaWYgKCgkcCsxKSUkd2luc2l6ZSAtZXEgMCkgeyRqKys7JEFwcGVuZGVyICs9ICIifQ0KICAgICAgICAgICAgIyAiIiskaisiIC0gIiskcA0KICAgICAgICAgICAgJEFwcGVuZGVyWyRqXSArPSAkVGV4dC5jaGFycygkcCkNCiAgICAgICAgfQ0KICAgICAgICByZXR1cm4gKEFsaWduLVRleHQgJEFwcGVuZGVyICRBbGlnbikNCiAgICB9DQogICAgaWYgKCRBbGlnbiAtZXEgIkNlbnRlciIpIHsNCiAgICAgICAgcmV0dXJuICgiICIqW21hdGhdOjp0cnVuY2F0ZSgoJFdpblNpemUtJENsZWFuVGV4dFNpemUpLzIpKyRUZXh0KQ0KICAgIH0NCiAgICAjIFJpZ2h0DQogICAgcmV0dXJuICgiICIqKCRXaW5TaXplLSRDbGVhblRleHRTaXplLTEpKyRUZXh0KQ0KfQoKZnVuY3Rpb24gR2V0LUVzY2FwZSB7DQogICAgaWYgKCEoQVAtUmVxdWlyZSAiYWJpbGl0eTplc2NhcGVfY29kZXMiKSkge3Rocm93ICJbR2V0LVJCR10gWW91ciBjb25zb2xlIGRvZXMgbm90IHN1cHBvcnQgQU5TSSBlc2NhcGUgY29kZXMifQ0KICAgICMgV2UgZG8gdGhpcywgYmVjYXVzZSBQb3dlclNoZWxsIE5hdGl2ZSBkb2Vzbid0IGtub3cgYGUNCiAgICByZXR1cm4gW0NoYXJdMHgxYiAjIGBlDQp9CgpmdW5jdGlvbiBLZXlQcmVzc2VkIHtwYXJhbShbUGFyYW1ldGVyKE1hbmRhdG9yeT0kVHJ1ZSldW1N0cmluZ1tdXSRLZXksICRTdG9yZSA9ICJeXl4iKQ0KDQogICAgaWYgKCRTdG9yZSAtZXEgIl5eXiIgLWFuZCAkSG9zdC5VSS5SYXdVSS5LZXlBdmFpbGFibGUpIHskU3RvcmUgPSAkSG9zdC5VSS5SYXdVSS5SZWFkS2V5KCJJbmNsdWRlS2V5VXAsTm9FY2hvIil9IGVsc2Uge2lmICgkU3RvcmUgLWVxICJeXl4iKSB7cmV0dXJuICRGYWxzZX19DQogICAgJEtleSB8ICUgew0KICAgICAgICAkU09VUkNFID0gJF8NCiAgICAgICAgRm9yZWFjaCAoJEsgaW4gJFNPVVJDRSkgew0KICAgICAgICAgICAgW1N0cmluZ10kSyA9ICRLDQogICAgICAgICAgICBpZiAoJEsgLW1hdGNoICJeYy0oXGQrKSQiKSB7DQogICAgICAgICAgICAgICAgaWYgKEtleVByZXNzZWRDb2RlICRNYXRjaGVzWzFdICRTdG9yZSkge3JldHVybiAkVHJ1ZX0NCiAgICAgICAgICAgIH0gZWxzZWlmICgkSyAtbWF0Y2ggIl5+figuKyl+fiQiKSB7DQogICAgICAgICAgICAgICAgJENvZGUgPSBLZXlUcmFuc2xhdGUgJEsNCiAgICAgICAgICAgICAgICBpZiAoJENvZGUgLWlzIFtoYXNodGFibGVdKSB7DQogICAgICAgICAgICAgICAgICAgICRIYXNFbmhhbmNlZCA9ICRTdG9yZS5Db250cm9sS2V5U3RhdGUuSGFzRmxhZyhbU3lzdGVtLk1hbmFnZW1lbnQuQXV0b21hdGlvbi5Ib3N0LkNvbnRyb2xLZXlTdGF0ZXNdOjpFbmhhbmNlZEtleSkNCiAgICAgICAgICAgICAgICAgICAgaWYgKCRDb2RlLmVuaGFuY2VkIC1uZSAkSGFzRW5oYW5jZWQpIHtjb250aW51ZX0NCiAgICAgICAgICAgICAgICAgICAgJENvZGUgPSAkQ29kZS5jb2RlDQogICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgICAgIGlmIChLZXlQcmVzc2VkQ29kZSAkQ29kZSAkU3RvcmUpIHtyZXR1cm4gJFRydWV9DQogICAgICAgICAgICB9IGVsc2Ugew0KICAgICAgICAgICAgICAgIGlmICgkSy5jaGFycygwKSAtaW4gJFN0b3JlLkNoYXJhY3Rlcikge3JldHVybiAkVHJ1ZX0NCiAgICAgICAgICAgIH0NCiAgICAgICAgfQ0KICAgIH0NCiAgICByZXR1cm4gJEZhbHNlDQp9CgpmdW5jdGlvbiBHZXQtV2hlcmUgew0KICAgIFtDbWRsZXRCaW5kaW5nKERlZmF1bHRQYXJhbWV0ZXJTZXROYW1lPSJOb3JtYWwiKV0NCiAgICBwYXJhbSgNCiAgICAgICAgW1BhcmFtZXRlcihNYW5kYXRvcnk9JHRydWUsIFBvc2l0aW9uPTApXVtzdHJpbmddJEZpbGUsDQogICAgICAgIFtTd2l0Y2hdJEFsbCwNCiAgICAgICAgW1BhcmFtZXRlcihQYXJhbWV0ZXJTZXROYW1lPSdOb3JtYWwnKV1bUGFyYW1ldGVyKFBhcmFtZXRlclNldE5hbWU9J1NjYW4nKV1bU3dpdGNoXSRNYW51YWxTY2FuLA0KICAgICAgICBbUGFyYW1ldGVyKFBhcmFtZXRlclNldE5hbWU9J1NjYW4nKV1bU3dpdGNoXSREYmcsDQogICAgICAgIFtQYXJhbWV0ZXIoUGFyYW1ldGVyU2V0TmFtZT0nU2NhbicpXVtzdHJpbmddJFBhdGhWYXIgPSAiUEFUSCINCiAgICApDQogICAgJElzVmVyYm9zZSA9ICREYmcgLW9yICRQU0NtZGxldC5NeUludm9jYXRpb24uQm91bmRQYXJhbWV0ZXJzLlZlcmJvc2UgLW9yICRQU0NtZGxldC5NeUludm9jYXRpb24uQm91bmRQYXJhbWV0ZXJzLkRlYnVnDQogICAgJFdoZXJlQmluRXhpc3RzID0gR2V0LUNvbW1hbmQgIndoZXJlIiAtZWEgU2lsZW50bHlDb250aW51ZQ0KICAgICRJc1VuaXggPSAkUFNWZXJzaW9uVGFibGUuUGxhdGZvcm0gLWVxICJVbml4Ig0KICAgIGlmICgkRmlsZSAtZXEgIndoZXJlIiAtb3IgJEZpbGUgLWVxICJ3aGVyZS5leGUiKSB7cmV0dXJuICRXaGVyZUJpbkV4aXN0c30NCiAgICBpZiAoJFdoZXJlQmluRXhpc3RzIC1hbmQgISRNYW51YWxTY2FuKSB7DQogICAgICAgICRPdXQ9JG51bGwNCiAgICAgICAgaWYgKCRJc1VuaXgpIHsNCiAgICAgICAgICAgICRPdXQgPSB3aGljaCAkZmlsZSAyPiRudWxsDQogICAgICAgIH0gZWxzZSB7JE91dCA9IHdoZXJlLmV4ZSAkZmlsZSAyPiRudWxsfQ0KICAgICAgICANCiAgICAgICAgaWYgKCEkT3V0KSB7cmV0dXJufQ0KICAgICAgICBpZiAoJEFsbCkge3JldHVybiAkT3V0fQ0KICAgICAgICByZXR1cm4gQCgkT3V0KVswXQ0KICAgIH0NCiAgICBmb3JlYWNoICgkRm9sZGVyIGluIChHZXQtUGF0aCAtUGF0aFZhciAkUGF0aFZhcikpIHsNCiAgICAgICAgaWYgKCRJc1VuaXgpIHsNCiAgICAgICAgICAgICRMb29rdXAgPSAiJEZvbGRlci8kRmlsZSINCiAgICAgICAgICAgIGlmICgkSXNWZXJib3NlKSB7V3JpdGUtQVAgIipDaGVja2luZyBbJExvb2t1cF0ifQ0KICAgICAgICAgICAgaWYgKCEoVGVzdC1QYXRoIC1QYXRoVHlwZSBMZWFmICRMb29rdXApKSB7Y29udGludWV9DQogICAgICAgICAgICBSZXNvbHZlLVBhdGggJExvb2t1cCB8ICUgUGF0aA0KICAgICAgICAgICAgaWYgKCEkQWxsKSB7cmV0dXJufQ0KICAgICAgICB9IGVsc2Ugew0KICAgICAgICAgICAgZm9yZWFjaCAoJEV4dGVuc2lvbiBpbiAoR2V0LVBhdGggLVBhdGhWYXIgUEFUSEVYVCkpIHsNCiAgICAgICAgICAgICAgICAkTG9va3VwID0gIiRGb2xkZXIvJEZpbGUkRXh0ZW5zaW9uIg0KICAgICAgICAgICAgICAgIGlmICgkSXNWZXJib3NlKSB7V3JpdGUtQVAgIipDaGVja2luZyBbJExvb2t1cF0ifQ0KICAgICAgICAgICAgICAgIGlmICghKFRlc3QtUGF0aCAtUGF0aFR5cGUgTGVhZiAkTG9va3VwKSkge2NvbnRpbnVlfQ0KICAgICAgICAgICAgICAgIFJlc29sdmUtUGF0aCAkTG9va3VwIHwgJSBQYXRoDQogICAgICAgICAgICAgICAgaWYgKCEkQWxsKSB7cmV0dXJufQ0KICAgICAgICAgICAgfQ0KICAgICAgICB9DQogICAgfQ0KfQoKZnVuY3Rpb24gU2V0LVBhdGggew0KICAgIFtjbWRsZXRiaW5kaW5nKCldDQogICAgcGFyYW0oDQogICAgICAgIFtQYXJhbWV0ZXIoTWFuZGF0b3J5ID0gJHRydWUsIFZhbHVlRnJvbVBpcGVsaW5lID0gJHRydWUpXVtzdHJpbmdbXV0kUGF0aCwNCiAgICAgICAgW3N0cmluZ10kUGF0aFZhciA9ICJQQVRIIg0KICAgICkNCiAgICBiZWdpbiB7DQogICAgICAgIFtzdHJpbmdbXV0kRmluYWxQYXRoDQogICAgfQ0KICAgIHByb2Nlc3Mgew0KICAgICAgICAkUGF0aCB8ICUgew0KICAgICAgICAgICAgJEZpbmFsUGF0aCArPSAkXw0KICAgICAgICB9DQogICAgfQ0KICAgIGVuZCB7DQogICAgICAgICRJc1VuaXggPSAkUFNWZXJzaW9uVGFibGUuUGxhdGZvcm0gLWVxICJVbml4Ig0KICAgICAgICAkUGF0aFNlcCA9ICQoaWYgKCRJc1VuaXgpIHsiOiJ9IGVsc2UgeyI7In0pDQogICAgICAgICRQdGggPSAkRmluYWxQYXRoIC1qb2luICRQYXRoU2VwDQogICAgICAgICRQdGggPSAoJFB0aCAtcmVwbGFjZSgiJFBhdGhTZXArIiwgJFBhdGhTZXApIC1yZXBsYWNlKCJcXCRQYXRoU2VwfFxcJCIsICRQYXRoU2VwKSkudHJpbSgkUGF0aFNlcCkNCiAgICAgICAgJFB0aCA9ICgoJFB0aCkuc3BsaXQoJFBhdGhTZXApIHwgc2VsZWN0IC11bmlxdWUpIC1qb2luICRQYXRoU2VwDQogICAgICAgIFtFbnZpcm9ubWVudF06OlNldEVudmlyb25tZW50VmFyaWFibGUoJFBhdGhWYXIsICRQdGgpDQogICAgfQ0KfQoKZnVuY3Rpb24gS2V5VHJhbnNsYXRlIHtwYXJhbShbUGFyYW1ldGVyKE1hbmRhdG9yeT0kVHJ1ZSldW1N0cmluZ10kS2V5KQ0KDQogICAgJEhhc2hLZXkgPSBAew0KICAgICAgICAifn5DdHJsQ35+Ij02Nw0KICAgICAgICAifn5TcGFjZX5+Ij0zMg0KICAgICAgICAifn5FU0NBUEV+fiI9MjcNCiAgICAgICAgIn5+RW50ZXJ+fiI9MTMNCiAgICAgICAgIn5+U2hpZnR+fiI9MTYNCiAgICAgICAgIn5+Q29udHJvbH5+Ij0xNw0KICAgICAgICAifn5Db250cm9sTGVmdH5+Ij1Ae2NvZGUgPSAxNzsgZW5oYW5jZWQgPSAkZmFsc2V9DQogICAgICAgICJ+fkNvbnRyb2xSaWdodH5+Ij1Ae2NvZGUgPSAxNzsgZW5oYW5jZWQgPSAkdHJ1ZX0NCiAgICAgICAgIn5+QWx0fn4iPTE4DQogICAgICAgICJ+fkJhY2tTcGFjZX5+Ij04DQogICAgICAgICJ+fkRlbGV0ZX5+Ij00Ng0KICAgICAgICAifn5mMX5+Ij0xMTINCiAgICAgICAgIn5+ZjJ+fiI9MTEzDQogICAgICAgICJ+fmYzfn4iPTExNA0KICAgICAgICAifn5mNH5+Ij0xMTUNCiAgICAgICAgIn5+ZjV+fiI9MTE2DQogICAgICAgICJ+fmY2fn4iPTExNw0KICAgICAgICAifn5mN35+Ij0xMTgNCiAgICAgICAgIn5+Zjh+fiI9MTE5DQogICAgICAgICJ+fmY5fn4iPTEyMA0KICAgICAgICAifn5mMTB+fiI9MTIxDQogICAgICAgICJ+fmYxMX5+Ij0xMjINCiAgICAgICAgIn5+ZjEyfn4iPTEyMw0KICAgICAgICAifn5NdXRlfn4iPTE3Mw0KICAgICAgICAifn5JbnNlcnR+fiI9NDUNCiAgICAgICAgIn5+UGFnZVVwfn4iPTMzDQogICAgICAgICJ+flBhZ2VEb3dufn4iPTM0DQogICAgICAgICJ+fkVORH5+Ij0zNQ0KICAgICAgICAifn5IT01Ffn4iPTM2DQogICAgICAgICJ+fnRhYn5+Ij05DQogICAgICAgICJ+fkNhcHNMb2Nrfn4iPTIwDQogICAgICAgICJ+fk51bUxvY2t+fiI9MTQ0DQogICAgICAgICJ+flNjcm9sbExvY2t+fiI9MTQ1DQogICAgICAgICJ+fldpbmRvd3N+fiI9OTENCiAgICAgICAgIn5+TGVmdH5+Ij0zNw0KICAgICAgICAifn5VcH5+Ij0zOA0KICAgICAgICAifn5SaWdodH5+Ij0zOQ0KICAgICAgICAifn5Eb3dufn4iPTQwDQogICAgICAgICJ+fktQMH5+Ij05Ng0KICAgICAgICAifn5LUDF+fiI9OTcNCiAgICAgICAgIn5+S1Ayfn4iPTk4DQogICAgICAgICJ+fktQM35+Ij05OQ0KICAgICAgICAifn5LUDR+fiI9MTAwDQogICAgICAgICJ+fktQNX5+Ij0xMDENCiAgICAgICAgIn5+S1A2fn4iPTEwMg0KICAgICAgICAifn5LUDd+fiI9MTAzDQogICAgICAgICJ+fktQOH5+Ij0xMDQNCiAgICAgICAgIn5+S1A5fn4iPTEwNQ0KICAgICAgICAifn5LUCp+fiI9MTA2DQogICAgICAgICJ+fktQK35+Ij0xMDcNCiAgICAgICAgIn5+S1Atfn4iPTEwOQ0KICAgICAgICAifn5LUC5+fiI9MTEwDQogICAgICAgICJ+fktQL35+Ij0xMTENCiAgICAgICAgIn5+S1AtRU5URVJ+fiI9QHtjb2RlID0gMTM7IGVuaGFuY2VkID0gJHRydWV9DQogICAgfQ0KICAgIGlmICgkQ29udmVydCA9ICRIYXNoS2V5LiRLZXkpIHtyZXR1cm4gJENvbnZlcnR9DQogICAgVGhyb3cgIkludmFsaWQgU3BlY2lhbCBLZXkgQ29udmVyc2lvbiBbJEtleV0iDQp9CgpmdW5jdGlvbiBUZXN0LUFkbWluaXN0cmF0b3Igew0KICAgIGlmICgkUFNWZXJzaW9uVGFibGUuUGxhdGZvcm0gLWVxICJVbml4Iikgew0KICAgICAgICBpZiAoJCh3aG9hbWkpIC1lcSAicm9vdCIpIHsNCiAgICAgICAgICAgIHJldHVybiAkdHJ1ZQ0KICAgICAgICB9DQogICAgICAgIGVsc2Ugew0KICAgICAgICAgICAgcmV0dXJuICRmYWxzZQ0KICAgICAgICB9DQogICAgfQ0KICAgICMgV2luZG93cw0KICAgIChOZXctT2JqZWN0IFNlY3VyaXR5LlByaW5jaXBhbC5XaW5kb3dzUHJpbmNpcGFsIChbU2VjdXJpdHkuUHJpbmNpcGFsLldpbmRvd3NJZGVudGl0eV06OkdldEN1cnJlbnQoKSkpLklzSW5Sb2xlKFtTZWN1cml0eS5QcmluY2lwYWwuV2luZG93c0J1aWx0aW5Sb2xlXTo6QWRtaW5pc3RyYXRvcikNCn0KCmZ1bmN0aW9uIEFQLVJlcXVpcmUge3BhcmFtKFtQYXJhbWV0ZXIoTWFuZGF0b3J5PSRUcnVlKV1bQWxpYXMoIkZ1bmN0aW9uYWxpdHkiLCJMaWJyYXJ5IildW0FyZ3VtZW50Q29tcGxldGVyKHsNCiAgICBbT3V0cHV0VHlwZShbU3lzdGVtLk1hbmFnZW1lbnQuQXV0b21hdGlvbi5Db21wbGV0aW9uUmVzdWx0XSldDQogICAgcGFyYW0oDQogICAgICAgIFtzdHJpbmddICRDb21tYW5kTmFtZSwNCiAgICAgICAgW3N0cmluZ10gJFBhcmFtZXRlck5hbWUsDQogICAgICAgIFtzdHJpbmddICRXb3JkVG9Db21wbGV0ZSwNCiAgICAgICAgW1N5c3RlbS5NYW5hZ2VtZW50LkF1dG9tYXRpb24uTGFuZ3VhZ2UuQ29tbWFuZEFzdF0gJENvbW1hbmRBc3QsDQogICAgICAgIFtTeXN0ZW0uQ29sbGVjdGlvbnMuSURpY3Rpb25hcnldICRGYWtlQm91bmRQYXJhbWV0ZXJzDQogICAgKQ0KICAgICRDb21wbGV0aW9uUmVzdWx0cyA9IFtTeXN0ZW0uQ29sbGVjdGlvbnMuR2VuZXJpYy5MaXN0W1N5c3RlbS5NYW5hZ2VtZW50LkF1dG9tYXRpb24uQ29tcGxldGlvblJlc3VsdF1dOjpuZXcoKQ0KICAgICRMaWIgPSBAKCJJbnRlcm5ldCIsIm9zOndpbmRvd3MiLCJvczpsaW51eCIsIm9zOnVuaXgiLCJhZG1pbmlzdHJhdG9yIiwicm9vdCIsImxpYjoiLCJsaWJfdGVzdDoiLCJmdW5jdGlvbjoiLCJzdHJpY3RfZnVuY3Rpb246IiwiYWJpbGl0eTplc2NhcGVfY29kZXMiLCJhYmlsaXR5OmVtb2ppcyIpDQogICAgSlMtT1IgeyRMaWIgfCA/IHskXyAtbGlrZSAiJFdvcmRUb0NvbXBsZXRlKiJ9fSB7JExpYiB8ID8geyRfIC1saWtlICIqJFdvcmRUb0NvbXBsZXRlKiJ9fSB8ICUgew0KICAgICAgICAkQ29tcGxldGlvblJlc3VsdHMuQWRkKFtTeXN0ZW0uTWFuYWdlbWVudC5BdXRvbWF0aW9uLkNvbXBsZXRpb25SZXN1bHRdOjpuZXcoJF8sICRfLCAnUGFyYW1ldGVyVmFsdWUnLCAkXykpDQogICAgfQ0KICAgIHJldHVybiAkQ29tcGxldGlvblJlc3VsdHMNCn0pXVtTdHJpbmddJExpYiwgW1NjcmlwdEJsb2NrXSRPbkZhaWwsIFtTd2l0Y2hdJFBhc3NUaHJ1KQ0KDQogICAgJExvYWRNb2R1bGUgPSB7DQogICAgICAgIHBhcmFtKCRGaWxlLFtib29sXSRJbXBvcnQpDQogICAgICAgIHRyeSB7SW1wb3J0LU1vZHVsZSAkRmlsZSAtZWEgc3RvcDtyZXR1cm4gMX0gY2F0Y2gge30NCiAgICAgICAgJExpYj1BUC1Db252ZXJ0UGF0aCAiPExJQj4iOyRMRiA9ICIkTGliXCRGaWxlIg0KICAgICAgICBbc3RyaW5nXSRmID0gaWYodGVzdC1wYXRoIC10IGxlYWYgJExGKXskTEZ9ZWxzZWlmKHRlc3QtcGF0aCAtdCBsZWFmICIkTEYuZGxsIil7IiRMRi5kbGwifQ0KICAgICAgICBpZiAoJGYgLWFuZCAkSW1wb3J0KSB7SW1wb3J0LU1vZHVsZSAkZn0NCiAgICAgICAgcmV0dXJuICRmDQogICAgfQ0KICAgICRJbnZva2VPclJldHVybiA9IHsNCiAgICAgICAgcGFyYW0oJENtZCkNCiAgICAgICAgaWYgKCRDbWQgLWlzIFtTY3JpcHRCbG9ja10pIHsmICRDbWR9IGVsc2UgeyRDbWR9DQogICAgfQ0KICAgIGlmICghJE9uRmFpbCkgeyRQYXNzVGhydSA9ICR0cnVlfQ0KICAgICRTdGF0ID0gJChzd2l0Y2ggLXJlZ2V4ICgkTGliLnRyaW0oKSkgew0KICAgICAgICAiXkludGVybmV0JCIgICAgICAgICAgICAgICAgICAge3Rlc3QtY29ubmVjdGlvbiBnb29nbGUuY29tIC1Db3VudCAxIC1RdWlldH0NCiAgICAgICAgIl5vczood2luKGRvd3MpP3xsaW51eHx1bml4KSQiIHskSXNVbml4ID0gJFBTVmVyc2lvblRhYmxlLlBsYXRmb3JtIC1lcSAiVW5peCI7aWYgKCRNYXRjaGVzWzFdIC1tYXRjaCAiXndpbiIpIHshJElzVW5peH0gZWxzZSB7JElzVW5peH19DQogICAgICAgICJeYWRtaW4oaXN0cmF0b3IpPyR8XnJvb3QkIiAgICB7VGVzdC1BZG1pbmlzdHJhdG9yfQ0KICAgICAgICAiXmRlcDooLiopJCIgICAgICAgICAgICAgICAgICAge0dldC1XaGVyZSAkTWF0Y2hlc1sxXX0NCiAgICAgICAgIl4obGlifG1vZHVsZSk6KC4qKSQiICAgICAgICAgIHskTG9hZE1vZHVsZS5pbnZva2UoJE1hdGNoZXNbMl0sICR0cnVlKX0NCiAgICAgICAgIl4obGlifG1vZHVsZSlfdGVzdDooLiopJCIgICAgIHskTG9hZE1vZHVsZS5pbnZva2UoJE1hdGNoZXNbMl0pfQ0KICAgICAgICAiXmZ1bmN0aW9uOiguKikkIiAgICAgICAgICAgICAge2djbSAkTWF0Y2hlc1sxXSAtZWEgU2lsZW50bHlDb250aW51ZX0NCiAgICAgICAgIl5zdHJpY3RfZnVuY3Rpb246KC4qKSQiICAgICAgIHtUZXN0LVBhdGggIkZ1bmN0aW9uOlwkKCRNYXRjaGVzWzFdKSJ9DQogICAgICAgICJeYWJpbGl0eTooZXNjYXBlX2NvZGVzfGVtb2ppcykkIiAgICAgeyYgJEludm9rZU9yUmV0dXJuIChAew0KICAgICAgICAgICAgZXNjYXBlX2NvZGVzID0gJEhvc3QuVUkuU3VwcG9ydHNWaXJ0dWFsVGVybWluYWwNCiAgICAgICAgICAgIGVtb2ppcyA9ICRlbnY6V1RfU0VTU0lPTiAtb3IgJGVudjpXVF9QUk9GSUxFX0lEDQogICAgICAgIH1bJE1hdGNoZXNbMV1dKX0NCiAgICAgICAgZGVmYXVsdCB7V3JpdGUtQVAgIiFJbnZhbGlkIHNlbGVjdG9yIHByb3ZpZGVkIFskKCIkTGliIi5zcGxpdCgnOicpWzBdKV0iO3Rocm93ICdCQURfU0VMRUNUT1InfQ0KICAgIH0pDQogICAgaWYgKCEkU3RhdCAtYW5kICRPbkZhaWwpIHsmICRPbkZhaWx9DQogICAgaWYgKCRQYXNzVGhydSAtb3IgISRPbkZhaWwpIHtyZXR1cm4gJFN0YXR9DQp9Cg==") 11 | # ========================================END=OF=COMPILER===========================================================| 12 | # ap-compile: Write-AP, Get-Escape, JS-OR, AP-ConvertPath, KeyTranslate, Test-Administrator, KeyPressed, Place-BufferedContent, Get-Path, Align-Text, KeyPressedCode, AP-Require, Strip-ColorCodes, Get-Where, Set-Path 13 | 14 | #= MAIN-GAME ==================================================================================| 15 | function Get-PlayerSpawn([Game]$Game) { 16 | if (!$Game) {throw "Game object not found, check if AV is blocking the script"} 17 | $c = @(-1, -1) 18 | $tmout = 100 19 | $H, $W = $(gv win | % Value) 20 | while (1) { 21 | $x,$y = $c = $Game.getEmptyCoord() 22 | if (--$tmout -gt 0 -and $x -lt (.1*$W) -or $x -gt (.9*$W) -or $y -lt (.45*$H) -or $y -gt (.95*$H)) {continue} 23 | break 24 | } 25 | return $c 26 | } 27 | function Start-Game([ScriptBlock]$CustomHook = {}) { 28 | [Console]::CursorVisible = $False 29 | cls 30 | $Map = $null 31 | if (($MapFile -ne '/*\') -and (Test-path -type leaf $MapFile)) { 32 | $Map = $MapFile 33 | } elseif ($LoadDefaultSave) { 34 | $Map = $SaveFile 35 | } 36 | $Game = [Game]::New($Map, $Trail) 37 | if (!$Game.Players -and ($Snakes -or $Bots)) { 38 | if ($Snakes) { 39 | 1..$Snakes | % { 40 | $c = Get-PlayerSpawn $Game 41 | $Game.attachPlayer([Snake]::new($c, $Game)) 42 | } 43 | } 44 | if ($Bots) { 45 | 1..$Bots | % { 46 | $c = Get-PlayerSpawn $Game 47 | $Game.attachPlayer([SnakeBot]::new($c, $Game)) 48 | } 49 | } 50 | } 51 | $Game.Debug = gv Debug | % Value 52 | $Game.Run({ 53 | param($Tick) 54 | try { 55 | if (!($Tick % 500)) { 56 | $c = $Game.getEmptyCoord() 57 | # Eventually a boss maybe? 58 | } elseif (!($Tick % 100)) { 59 | $c = $Game.getEmptyCoord() 60 | $Game.attachLiveObject([GoldenSnitch]::New($c, $Game)) 61 | } 62 | $CustomHook.Invoke($Game, $Tick) 63 | } catch { 64 | Write-Host Error ($_ | Out-String) 65 | } 66 | }.GetNewClosure()) 67 | } 68 | function Write-ToPos ([string]$str, [int]$x = 0, [int]$y = 0, [ConsoleColor]$bgc = [console]::BackgroundColor, [ConsoleColor]$fgc = [Console]::ForegroundColor) { 69 | if ($x -lt 0 -or $y -lt 0 -or $x -gt [Console]::WindowWidth -or $y -gt [Console]::WindowHeight) {return} 70 | 71 | try { 72 | $Host.UI.RawUI.SetBufferContents( 73 | $Lib.Rect::new($x, ($WTY+$y), $x, $y), 74 | $Lib.Cell::new( 75 | $str[0], 76 | $fgc, 77 | $bgc, 78 | $Lib.CellType::Complete)) 79 | } catch {} 80 | } 81 | function Modify-Coord ([int]$x, [int]$y, [Direction]$bgc, [int]$Scale = 1) { 82 | switch ([int]$bgc) { 83 | 0 {$x-=$Scale} 84 | 1 {$y-=$Scale} 85 | 2 {$x+=$Scale} 86 | 3 {$y+=$Scale} 87 | } 88 | return @($x,$y) 89 | } 90 | function Place-BufferedContent($Text, $x, $y, [ConsoleColor]$ForegroundColor=[Console]::ForegroundColor, [ConsoleColor]$BackgroundColor=[Console]::BackgroundColor) { 91 | $crd = [Management.Automation.Host.Coordinates]::new($x,$y) 92 | $b = $Host.UI.RawUI 93 | $arr = $b.NewBufferCellArray(@($Text), $ForegroundColor, $BackgroundColor) 94 | $x = [Console]::BufferWidth-1-$Text.length 95 | $b.SetBufferContents($crd, $arr) 96 | } 97 | 98 | #= GAME ENTITIES ==============================================================================| 99 | Get-ChildItem $PSScriptRoot/entities | % { 100 | . $_.FullName 101 | } -------------------------------------------------------------------------------- /entities/1_Game.ps1: -------------------------------------------------------------------------------- 1 | # using module ../PowerSneks_BaseObjects.ps1 2 | 3 | class Game : LiveObject { 4 | [int]$Score = 0 5 | [int]$Ticks = 0 6 | [int]$quits_issued = 0 7 | [Collections.ArrayList]$Players = @() 8 | [Collections.ArrayList]$LiveObjects = @() 9 | [char[][]]$Grid 10 | [ScoreBoard]$ScoreBoard 11 | [GameMode]$GameMode = [GameMode]::SinglePlayer 12 | [Decimal]$SleepAdj = 0 13 | [Decimal]$SlowTime = 0 14 | [Bool]$Paused = $false 15 | [Bool]$Debug = $false 16 | [Management.Automation.Host.BufferCell[,]]$GameBkp 17 | 18 | Game($Map, [bool]$Trail) { 19 | [Console]::BackgroundColor = $(gv ColorMap | % Value).Game.Background 20 | [Console]::Title = "PowerSneks by AP" 21 | if ($Trail) {$this.Tail = '.'} 22 | $this.ScoreBoard = [ScoreBoard]::new($this, [Console]::Title) 23 | $this.loadMap($Map, $true) 24 | } 25 | run() {$this.run({})} 26 | run([ScriptBlock]$Code) { 27 | $Sleep = $(gv GameKnobs | % Value).Sleep.Tick 28 | $this.ticks = 0 29 | $i = 0 30 | while ($this.isAlive()) { 31 | if (!$this.paused) { 32 | $this.ticks++ 33 | $Code.Invoke($this.ticks) 34 | } 35 | $i = ($i + 1) % 5 36 | try {$this.tick()} 37 | catch { 38 | if ("$_" -eq 'Quit') {$this.ScoreBoard.RenderScores();continue} 39 | $this.ScoreBoard.Write("Error: $_") 40 | Write-Host ("$_" | Out-String) 41 | try { 42 | $this.quit() 43 | } catch {throw 'Error-State'} 44 | continue 45 | } 46 | $PixRatio = $(if (!$this.isMultiPlayer() -and $this.Players[0].Direction%2) {$(gv GameKnobs | % Value).Sleep.CursorRatio} else {1}) 47 | if ($this.SlowTime) { 48 | $this.SlowTime = [Math]::Max($this.SlowTime * $(gv GameKnobs | % Value).SlowTimeDeteriorate, 3)-3 49 | } 50 | $SS = [Math]::Max((($Sleep + $this.SleepAdj + $this.SlowTime) * $PixRatio), 0) 51 | if ($this.Debug) {Place-BufferedContent "Sleeping for $SS$('.' * $i)$(' '*5)" 3 4} 52 | sleep -m $SS 53 | } 54 | } 55 | loadMap($Map) {$this.loadMap($Map, $false)} 56 | loadMap($Map, [bool]$OptimizeForFirstLoad) { 57 | $this.Grid = 0..($(gv win | % Value)[0]-1) 58 | 0..($(gv win | % Value)[0]-1) | % { 59 | $W = $(gv SymbolMap | % Value).Wall 60 | $Char = $(gv SymbolMap | % Value).Space 61 | if ($_ -in @(0, ($(gv win | % Value)[0]-1))) {$Char = $W} 62 | $this.Grid[$_] = @($W)+((,$Char)*($(gv win | % Value)[1]-2))+@(,$W) 63 | } 64 | $this.drawDefaultGrid() 65 | if ($Map) { 66 | $GRID_s = @() 67 | $Launch = @{} 68 | $Loader = 0 69 | if ($Map -is [array]) { 70 | $Grid_s = $Map 71 | $this.drawGrid($OptimizeForFirstLoad) 72 | $this.ScoreBoard.write() 73 | $this.refreshScreen() 74 | } else { 75 | cat "$Map" | % { 76 | if ($_ -match "AP\|\-*>") {$Loader++;} else { 77 | if ($Loader -eq 1) {$Tr = $_.split("=").trim().trim(".");$Launch += @{$TR[0] = ($TR[1..($tr.count-1)] -join(""))}} 78 | if ($Loader -eq 2) {$GRID_s += ,@("$_".toCharArray())} 79 | } 80 | } 81 | } 82 | if ($Grid_s.count -eq 0) {$this.ScoreBoard.Write("Invalid Map File ... Will Load Normal Game");return} 83 | $i=$j=0 84 | 1..$GRID_s.length | % {$i=$_-1 85 | if ($i -ge $this.Grid.length) {return} 86 | 1..$this.Grid[$i].length | % {$j=$_-1 87 | if ($j -ge $this.Grid[$i].length) {return} 88 | $this.Grid[$i][$j] = $GRID_s[$i][$j] 89 | } 90 | } 91 | $this.drawGrid($OptimizeForFirstLoad) 92 | $this.deSerializeState(($Launch.Game | ConvertFrom-Json)) 93 | $this.ScoreBoard.write() 94 | $this.refreshScreen() 95 | return 96 | } 97 | $this.generateFood() 98 | } 99 | drawDefaultGrid() { 100 | $Wall = @() 101 | $WFill = @() 102 | 0..3 | % { 103 | $Wall += $Lib.Rect::new(0,0,0,0) 104 | $WFill += New-Object System.Management.Automation.Host.BufferCell 105 | if ($_%2 -eq 0) {$Wall[$_].Bottom = $(gv win | % Value)[0]-1} 106 | else {$Wall[$_].Left++;$Wall[$_].Right = $(gv win | % Value)[1]-2} 107 | } 108 | # ------------- 109 | "Left","Right" | % {$Wall[2].$_ = $(gv win | % Value)[1]-1} 110 | "Top","Bottom" | % {$Wall[3].$_ = $(gv win | % Value)[0]-1} 111 | $Ch = $(gv CharMap | % Value).Wall 112 | 0,2 | % {$WFill[$_].Character = $Ch.Vertical} #475 113 | $WFill[1].Character = $Ch.Top 114 | $WFill[3].Character = $Ch.Bottom 115 | # ------------- 116 | 0..3 | % {$WFill[$_].ForegroundColor = $(gv ColorMap | % Value).Wall;$WFill[$_].BackgroundColor = $(gv ColorMap | % Value).Wall} 117 | #= BASIC-WALL =============| 118 | 0..3 | % {$Global:Host.UI.RawUI.SetBufferContents($Wall[$_],$WFill[$_])} 119 | $OWall = $Wall[0,2,3] 120 | "Top","Bottom" | % {$prp = $_;0..2 | % {$OWall[$_].$prp += 2}} 121 | 0..2 | % {$Global:Host.UI.RawUI.SetBufferContents($OWall[$_],$WFill[0])} 122 | "Top","Bottom" | % {$prp = $_;0..2 | % {$OWall[$_].$prp -= 2}} 123 | } 124 | drawGrid() {$this.drawGrid($false)} 125 | drawGrid([bool]$FastLoad) {$this.drawGrid($FastLoad, 'Loading Map', -1)} 126 | drawGrid([bool]$FastLoad, [int]$Lines) {$this.drawGrid($FastLoad, 'Loading Map', $Lines)} 127 | drawGrid([bool]$FastLoad, [String]$Message) {$this.drawGrid($FastLoad, $Message, -1)} 128 | drawGrid([int]$Lines) {$this.drawGrid($false, 'Loading Map', $Lines)} 129 | drawGrid([String]$Message, [int]$Lines) {$this.drawGrid($false, $Message, $Lines)} 130 | drawGrid([bool]$FastLoad, [String]$Message, [int]$Lines) { 131 | $H, $W = $(gv win | % Value) 132 | $Lines = $(if ($Lines -gt 0) {[Math]::Min($H-1, $Lines)} else {$H-1}) 133 | foreach ($p in 0..$Lines) { 134 | $this.ScoreBoard.write("$Message : $([Math]::Floor($p/$Lines*100))%") 135 | foreach ($d in ($W-1)..0) { 136 | if ($p -gt 32) { 137 | # [Console]::CursorLeft=0*([Console]::CursorTop=0) 138 | # Write-AP "nx-<", 139 | # "nx!$(print-list $p,$d)", 140 | # "nx# vs ", 141 | # "nx+$(print-list $GRID.length,$GRID[$p].length)", 142 | # "nx# vs ", 143 | # "nx*$(print-list $win)", 144 | # "nx->" 145 | } 146 | $Kt = $this.Grid[$p][$d] 147 | if ($FastLoad -and $Kt -eq $(gv SymbolMap | % Value).Space -and !($p -in 0,($H-1)) -and !($d -in 0,($W-1))) {continue} 148 | $this.RenderPoint($d, $p) 149 | } 150 | } 151 | } 152 | pause() { 153 | $this.paused = !$this.paused 154 | if (!$this.paused) { 155 | $this.ScoreBoard.write() 156 | $Global:Host.UI.RawUI.SetBufferContents($(gv Lib | % Value).Coord::new(0,0), $this.GameBKP) 157 | return 158 | } 159 | $Area = $(gv Lib | % Value).Rect::new(0, 0, $(gv win | % Value)[1]+1, $(gv win | % Value)[0]-2) 160 | $this.GameBKP = $Global:Host.UI.RawUI.GetBufferContents($Area) 161 | $Fill = $(gv Lib | % Value).Cell::new(' ', 'Black', 'Black', 0) 162 | $Global:Host.UI.RawUI.SetBufferContents($Area, $Fill) 163 | 164 | $Colm = $(gv Lib | % Value).Rect::new(0, 0, 0, $(gv win | % Value)[0]-2) 165 | $Fill = $(gv Lib | % Value).Cell::new($(gv CharMap | % Value).Symbol.Food, 'DarkGray', $(gv ColorMap | % Value).Game.Background, 0) 166 | 1..[Math]::Floor($(gv win | % Value)[1]/3) | % { 167 | $Colm.Right = ($Colm.Left = $_ * 3 - 2) 168 | # Write-Host ($Colm | Out-String) 169 | $Global:Host.UI.RawUI.SetBufferContents($Colm, $Fill) 170 | } 171 | $this.ScoreBoard.write('This game is now paused, press "p" to unpause!') 172 | } 173 | attachPlayer([Player]$obj) { 174 | $obj.Id = @($this.Players).Length + 1 175 | $this.Players += $obj 176 | if ($obj.Id -gt 1) {$this.GameMode = [GameMode]::MultiPlayer} 177 | $this.ScoreBoard.Write() 178 | } 179 | attachLiveObject([LiveObject]$obj) { 180 | $obj.Id = @($this.LiveObjects).Length + 1 181 | $this.LiveObjects += $obj 182 | } 183 | [bool]isMultiPlayer() {return $this.GameMode -eq [GameMode]::MultiPlayer} 184 | sendPlayer([Direction]$Dir) {$this.sendPlayer($Dir, 1)} 185 | sendPlayer([Direction]$Dir, [int]$Player) { 186 | $P1, $P2 = $this.Players 187 | if (!$this.isMultiPlayer()) {$P1.turn($Dir)} 188 | $this.Players | ? {$_.ID -eq $Player} | % {$_.turn($Dir)} 189 | } 190 | ShootLaser($P) { 191 | if (!($P -is [Player])) {Write-Host 'Object passed is not an instance of Player!' ($P | Out-String);$this.quit();return} 192 | if ($P.Lasers -le 0 -and $P.Lasers -ne -100) {$this.ScoreBoard.write("- - - - - - - - P$($P.Id) No More Lasers - - - - - - - -");return} 193 | if ($P.Lasers -gt 0) {$P.Lasers--} 194 | $x, $y = $P.Head 195 | 196 | # Laser Bomb 197 | $xRad, $yRad = $(gv GameKnobs | % Value).Laser.BombRadius 198 | if ($xRad -gt 0 -and $yRad -gt 0) { 199 | try { 200 | -$yRad..$yRad | % { 201 | $ny = $y + $_ 202 | $eraseX = -$xRad..$xRad | ? { 203 | $nx = $x + $_ 204 | $this.laserRemoveBlock($nx, $ny, 1) 205 | } 206 | $eraseX | % { 207 | $nx = $x + $_ 208 | $this.laserRemoveBlock($nx, $ny, 2) 209 | } 210 | } 211 | $P.draw() 212 | } catch { 213 | Write-Host "-Error$_" 214 | sleep 3 215 | } 216 | } 217 | 218 | # Laser Beam 219 | if ($P.Direction -in 0,2) { 220 | $Set = 0..($(gv win | % Value)[1]-1) 221 | if ($P.Direction -eq 0) {[Array]::Reverse($Set)} 222 | foreach ($p in $Set) { 223 | $this.laserRemoveBlock($p, $y) 224 | } 225 | } else { 226 | $Set = 0..($(gv win | % Value)[0]-1) 227 | if ($P.Direction -eq 1) {[Array]::Reverse($Set)} 228 | foreach ($p in $Set){ 229 | $this.laserRemoveBlock($x, $p) 230 | } 231 | } 232 | $this.ScoreBoard.write() 233 | } 234 | [bool]laserRemoveBlock([int]$x, [int]$y) {return $this.laserRemoveBlock($x, $y, 0)} 235 | [bool]laserRemoveBlock([int]$x, [int]$y, [int]$Mode) { 236 | $x, $y = [Game]::WarpCoords($x, $y) 237 | if ($Mode -ne 2 -and $this.Grid[$y][$x] -ne $(gv SymbolMap | % Value).Wall) {return $false} 238 | if ($Mode -lt 2) { 239 | $Laser = $(gv CharMap | % Value).Laser 240 | $this.Grid[$y][$x] = $(gv SymbolMap | % Value).Space 241 | Write-ToPos $Laser -y $y -x $x -fgc $(gv ColorMap | % Value).Laser 242 | } 243 | if ($Mode -ne 1) { 244 | sleep -m $(gv GameKnobs | % Value).Laser.PrintDelay 245 | $this.RenderPoint($x, $y) 246 | } 247 | return $true 248 | } 249 | readKeys() { 250 | while ($Global:Host.UI.RawUI.KeyAvailable) { 251 | $Store = $Global:Host.UI.RawUI.ReadKey("NoEcho,IncludeKeydown,IncludeKeyUp") 252 | if (!$Store.KeyDown) {Continue} 253 | 254 | #Critical Hotkeys 255 | If (KeyPressed "~~Escape~~" $Store) {$this.ScoreBoard.write(". . . QUIT Signal . . .");$this.quit()} 256 | ElseIf (KeyPressed "p" $Store) {$this.pause()} 257 | ElseIf (KeyPressed "~~F1~~" $Store) {$this.ScoreBoard.RenderScores()} 258 | ElseIf (KeyPressed '`' $Store) { 259 | $this.Debug = !$this.Debug 260 | if ($this.Debug) {return} 261 | $this.drawGrid('Removing debug statements', 10) 262 | $this.ScoreBoard.Write() 263 | 'Players','LiveObjects' | % { 264 | foreach ($ob in $this.$_) { 265 | if ($ob.head[1] -gt 10) {continue} 266 | $ob.draw($true) 267 | } 268 | } 269 | } 270 | 271 | #Game Hotkeys 272 | $P2 = $(if($this.isMultiPlayer()) {1}else{0}) 273 | $P3 = $(if($this.isMultiPlayer()) {2}else{0}) 274 | $P4 = $(if($this.isMultiPlayer()) {3}else{0}) 275 | if ($this.paused) {continue} 276 | ElseIf (KeyPressed "~~left~~" $Store) {$this.sendPlayer(0)} 277 | ElseIf (KeyPressed "~~up~~" $Store) {$this.sendPlayer(1)} 278 | ElseIf (KeyPressed "~~right~~" $Store) {$this.sendPlayer(2)} 279 | ElseIf (KeyPressed "~~down~~" $Store) {$this.sendPlayer(3)} 280 | ElseIf (KeyPressed "A" $Store) {$this.sendPlayer(0, 2)} 281 | ElseIf (KeyPressed "W" $Store) {$this.sendPlayer(1, 2)} 282 | ElseIf (KeyPressed "D" $Store) {$this.sendPlayer(2, 2)} 283 | ElseIf (KeyPressed "S" $Store) {$this.sendPlayer(3, 2)} 284 | ElseIf (KeyPressed "J" $Store) {$this.sendPlayer(0, 3)} 285 | ElseIf (KeyPressed "I" $Store) {$this.sendPlayer(1, 3)} 286 | ElseIf (KeyPressed "L" $Store) {$this.sendPlayer(2, 3)} 287 | ElseIf (KeyPressed "K" $Store) {$this.sendPlayer(3, 3)} 288 | ElseIf (KeyPressed "~~KP4~~" $Store) {$this.sendPlayer(0, 4)} 289 | ElseIf (KeyPressed "~~KP8~~" $Store) {$this.sendPlayer(1, 4)} 290 | ElseIf (KeyPressed "~~KP6~~" $Store) {$this.sendPlayer(2, 4)} 291 | ElseIf (KeyPressed "~~KP5~~" $Store) {$this.sendPlayer(3, 4)} 292 | ElseIf (KeyPressed "-" $Store) {$this.SleepAdj += 2} 293 | ElseIf (KeyPressed "+" $Store) {$this.SleepAdj -= 2} 294 | ElseIf (KeyPressed "~~Backspace~~","~~End~~","~~ControlRight~~" $Store) {$this.ShootLaser($this.Players[0])} 295 | ElseIf (KeyPressed "~~ControlLeft~~","C" $Store) {$this.ShootLaser($this.Players[$P2])} 296 | ElseIf (KeyPressed "~~space~~","{" $Store) {if (!$P3 -or $this.Players.length -ge 3) {$this.ShootLaser($this.Players[$P3]);return} $this.ScoreBoard.write("No Player 3")} 297 | ElseIf (KeyPressed "~~KP0~~","~~KP-Enter~~" $Store) {if (!$P4 -or $this.Players.length -ge 4) {$this.ShootLaser($this.Players[$P4]);return} $this.ScoreBoard.write("No Player 4")} 298 | ElseIf (KeyPressed "." $Store) {$this.generateFood()} 299 | ElseIf (KeyPressed "~~f12~~" $Store) {$this.ScoreBoard.write("Loading Full Screen");cmd /c start /max conhost cmd /k "echo ""Loading Sneks..."" & ""$(Get-PowerShellProcessPath)"" -ep bypass -noprofile $PSScriptRoot\..\PowerSneks.ps1";$this.quit()} 300 | ElseIf (KeyPressed "~~Tab~~" $Store) {$this.ScoreBoard.write(". . . Saving Game State . . .");$this.SaveMap();$this.ScoreBoard.write(". . . Saved Game State . . .")} 301 | ElseIf (KeyPressed "~~f5~~" $Store) {$this.refreshScreen()} 302 | } 303 | } 304 | saveMap() {$this.saveMap($(gv SaveFile | % Value))} 305 | saveMap($File) { 306 | md (Split-Path $File) -Force | Out-Null 307 | attrib -h -r -s +a $File 2>&1 | Out-Null 308 | "This is a Save File from PowerSneks by Apoorv Verma [AP] 309 | Copyright 2025. All Rights Reserved 310 | AP|---------------------------------> 311 | >>Game......= $($this.serializeState() | ConvertTo-Json -Compress) 312 | >>Window....= $(gv win | % Value) 313 | AP|--------------------------------->" | % {$_.replace((" "*4),"").replace(">>",(" "*4))} | out-file -Encoding ascii $File 314 | $this.Grid | % {$_ -join("")} | Out-File -Append -Encoding ascii $File 315 | attrib +h +r +s -a $File 2>&1 | Out-Null 316 | } 317 | [object] serializeState() { 318 | return @{ 319 | Score = $this.Score 320 | Ticks = $this.Ticks 321 | Players = $this.Players | % {$_.serializeState()} 322 | LiveObjects = $this.LiveObjects | % {$_.serializeState()} 323 | GameMode = $this.GameMode 324 | SleepAdj = $this.SleepAdj 325 | } 326 | } 327 | deSerializeState([Object]$State) { 328 | ([Serializeable]$this).deSerializeState($state) 329 | 'Players','LiveObjects' | % {$cl = $_ 330 | $this.$cl = @($State.$cl | ? {$_.Class} | % { 331 | # Write-Host Loading $_.Class 332 | $o = New-Object $_.Class (0,0),$this 333 | $o.deSerializeState($_) 334 | if ($o.respawn) {$o.respawn()} 335 | return $o 336 | }) 337 | } 338 | $this.ScoreBoard.Write() 339 | } 340 | refreshScreen() { 341 | $OP = $(if([console]::WindowHeight -gt 3){-1}else{1}) 342 | [console]::WindowHeight += $OP 343 | [console]::WindowHeight -= $OP 344 | } 345 | [int[]] getEmptyCoord() { 346 | $H, $W = $(gv win | % Value) 347 | $TmOut = .5*$W*$H 348 | while ($true) { 349 | $y = Get-Random -min 0 -max $H 350 | $x = Get-Random -min 0 -max $W 351 | # Write-AP "*Finding $x,$y = [$($this.Grid[$y][$x])]" 352 | if ($tmout -le 0) {break} 353 | $TmOut-- 354 | if ($this.Grid[$y][$x] -eq $(gv SymbolMap | % Value).Space) {return $x,$y} 355 | } 356 | $this.ScoreBoard.write("Game Took Too Long to Find Coords") 357 | $this.quit() 358 | return -1,-1 359 | } 360 | quit() { 361 | $this.quits_issued++ 362 | [Console]::Title = $(gv TitleBkp | % Value) 363 | [Console]::BackGroundColor = $(gv BkColBkp | % Value) 364 | $Global:Host.UI.RawUI.FlushInputBuffer() 365 | try { 366 | [Console]::CursorTop = $(gv win | % Value)[0]+2 367 | } catch { 368 | # Fix for windows terminal like emulators 369 | Write-Host -NoNewLine ("`n"*($(gv win | % Value)[0]+2)) 370 | } 371 | $this.die() 372 | if ($this.quits_issued -gt 20) { 373 | $this.ScoreBoard.write('Game cannot quit gracefully, force quitting!') 374 | exit 375 | } 376 | throw 'Quit' 377 | } 378 | generateFood() { 379 | $x, $y = $this.getEmptyCoord() 380 | $this.Grid[$y][$x] = $(gv SymbolMap | % Value).Food 381 | Write-ToPos $(gv CharMap | % Value).Symbol.Food -y $y -x $x -fgc $(gv ColorMap | % Value).Food 382 | } 383 | tick() { 384 | $this.readKeys() 385 | if ($this.paused) {return} 386 | $ToRemove = @() 387 | foreach ($Ob in $this.LiveObjects) { 388 | if ($this.Debug) {Place-BufferedContent ("Ticking: {0,-16} {1}" -f "LiveObject $($Ob.ID)",$This.Ticks) 3 3} 389 | $ob.tick() 390 | if ($ob.isAlive()) {$ob.draw()} 391 | } 392 | if ($this.Debug) { 393 | $Dirs = $($this.Players | % {$_.Direction} | select -f 4) 394 | $Curs = $($this.Players | % {"{$($_.Head -join ', ')}"} | select -f 4) 395 | $c = " P> Direction: $Dirs | Old Position: $Curs" 396 | Place-BufferedContent $c ($(gv win | % Value)[1]-2-$c.length) 3 397 | } 398 | foreach ($Player in $this.Players) { 399 | if ($this.Debug) {Place-BufferedContent ("Ticking: {0,-16} {1}" -f "Player $($Player.ID)",$This.Ticks) 3 3} 400 | $Player.tick() 401 | } 402 | if ($this.Debug) { 403 | $c = " New Position: $($this.Players | % {"{$($_.Head -join ', ')}"} | select -f 4)" 404 | Place-BufferedContent $c ($(gv win | % Value)[1]-2-$c.length) 4 405 | } 406 | foreach ($Player in $this.Players) { 407 | if ($this.Debug) {Place-BufferedContent ("Drawing: {0,-16} {1}" -f "Player $($Player.ID)",$This.Ticks) 3 3} 408 | $Player.draw() 409 | if ($Player.isAlive()) {continue} 410 | $ToRemove += $Player 411 | } 412 | $ToRemove | % {$this.Players.Remove($_)};$ToRemove = @() 413 | foreach ($Ob in $this.LiveObjects) { 414 | if ($Ob.isAlive()) {continue} 415 | $ToRemove += $Ob 416 | } 417 | $ToRemove | % {$this.LiveObjects.Remove($_)} 418 | [int]$RemainingPlayers = @($this.Players).length 419 | if ($this.Debug) { 420 | $C = "[$(if($this.isMultiPlayer()){' Multi'}else{'Single'})Player] Loaded Players: $RemainingPlayers" 421 | Place-BufferedContent $C -y 2 -x ($(gv win | % Value)[1]-$C.length-2) 422 | } 423 | if ($this.isMultiPlayer() -and $RemainingPlayers -eq 1) { 424 | $this.ScoreBoard.Write("Player $($this.Players[0].Id) has won the game!") 425 | $this.quit() 426 | return 427 | } 428 | if ($RemainingPlayers -eq 0) {$this.quit()} 429 | } 430 | digestFood($x, $y) { 431 | $this.incrementScore() 432 | $this.Grid[$y][$x] = $(gv CharMap | % Value).Space 433 | $this.generateFood() 434 | $this.SleepAdj += $(gv GameKnobs | % Value).Sleep.AdjPerFood 435 | $this.createObstruction() 436 | $this.ScoreBoard.write() 437 | } 438 | createObstruction() { 439 | $Coord = @() 440 | $H, $W = $(gv win | % Value) 441 | $tmout = 40 # Since this is tmout x (.5 * H * W) which gets pretty big 442 | $HDist, $WDist = $H, $W | % {[Math]::Ceiling($(gv GameKnobs | % Value).Obstruction.SnakeDistanceRatio*$_)} 443 | $ValidatePoint = {param($tx,$ty,$px,$py) 444 | return ( 445 | [Math]::Abs($tx-$px) -gt $WDist 446 | ) -and ( 447 | [Math]::Abs($ty-$py) -gt $HDist 448 | ) 449 | } 450 | While ($True) { 451 | $x, $y = $Coord = $this.getEmptyCoord() 452 | $fail = 0 453 | foreach ($P in $this.Players) { 454 | $px, $py = $P.Head 455 | if (!$ValidatePoint.invoke($x, $y, $px, $py)) {$fail++;break} 456 | } 457 | if (!$fail) {break} 458 | if ($tmout-- -le 0) {$this.ScoreBoard.Write("Game Took Too Long to Find Non-Impact Coords");$this.quit();return} 459 | } 460 | $x, $y = $Coord 461 | $xLen = get-random -min 1 -max ($W*$(gv GameKnobs | % Value).Obstruction.MaxWidth+1) 462 | $yLen = get-random -min 1 -max ($H*$(gv GameKnobs | % Value).Obstruction.MaxHeight+1) 463 | $Items2Respawn = @() 464 | foreach ($p in $y..($y+$yLen)) { 465 | foreach ($d in $x..($x+$xLen)) { 466 | $nx,$ny = [Game]::WarpCoords($d,$p) 467 | if ($this.Grid[$ny][$nx] -eq $(gv SymbolMap | % Value).Wall) {continue} 468 | if ($this.Grid[$ny][$nx] -eq $(gv SymbolMap | % Value).Food) { 469 | $this.generateFood() 470 | } else { 471 | foreach ($Ob in $this.Players) { 472 | if (![Game]::CoordsEqual($Ob.Head, ($nx, $ny))) {continue} 473 | $Items2Respawn += $Ob 474 | $Ob.erase() 475 | } 476 | foreach ($Ob in $this.LiveObjects) { 477 | if (!$Ob.canCollide -or ![Game]::CoordsEqual($Ob.Head, ($nx, $ny))) {continue} 478 | $Items2Respawn += $Ob 479 | if ($Ob.erase) {$Ob.erase()} 480 | } 481 | } 482 | $this.Grid[$ny][$nx] = $(gv SymbolMap | % Value).Wall 483 | Write-ToPos $(gv CharMap | % Value).Wall.Vertical -y $ny -x $nx -fgc $(gv ColorMap | % Value).Wall -bgc $(gv ColorMap | % Value).Wall 484 | } 485 | } 486 | $this.SlowTime = $(gv GameKnobs | % Value).Sleep.SlowTime 487 | foreach ($p in $Items2Respawn) { 488 | $P.Head = $this.getEmptyCoord() 489 | $P.respawn() 490 | } 491 | if ($Items2Respawn) {$this.readySetGo()} 492 | } 493 | readySetGo() { 494 | $Global:Host.UI.RawUI.FlushInputBuffer() 495 | $this.ScoreBoard.write(". . . READY . . .") 496 | Start-Sleep -s 1 497 | $this.ScoreBoard.write(". . . STEADY . . .") 498 | Start-Sleep -s 1 499 | $this.ScoreBoard.write(". . . ! GO ! . . .") 500 | } 501 | checkCollision([Player]$Obj, [int[]]$pt) {$this.checkCollision($obj, $pt[0], $pt[1])} 502 | checkCollision([Player]$Obj, [int]$x, [int]$y) { 503 | $Symbol = $this.Grid[$y][$x] 504 | $Key = $(gv MapToSymbol | % Value).$Symbol 505 | if (!$Key) {Write-Error "Cannot translate $Symbol to Key, fatal";$this.exit();return} 506 | if ($this.Debug) {Place-BufferedContent "Current Block: $Key " 2 2} 507 | if ($Key -eq 'Food') { 508 | $this.digestFood($x,$y) 509 | $obj.incrementScore() 510 | return 511 | } 512 | if ($Key -eq 'Wall') { 513 | $obj.collided("Player $($Obj.Id) hit a wall!") 514 | if ($this.isMultiPlayer()) {$obj.erase()} 515 | return 516 | } 517 | foreach ($o in $this.LiveObjects) { 518 | if (!$o.collided) {continue} 519 | $dist = [Game]::CoordsDistance($o.head, $obj.Head) - $o.Size 520 | if ($this.Debug) {Place-BufferedContent "Distance-O: $dist " 3 5} 521 | if ($dist -gt 0) {continue} 522 | if (!$o.checkForCollision($obj.getCollideablePoints())) {continue} 523 | $o.collided($obj) 524 | } 525 | if (!$this.isMultiPlayer()) {return} 526 | foreach ($p in $this.Players) { 527 | if ($p.Id -eq $Obj.Id) {continue} 528 | $dist = [Game]::CoordsDistance($p.head, $obj.Head) - $p.Size 529 | if ($this.Debug) {Place-BufferedContent "Distance-P: $dist " 3 5} 530 | if ($dist -gt 0) {continue} 531 | if (!$p.checkForCollision($obj.getCollideablePoints())) {continue} 532 | $obj.collided("Player $($Obj.ID) died by colliding into Player $($p.ID)") 533 | } 534 | } 535 | RenderPoint([int[]]$Coord) {$this.RenderPoint($Coord[0], $Coord[1])} 536 | RenderPoint([int]$x, [int]$y) { 537 | $Symbol = $this.Grid[$y][$x] 538 | $Key = JS-OR $(gv MapToSymbol | % Value).$Symbol 'Space' 539 | $Char = $(gv CharMap | % Value).Symbol.$Key 540 | if ($Key -eq 'Wall' -and $x -notin (0, $(gv win | % Value)[1])) { 541 | if ($y -eq 0) {$Char = $(gv CharMap | % Value).Wall.Top} 542 | if ($y -eq $(gv win | % Value)[0]-1) {$Char = $(gv CharMap | % Value).Wall.Bottom} 543 | } 544 | $Bg = [Console]::BackgroundColor 545 | $Fg = $(gv ColorMap | % Value).$Key 546 | if ($Key -eq 'Wall') {$Bg = $Fg} 547 | Write-ToPos $Char $x $y -f $Fg -b $Bg 548 | } 549 | static [bool] CoordsEqual([int[]]$A,[int[]]$B) { 550 | return $A[0] -eq $B[0] -and $A[1] -eq $B[1] 551 | } 552 | static [int] CoordsDistance([int[]]$A,[int[]]$B) { 553 | return [Math]::Abs($A[0] - $B[0]) + [Math]::Abs($A[1] - $B[1]) 554 | } 555 | static [int[]] WarpCoords([int[]]$c) {return [Game]::WarpCoords($c[0], $c[1])} 556 | static [int[]] WarpCoords($x, $y) { 557 | $H, $W = $(gv win | % Value) 558 | if ($x -lt 0) {$x += $W} 559 | if ($y -lt 0) {$y += $H} 560 | return ($x%$W), ($y%$H) 561 | } 562 | } 563 | 564 | class ScoreBoard { 565 | [String]$FallBackTitle = '' 566 | [Management.Automation.Host.Rectangle]$ScoreBoard 567 | [Management.Automation.Host.BufferCell]$SBFiller 568 | [Game]$Game 569 | [Bool]$ShowingScores 570 | [Hashtable]$ScoreMap = @{} 571 | [Management.Automation.Host.BufferCell[,]]$BufferBkp 572 | 573 | ScoreBoard([Game]$Game, [String]$Title) { 574 | $this.FallBackTitle = $Title 575 | $this.Game = $Game 576 | $W, $H = gv win | % Value 577 | $this.ScoreBoard = $(gv Lib | % Value).Rect::new(1, $W, $H-2, $W) 578 | $this.SBFiller = $(gv Lib | % Value).Cell::new(' ', $(gv ColorMap | % Value).Game.ConsoleText, $(gv ColorMap | % Value).Game.ConsoleTextBg, 0) 579 | $Global:Host.UI.RawUI.SetBufferContents($this.ScoreBoard,$this.SBFiller) 580 | $this.Write() 581 | } 582 | RenderScores() { 583 | $LIB = gv Lib | % Value 584 | if ($this.Game.paused -and !$this.ShowingScores) {return} 585 | $this.ShowingScores = !$this.ShowingScores 586 | $this.Game.paused = $this.ShowingScores 587 | $Global:PowerSneks_Scores = $this.ScoreMap 588 | $SortingOrder = @{Expression='Score'; Descending=$true},'Player' 589 | $Table = $this.ScoreMap.GetEnumerator() | % {[PsCustomObject]@{Player=$_.Key;' '='';Score=$_.Value}} | sort $SortingOrder | Out-String 590 | $Table = ("PowerSneks Score-Board`n$('-'*22)`n*-*`n"+$Table) -Split "`n|\s{30,}" | % {"$_".TrimEnd()} | ? {"$_" -replace '\s+'} 591 | $TW = ($Table | % Length | Measure -Max | % Maximum)+2 592 | $TH = ($Table.Length) + 2 593 | $H, $W = gv win | % Value 594 | $X, $Y = (($W - $TW) / 2),(($H - $TH) / 2) | % {[Math]::Floor($_)} 595 | if (!$this.ShowingScores) { 596 | $Global:Host.UI.RawUI.SetBufferContents($LIB.Coord::new($X-1,$Y-1), $this.BufferBkp) 597 | return 598 | } 599 | $Area = $LIB.Rect::new($X-1, $Y-1, $X+$TW+1+1, $Y+$TH+2+1) 600 | $this.BufferBkp = $Global:Host.UI.RawUI.GetBufferContents($Area) 601 | $Fill = $LIB.Cell::new(' ', 'Black', 'DarkRed', 0) 602 | $Global:Host.UI.RawUI.SetBufferContents($Area, $Fill) 603 | 604 | $Area = $LIB.Rect::new($X, $Y, $X+$TW+1, $Y+$TH+2) 605 | $Fill = $LIB.Cell::new(' ', 'Black', 'Red', 0) 606 | $Global:Host.UI.RawUI.SetBufferContents($Area, $Fill) 607 | 608 | 1..[Math]::Floor($TH-1) | % { 609 | $Color = 'white' 610 | if ($_ -le 5) {$Color = 'yellow'} 611 | $L = $Table[$_-1] 612 | if (!$L -or !$L.trim() -or $L -eq '*-*') {return} 613 | if ($L.Length -ne $TW) { 614 | $L = $(' '*(($TW-$L.Length)/2))+$L 615 | } 616 | $L = $L -replace '- -+ -','- -' -replace '-',([char]9472) 617 | Place-BufferedContent "$L" -x ($X+1) -y ($Y+$_) -b Red -f $Color 618 | } 619 | } 620 | Write() {$this.write($this.FallBackTitle)} 621 | Write([String]$title) { 622 | $title = $title 623 | $Colors = $(gv ColorMap | % Value).Game 624 | $SB, $SF = $this.ScoreBoard, $this.SBFiller 625 | $Global:Host.UI.RawUI.SetBufferContents($SB, $SF) 626 | $this.Game.Players | % { 627 | $this.ScoreMap[$_.Id] = $_.Score 628 | } 629 | $Lasers = JS-OR (($this.Game.Players | % {$_.Lasers} | % {if ($_ -eq -100) {[char]8734} else {$_}}) -join '|') '0' 630 | $Scores = JS-OR (($this.Game.Players | % {$_.Score}) -join '|') '0' 631 | $LaserText = " Lasers : $Lasers " 632 | $ScoreText = " SCORE : $Scores " 633 | $OffSetS = $ScoreText.Length 634 | $OffSetL = 1 + $LaserText.Length 635 | $OffSet = $OffSetS+$OffsetL 636 | $Area = $SB.Right-$SB.Left-$Offset 637 | $Y = $(gv win | % Value)[0] 638 | if ($Title.length -gt $Area) {$Title = $Title.substring(0,$Area-3)+"..."} 639 | Place-BufferedContent (Align-Text $Title).substring([Math]::Floor($OffSet/2)) 1 $Y -f $Colors.ConsoleText -b $Colors.ConsoleTextBg 640 | # Dividers 641 | $X_Off = $SB.Left + $Area 642 | $DIV = $(gv CharMap | % Value).Wall.Vertical 643 | $X_Off,($X_Off + $OffsetL) | % { 644 | Place-BufferedContent $DIV $_ $Y -f $Colors.ConsoleBorder -b $Colors.ConsoleTextBg 645 | } 646 | Place-BufferedContent $LaserText ($X_Off+1) $Y -f $Colors.ConsoleText -b $Colors.ConsoleTextBg 647 | Place-BufferedContent $ScoreText ($X_Off+$OffsetL+1) $Y -f $Colors.ConsoleText -b $Colors.ConsoleTextBg 648 | } 649 | } 650 | --------------------------------------------------------------------------------