functions/utility/New-MiniGameRunUtility.ps1

function New-MiniGameRunUtility {
    <#
    .SYNOPSIS
    This return a utility class for running a game loop.
 
    .DESCRIPTION
    This return a utility class for running a game loop.
 
    It combines several other utility classes:
    - MiniGameObjectUtility
    - MiniGameRunUtility
    - MiniGameSoundUtility
 
    Itself can handle:
    - reading keyboard input
    - easy aliases for keypresses (LeftArrow => A)
    - starting a gameloop with vars, functions and all utility classes
 
    .OUTPUTS
 
     
    .EXAMPLE
 
    Create a utility instance to print "Hello World" when the key "A" or "LetfArrow" is pressed.
 
    PS> $gameUtility = New-MiniGameRunUtility -InvertY -Mute
 
        # Variables for the gameloop can be defined here.
        # These are availabe as the '$var'-Parameter.
        $gameUtility.set(
            @{
                LifePoints = 5
                Damage = 2
            }
        )
         
        # Custom sound from files can be registered here.
        # These are availabe in the 'once' and 'loop'-function via sound.once("damage") or sound.loop("music").
        $gameUtility.sound.add("music", "./path/to/music.wav")
        $gameUtility.sound.add("damage", "./path/to/hit.wav")
 
 
        # Define a player as an object.
        $player = $gameUtility.object.Add("player 1")
        $player.setPos(40,0)
        $player.AddCanvas('Right',
            @(
                " O_",
                " Ä "
            )
        )
        $player.AddCanvas('Left',
            @(
                "_O ",
                " Ä "
            )
        )
 
        # For Key Pressed any number of aliases can be registered
        $gameUtility.alias([System.ConsoleKey]::A, [System.ConsoleKey]::LeftArrow)
        $gameUtility.alias([System.ConsoleKey]::D, [System.ConsoleKey]::RightArrow)
        # Move the player on key presses
        $gameUtility.onKey([System.ConsoleKey]::A,
            {
                param($key, $var, $func, $canvas, $sound, $object, $stop)
                $object.Get("Player 1").SetCanvas('Left')
                $object.Get("Player 1").Move(-1, 0)
                $object.Get("Player 1").Redraw()
            }
        )
        $gameUtility.onKey([System.ConsoleKey]::D,
            {
                param($key, $var, $func, $canvas, $sound, $object, $stop)
                $object.Get("Player 1").SetCanvas('Right')
                $object.Get("Player 1").Move(1, 0)
                $object.Get("Player 1").Redraw()
            }
        )
 
        $gameUtility.once(
            {
                # This is similar to the gameloop but only executed once.
                # This is useful for setting up variables, a looping music and other setups that only need to be executed once.
                param($var, $func, $canvas, $sound, $object, $stop)
 
                $sound.loop("music")
                $object.draw()
            }
        )
        $gameUtility.loop(
            {
                # The first script-block gets executed in a gameloop
                param($var, $func, $canvas, $sound, $object, $stop)
 
                # Access and modify the defined Variables
                if($wasHit) {
                    $var.LifePoints -= 1
                }
 
                # Look at New-MiniGameDrawUtility
                # $canvas can be used to draw text in certain colors on the screen
                $canvas.ForeGround.onceColor("Red")
                $canvas.draw(0, 10, "Hello World")
 
                # Look at New-MiniGameSoundUtility
                # $sound can be used to play custom sounds from files
                $sound.once("My Sound")
 
                # Stops the gameloop when a condition is met
                if($var.LifePoints -LE 0){
                    $stop.Invoke()
                }
            },
            {
                # The second (optional) script-block always gets executed
                # - when the game ended
                # - was exited unexpectedly
                param($var, $func, $canvas, $sound, $object, $static)
            }
        )
 
    .LINK
     
    #>


    <#
     
        TODO:
 
        - Mouse input handling with [System.Windows.Forms.Curosr]::Position
    #>


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


        <#
         
            Explicitly mutes the sound, even when the sound.play() function is called.
             
            Sound is by default only played when called via $sound.play() in the game.loop or game.once function.
         
        #>

        [Parameter(
            Position = 1,
            Mandatory = $false
        )]
        [switch]
        $Mute,


        <#
         
            Setting custom boundaries for drawing objects.
 
            Defaults to full console window size.
 
        #>


        [Parameter(
            Mandatory = $false
        )]
        [System.Int32]
        $MaximumX = -1,
    
        [Parameter(
            Mandatory = $false
        )]
        [System.Int32]
        $MaximumY = -1,
    
        [Parameter(
            Mandatory = $false
        )]
        [System.Int32]
        $MinimumX = 0,
    
        [Parameter(
            Mandatory = $false
        )]
        [System.Int32]
        $MinimumY = 0,
    
    
        # Inverts drawing on the Y-Axis. Normally Y=0 points to the top of the screen.
        # This is usefull for having elements fly from the bottom upwards.
        [Parameter()]
        [switch]
        $InvertY,

        # Inverts drawing on the X-Axis. Normally X=0 points to the left side of the screen.
        # Might not be useful but complement to InvertY
        $InvertX,
    
        # This automatically trims empty spaces from inputet strings.
        [Parameter(
            Mandatory = $false
        )]
        [System.Boolean]
        $TrimSpaces = $true
    )

    $drawSettings = @{
        MinimumX   = $MinimumX
        MinimumY   = $MinimumY
        MaximumX   = $MaximumX
        MaximumY   = $MaximumY
        TrimSpaces = $TrimSpaces
        InvertY    = $InvertY.IsPresent
        InvertX    = $InvertX.IsPresent
    }
    $objectUtility = New-MiniGameObjectUtility @drawSettings
    $soundUtility = New-MiniGameSoundUtility -Mute:$Mute
    class MiniGameRunUtility {

        [System.Collections.Hashtable] $Functions = @{}
        [System.Collections.Hashtable] $Variables = @{
            ticks = 0
        }
        [System.Collections.Hashtable] $Settings = @{
            TickSpanMilliseconds = $TickSpanMilliseconds
            GameRunning          = $true
            CursorVisibility     = $false
            OnceScriptRun        = $false
        }
        [System.Collections.Hashtable] $EventHandler = @{
            keyEvent = @{}
        }

        $Sound = $soundUtility
        $Object = $objectUtility
        $Canvas = $objectUtility.canvas
        MiniGameRunUtility() {}



        [Void] Stop() {
            $this.Settings.GameRunning = $false
        }
        [Void] Clear() {
            $this.Canvas.Clear()
        }



        [Void] Set([System.String] $name, [System.Object] $value) {
            $this.Variables.Remove($name)
            $this.Variables.Add($name, $value)
        }

        [Void] Func([System.String] $name, [scriptblock] $action) {
            $this.Functions.Remove($name)
            $this.Functions.Add($name, $action)
        }

        [Void] Set([System.Collections.Hashtable] $variables) {
            $variables.GetEnumerator() 
            | ForEach-Object {
                $this.set($_.Key, $_.Value)
            }
        }



        [Void] Once([scriptblock] $action) {
            $this.Clear()
            $this.Settings.OnceScriptRun = $true
            $action.Invoke(
                $this.Variables,
                $this.Functions,
                $this.Canvas,
                $this.Sound,
                $this.Object,
                $this.Stop
            )
        }

        [Void] Loop([scriptblock] $action) {
            $this.loop($action, {})
        }
        [Void] Loop([scriptblock] $action, [scriptblock] $final) {

            if (-NOT $this.Settings.OnceScriptRun) {
                $this.Clear()
            }

            try {
                :GameLoop
                while ($this.Settings.GameRunning) {
                    [System.Console]::CursorVisible = $this.CursorVisibility

                    $this.processKeys()
                    $action.Invoke(
                        $this.Variables,
                        $this.Functions,
                        $this.Canvas,
                        $this.Sound,
                        $this.Object,
                        $this.Stop
                    )
                    $this.Variables.ticks += 1
                    
                    Start-Sleep -Milliseconds $this.Settings.TickSpanMilliseconds
                }
            }
            finally {
                $this.Sound.Stop()
                $final.Invoke(
                    $this.Variables,
                    $this.Functions,
                    $this.Canvas,
                    $this.Sound,
                    $this.Object
                )
            }
        }



        <#
 
          This processes key inputs and execute the action associated with the key.
     
        #>

        [void] ProcessKeys() {
            $this.processKeys($false, 1)
        }
        [Void] ProcessKeys([System.Boolean] $blocking) {
            $this.processKeys($blocking, 1)
        }
        [Void] ProcessKeys([System.Boolean] $blocking, [System.Byte] $expectedKeys) {
            while ([System.Console]::KeyAvailable -OR $blocking ) {
                $keyEvent = [System.Console]::ReadKey($true)
                $handlers = $this.EventHandler.keyEvent
                $script = $handlers[$keyEvent.Key]

                # If the script is a key alias, get the actual script
                while ($script -IS [System.ConsoleKey]) {
                    $script = $handlers[$script]
                }
    
                if ($null -NE $script) {

                    $script.Invoke(
                        $keyEvent,
                        $this.Variables,
                        $this.Functions,
                        $this.Canvas,
                        $this.Sound,
                        $this.Object,
                        $this.Stop
                    )
                }
                
                # When blocking execute only for expected key amounts
                if ($blocking -AND --$expectedKeys -LE 0) {
                    return
                }
            }
        }

        <#
 
          This defines actions in key pressens:
          key: The name of the pressed key
          action: A script block
     
        #>

        [Void] OnKey([System.ConsoleKey] $key, [ScriptBlock] $action) {
            $this.EventHandler.keyEvent.Remove($key)
            $this.EventHandler.keyEvent.Add($key, $action)
        }

        <#
 
          This processes key inputs and execute the action associated with the key.
          Source: The name of the pressed key
          Alias: The Alias to set for the key
        #>

        [void] Alias([System.ConsoleKey] $Source, [System.ConsoleKey] $Alias) {
            $this.EventHandler.keyEvent.Remove($Alias)
            $this.EventHandler.keyEvent.Add($Alias, $Source)
        }

        [void] RemoveKey([System.ConsoleKey] $key) {
            $this.EventHandler.keyEvent.Remove($key)
        }
        [void] RemoveAlias([System.ConsoleKey] $key) {
            $this.EventHandler.keyEvent.Remove($key)
        }
    }
    
    return [MiniGameRunUtility]@{}

}