Functions/GenXdev.Console/Start-SnakeGame.ps1
|
<##############################################################################
Part of PowerShell module : GenXdev.Console Original cmdlet filename : Start-SnakeGame.ps1 Original author : René Vaessen / GenXdev Version : 2.3.2026 ################################################################################ Copyright (c) René Vaessen / GenXdev Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ################################################################################> # filepath: Start-SnakeGame.ps1 ############################################################################### <# .SYNOPSIS Starts a simple Snake game in the console. .DESCRIPTION This function initializes and runs a basic Snake game within the PowerShell console. The player controls the snake using the arrow keys or WASD keys, aiming to eat food and grow longer while avoiding collisions with the walls or itself. The game features dynamic speed adjustment based on available space and snake length. By default, the console is cleared before starting. .PARAMETER InitialLength Sets the initial length of the snake. Valid range is 3-20 segments. Default is 5 segments. .PARAMETER Speed Sets the base game speed in milliseconds between moves. Lower values create a faster game. Valid range is 50-2000ms. Default is 300ms. Actual speed adjusts dynamically during gameplay. .PARAMETER NoClear Prevents clearing the console before starting the game. By default, the console is cleared to provide a clean playing field. .PARAMETER WithMaze Draws a maze within the playfield using ASCII drawing characters for walls and lines, similar to the border. .PARAMETER ShowRoute Displays the shortest path from the snake's head to the food using small green centered dots (·). The dots are visual overlays only and do not affect collision detection. .PARAMETER MazeWidth Sets the minimum pathway width for the maze. Valid range is 1-10. Default is 2. Higher values create wider pathways making the maze easier to navigate. .EXAMPLE Start-SnakeGame Starts the Snake game with default settings (5 segments, 300ms speed). .EXAMPLE Start-SnakeGame -NoClear -InitialLength 3 -Speed 200 Starts the Snake game without clearing console, with shorter snake and faster speed. .EXAMPLE snake -InitialLength 10 Starts the game using the alias with a longer initial snake. .EXAMPLE Start-SnakeGame -WithMaze Starts the Snake game with a maze in the playfield. .EXAMPLE Start-SnakeGame -WithMaze -ShowRoute Starts the Snake game with a maze and displays the shortest path from the snake to the food with green dots. .EXAMPLE Start-SnakeGame -WithMaze -MazeWidth 5 Starts the Snake game with a maze that has wider pathways (minimum 5 spaces) for easier navigation. #> ############################################################################### function Start-SnakeGame { [CmdletBinding(SupportsShouldProcess)] [Alias('snake')] param( ########################################################################### [Parameter( Mandatory = $false, Position = 0, HelpMessage = "Initial length of the snake (default: 5)" )] [ValidateRange(3, 20)] [int] $InitialLength = 5, ########################################################################### [Parameter( Mandatory = $false, Position = 1, HelpMessage = ( "Game speed in milliseconds between moves (default: 300)" ) )] [ValidateRange(50, 2000)] [int] $Speed = 300, ########################################################################### [Parameter( Mandatory = $false, HelpMessage = ( "Prevents clearing the console before starting the game" ) )] [switch] $NoClear, ########################################################################### [Parameter( Mandatory = $false, HelpMessage = ( "Draws a maze within the playfield using ASCII drawing " + "characters for walls and lines, similar to the border" ) )] [switch] $WithMaze, ########################################################################### [Parameter( Mandatory = $false, HelpMessage = ( "Displays the shortest path from the snake's head to the " + "food using small green centered dots" ) )] [switch] $ShowRoute, ########################################################################### [Parameter( Mandatory = $false, HelpMessage = ( "Minimum pathway width for the maze (1-10, default: 2)" ) )] [ValidateRange(1, 10)] [int] $MazeWidth = 2 ########################################################################### ) begin { # initialize game state hashtable to store all game variables $script:gameState = @{ # current console window width in characters width = 0 # current console window height in characters height = 0 # left boundary of playable area (x coordinate) playFieldLeft = 1 # top boundary of playable area (y coordinate) playFieldTop = 1 # right boundary of playable area (x coordinate) playFieldRight = 0 # bottom boundary of playable area (y coordinate) playFieldBottom = 0 # current player score (food items eaten) score = 0 # initial length of snake snakeLength = $InitialLength # current game speed in milliseconds gameSpeed = $Speed # current movement direction direction = 'Right' # list of snake segment positions as tuples snake = $null # current food position as tuple food = $null # array of console lines for collision detection screenBuffer = $null # temporary storage for snake placement snakePositions = $null # temporary x coordinate variable x = 0 # temporary y coordinate variable y = 0 # character to restore when snake moves originalChar = '' # flag to enable debug logging loggingEnabled = $false # string builder for log messages logBuilder = $null # path to log file logPath = '' # timestamp of last direction change LastMoveTime = [DateTime]::UtcNow # position where last direction change occurred LastMoveCoord = $null # list of positions in current route path routePath = $null } # initialize logging infrastructure for debugging purposes $script:gameState.logBuilder = [System.Text.StringBuilder]::new() $script:gameState.logPath = [System.IO.Path]::Join( $PSScriptRoot, 'snake.log' ) # delete old log file to start fresh if ([System.IO.File]::Exists($script:gameState.logPath)) { [System.IO.File]::Delete($script:gameState.logPath) } ########################################################################### # internal logging function to write timestamped debug messages function Log { param([string]$Line) if ($script:gameState.loggingEnabled) { $timestamp = Microsoft.PowerShell.Utility\Get-Date -Format ( 'yyyy-MM-dd HH:mm:ss.fff' ) $logEntry = "[$timestamp] $Line" $null = $script:gameState.logBuilder.AppendLine($logEntry) } } ########################################################################### # start logging game initialization Log "Starting Snake Game initialization" # capture current console dimensions for playfield calculation $script:gameState.width = [Console]::WindowWidth $script:gameState.height = [Console]::WindowHeight # calculate playfield boundaries avoiding last column and rows $script:gameState.playFieldRight = $script:gameState.width - 2 $script:gameState.playFieldBottom = $script:gameState.height - 3 Log ( "Console dimensions: Width=$($script:gameState.width), " + "Height=$($script:gameState.height)" ) Log ( "Playfield boundaries: Left=$($script:gameState.playFieldLeft), " + "Top=$($script:gameState.playFieldTop), " + "Right=$($script:gameState.playFieldRight), " + "Bottom=$($script:gameState.playFieldBottom)" ) # clear console unless user specified not to if (-not $NoClear) { Clear-Host Log "Screen cleared for border drawing" } # draw border using box drawing characters [Console]::SetCursorPosition(0, 0) Microsoft.PowerShell.Utility\Write-Host '┌' -NoNewline for ($i = 1; $i -lt $script:gameState.width - 1; $i++) { Microsoft.PowerShell.Utility\Write-Host '─' -NoNewline } Microsoft.PowerShell.Utility\Write-Host '┐' -NoNewline for ($y = 1; $y -lt $script:gameState.playFieldBottom + 1; $y++) { [Console]::SetCursorPosition(0, $y) Microsoft.PowerShell.Utility\Write-Host '│' -NoNewline [Console]::SetCursorPosition($script:gameState.width - 1, $y) Microsoft.PowerShell.Utility\Write-Host '│' -NoNewline } [Console]::SetCursorPosition(0, $script:gameState.playFieldBottom + 1) Microsoft.PowerShell.Utility\Write-Host '└' -NoNewline for ($i = 1; $i -lt $script:gameState.width - 1; $i++) { Microsoft.PowerShell.Utility\Write-Host '─' -NoNewline } Microsoft.PowerShell.Utility\Write-Host '┘' -NoNewline Log "Border drawn to console" # read current screen buffer after drawing border Log "Reading screen buffer" $script:gameState.screenBuffer = @( [GenXdev.Helpers.ConsoleReader]::ReadFromBuffer( 0, 0, $script:gameState.width, $script:gameState.height ) ) Log ( "Screen buffer read successfully, lines: " + "$($script:gameState.screenBuffer.Count)" ) if ($WithMaze) { # define maze dimensions based on playfield, leaving space around $mazeLeft = $script:gameState.playFieldLeft + 1 $mazeTop = $script:gameState.playFieldTop + 1 $mazeTotalWidth = $script:gameState.playFieldRight - $script:gameState.playFieldLeft - 2 $mazeTotalHeight = $script:gameState.playFieldBottom - $script:gameState.playFieldTop - 3 # Leave extra space below for snake # calculate pathway width based on available size and MazeWidth parameter $pathWidth = [math]::Max( $MazeWidth, [math]::Min( 10, [math]::Floor( [math]::Min($mazeTotalWidth, $mazeTotalHeight) / 20 ) ) ) Log "Calculated pathway width: $pathWidth (MazeWidth parameter: $MazeWidth)" # calculate number of maze cells to fit $cellsWidth = [math]::Floor(($mazeTotalWidth - 1) / ($pathWidth + 1)) $cellsHeight = [math]::Floor(($mazeTotalHeight - 1) / ($pathWidth + 1)) if ($cellsWidth -lt 1 -or $cellsHeight -lt 1) { $pathWidth = 2 $cellsWidth = [math]::Floor( ($mazeTotalWidth - 1) / ($pathWidth + 1) ) $cellsHeight = [math]::Floor( ($mazeTotalHeight - 1) / ($pathWidth + 1) ) Log "Adjusted pathway width to 2 due to small size" } Log ( "Maze cells: Width=$cellsWidth, Height=$cellsHeight" ) if ($cellsWidth -lt 1 -or $cellsHeight -lt 1) { Log "Maze dimensions too small, skipping maze generation" $WithMaze = $false } else { # create cells with wall properties $cells = Microsoft.PowerShell.Utility\New-Object 'psobject[,]' $cellsHeight, $cellsWidth for ($i = 0; $i -lt $cellsHeight; $i++) { for ($j = 0; $j -lt $cellsWidth; $j++) { $cells[$i, $j] = [pscustomobject]@{ N = $true S = $true E = $true W = $true } } } # create visited array for maze generation $visited = Microsoft.PowerShell.Utility\New-Object 'bool[,]' $cellsHeight, $cellsWidth # define function to carve maze paths function Carve { param ($x, $y) $visited[$y, $x] = $true $directions = @( [pscustomobject]@{dir = 'N'; dx = 0; dy = -1 }, [pscustomobject]@{dir = 'S'; dx = 0; dy = 1 }, [pscustomobject]@{dir = 'E'; dx = 1; dy = 0 }, [pscustomobject]@{dir = 'W'; dx = -1; dy = 0 } ) | Microsoft.PowerShell.Utility\Get-Random -Count 4 foreach ($d in $directions) { $nx = $x + $d.dx $ny = $y + $d.dy if ($nx -ge 0 -and $nx -lt $cellsWidth -and $ny -ge 0 -and ( $ny -lt $cellsHeight ) -and -not $visited[$ny, $nx]) { $cells[$y, $x].($d.dir) = $false $opp = switch ($d.dir) { 'N' { 'S' } 'S' { 'N' } 'E' { 'W' } 'W' { 'E' } } $cells[$ny, $nx].$opp = $false Carve $nx $ny } } } # start carving maze from top-left Carve 0 0 # add braiding to create loops for cooler mazes $braidProb = 0.3 for ($y = 0; $y -lt $cellsHeight; $y++) { for ($x = 0; $x -lt $cellsWidth; $x++) { $walls = @('N', 'S', 'E', 'W') | Microsoft.PowerShell.Core\Where-Object { $cells[$y, $x].$_ } if ($walls.Count -eq 3 -and ( Microsoft.PowerShell.Utility\Get-Random -Maximum 1.0 ) -lt $braidProb) { $dir = $walls | Microsoft.PowerShell.Utility\Get-Random $dx, $dy = switch ($dir) { 'N' { 0, -1 } 'S' { 0, 1 } 'E' { 1, 0 } 'W' { -1, 0 } } $nx = $x + $dx $ny = $y + $dy if ($nx -ge 0 -and $nx -lt $cellsWidth -and $ny -ge 0 -and ( $ny -lt $cellsHeight )) { $cells[$y, $x].$dir = $false $opp = switch ($dir) { 'N' { 'S' } 'S' { 'N' } 'E' { 'W' } 'W' { 'E' } } $cells[$ny, $nx].$opp = $false } } } } # create wide opening at the bottom right if ($cellsHeight -gt 0 -and $cellsWidth -gt 0) { $exitCellX = $cellsWidth - 1 $cells[($cellsHeight - 1), $exitCellX].S = $false if ($cellsWidth -gt 1) { $exitCellX2 = $cellsWidth - 2 $cells[($cellsHeight - 1), $exitCellX2].S = $false } if ($cellsWidth -gt 2) { $exitCellX3 = $cellsWidth - 3 $cells[($cellsHeight - 1), $exitCellX3].S = $false } } # define box drawing characters lookup $boxChars = @{ '0000' = ' ' '0001' = '─' '0010' = '─' '0011' = '─' '0100' = '│' '0101' = '┌' '0110' = '┐' '0111' = '┬' '1000' = '│' '1001' = '└' '1010' = '┘' '1011' = '┴' '1100' = '│' '1101' = '├' '1110' = '┤' '1111' = '┼' } # build maze lines for rendering $mazeLines = Microsoft.PowerShell.Utility\New-Object System.Collections.ArrayList for ($h = 0; $h -le $cellsHeight; $h++) { $hline = '' for ($k = 0; $k -le $cellsWidth; $k++) { $north = $false $south = $false $left = $false $right = $false if ($h -gt 0) { $north = if ($k -eq 0) { $true } else { $cells[($h - 1), ($k - 1)].E } } if ($h -lt $cellsHeight) { $south = if ($k -eq 0) { $true } else { $cells[$h, ($k - 1)].E } } if ($h -eq 0 -or $h -eq $cellsHeight) { $left = ($k -gt 0) $right = ($k -lt $cellsWidth) } else { $left = ($k -gt 0) -and $cells[($h - 1), ($k - 1)].S $right = ($k -lt $cellsWidth) -and ( $cells[($h - 1), $k].S ) } $key = [int]$north, [int]$south, [int]$left, [int]$right -join '' $char = $boxChars[$key] if (-not $char) { $char = ' ' } $hline += $char if ($k -lt $cellsWidth) { $hasHorizontal = if ($h -eq 0 -or $h -eq $cellsHeight -or $k -eq $cellsWidth) { $true } else { $cells[($h - 1), $k].S } if ($hasHorizontal) { $hline += ('─' * $pathWidth) } else { $hline += (' ' * $pathWidth) } } } $null = $mazeLines.Add($hline) if ($h -lt $cellsHeight) { $vline = '' for ($k = 0; $k -le $cellsWidth; $k++) { if ($k -eq 0) { $vline += '│' } else { if ($cells[$h, ($k - 1)].E) { $vline += '│' } else { $vline += ' ' } } if ($k -lt $cellsWidth) { $vline += (' ' * $pathWidth) } } for ($ph = 0; $ph -lt $pathWidth; $ph++) { $null = $mazeLines.Add($vline) } } } # update screen buffer with maze walls for collision detection for ($i = 0; $i -lt $mazeLines.Count; $i++) { if ($mazeTop + $i -ge $script:gameState.screenBuffer.Count) { break } $line = $mazeLines[$i] if ($line.Length -lt $mazeTotalWidth) { $line += ' ' * ($mazeTotalWidth - $line.Length) } $lineChars = $script:gameState.screenBuffer[ $mazeTop + $i ].ToCharArray() for ($j = 0; $j -lt $line.Length; $j++) { if ($mazeLeft + $j -lt $lineChars.Length) { $lineChars[$mazeLeft + $j] = $line[$j] } } $script:gameState.screenBuffer[$mazeTop + $i] = -join $lineChars } Log "Screen buffer updated with polished maze" # draw maze to console Log "Drawing maze to console" for ($i = 0; $i -lt $mazeLines.Count; $i++) { if ($mazeTop + $i -ge $script:gameState.height) { break } [Console]::SetCursorPosition($mazeLeft, $mazeTop + $i) Microsoft.PowerShell.Utility\Write-Host $mazeLines[$i] -NoNewline } Log "Maze drawn to console" # Create opening in bottom-right corner of maze border $openingSize = [math]::Min(5, [math]::Max(2, [math]::Floor($mazeTotalWidth / 10))) $openingY = $mazeTop + $mazeLines.Count - 1 # Last line of maze # Use actual rendered maze line length instead of $mazeTotalWidth $actualMazeWidth = $mazeLines[$mazeLines.Count - 1].Length $openingStartX = $mazeLeft + $actualMazeWidth - $openingSize Log "Creating maze opening: Y=$openingY, StartX=$openingStartX, Size=$openingSize, MazeLines=$($mazeLines.Count), ActualWidth=$actualMazeWidth" # Clear the opening in screen buffer and on console for ($ox = 0; $ox -lt $openingSize; $ox++) { $clearX = $openingStartX + $ox if ($clearX -ge 0 -and $clearX -lt $script:gameState.width -and $openingY -ge 0 -and $openingY -lt $script:gameState.height) { # Update screen buffer $lineChars = $script:gameState.screenBuffer[$openingY].ToCharArray() if ($clearX -lt $lineChars.Length) { $lineChars[$clearX] = ' ' } $script:gameState.screenBuffer[$openingY] = -join $lineChars # Clear on console [Console]::SetCursorPosition($clearX, $openingY) Microsoft.PowerShell.Utility\Write-Host ' ' -NoNewline } } Log "Maze opening created in bottom-right corner" } } # initialize snake as strongly-typed list of coordinate tuples $script:gameState.snake = ( Microsoft.PowerShell.Utility\New-Object 'System.Collections.Generic.List[System.Tuple[int, int]]' ) Log "Snake object created" ########################################################################### # internal function to detect if console window has been resized function ScreenIsResized { return ( $script:gameState.width -ne [Console]::WindowWidth -or $script:gameState.height -ne [Console]::WindowHeight ) } ########################################################################### ########################################################################### # internal function to check if food position is reachable from snake head function IsPositionReachable { param( [Tuple[int, int]]$fromPos, [Tuple[int, int]]$toPos ) # use breadth-first search to find if path exists $queue = Microsoft.PowerShell.Utility\New-Object 'System.Collections.Generic.Queue[System.Tuple[int, int]]' $visited = @{} $queue.Enqueue($fromPos) $visited["$($fromPos.Item1),$($fromPos.Item2)"] = $true $directions = @( @{ dx = 0; dy = -1 }, # Up @{ dx = 0; dy = 1 }, # Down @{ dx = -1; dy = 0 }, # Left @{ dx = 1; dy = 0 } # Right ) $maxIterations = 10000 $iterations = 0 while ($queue.Count -gt 0 -and $iterations -lt $maxIterations) { $iterations++ $current = $queue.Dequeue() # check if we reached the target if ($current.Item1 -eq $toPos.Item1 -and $current.Item2 -eq $toPos.Item2) { Log "Path found to food after $iterations iterations" return $true } # explore neighbors foreach ($dir in $directions) { $newX = $current.Item1 + $dir.dx $newY = $current.Item2 + $dir.dy $key = "$newX,$newY" # skip if already visited if ($visited.ContainsKey($key)) { continue } # check if position is valid and passable if ( $newX -ge $script:gameState.playFieldLeft -and $newX -lt $script:gameState.playFieldRight -and $newY -ge $script:gameState.playFieldTop -and $newY -lt $script:gameState.playFieldBottom -and $script:gameState.screenBuffer[$newY][$newX] -eq ' ' ) { $newPos = [Tuple]::Create($newX, $newY) # skip if position is on snake body (except we allow target position) if ($newPos -in $script:gameState.snake -and -not ( $newPos.Item1 -eq $toPos.Item1 -and $newPos.Item2 -eq $toPos.Item2 )) { continue } $visited[$key] = $true $queue.Enqueue($newPos) } } } Log "No path found to food after $iterations iterations" return $false } ########################################################################### ########################################################################### # internal function to find shortest path and return it as a list function FindShortestPath { param( [Tuple[int, int]]$fromPos, [Tuple[int, int]]$toPos ) # use breadth-first search to find shortest path $queue = Microsoft.PowerShell.Utility\New-Object 'System.Collections.Generic.Queue[System.Tuple[int, int]]' $visited = @{} $parent = @{} $queue.Enqueue($fromPos) $visited["$($fromPos.Item1),$($fromPos.Item2)"] = $true $directions = @( @{ dx = 0; dy = -1 }, # Up @{ dx = 0; dy = 1 }, # Down @{ dx = -1; dy = 0 }, # Left @{ dx = 1; dy = 0 } # Right ) $maxIterations = 10000 $iterations = 0 $found = $false while ($queue.Count -gt 0 -and $iterations -lt $maxIterations) { $iterations++ $current = $queue.Dequeue() # check if we reached the target if ($current.Item1 -eq $toPos.Item1 -and $current.Item2 -eq $toPos.Item2) { $found = $true Log "Shortest path found after $iterations iterations" break } # explore neighbors foreach ($dir in $directions) { $newX = $current.Item1 + $dir.dx $newY = $current.Item2 + $dir.dy $key = "$newX,$newY" # skip if already visited if ($visited.ContainsKey($key)) { continue } # check if position is valid and passable if ( $newX -ge $script:gameState.playFieldLeft -and $newX -lt $script:gameState.playFieldRight -and $newY -ge $script:gameState.playFieldTop -and $newY -lt $script:gameState.playFieldBottom -and $script:gameState.screenBuffer[$newY][$newX] -eq ' ' ) { $newPos = [Tuple]::Create($newX, $newY) # skip if position is on snake body (except target) if ($newPos -in $script:gameState.snake -and -not ( $newPos.Item1 -eq $toPos.Item1 -and $newPos.Item2 -eq $toPos.Item2 )) { continue } $visited[$key] = $true $parent[$key] = $current $queue.Enqueue($newPos) } } } # reconstruct path if found if ($found) { $path = Microsoft.PowerShell.Utility\New-Object 'System.Collections.Generic.List[System.Tuple[int, int]]' $currentPos = $toPos $currentKey = "$($toPos.Item1),$($toPos.Item2)" while ($parent.ContainsKey($currentKey)) { $path.Insert(0, $currentPos) $currentPos = $parent[$currentKey] $currentKey = "$($currentPos.Item1),$($currentPos.Item2)" } # don't include the starting position Log "Path reconstructed with $($path.Count) positions" return $path } Log "No path could be reconstructed" return $null } ########################################################################### ########################################################################### # internal function to draw the route path with green centered dot function DrawRoutePath { Log "DrawRoutePath called, ShowRoute=$ShowRoute" # clear old route if exists if ($null -ne $script:gameState.routePath -and $script:gameState.routePath.Count -gt 0) { Log "Clearing old route with $($script:gameState.routePath.Count) positions" foreach ($pos in $script:gameState.routePath) { # skip if position is now part of snake or is food if ($pos -in $script:gameState.snake -or ( $pos.Item1 -eq $script:gameState.food.Item1 -and $pos.Item2 -eq $script:gameState.food.Item2 )) { continue } # restore original character from screen buffer [Console]::SetCursorPosition($pos.Item1, $pos.Item2) $char = $script:gameState.screenBuffer[$pos.Item2][$pos.Item1] Microsoft.PowerShell.Utility\Write-Host $char -NoNewline } } # find and draw new route if ShowRoute is enabled if ($ShowRoute -and $script:gameState.snake.Count -gt 0) { $snakeHead = $script:gameState.snake[0] Log "Finding path from ($($snakeHead.Item1),$($snakeHead.Item2)) to ($($script:gameState.food.Item1),$($script:gameState.food.Item2))" $script:gameState.routePath = FindShortestPath -fromPos $snakeHead -toPos $script:gameState.food if ($null -ne $script:gameState.routePath -and $script:gameState.routePath.Count -gt 0) { Log "Drawing route with $($script:gameState.routePath.Count) positions" foreach ($pos in $script:gameState.routePath) { # skip snake head and food positions if ($pos -in $script:gameState.snake -or ( $pos.Item1 -eq $script:gameState.food.Item1 -and $pos.Item2 -eq $script:gameState.food.Item2 )) { continue } # draw with small green centered dot (·) - does not update screen buffer [Console]::SetCursorPosition($pos.Item1, $pos.Item2) Microsoft.PowerShell.Utility\Write-Host '·' -NoNewline -ForegroundColor Green } Log "Route drawn" } else { Log "No route path found or path is empty" } } else { Log "ShowRoute disabled or no snake" $script:gameState.routePath = $null } } ########################################################################### ########################################################################### # internal function to find unoccupied space for snake or food placement function FindUnoccupiedSpace { param( [int]$requiredLength = 1, [switch]$allowVertical = $false, [switch]$insideMaze = $false ) Log ( "FindUnoccupiedSpace called: RequiredLength=$requiredLength, " + "AllowVertical=$allowVertical, InsideMaze=$insideMaze" ) # adjust boundaries if insideMaze $minX = if ($insideMaze) { $mazeLeft + 1 } else { $script:gameState.playFieldLeft } $maxX = if ($insideMaze) { $mazeLeft + $mazeTotalWidth - 1 } else { $script:gameState.playFieldRight - 1 } $minY = if ($insideMaze) { $mazeTop + 1 } else { $script:gameState.playFieldTop } $maxY = if ($insideMaze) { $mazeTop + $mazeTotalHeight - 1 } else { $script:gameState.playFieldBottom - 1 } if ($insideMaze -and ($maxX -lt $minX -or $maxY -lt $minY)) { Log "Maze boundaries invalid for placement, returning null" return $null } # calculate maximum attempts based on playfield size $maxAttempts = ( $script:gameState.width * $script:gameState.height * 4 ) $attempts = 0 # attempt to find suitable placement location while ($attempts -lt $maxAttempts) { # generate random starting coordinates within playfield $startX = Microsoft.PowerShell.Utility\Get-Random ` -Minimum $minX ` -Maximum ($maxX + 1) $startY = Microsoft.PowerShell.Utility\Get-Random ` -Minimum $minY ` -Maximum ($maxY + 1) # handle single space placement for food items if ($requiredLength -eq 1) { # verify position is within bounds and unoccupied if ( $startX -ge $minX -and $startX -le $maxX -and $startY -ge $minY -and $startY -le $maxY -and $script:gameState.screenBuffer[$startY][$startX] -eq ( ' ' ) -and -not ([Tuple]::Create($startX, $startY) -in $script:gameState.snake) ) { $candidatePos = [Tuple]::Create($startX, $startY) # check if position is reachable from snake head (only if snake exists and WithMaze is enabled) if ($WithMaze -and $script:gameState.snake.Count -gt 0) { $snakeHead = $script:gameState.snake[0] if (-not (IsPositionReachable -fromPos $snakeHead -toPos $candidatePos)) { Log "Food position ($startX, $startY) is not reachable, trying another position" $attempts++ continue } } Log ( "Found unoccupied space for food at " + "($startX, $startY) after $attempts attempts" ) return $candidatePos } $attempts++ continue } # define possible placement directions for multi-segment snake $directions = @( @{ dx = -1 dy = 0 name = 'Left' opposite = 'Right' }, @{ dx = 1 dy = 0 name = 'Right' opposite = 'Left' }, @{ dx = 0 dy = -1 name = 'Up' opposite = 'Down' }, @{ dx = 0 dy = 1 name = 'Down' opposite = 'Up' } ) # restrict to horizontal directions if vertical not allowed if (-not $allowVertical) { $directions = $directions[0..1] } # try each direction for snake placement foreach ($dir in $directions) { $positions = @() $canPlace = $true # attempt to place all segments in current direction for ($i = 0; $i -lt $requiredLength; $i++) { $checkX = $startX + ($dir.dx * $i) $checkY = $startY + ($dir.dy * $i) # verify each position is valid and unoccupied if ( $checkX -lt $minX -or $checkX -gt $maxX -or $checkY -lt $minY -or $checkY -gt $maxY -or $script:gameState.screenBuffer[$checkY][$checkX] -ne ( ' ' ) ) { $canPlace = $false break } $positions += [Tuple]::Create($checkX, $checkY) } # placement successful in this direction if ($canPlace) { # set initial movement direction opposite to placement if ($requiredLength -gt 1) { $script:gameState.direction = $dir.opposite Log ( "Snake placed successfully in direction " + "$($dir.name), setting initial direction to " + "$($dir.opposite)" ) } Log ( "Found unoccupied space for snake " + "($($positions.Count) segments) after " + "$attempts attempts" ) return $positions } # attempt l-shaped placement if linear placement failed if (-not $canPlace -and $i -gt 0) { $positions = @() $canPlace = $true $placedCount = $i # place segments that fit in original direction for ($j = 0; $j -lt $placedCount; $j++) { $positions += [Tuple]::Create( $startX + ($dir.dx * $j), $startY + ($dir.dy * $j) ) } # calculate remaining segments needed $remainingLength = $requiredLength - $placedCount # get alternative directions for remaining segments $otherDirections = $directions | Microsoft.PowerShell.Core\Where-Object { $_.name -ne $dir.name } # try each alternative direction foreach ($otherDir in $otherDirections) { $tempCanPlace = $true # verify remaining segments fit in new direction for ($k = 1; $k -le $remainingLength; $k++) { $checkX = $startX + ($otherDir.dx * $k) $checkY = $startY + ($otherDir.dy * $k) if ( $checkX -lt $minX -or $checkX -gt $maxX -or $checkY -lt $minY -or $checkY -gt $maxY -or $script:gameState.screenBuffer[ $checkY ][$checkX] -ne ' ' ) { $tempCanPlace = $false break } } # add remaining segments if placement valid if ($tempCanPlace) { for ($k = 1; $k -le $remainingLength; $k++) { $positions += [Tuple]::Create( $startX + ($otherDir.dx * $k), $startY + ($otherDir.dy * $k) ) } # determine safe direction for l-shaped snake if ($requiredLength -gt 1) { $head = $positions[0] $neck = $positions[1] # calculate direction from neck to head $headDirection = '' if ($head.Item1 -gt $neck.Item1) { $headDirection = 'Right' } elseif ($head.Item1 -lt $neck.Item1) { $headDirection = 'Left' } elseif ($head.Item2 -gt $neck.Item2) { $headDirection = 'Down' } elseif ($head.Item2 -lt $neck.Item2) { $headDirection = 'Up' } # set direction away from neck $script:gameState.direction = $headDirection Log ( "L-shaped snake placed, setting " + "initial direction to $headDirection" ) } Log ( "Found L-shaped placement for snake after " + "$attempts attempts" ) return $positions } } } } $attempts++ } # use fallback center placement if no valid position found Log "Using fallback placement for RequiredLength=$requiredLength" if ($requiredLength -eq 1) { # return center position for single space return [Tuple]::Create( ( ($script:gameState.playFieldRight - ( $script:gameState.playFieldLeft )) / 2 + $script:gameState.playFieldLeft ), ( ($script:gameState.playFieldBottom - ( $script:gameState.playFieldTop )) / 2 + $script:gameState.playFieldTop ) ) } else { # create horizontal snake in center for multi-segment $fallbackPositions = @() $centerX = ( ($script:gameState.playFieldRight - ( $script:gameState.playFieldLeft )) / 2 + $script:gameState.playFieldLeft ) $centerY = ( ($script:gameState.playFieldBottom - ( $script:gameState.playFieldTop )) / 2 + $script:gameState.playFieldTop ) for ($i = 0; $i -lt $requiredLength; $i++) { $fallbackPositions += [Tuple]::Create( $centerX - $i, $centerY ) } # set safe direction for fallback horizontal snake $script:gameState.direction = 'Right' Log "Fallback snake placement, setting direction to Right" return $fallbackPositions } } ########################################################################### ########################################################################### # internal function to draw snake and food on screen function DrawInitialScreen { Log "Starting to draw initial screen - snake and food" $blink = if ($WithMaze) { $PSStyle.Blink } else { '' } # draw snake segments $isHead = $true foreach ($segment in $script:gameState.snake) { [Console]::SetCursorPosition($segment.Item1, $segment.Item2) if ($isHead) { Microsoft.PowerShell.Utility\Write-Host "${blink}O$($PSStyle.Reset)" -NoNewline -ForegroundColor Green $isHead = $false } else { Microsoft.PowerShell.Utility\Write-Host "${blink}o$($PSStyle.Reset)" -NoNewline -ForegroundColor Green } } # draw food item [Console]::SetCursorPosition($script:gameState.food.Item1, $script:gameState.food.Item2) Microsoft.PowerShell.Utility\Write-Host "$($PSStyle.Blink)*$($PSStyle.Reset)" -NoNewline -ForegroundColor Red Log ( "Initial screen drawn - snake ($($script:gameState.snake.Count) segments), " + "food at ($($script:gameState.food.Item1), $($script:gameState.food.Item2))" ) if ($WithMaze) { # Redraw snake without blink $isHead = $true foreach ($segment in $script:gameState.snake) { [Console]::SetCursorPosition($segment.Item1, $segment.Item2) if ($isHead) { Microsoft.PowerShell.Utility\Write-Host 'O' -NoNewline -ForegroundColor Green $isHead = $false } else { Microsoft.PowerShell.Utility\Write-Host 'o' -NoNewline -ForegroundColor Green } } Log "Snake redrawn without blink after pause" } # draw route path if ShowRoute is enabled DrawRoutePath } ########################################################################### ########################################################################### # internal function to count consecutive free spaces in a direction function CountFreeSpaces { param( [int]$startX, [int]$startY, [string]$direction ) # initialize direction vectors for movement calculation $dx = 0 $dy = 0 # set direction vector based on movement direction switch ($direction) { 'Up' { $dy = -1 } 'Down' { $dy = 1 } 'Left' { $dx = -1 } 'Right' { $dx = 1 } } $count = 0 # count spaces until obstacle or boundary encountered while ($true) { # calculate next position to check $checkX = $startX + ($dx * ($count + 1)) $checkY = $startY + ($dy * ($count + 1)) # stop if boundary reached if ( $checkX -lt $script:gameState.playFieldLeft -or $checkX -ge $script:gameState.playFieldRight -or $checkY -lt $script:gameState.playFieldTop -or $checkY -ge $script:gameState.playFieldBottom ) { break } # stop if obstacle found in screen buffer if ($script:gameState.screenBuffer[$checkY][$checkX] -ne ' ') { break } $count++ } return $count } ########################################################################### ########################################################################### # internal function to dynamically adjust game speed based on context function SetCurrentSpeed { Log "SetCurrentSpeed function called" # initialize counters for space calculation $nrOfFreePositionsInCurrentDirection = 0 $totalNumberOfFreePositionsSinceLastTurn = 0 # get current snake head position $head = $script:gameState.snake[0] $currentX = $head.Item1 $currentY = $head.Item2 Log ( "Snake head position: ($currentX, $currentY), direction: " + "$($script:gameState.direction)" ) # count free spaces ahead in current direction $nrOfFreePositionsInCurrentDirection = CountFreeSpaces ` -startX $currentX ` -startY $currentY ` -direction $script:gameState.direction Log ( "Free positions in current direction " + "($($script:gameState.direction)): " + "$nrOfFreePositionsInCurrentDirection" ) # calculate movement since last direction change Log ( "LastMoveCoord available: " + "($($script:gameState.LastMoveCoord.Item1), " + "$($script:gameState.LastMoveCoord.Item2))" ) # determine movement delta from last turn position $deltaX = $head.Item1 - $script:gameState.LastMoveCoord.Item1 $deltaY = $head.Item2 - $script:gameState.LastMoveCoord.Item2 Log "Delta from last turn: X=$deltaX, Y=$deltaY" # determine primary movement axis since last turn $movementDirection = '' if ([Math]::Abs($deltaX) -gt [Math]::Abs($deltaY)) { # horizontal movement is dominant $movementDirection = if ($deltaX -gt 0) { 'Right' } else { 'Left' } } else { # vertical movement is dominant $movementDirection = if ($deltaY -gt 0) { 'Down' } else { 'Up' } } Log ( "Primary movement direction since last turn: " + "$movementDirection" ) # count free spaces from last turn position $totalNumberOfFreePositionsSinceLastTurn = CountFreeSpaces ` -startX $script:gameState.LastMoveCoord.Item1 ` -startY $script:gameState.LastMoveCoord.Item2 ` -direction $movementDirection Log ( "Free spaces from last turn position in " + "$movementDirection direction: " + "$totalNumberOfFreePositionsSinceLastTurn" ) Log ( "Analysis: FreeAhead=$nrOfFreePositionsInCurrentDirection, " + "FreeSpacesAtLastTurn=" + "$totalNumberOfFreePositionsSinceLastTurn" ) # define speed range boundaries for calculation $fastest = 20.0 $slowest = [Math]::Min([Math]::Max(20.0, $speed), 2000.0) $delta = $slowest - $fastest $fastestLength = 30.0 Log ( "Speed calculation parameters: fastest=$fastest, " + "slowest=$slowest, delta=$delta, " + "fastestLength=$fastestLength" ) # calculate ratio based on snake length $ratioSnakeLength = 1.0 - [Math]::Min( 1.0, ($script:gameState.snake.Count / $fastestLength) ) Log ( "Snake length ratio: 1.0 - " + "$($script:gameState.snake.Count)/$fastestLength = " + "$ratioSnakeLength" ) # calculate ratio based on remaining free space $ratioFreeSpaceLeft = 1.0 - [Math]::Min( 1.0, ( $nrOfFreePositionsInCurrentDirection / [Math]::Max( 1.0, $totalNumberOfFreePositionsSinceLastTurn ) ) ) Log ( "Free space ratio: 1.0 - " + "$nrOfFreePositionsInCurrentDirection/" + "[Math]::Max(1.0, $totalNumberOfFreePositionsSinceLastTurn) " + "= $ratioFreeSpaceLeft" ) # calculate distance from center of free space $ratioAwayFromCenterOfFreeSpace = ( [Math]::Abs(0.5 - $ratioFreeSpaceLeft) * 2.0 ) Log ( "Away from center ratio: [Math]::Abs(0.5 - " + "$ratioFreeSpaceLeft) * 2.0 = " + "$ratioAwayFromCenterOfFreeSpace" ) # calculate console aspect ratio for speed adjustment $expectRatio = ( [double] $script:gameState.width / [double] $script:gameState.height ) Log "Console aspect ratio: $expectRatio (Width/Height)" # adjust ratio based on movement direction and aspect ratio $ratioDirection = if ( $script:gameState.direction -in @('Left', 'Right') ) { # horizontal movement speed adjustment if ($expectRatio -lt 1) { # landscape orientation ( [double] $script:gameState.width / [double] $script:gameState.height ) } else { # portrait orientation ( [double] $script:gameState.height / [double] $script:gameState.width ) } } else { # vertical movement speed adjustment if ($expectRatio -lt 1) { # landscape orientation ( [double] $script:gameState.height / [double] $script:gameState.width ) } else { # portrait orientation ( [double] $script:gameState.width / [double] $script:gameState.height ) } } Log ( "Direction ratio based on movement direction " + "($($script:gameState.direction)): $ratioDirection" ) Log ( "$fastest + ($delta * $ratioSnakeLength * " + "$ratioAwayFromCenterOfFreeSpace * $ratioDirection)" ) # calculate final game speed using all ratios $script:gameState.gameSpeed = [int] [Math]::Round( [Math]::Min( $slowest, $fastest + ( $delta * $ratioDirection * ( $ratioAwayFromCenterOfFreeSpace + $ratioSnakeLength ) / 2.0 ) ), 0 ) Log "Final game speed: $($script:gameState.gameSpeed)" return $script:gameState.gameSpeed } ########################################################################### ########################################################################### # internal function to update snake position and handle collisions function UpdateSnake { param( [string]$direction ) Log "UpdateSnake called with direction: $direction" # declare strongly-typed variables for snake movement [Tuple[int, int]] $previousTail = $null [Tuple[int, int]] $head = $null [int] $newX = 0 [int] $newY = 0 [Tuple[int, int]] $newHead = $null [bool] $ateFood = $false [bool] $collision = $false # store tail position before movement for potential restoration $previousTail = $script:gameState.snake[ $script:gameState.snake.Count - 1 ] Log ( "Previous tail at: ($($previousTail.Item1), " + "$($previousTail.Item2))" ) # get current head position $head = $script:gameState.snake[0] $newX = $head.Item1 $newY = $head.Item2 Log "Current head at: ($($head.Item1), $($head.Item2))" # calculate new head position based on movement direction switch ($direction) { 'Up' { $newY = $head.Item2 - 1 } 'Down' { $newY = $head.Item2 + 1 } 'Left' { $newX = $head.Item1 - 1 } 'Right' { $newX = $head.Item1 + 1 } } $newHead = [Tuple]::Create($newX, $newY) Log "New head position: ($newX, $newY)" # check for boundary collisions if ( $newX -lt $script:gameState.playFieldLeft -or $newX -ge $script:gameState.playFieldRight -or $newY -lt $script:gameState.playFieldTop -or $newY -ge $script:gameState.playFieldBottom ) { $collision = $true Log "COLLISION: Boundary collision detected at ($newX, $newY)" } elseif ( $script:gameState.screenBuffer[$newY][$newX] -ne ' ' ) { # check for screen buffer content collisions $collision = $true Log ( "COLLISION: Screen buffer collision detected at " + "($newX, $newY), char: " + "'$($script:gameState.screenBuffer[$newY][$newX])'" ) } elseif ($newHead -in $script:gameState.snake) { # check for self-collision with snake body $collision = $true Log "COLLISION: Self collision detected at ($newX, $newY)" } # return immediately if any collision detected if ($collision) { Log "UpdateSnake returning false due to collision" return $false } # determine if food was eaten at new position $ateFood = ( $newHead.Item1 -eq $script:gameState.food.Item1 -and $newHead.Item2 -eq $script:gameState.food.Item2 ) Log ( "Food eaten: $ateFood (food at: " + "$($script:gameState.food.Item1), " + "$($script:gameState.food.Item2))" ) # add new head segment to front of snake $script:gameState.snake.Insert(0, $newHead) Log ( "New head added to snake, snake length now: " + "$($script:gameState.snake.Count)" ) # remove tail if food not eaten to maintain length if (-not $ateFood) { # restore original character at previous tail position [Console]::SetCursorPosition( $previousTail.Item1, $previousTail.Item2 ) $originalChar = $script:gameState.screenBuffer[ $previousTail.Item2 ][$previousTail.Item1] Microsoft.PowerShell.Utility\Write-Host $originalChar -NoNewline # remove tail segment from snake $script:gameState.snake.RemoveAt( $script:gameState.snake.Count - 1 ) Log ( "Tail removed, restored char '$originalChar' at " + "($($previousTail.Item1), $($previousTail.Item2))" ) } # render new head position on screen [Console]::SetCursorPosition($newHead.Item1, $newHead.Item2) Microsoft.PowerShell.Utility\Write-Host 'O' -NoNewline ` -ForegroundColor Green Log "New head drawn at ($($newHead.Item1), $($newHead.Item2))" # update previous head to body segment character if ($script:gameState.snake.Count -gt 1) { [Console]::SetCursorPosition( $script:gameState.snake[1].Item1, $script:gameState.snake[1].Item2 ) Microsoft.PowerShell.Utility\Write-Host 'o' -NoNewline ` -ForegroundColor Green Log ( "Previous head updated to body segment at " + "($($script:gameState.snake[1].Item1), " + "$($script:gameState.snake[1].Item2))" ) } # handle food consumption and new food placement if ($ateFood) { # increment player score $script:gameState.score++ Log "Score increased to: $($script:gameState.score)" $oldFood = $script:gameState.food Log ( "Attempting to place new food after eating food at: " + "($($oldFood.Item1), $($oldFood.Item2))" ) # find new unoccupied position for food $script:gameState.food = FindUnoccupiedSpace -requiredLength 1 -insideMaze # validate new food placement succeeded if ($null -eq $script:gameState.food) { Log ( "ERROR: Failed to place new food - " + "FindUnoccupiedSpace returned null" ) # use emergency fallback center position $script:gameState.food = [Tuple]::Create( ( ($script:gameState.playFieldRight - ( $script:gameState.playFieldLeft )) / 2 + $script:gameState.playFieldLeft ), ( ($script:gameState.playFieldBottom - ( $script:gameState.playFieldTop )) / 2 + $script:gameState.playFieldTop ) ) Log ( "Emergency food placement at center: " + "($($script:gameState.food.Item1), " + "$($script:gameState.food.Item2))" ) } elseif ( $script:gameState.food.Item1 -eq $oldFood.Item1 -and $script:gameState.food.Item2 -eq $oldFood.Item2 ) { Log ( "WARNING: New food placed at same location as old " + "food: ($($script:gameState.food.Item1), " + "$($script:gameState.food.Item2))" ) } else { Log ( "New food placed successfully at: " + "($($script:gameState.food.Item1), " + "$($script:gameState.food.Item2)) (was at: " + "($($oldFood.Item1), $($oldFood.Item2)))" ) } # perform additional validation of new food position if ( $script:gameState.food.Item1 -lt ( $script:gameState.playFieldLeft ) -or $script:gameState.food.Item1 -ge ( $script:gameState.playFieldRight ) -or $script:gameState.food.Item2 -lt ( $script:gameState.playFieldTop ) -or $script:gameState.food.Item2 -ge ( $script:gameState.playFieldBottom ) ) { Log ( "ERROR: New food placed outside playfield boundaries " + "at ($($script:gameState.food.Item1), " + "$($script:gameState.food.Item2))" ) } elseif ($script:gameState.food -in $script:gameState.snake) { Log ( "ERROR: New food placed on snake body at " + "($($script:gameState.food.Item1), " + "$($script:gameState.food.Item2))" ) } else { Log ( "Food position validation passed: " + "($($script:gameState.food.Item1), " + "$($script:gameState.food.Item2)) is within bounds " + "and not on snake" ) } # render new food on screen [Console]::SetCursorPosition( $script:gameState.food.Item1, $script:gameState.food.Item2 ) Microsoft.PowerShell.Utility\Write-Host ( "$($PSStyle.Blink)*$($PSStyle.Reset)" ) -NoNewline -ForegroundColor Red Log ( "New food drawn at: ($($script:gameState.food.Item1), " + "$($script:gameState.food.Item2))" ) # update route path after food placement DrawRoutePath } Log "UpdateSnake completed successfully" # return success if no collision occurred return $true } ########################################################################### ########################################################################### # internal function to find row with fewest snake segments function getRowWithLeastSnake { # count snake segments per row $rowCounts = @{} foreach ($segment in $script:gameState.snake) { if ($rowCounts.ContainsKey($segment.Item2)) { $rowCounts[$segment.Item2]++ } else { $rowCounts[$segment.Item2] = 1 } } # find row with minimum segment count $minCount = [int]::MaxValue $bestRow = -1 foreach ($row in $rowCounts.Keys) { if ($rowCounts[$row] -lt $minCount) { $minCount = $rowCounts[$row] $bestRow = $row } } return $bestRow } ########################################################################### # find suitable positions for initial snake placement Log "Starting snake initialization" if ($WithMaze) { # Calculate maze bottom y $totalMazeRows = [int](($cellsHeight + 1) + $cellsHeight * $pathWidth) $mazeBottom = [int]($mazeTop + $totalMazeRows - 1) # Place snake below maze left corner, moving right $startY = [int]($mazeBottom + 1) if ($startY -gt $script:gameState.playFieldBottom) { $startY = $script:gameState.playFieldBottom } $startX = [int]($script:gameState.playFieldLeft + $script:gameState.snakeLength - 1) # Head at rightmost of snake $script:gameState.snakePositions = @() $canPlace = $true for ($i = 0; $i -lt $script:gameState.snakeLength; $i++) { $checkX = $startX - $i $checkY = $startY if ( $checkX -lt $script:gameState.playFieldLeft -or $checkX -ge $script:gameState.playFieldRight -or $checkY -lt $script:gameState.playFieldTop -or $checkY -ge $script:gameState.playFieldBottom -or $script:gameState.screenBuffer[$checkY][$checkX] -ne ' ' ) { $canPlace = $false break } $script:gameState.snakePositions += [Tuple]::Create($checkX, $checkY) } if (-not $canPlace) { Log "Could not place snake below maze left corner, using random placement" $script:gameState.snakePositions = FindUnoccupiedSpace ` -requiredLength $script:gameState.snakeLength ` -allowVertical } else { $script:gameState.direction = 'Right' Log "Snake placed below maze left corner moving right" } # Place initial food inside maze Log "Placing initial food inside maze" $script:gameState.food = FindUnoccupiedSpace -requiredLength 1 -insideMaze if ($null -eq $script:gameState.food) { Log "Could not place food inside maze, using regular placement" $script:gameState.food = FindUnoccupiedSpace -requiredLength 1 } } else { $script:gameState.snakePositions = FindUnoccupiedSpace ` -requiredLength $script:gameState.snakeLength ` -allowVertical $script:gameState.food = FindUnoccupiedSpace -requiredLength 1 } Log ( "Snake positions found: " + "$($script:gameState.snakePositions.Count) segments" ) # add all positions to snake list at once $script:gameState.snake.AddRange( [System.Tuple[int, int][]]$script:gameState.snakePositions ) Log ( "Snake initialized with $($script:gameState.snake.Count) " + "segments" ) Log ( "Food placed at: ($($script:gameState.food.Item1), " + "$($script:gameState.food.Item2))" ) # store initial position for speed calculation $script:gameState.LastMoveCoord = $script:gameState.snake[0] # render initial game screen Log "Drawing initial screen" DrawInitialScreen Log ( "Begin block completed, current direction: " + "$($script:gameState.direction)" ) } process { Log "Entering main game loop" if ($PSCmdlet.ShouldProcess("Start Snake Game", "Modify console state")) { # main game loop continues until game over while ($true) { Log ( "Game loop iteration started, direction: " + "$($script:gameState.direction)" ) # check if console window has been resized if (ScreenIsResized) { Log "Screen resize detected, ending game" # update dimensions for final message $script:gameState.height = [Console]::WindowHeight $script:gameState.width = [Console]::WindowWidth Clear-Host # display final score message Microsoft.PowerShell.Utility\Write-Host ( "`e[93;44m" + ( "Screen resized. Game over! Final Score: " + "$($script:gameState.score)" ).PadRight($script:gameState.width - 1, ' ') + "`e[0m" ) -NoNewline break } # process keyboard input if available if ([Console]::KeyAvailable) { $key = [Console]::ReadKey($true) Log ( "Key pressed: $($key.Key) (KeyChar: '$($key.KeyChar)')" ) # define mappings for both arrow keys and wasd controls $keyMappings = @{ [ConsoleKey]::UpArrow = @{ direction = 'Up' opposite = 'Down' wasd = $false } [ConsoleKey]::DownArrow = @{ direction = 'Down' opposite = 'Up' wasd = $false } [ConsoleKey]::LeftArrow = @{ direction = 'Left' opposite = 'Right' wasd = $false } [ConsoleKey]::RightArrow = @{ direction = 'Right' opposite = 'Left' wasd = $false } [ConsoleKey]::W = @{ direction = 'Up' opposite = 'Down' wasd = $true } [ConsoleKey]::A = @{ direction = 'Left' opposite = 'Right' wasd = $true } [ConsoleKey]::S = @{ direction = 'Down' opposite = 'Up' wasd = $true } [ConsoleKey]::D = @{ direction = 'Right' opposite = 'Left' wasd = $true } } # process direction change if valid key pressed if ($keyMappings.ContainsKey($key.Key)) { $mapping = $keyMappings[$key.Key] Log ( "Found mapping for key $($key.Key): direction=" + "$($mapping.direction), opposite=" + "$($mapping.opposite)" ) # prevent 180-degree turns into snake body if ( $script:gameState.direction -ne $mapping.opposite ) { # store position where direction changed $script:gameState.LastMoveCoord = ( $script:gameState.snake[0] ) # update movement direction $script:gameState.direction = $mapping.direction # record time of direction change $script:gameState.LastMoveTime = ( [DateTime]::UtcNow ) # identify control type for logging $controlType = if ($mapping.wasd) { ' (WASD)' } else { '' } Log ( "Direction changed to: " + "$($mapping.direction)$controlType" ) } else { Log ( "Direction change blocked: current=" + "$($script:gameState.direction), trying " + "opposite=$($mapping.opposite)" ) } } elseif ($key.Key -eq 'Escape') { # exit game when escape key pressed Log "Escape key pressed, exiting game" [Console]::SetCursorPosition( 0, $script:gameState.height - 2 ) Microsoft.PowerShell.Utility\Write-Host ( "`e[93;44m" + ( "Game exited. Final Score: " + "$($script:gameState.score)" ).PadRight($script:gameState.width - 1, ' ') + "`e[0m" ) -NoNewline return } else { Log "Unhandled key: $($key.Key)" } } # update snake position and check for collisions Log ( "Calling UpdateSnake with direction: " + "$($script:gameState.direction)" ) $result = UpdateSnake -direction $script:gameState.direction Log "UpdateSnake returned: $result" # handle collision game over if (-not $result) { Log "Game over due to collision" [Console]::SetCursorPosition( 0, $script:gameState.height - 2 ) Microsoft.PowerShell.Utility\Write-Host ( "`e[93;44m" + ( "Collision detected! Game over! Final Score: " + "$($script:gameState.score)" ).PadRight($script:gameState.width - 1, ' ') + "`e[0m" ) -NoNewline break } # calculate and apply dynamic game speed $script:gameState.gameSpeed = SetCurrentSpeed Log "Sleeping for $($script:gameState.gameSpeed) milliseconds" # position cursor at bottom for status display [Console]::SetCursorPosition(0, $script:gameState.height - 1) Microsoft.PowerShell.Utility\Start-Sleep ` -Milliseconds $script:gameState.gameSpeed } Log "Game loop ended" } } end { Log ( "Snake game ended, final score: $($script:gameState.score)" ) # save debug log if logging enabled if ($script:gameState.loggingEnabled) { try { $logContent = $script:gameState.logBuilder.ToString() [System.IO.File]::WriteAllText( $script:gameState.logPath, $logContent, [System.Text.Encoding]::UTF8 ) } catch { Microsoft.PowerShell.Utility\Write-Warning "Failed to save log file: $_" } } } } ############################################################################### |