poshTic_Tac_Toe.psm1

<#
.SYNOPSIS
    This starts an interactive Tic_Tac_Toe game in the console. Press Esc key to exit game.
.DESCRIPTION
    This starts an interactive Tic_Tac_Toe game in the console. Mostly using using mostly [System.Console] class. Press Esc key to exit game.
 
    Was largely influenced and inspirated b the ASCII Box menues created by mossrich
    https://github.com/mossrich/PowershellRecipes/blob/master/AsciiMenu.ps1
 
    I also re-used a lot of code I wrote in my Fun.Arduino repo where I created Tic Tac Toe on an adruino with C.
.EXAMPLE
    PS C:\> Start-Tic_Tac_Toe -DifficultyLevel "Easy" -Bestof 3
 
    Starts a new Tic Tac Toe game on hard mode with 3 games being played and the winner of 2 wins the game.
.INPUTS
    Inputs (if any)
.OUTPUTS
    Output (if any)
.NOTES
    General notes
#>

function Start-Tic_Tac_Toe{
    param(
        [ValidateSet("Easy","Hard")]
        [string]$DifficultyLevel = "Hard",
        [ValidateSet(1,3,5,99)]
        [int]$Bestof = 1,
        [switch]$PlayVictorySong
    )

    $Script:Player = "X" 
    $Script:top = [console]::CursorTop
    $Script:PlayerOScore = 0
    $Script:PLayerXScore = 0
    $Script:CurrentRound = 0
    $Script:cursor = 0
    $Script:gameGrid = New-VirtualGameBoard

    Draw-Board -PlayerXScore $PLayerXScore -PlayerOScore $PlayerOScore
    [console]::SetCursorPosition($gameGrid[0].x,$gameGrid[0].y)

    do {
        $Key = [Console]::ReadKey($true)
        if ($Key.key -eq [ConsoleKey]::DownArrow){
            $previousPosition = $cursor
            $cursor = $cursor + 3
            if ($cursor -ge 9){
                $cursor = $cursor % 9
            }
            if ($gameGrid[$cursor].Value -eq " "){
                [console]::SetCursorPosition($gameGrid[$cursor].X,$gameGrid[$cursor].Y)
            }
            else{
                $cursor = $previousPosition
                $cursor = $cursor + 6
                if ($cursor -ge 9){
                    $cursor = $cursor % 9
                }
                if ($gameGrid[$cursor].Value -eq " "){
                    [console]::SetCursorPosition($gameGrid[$cursor].X,$gameGrid[$cursor].Y)
                }
                else{
                    $cursor = $previousPosition
                }
            }
        } #if down key
        elseif ($Key.key -eq [ConsoleKey]::UpArrow){
                $previousPosition = $cursor
                $cursor = $cursor - 3
                if ($cursor -le -1){
                    $cursor = 9 + $cursor
                }
                if ($gameGrid[$cursor].Value -eq " "){
                    [console]::SetCursorPosition($gameGrid[$cursor].X,$gameGrid[$cursor].Y)
                }
                else{
                    $cursor = $previousPosition
                    $cursor = $cursor - 6
                    if ($cursor -le -1){
                        $cursor = 9 + $cursor
                    }
                    if ($gameGrid[$cursor].Value -eq " "){
                        [console]::SetCursorPosition($gameGrid[$cursor].X,$gameGrid[$cursor].Y)
                    }
                    else{
                        $cursor = $previousPosition
                    }
                }
        }
        elseif ($Key.key -eq [ConsoleKey]::RightArrow){
            $cursor = $cursor + 1
            if ($cursor -ge 9){
                $cursor = 0
            }
            while ($gameGrid[$cursor].Value -ne " "){
                $cursor++
            }
            [console]::SetCursorPosition($gameGrid[$cursor].X,$gameGrid[$cursor].Y)
        }
        elseif ($Key.key -eq [ConsoleKey]::LeftArrow){
            $cursor = $cursor - 1
            if ($cursor -lt 0){
                $cursor = 8
            }
            while ($gameGrid[$cursor].Value -ne " "){
                $cursor--
            }
            [console]::SetCursorPosition($gameGrid[$cursor].X,$gameGrid[$cursor].Y)
        }
        elseif ($key.Key -eq [ConsoleKey]::Enter){
            Set-tttSquare -Player $Player -move $cursor
            [console]::SetCursorPosition(0,$top)
            Draw-Board -PlayerXScore $PLayerXScore -PlayerOScore $PlayerOScore

            if (Test-WinConditions $gameGrid){
                $PLayerXScore++
                if (Test-BestofWinner -NumberofGames $Bestof -Score $PLayerXScore){
                    [console]::SetCursorPosition(0,$top)
                    Write-GameResults -Results "YouWin"
                    if ($PlayVictorySong){
                        Play-VictorySong
                    }
                    sleep 2
                    Clear-GameScreen
                    break
                }
                $CurrentRound++
                $gameGrid = New-VirtualGameBoard
                [console]::SetCursorPosition(0,$top)
                Write-GameResults -Results "X"
                sleep 2
                Clear-GameScreen
                Draw-Board -PlayerXScore $PLayerXScore -PlayerOScore $PlayerOScore
            }

            #Test Draw
             if (Test-DrawCondition -game $gameGrid){
                $gameGrid = New-VirtualGameBoard
                $CurrentRound++
                [console]::SetCursorPosition(0,$top)
                Write-GameResults -Results "Draw"
                sleep 2
                Clear-GameScreen
                Draw-Board -PlayerXScore $PLayerXScore -PlayerOScore $PlayerOScore
            }

            $Player = 'O'
            [console]::CursorVisible = $false
            sleep -Milliseconds (Get-Random -Minimum 10 -Maximum 1000)
            $botMove = New-BotMove -Difficulty $DifficultyLevel -Grid $gameGrid
            [console]::SetCursorPosition(0,$top)
            [console]::CursorVisible = $true
            Draw-Board -PlayerXScore $PLayerXScore -PlayerOScore $PlayerOScore
            if (Test-WinConditions $gameGrid){
                $PLayerOScore++
                if (Test-BestofWinner -NumberofGames $Bestof -Score $PLayerOScore){
                    [console]::SetCursorPosition(0,$top)
                    Write-GameResults -Results "YouLost"
                    sleep 2
                    Clear-GameScreen
                    break
                }
                $CurrentRound++
                $gameGrid = New-VirtualGameBoard
                [console]::SetCursorPosition(0,$top)
                Write-GameResults -Results "O"
                sleep 2
                Clear-GameScreen
                Draw-Board -PlayerXScore $PLayerXScore -PlayerOScore $PlayerOScore
            }
            
            #Test Draw
            if (Test-DrawCondition -game $gameGrid){
                $gameGrid = New-VirtualGameBoard
                $CurrentRound++
                [console]::SetCursorPosition(0,$top)
                Write-GameResults -Results "Draw"
                sleep 2
                Clear-GameScreen
                Draw-Board -PlayerXScore $PLayerXScore -PlayerOScore $PlayerOScore
            }

            $Player = 'X'
            for ($i = 0; $i -le 8; $i++){
                if ($gameGrid[$i].Value -eq " "){
                    $cursor = $i
                    [console]::SetCursorPosition($gameGrid[$cursor].x,$gameGrid[$cursor].y)
                    break
                }
            }
        }
    }
    While( -not ([ConsoleKey]::Escape -eq $key.Key) )
}
function Clear-GameScreen {
    [console]::SetCursorPosition(0,$top)
    $WhiteSpace = (0.. ([console]::WindowWidth - 2) | foreach {"$([char]32)"})
    Write-Host "`n$WhiteSpace`n$WhiteSpace`n$WhiteSpace`n$WhiteSpace`n$WhiteSpace`n$WhiteSpace`n$WhiteSpace"
    [console]::SetCursorPosition(0,$top)
}
function Draw-Board {
    param(
        $PlayerXScore,
        $PlayerOScore
    )
    $line = (0.. 12 | foreach {[char]9472}) -join ''
    $topborder = ([char]9484,$line,[char]9488) -join ''

    $middleLine = (0.. 2 | foreach {[char]9472}) -join ''
    $middleborder = $middleLine,[char]9532,$middleLine,[char]9532,$middleLine -join ''

    $bottomborder = ([char]9492,$line,[char]9496) -join ''

    $Board = "
    $topborder $topborder
    $([char]9474) $(Write-tttColorText -Text $gameGrid[0].Value) $([char]9474) $(Write-tttColorText -Text $gameGrid[1].Value) $([char]9474) $(Write-tttColorText -Text $gameGrid[2].Value) $([char]9474) $([char]9474) SCORE $([char]9474)
    $([char]9474) $middleborder $([char]9474) $([char]9500)$line$([char]9508)
    $([char]9474) $(Write-tttColorText -Text $gameGrid[3].Value) $([char]9474) $(Write-tttColorText -Text $gameGrid[4].Value) $([char]9474) $(Write-tttColorText -Text $gameGrid[5].Value) $([char]9474) $([char]9474) $(Write-tttColorText -Text "Player X: " -Color "Green")$($PlayerXScore.ToString("0")) $([char]9474)
    $([char]9474) $middleborder $([char]9474) $([char]9474) $(Write-tttColorText -Text $("Player O: ") -Color "Red")$($PlayerOScore.ToString("0")) $([char]9474)
    $([char]9474) $(Write-tttColorText -Text $gameGrid[6].Value) $([char]9474) $(Write-tttColorText -Text $gameGrid[7].Value) $([char]9474) $(Write-tttColorText -Text $gameGrid[8].Value) $([char]9474) $bottomborder
    $bottomborder
        "

    Write-Host $Board
}
function Get-MusicNote {
    param($note)
    switch ($note){
"NOTE_B0"  {31}
"NOTE_C1"  {33}
"NOTE_CS1" {35}
"NOTE_D1"  {37}
"NOTE_DS1" {39}
"NOTE_E1"  {41}
"NOTE_F1"  {44}
"NOTE_FS1" {46}
"NOTE_G1"  {49}
"NOTE_GS1" {52}
"NOTE_A1"  {55}
"NOTE_AS1" {58}
"NOTE_B1"  {62}
"NOTE_C2"  {65}
"NOTE_CS2" {69}
"NOTE_D2"  {73}
"NOTE_DS2" {78}
"NOTE_E2"  {82}
"NOTE_F2"  {87}
"NOTE_FS2" {93}
"NOTE_G2"  {98}
"NOTE_GS2" {104}
"NOTE_A2"  {110}
"NOTE_AS2" {117}
"NOTE_B2"  {123}
"NOTE_C3"  {131}
"NOTE_CS3" {139}
"NOTE_D3"  {147}
"NOTE_DS3" {156}
"NOTE_E3"  {165}
"NOTE_F3"  {175}
"NOTE_FS3" {185}
"NOTE_G3"  {196}
"NOTE_GS3" {208}
"NOTE_A3"  {220}
"NOTE_AS3" {233}
"NOTE_B3"  {247}
"NOTE_C4"  {262}
"NOTE_CS4" {277}
"NOTE_D4"  {294}
"NOTE_DS4" {311}
"NOTE_E4"  {330}
"NOTE_F4"  {349}
"NOTE_FS4" {370}
"NOTE_G4"  {392}
"NOTE_GS4" {415}
"NOTE_A4"  {440}
"NOTE_AS4" {466}
"NOTE_B4"  {494}
"NOTE_C5"  {523}
"NOTE_CS5" {554}
"NOTE_D5"  {587}
"NOTE_DS5" {622}
"NOTE_E5"  {659}
"NOTE_F5"  {698}
"NOTE_FS5" {740}
"NOTE_G5"  {784}
"NOTE_GS5" {831}
"NOTE_A5"  {880}
"NOTE_AS5" {932}
"NOTE_B5"  {988}
"NOTE_C6"  {1047}
"NOTE_CS6" {1109}
"NOTE_DS6" {1245}
"NOTE_E6"  {1319}
"NOTE_F6"  {1397}
"NOTE_FS6" {1480}
"NOTE_G6"  {1568}
"NOTE_GS6" {1661}
"NOTE_A6"  {1760}
"NOTE_AS6" {1865}
"NOTE_B6"  {1976}
"NOTE_C7"  {2093}
"NOTE_CS7" {2217}
"NOTE_D7"  {2349}
"NOTE_DS7" {2489}
"NOTE_E7"  {2637}
"NOTE_F7"  {2794}
"NOTE_FS7" {2960}
"NOTE_G7"  {3136}
"NOTE_GS7" {3322}
"NOTE_A7"  {3520}
"NOTE_AS7" {3729}
"NOTE_B7"  {3951}
"NOTE_C8"  {4186}
"NOTE_CS8" {4435}
"NOTE_D8"  {4699}
"NOTE_DS8" {4978}
    }
}
function New-ASCIIArt {
    param(
        $Text,
        $Font
    )
    Invoke-RestMethod -Uri "https://artii.herokuapp.com/make?text=$Text"
}
function New-BotMove {
    param(
        $Difficulty,
        $grid
    )

    $freeSquares = $grid | where Value -eq " "
    if ($Difficulty -eq "Easy"){
      $randomNumber = $freeSquares | Get-Random
      $grid[$randomNumber.ID].Value = "O"
      return
    }
    elseif ($Difficulty -eq "Hard"){
      $hardMove = New-HardBotMove $grid
      if ($hardMove -eq 9){
        New-BotMove -Difficulty "Easy" -grid $grid
      }
      else{
          $grid[$hardMove].Value = "O"
      }
    }
}
function New-HardBotMove($grid){
    for ($x = 0; $x -le 8; $x += 3){
        if ($grid[$x].Value -eq " " -and $grid[$x+1].Value -eq $grid[$x+2].Value -and $grid[$x+1].Value -ne " "){
          return $x;
        }
        elseif ($grid[$x+1].Value -eq " " -and $grid[$x].Value -eq $grid[$x+2].Value -and $grid[$x].Value -ne " "){
          return $x+1;

        }
        elseif ($grid[$x+2].Value -eq " " -and $grid[$x].Value -eq $grid[$x+1].Value -and $grid[$x].Value -ne " "){
          return $x+2;
        }
    }
    for ($x = 0; $x -le 2; $x++){
      if ($grid[$x].Value -eq " " -and $grid[$x+3].Value -eq $grid[$x + 6].Value -and $grid[$x+3].Value -ne " "){
        return $x;
      }
      elseif ($grid[$x+3].Value -eq " " -and $grid[$x].Value -eq $grid[$x+6].Value -and $grid[$x].Value -ne " "){
          return $x+3;
      }
      elseif ($grid[$x+6].Value -eq " " -and $grid[$x].Value -eq $grid[$x+3].Value -and $grid[$x].Value -ne " "){
          return $x+6;
      }
    }
    if ($grid[0].Value -eq " " -and $grid[4].Value -eq $grid[8].Value -and $grid[4].Value -ne " "){
      return 0;
    }
    elseif ($grid[4].Value -eq " " -and $grid[0].Value -eq $grid[8].Value -and $grid[0].Value -ne " "){
      return 4;
    }
    elseif ($grid[8].Value -eq " " -and $grid[0].Value -eq $grid[4].Value -and $grid[4].Value -ne " "){
      return 8;
    }
    elseif ($grid[2].Value -eq " " -and $grid[4].Value -eq $grid[6].Value -and $grid[4].Value -ne " "){
      return 2;
    }
    elseif ($grid[4].Value -eq " " -and $grid[2].Value -eq $grid[6].Value -and $grid[2].Value -ne " "){
      return 4;
    }
    elseif ($grid[6].Value -eq " " -and $grid[2].Value -eq $grid[4].Value -and $grid[4].Value -ne " "){
      return 6;
    }
    return 9;
}
function New-VirtualGameBoard{
    #this creates the virtual gameboard 0 being the top most left, and 8 being the right most bottom
    0.. 8 | foreach {
        $YAdjust = if ($_ -lt 3){2}
        elseif($_ -gt 5){6}
        else{4}
        [PSCustomObject]@{
            Value = " "
            X = ($_ % 3) + 7 + (($_ % 3) * 3)
            Y = ($top + $YAdjust)
            ID = $_
        }
    }
}
function Play-VictorySong{
    <#these notes were mainly adopted from https://github.com/NaWer/beep/blob/master/Gaming/Final%20Fantasy%20-%20Victory%20Theme
    and my arduino Tic Tac Toe project https://github.com/MrPig91/Fun.Arduino/tree/master/Projects#>

    
    $notes = (
        "NOTE_B5","0","NOTE_B5","0","NOTE_B5","0","NOTE_B5",
        "NOTE_G5","NOTE_A5","NOTE_B5","0","NOTE_A5","NOTE_B5",
        "NOTE_FS5","NOTE_E5","NOTE_FS5","NOTE_E5","0",
        "NOTE_A5","NOTE_A5","0","NOTE_GS5","NOTE_A5",
        "0","NOTE_GS5","NOTE_GS5","0","NOTE_FS5",
        "NOTE_E5","NOTE_DS5","NOTE_E5","0","NOTE_CS5",
        "NOTE_FS5","NOTE_E5","NOTE_FS5","NOTE_E5","0",
        "NOTE_A5","NOTE_A5","0","NOTE_GS5","NOTE_A5",
        "0","NOTE_GS5","NOTE_GS5","0","NOTE_FS5",
        "NOTE_E5","NOTE_FS5","NOTE_A5","0","NOTE_B5"
    )

    $dur = (
            100, 100, 100, 100, 100, 100, 428,
           428, 428, 175, 214, 175, 857,
           428, 428, 428, 175, 175,
           428, 175, 175, 428, 175,
           175, 428, 175, 175, 428,
           428, 428, 175, 175, 1714,
           428, 428, 428, 175, 175,
           428, 175, 175, 428, 175,
           175, 428, 175, 175, 428,
           428, 428, 175, 175, 1714
    )
    $ErrorActionPreference = "STOP"

    for ($x=0;$x -le 52;$x++){
        try{
            if ($notes[$x] -eq "0"){
                Sleep -Milliseconds $dur[$x]
            }
            else{
                [System.Console]::Beep((Get-MusicNote -Note $notes[$x]),($dur[$x]))
            }
        }
        catch{
            Write-Host $notes[$x]
        }
    }
}
function Set-tttSquare ($Player,$move){
    $gameGrid[$move].Value = $Player
}
function Test-BestofWinner {
    param(
        $NumberofGames,
        $Score
    )

    $winNeeded = switch ($NumberofGames){
        1 {1}
        3 {2}
        5 {3}
        99 {50}
    }
    if ($score -eq $winNeeded){
        $true
    }
    else{
        $false
    }
}
function Test-DrawCondition {
    param(
        $game
    )

  for ($x = 0; $x -le 8; $x++){
    if ($game[$X].Value -eq " "){
      return $false
    }
  }
  return $true;
}
function Test-WinConditions{
    param(
        $moves
    )
    for ($x = 0;$x -le 8; $x += 3){
      if ($moves[$x].Value -ne " "){
        if (($moves[$x].Value -eq $moves[$x+1].Value) -and ($moves[$x].Value -eq $moves[$x+2].Value)){
        return $true;
        }
      }
    }
    for ($x = 0; $x -le 2; $x++){
      if (($moves[$x].Value -ne " ") -and ($moves[$x].Value -eq $moves[$x + 3].Value) -and ($moves[$x].Value -eq $moves[$x + 6].Value)){
        return $true;
      }
    }
    if (($moves[0].Value -ne " ") -and ($moves[0].Value -eq $moves[4].Value) -and ($moves[0].Value -eq $moves[8].Value)){
      return $true;
    }
    elseif (($moves[2].Value -ne " ") -and ($moves[2].Value -eq $moves[4].Value) -and ($moves[2].Value -eq $moves[6].Value)){
      return $true;
    }
    else{
     return $false; 
    }
}
function Write-GameResults {
    param(
        $Results 
    )

    $WhiteSpace = (0.. ([console]::WindowWidth - 2) | foreach {"$([char]32)"})

    if ($Results -eq "X"){Write-Host ("
         _____ _ __ __ __ ___ _
        | __ \| | \ \ / / \ \ / (_) | |
        | |__) | | __ _ _ _ ___ _ __ \ V / \ \ /\ / / _ _ __ ___ | |
        | ___/| |/ _ | | | |/ _ \ '__| > < \ \/ \/ / | | '_ \/ __| | |
        | | | | (_| | |_| | __/ | / . \ \ /\ / | | | | \__ \ |_|
        |_| |_|\__,_|\__, |\___|_| /_/ \_\ \/ \/ |_|_| |_|___/ (_)
                         __/ |
                        |___/ "
 -join "") -ForegroundColor Green

    }
    elseif ($Results -eq "O"){Write-Host ("
     _____ _ ____ __ ___ _
    | __ \| | / __ \ \ \ / (_) | |
    | |__) | | __ _ _ _ ___ _ __ | | | | \ \ /\ / / _ _ __ ___ | |
    | ___/| |/ _ | | | |/ _ \ '__| | | | | \ \/ \/ / | | '_ \/ __| | |
    | | | | (_| | |_| | __/ | | |__| | \ /\ / | | | | \__ \ |_|
    |_| |_|\__,_|\__, |\___|_| \____/ \/ \/ |_|_| |_|___/ (_)
                     __/ |
                    |___/
   "
 -join "") -ForegroundColor Red
    }
    elseif ($Results -eq "YouWin"){Write-Host ("
    __ __ __ _______ _ _ _ _ _
    \ \ / / \ \ / /_ _| \ | | | | | | | |
     \ \_/ /__ _ _ \ \ /\ / / | | | \| | | | | | | |
      \ / _ \| | | | \ \/ \/ / | | | . | | | | | | |
       | | (_) | |_| | \ /\ / _| |_| |\ | |_| |_| |_|
       |_|\___/ \__,_| \/ \/ |_____|_| \_| (_) (_) (_)
                                                               "
 -join "") -ForegroundColor Green
    }
    elseif ($Results -eq "YouLost"){Write-Host ("
    __ __ _ ____ _____ _______ _ _ _
    \ \ / / | | / __ \ / ____|__ __| | | | | | |
     \ \_/ /__ _ _ | | | | | | (___ | | | | | | | |
      \ / _ \| | | | | | | | | |\___ \ | | | | | | | |
       | | (_) | |_| | | |____ |__| |____) | | | |_| |_| |_|
       |_|\___/ \__,_| |______\____/|_____/ |_| (_) (_) (_)$whiteSpace`n$whiteSpace
   "
 -join "") -ForegroundColor Red
   Write-Host "$whiteSpace`n$whiteSpace"

    }
    else{Write-Host ("
         _____ _____ __ __
        | __ \| __ \ /\ \ / /
        | | | | |__) | / \ \ /\ / /
        | | | | _ / / /\ \ \/ \/ /
        | |__| | | \ \ / ____ \ /\ /
        |_____/|_| \_\_/ \_\/ \/ $whiteSpace
$whiteSpace`n$whiteSpace"
 -join "")
        Write-Host "$whiteSpace`n$whiteSpace"

    }
}
function Write-tttColorText {
    param(
        [Alias("T")]
        [Parameter(Mandatory)]
        [string]$Text,
        [ValidateSet("Green","Red","Yellow")]
        [Alias("C")]
        [string]$Color
    )

    $escape = [char]27 + '['
    $resetAttributes = "$($escape)0m"
    if ($PSBoundParameters.ContainsKey("Color")){
        $FGColor = switch ($Color){
            "Green" {"92m"}
            "Red" {"91m"}
            "Yellow" {"93m"}
        }
    }
    else{
        $FGColor = switch ($Text){
            "X" {"92m"}
            "O" {"91m"}
            default {"93m"}
        }
    }

    $WriteHost = $escape + $FGColor + $Text + $resetAttributes
    $WriteHost
}
Export-ModuleMember -function Start-Tic_Tac_Toe