functions/outdated/Start-MiniGameLoop.ps1

function Start-MiniGameLoop {
    <#
    .SYNOPSIS
    [OUDATED VERSION] Replaced by New-MiniGameRunUtility
 
    (NOT FINISHED YET)
    Runs a generic game loop for managing and drawing interatctive objects in the Terminal.
 
    .DESCRIPTION
    (NOT FINISHED YET)
    Runs a generic game loop for managing and drawing interatctive objects in the Terminal.
    This is supposed to be a generic helper function to create different interactive Terminal-Games.
 
    .OUTPUTS
     
 
 
    .EXAMPLE
 
    PS> Start-InvadersGame:
 
                 U u U
                [~{T}~]
                 `\|/´
                   Y
 
                   O
                   V
 
  
 
  
 
                          O
                          V
 
  
    .EXAMPLE
 
     
 
 
    .LINK
     
    #>


    

    [CmdletBinding()]
    param (
        [Parameter(
            Position = 1,
            Mandatory = $false
        )]
        [ValidateRange(1, 1000)]
        [System.Int32]
        $TickIntervall = 1,


        [Parameter(
            Mandatory = $false
        )]
        [System.Int32]
        $GameHeight,

        [Parameter(
            Mandatory = $false
        )]
        [System.Int32]
        $GameWidth,


        [Parameter(
            Mandatory = $false
        )]
        [System.Int32]
        $RequiredGameHeight,

        [Parameter(
            Mandatory = $false
        )]
        [System.Int32]
        $RequiredGameWidth,

        # A script that gets run every tick to update any custom parameters.
        <#
            {
                param($GameObjects, $GameWidth, $GameHeight)
 
                # Something...
            },
        #>

        [Parameter(
            Mandatory = $true
        )]
        [System.Management.Automation.ScriptBlock]
        $onEveryTickDo,



        # This is a hastable of all obects managed by the gameloop.
        <#
            # Objects get drawn in that order. Following objects will potentially hide previous objects.
 
            [ordered]@{
 
                # Template for an object to be managed
                object_name = [PSCustomObject]@{
                    # Parameters required by gameloop
                    position = [System.Numerics.Vector2]::new(0, 0) # Coordinates from upper left corner of object canvas.
                    velocity = [System.Numerics.Vector2]::new(0, 0) # optional
                    canvas = @( # How the object is dran in the terminal. Each line of the array is a terminal line.
                        '~U~',
                        " ' "
                    )
 
                    (optional) collidable = $true # Passivley collidable, but generates no collision-events for itself
 
                    (optional) collidableWith = '*' # Activley collidable, generating collision-events.
                    (optional) collidableWith = [System.String[]]@($objectNames...) # Activley collidable, generating collision-events.)
 
                    # Custom parameters
                }
 
                # Allowed types are:
                # - [PSCustomObject] <== A single custom object
                # - [PSCustomObject[]] <== A list of objects
                 
            }
        #>

        [Parameter(
            Mandatory = $true
        )]
        [System.Collections.Specialized.OrderedDictionary]
        $GameObjects,



        # A customizable script-block for handeling key event.
        # Gets called on every input, provding the key event, as well as the hashtable of gameobjects.
        # Invidual Game Objects can be access by their defined name corresponding to the key in the hashtable.
        <#
        {
            param($KeyEvent, $GameObjects, $GameWidth, $GameHeight)
 
            $InvaderShip = $GameObjects['InvaderShip']
                 
            switch ($KeyEvent.Key) {
 
                { $_ -in @([System.ConsoleKey]::A, [System.ConsoleKey]::LeftArrow) } {
                 
                    # Something...
                    break;
                }
 
                Default {}
            }
 
        }
        #>

        [Parameter(
            Mandatory = $true
        )]
        [System.Management.Automation.ScriptBlock]
        $onKeyEvent,




        # Script block for handeling collision events.
        <#
            {
                param($collider, $participants)
            }
        #>

        [Parameter(
            Mandatory = $true
        )]
        [System.Management.Automation.ScriptBlock]
        $onCollision,
    
    
        <#
        {
            param($object, $didExitLeft, $didExitRigth, $didExitUp, $didExitDown)
        }
        #>

        [Parameter(
            Mandatory = $true
        )]
        [System.Management.Automation.ScriptBlock]
        $onExitScreen
    )

    ########################################################
    ###### short internal helper function

    function Get-LineOfChars {
        param (
            [Parameter()]
            $Length,

            [Parameter()]
            $Char
        )
        
        return (1..$Length | ForEach-Object { $Char }) -join ''
    }


    # TODO dynamic resize of window?
    $WindowHeight = $host.UI.RawUI.WindowSize.Height
    $WindowWidth = $host.UI.RawUI.WindowSize.Width

    $GameHeight = $PSBoundParameters.ContainsKey('GameHeight') ? $GameHeight : $WindowHeight
    $GameWidth = $PSBoundParameters.ContainsKey('GameWidth') ? $GameWidth : $WindowWidth
    # $GameOffsetLength = [System.Math]::floor($WindowWidth / 2 - $GameWidth / 2 - 2) TODO

    $RequiredGameHeight = $PSBoundParameters.ContainsKey('RequiredGameHeight') ? $RequiredGameHeight : $GameHeight
    $RequiredGameWidth = $PSBoundParameters.ContainsKey('RequiredGameWidth') ? $RequiredGameWidth : $GameWidth

    if ($WindowHeight -LT $RequiredGameHeight) {
        throw "Terminal Height must be at least: $RequiredGameHeight (Currently: $WindowHeight)"
    }

    if ($WindowWidth -LT $RequiredGameWidth) {
        throw "Terminal Width must be at least: $RequiredGameWidth (Currently: $WindowWidth)"
    }

    ########################################################
    ###### Some initial values

    [System.Console]::Clear()
    [System.Console]::WriteLine()
    [System.Console]::CursorVisible = $false

    $EMPTY_TILE = ' '
    $TICK_COUNT = 0

    ################################################################################################################
    ###### Script block for updating elements gets called on each object every tick.

    $update = {
        param($object, $queue)

        # Skip any dead objects.
        if ($object.isDead) {
            return
        }

        if ($null -EQ $object.canvas) {
            throw "Object '$($object.name)' doesn't have a canvas array defining its shape."
        }

        if ($null -EQ $object.colorCodes) {
            $null = $object
            | Add-Member -MemberType NoteProperty -Force -Name code -Value ""
        }

        if ($null -EQ $object.lastCanvas) {
            $null = $object
            | Add-Member -MemberType NoteProperty -Force -Name lastCanvas -Value object.canvas
        }

        if ($null -EQ $object.collisionMark) {
            $null = $object
            | Add-Member -MemberType NoteProperty -Force -Name collisionMark -Value $false
        }

        if ($null -EQ $object.redrawMark) {
            $null = $object
            | Add-Member -MemberType NoteProperty -Force -Name redrawMark -Value $false
        }

        if ($null -EQ $object.initialDraw) {
            $null = $object
            | Add-Member -MemberType NoteProperty -Force -Name initialDraw -Value $false
        }

        if ($null -EQ $object.position) {
            throw "Object '$($object.name)' doesn't have a position."
        }

        # Dead objects won't be drawn.
        if ($null -EQ $object.isDead) {
            $null = $object
            | Add-Member -MemberType NoteProperty -Force -Name isDead -Value $false
        }

        # This is just to add a possibly non-existen properties as $null.
        if ($null -EQ $object.velocity) {
            $null = $object
            | Add-Member -MemberType NoteProperty -Force -Name velocity -Value $null
        }

        if ($null -EQ $object.lastPosition) {
            $null = $object
            | Add-Member -MemberType NoteProperty -Force -Name lastPosition -Value $null
        }


        # At this point velocity is definitly a property of $object. If it's $null won't be processed.
        if ($null -NE $object.velocity) {
            # Also update the last position if the position changes here.
            # $object.lastPosition = [System.Numerics.Vector2]::new($object.position.x, $object.position.y)
            $object.position = [System.Numerics.Vector2]::add($object.position, $object.velocity)
        }

        $didExitDown = [System.Math]::round($object.position.y) -GT ($GameHeight - $object.canvas.Count - 1)
        $didExitUp = [System.Math]::round($object.position.y) -LT 1
        $didExitRight = [System.Math]::round($object.position.x) -GT ($WindowWidth - $object.canvas[0].Length - 1)
        $didExitLeft = [System.Math]::round($object.position.x) -LT 0


        if ($didExitDown -OR $didExitUp -OR $didExitRight -OR $didExitLeft) {
            $null = Invoke-Command -ScriptBlock $onExitScreen -ArgumentList $object, $GameObjects, $didExitLeft, $didExitRight, $didExitUp, $didExitDown
        }

        $roundedX = [System.Math]::Round($object.position.X)
        $roundedY = [System.Math]::Round($object.position.y)

        $roundedLastX = [System.Math]::Round($object.lastPosition.X)
        $roundedLastY = [System.Math]::Round($object.lastPosition.y)

        # Objects wich previously collided with other objects get redrawn for correct appearance.
        if (!$object.alwaysDraw -AND !$object.collisionMark -AND !$object.redrawMark) {
            if (!$object.initialDraw) {
                $object.initialDraw = $true
            }
            elseif ($roundedX -EQ $roundedLastX -AND $roundedY -EQ $roundedLastY) {
                return # skip redrawing elements whose position hasn't change.
            }
        }

        $null = $queue.add($object)

    }

        
    ################################################################################################################
    ###### Script block for drawing elements gets called on each object every tick.

    $draw = {
        param($object, $action)

        $roundedX = [System.Math]::Round($object.position.X)
        $roundedY = [System.Math]::Round($object.position.y)

        $roundedLastX = [System.Math]::Round($object.lastPosition.X)
        $roundedLastY = [System.Math]::Round($object.lastPosition.y)

        if ($action -EQ "undraw") {

            # Redraw Empty-Tiles on the old position.
            for ($index = 0; $index -LT $object.lastCanvas.Count; $index++) {

                # Allow for moving partially outside of screen with object on the left
                $offScreenOffset = 0
                if ($roundedLastX -LT 0) {
                    $offScreenOffset = [System.Math]::Abs($roundedLastX)
                } 
                #elseif($roundedLastX -GT $WindowWidth-$object.canvas[$index].Length){
                # $offScreenOffset =
                #}

                # Prevent string with single-length getting interpreted as System.Char
                $substringLine = "$($object.lastCanvas[$index])".substring($offScreenOffset)

                # This ignores and offsets any empty tile at the start of the currents canva's line of the object.
                $emptyOffset = 0 #$substringLine.Length - $substringLine.TrimStart().Length

                [System.Console]::SetCursorPosition($roundedLastX + $emptyOffset + $offScreenOffset, $roundedLastY + $index)
                $emptyLine = Get-LineOfChars -Length $substringLine.length -Char $EMPTY_TILE
                [System.Console]::Write($emptyLine)

            }
        }
        elseif ($action -EQ "draw") {

            $object.lastCanvas = @()
            $object.lastPosition = [System.Numerics.Vector2]::new($object.position.x, $object.position.y)

            for ($index = 0; $index -LT $object.canvas.Count; $index++) {

                # Allow for moving partially outside of screen with object
                $offScreenOffset = 0
                if ($roundedX -LT 0) {
                    $offScreenOffset = [System.Math]::Abs($roundedX)
                }
                
                # Prevent string with single-length getting interpreted as System.Char
                $substringLine = "$($object.canvas[$index])".substring($offScreenOffset)

                # This ignores and offsets any empty tile at the start of the currents canva's line of the object.
                $emptyOffset = $substringLine.Length - $substringLine.TrimStart().Length

                [System.Console]::SetCursorPosition($roundedX + $emptyOffset + $offScreenOffset, $roundedY + $index)
                [System.Console]::Write($object.colorCodes) # Set color
                [System.Console]::Write($substringLine.Trim())
                [System.Console]::Write("`e[0m") # Reset colors
                
                $object.lastCanvas += $object.canvas[$index]
                $object.collisionMark = $false
                $object.redrawMark = $false

            }
        }
    }


    ################################################################################################################
    ###### Script block for processing all tiles that an object occupies.

    $processOccupiedSpace = {
        param($object, $hashTable)

        # Skip any dead objects and non-collidable objects.
        if ($object.isDead -OR $object.ignoreCollisions) {
            return
        }
        
        if ($null -NE $object.collidableWith) {
            if ($object.collidableWith -NE '*' -AND $object.collidableWith -isnot [System.String[]]) {
                throw "'$($object.name)' - collidableWith only allows for '*' or 'String[]'"
            }
        }
                
        for ($row = 0; $row -LT $object.canvas.Count; $row++) {
            for ($col = 0; $col -LT $object.canvas[$row].Length; $col++) {

                if ($object.canvas[$row][$col] -EQ $EMPTY_TILE) {
                    continue; # Skip any empty tile for the collision check.
                }

                $tilePosition = [System.Numerics.Vector2]::new(
                    ($object.position.x + $col), ($object.position.y + $row) 
                )
                if ($hashTable.ContainsKey($tilePosition)) {
                    $hashTable[$tilePosition].objects += $object
                }
                else {
                    $hashTable[$tilePosition] = [PSCustomObject]@{
                        position = $tilePosition
                        objects  = [PSCustomObject[]]@($object)
                    }
                }

            }
        }

    }

    ################################################################################################################
    ###### The Gameloop processing and drawing all objects on each tick.

    # NOTE
    # This will be a hastable containing all positions with an array of GameObjects occupying that space.
    # All positions with more than one occupants are colliding with each other
    $CollisionHashTable = [System.Collections.Hashtable]::new()

    # Gameobjects and their colliding participants
    $collisionsGrouped = [System.Collections.Hashtable]::new()

    $drawingQueue = [System.Collections.ArrayList]::new()

    try {
        $gameEndingMessage = $null

        :GameLoop
        do {

            if ($TICK_COUNT -GT 0) {
                # Process collisions of last tick
                foreach ($collisionData in $collisionsGrouped.Values) {
                    if ($collisionData.participants.Count -EQ 0) {
                        continue
                    }
                    Invoke-Command -ScriptBlock $onCollision -ArgumentList ($collisionData.collider), ($collisionData.participants)
                }

                $collisionsGrouped.clear()
                $CollisionHashTable.clear()
                $drawingQueue.Clear()

                # Call the script block for updating custom parameters on each tick.
                $null = Invoke-Command -ScriptBlock $onEveryTickDo -ArgumentList $GameObjects, $GameWidth, $GameHeight

                # Only process key events when a key was pressed
                if ([System.Console]::KeyAvailable) {
                    $keyEvent = [System.Console]::ReadKey($true)
                    $null = Invoke-Command -ScriptBlock $onKeyEvent -ArgumentList $keyEvent, $GameObjects, $GameWidth, $GameHeight
                }
            }

            # Key events are processed before each update and draw.
            foreach ($objectName in ([System.String[]]$GameObjects.Keys) ) {

                $objectData = $GameObjects[$objectName]

                # If it's a list, draw each individual obecjt.
                if ($objectData -is [PSCustomObject[]] -OR $objectData -is [System.Object[]]) {

                    $processedList = [PSCustomObject]@()
                    for ($index = 0; $index -LT $objectData.Count; $index++) {
                        $null = $objectData[$index] | Add-Member -MemberType NoteProperty -Force -Name ParentName -Value $objectName
                        $null = $objectData[$index] | Add-Member -MemberType NoteProperty -Force -Name Name -Value "$objectName[$index]"
                        Invoke-Command -ScriptBlock $update -ArgumentList $objectData[$index], $drawingQueue
                        Invoke-Command -ScriptBlock  $processOccupiedSpace -ArgumentList $objectData[$index], $CollisionHashTable
                        if (!$objectData[$index].isDead) {
                            $processedList += $objectData[$index]
                        }
                        else {
                            $null = $drawingQueue.add($objectData[$index])
                        }
                    }
                    $GameObjects[$objectName] = $processedList
                }

                # If it's an object, only draw this single object.
                elseif ($objectData -is [PSCustomObject] -OR $objectData -is [System.Object]) {
                    $null = $objectData | Add-Member -MemberType NoteProperty -Force -Name Name -Value $objectName
                    Invoke-Command -ScriptBlock $update -ArgumentList $objectData, $drawingQueue
                    Invoke-Command -ScriptBlock $processOccupiedSpace -ArgumentList $objectData, $CollisionHashTable
                }

                # Throw error if object type is not allowed.
                else {
                    throw "$($objectData.GetType().ToString()) of '$objectName' is not allowed."
                }

            }


            # Make sure to undraw all objects, before starting to draw objects. To fix a bug.
            foreach ($object in $drawingQueue) {
                Invoke-Command -ScriptBlock $draw -ArgumentList $object, "undraw"
            }
            foreach ($object in $drawingQueue) {
                if (!$object.isDead) {
                    Invoke-Command -ScriptBlock $draw -ArgumentList $object, "draw"
                }
            }


            # Still prototyping and testing

            $CollisionHashTable.Keys
            | Where-Object {
                $CollisionHashTable[$_].objects.Count -GT 1
            }
            | ForEach-Object { # Loops through every tile position with a collision

                $collidingObjects = $CollisionHashTable[$_].objects
                $currentPosition = $CollisionHashTable[$_].position

                # Loops through every colliding object at the current tile position
                :colliderLoop
                foreach ($collider in $collidingObjects) {

                    # Mark for redrawing after collision occured.
                    $collider.collisionMark = $true

                    # if only passively collidable, generate no collision events.
                    if ($null -EQ $collider.collidableWith) {
                        continue colliderLoop
                    }

                    # Creates a collision group for the current collider if not existent.
                    if (!$collisionsGrouped.ContainsKey($collider.name)) {
                        $collisionsGrouped[$collider.name] = [PSCustomObject]@{
                            collider     = $collider
                            name         = $collider.name
                            participants = [PSCustomObject[]]@()
                            references   = [System.Collections.Hashtable]::new()
                        }
                    }

                    # The data for the currents collider collisions.
                    $collisionData = $collisionsGrouped[$collider.name]


                    # Loops through every collider at the current position, adding collision data.
                    :CollisionParticipentLoop
                    foreach ($participant in $collidingObjects) {
                        # Skip if the participant is the collider itself
                        if ($participant.name -EQ $collider.name) {
                            continue CollisionParticipentLoop
                        }

                        $nonIndexName = $participant.name -replace '\[\d+\]$', ''
                        if ($collider.collidableWith -NE '*' -AND $nonIndexName -notin $collider.collidableWith) {
                            continue CollisionParticipentLoop
                        }

                        # If there is no collision data for the current particpant, create it.
                        if (!$collisionData.references.ContainsKey($participant.name)) {
                            $collisionData.references[$participant.name] = @{
                                objectRef = $participant
                                name      = $participant.name
                                positions = [System.Numerics.Vector2[]]@($currentPosition)
                            }
                            $collisionData.participants += $collisionData.references[$participant.name]
                        }
                        # Else add the current tile position as information about where the collisions occured.
                        elseif ($currentPosition -notin $collisionData.participants[$participant.name].positions) {
                            $collisionData.references[$participant.name].positions += $currentPosition
                        }
                    }
                }
            }




            [System.Console]::CursorVisible = $false
            Start-Sleep -Milliseconds $TickIntervall
            $TICK_COUNT = $TICK_COUNT + 1

        } while ($null -EQ $keyEvent -OR $keyEvent.Key -NE [System.ConsoleKey]::Escape)

    }
    catch {
        Write-Error $_
    }
    finally {
        # Always leave function with a visible cursor in case of errors.
        [System.Console]::CursorVisible = $true
    }

    [System.Console]::SetCursorPosition($InvaderShip.position.x, $InvaderShip.position.y + 2)
    [System.Console]::Write("Press any key to continue...")
    $null = [System.Console]::ReadKey($true)
}