VBAF.Art.CastleCompetition.ps1

#Requires -Version 5.1

<#
.SYNOPSIS
    Multi-Agent Castle Competition - Three RL agents compete for aesthetic space
.DESCRIPTION
    ClassicAgent, WhimsicalAgent, and ModernAgent compete to generate the most
    aesthetically pleasing castle parade. Features emergent coordination behaviors.
    
    WEEK 8 GRAND FINALE - Brings together:
    - Neural networks (from Week 1)
    - Q-Learning (from Week 2)
    - Visualization (from Week 3)
    - Multi-agent systems (from Week 6)
    - Aesthetic rewards (Option 3)
    
.NOTES
    Part of VBAF - Art/Generative Module
    The culmination of Phase 1-2!
.EXAMPLE
    . C:\Users\henni\OneDrive\WindowsPowerShell\VBAF.Art.CastleCompetition.ps1
#>


# Load dependencies
$basePath = $PSScriptRoot
. "$basePath\VBAF.RL.QTable.ps1"
. "$basePath\VBAF.RL.ExperienceReplay.ps1"
. "$basePath\VBAF.RL.QLearningAgent.ps1"
. "$basePath\VBAF.Art.AestheticReward.ps1"

Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

# ==================== CASTLE AGENT SPECIALIZATIONS ====================

class CastleAgent {
    [string]$Name
    [string]$AgentType
    [System.Drawing.Color]$Color
    [string[]]$PreferredTypes
    [object]$Brain  # QLearningAgent
    [double]$TotalReward
    [int]$CastlesGenerated
    [System.Collections.ArrayList]$RewardHistory
    [System.Collections.ArrayList]$CastleHistory
    
    CastleAgent([string]$name, [string]$type, [System.Drawing.Color]$color, [string[]]$preferred) {
        $this.Name = $name
        $this.AgentType = $type
        $this.Color = $color
        $this.PreferredTypes = $preferred
        
        # All castle types available as actions
        $allTypes = @("Gothic", "FairyTale", "Cathedral", "Wizard", "Palace", "Oriental", "Fortress", "Ruins")
        $this.Brain = New-Object QLearningAgent -ArgumentList (,$allTypes), 0.15, 0.8
        
        $this.TotalReward = 0.0
        $this.CastlesGenerated = 0
        $this.RewardHistory = New-Object System.Collections.ArrayList
        $this.CastleHistory = New-Object System.Collections.ArrayList
    }
    
    # Decide which castle to generate
    [string] DecideCastle([hashtable]$context) {
        # Get state from context
        $state = $this.GetState($context)
        
        # Choose action using epsilon-greedy
        $castleType = $this.Brain.ChooseAction($state)
        
        # Bias toward preferred types (early exploration)
        if ($this.Brain.Epsilon -gt 0.5) {
            if ((Get-Random -Minimum 0.0 -Maximum 1.0) -lt 0.3) {
                # 30% chance to pick from preferred types
                $randomIndex = Get-Random -Minimum 0 -Maximum $this.PreferredTypes.Count
                $castleType = $this.PreferredTypes[$randomIndex]
            }
        }
        
        return $castleType
    }
    
    # Get state representation
    [string] GetState([hashtable]$context) {
        # State = combination of:
        # - Recent castle types (all agents)
        # - Current screen crowding
        # - Own recent performance
        
        $recentCastles = if ($context.ContainsKey("RecentCastles") -and $context.RecentCastles.Count -gt 0) {
            $context.RecentCastles[-1]
        } else {
            "EMPTY"
        }
        
        $crowding = if ($context.ContainsKey("CurrentCastleCount")) {
            if ($context.CurrentCastleCount -le 3) { "LOW" }
            elseif ($context.CurrentCastleCount -le 6) { "MED" }
            else { "HIGH" }
        } else {
            "MED"
        }
        
        return "$recentCastles|$crowding"
    }
    
    # Learn from outcome
    [void] Learn([string]$state, [string]$action, [double]$reward, [string]$nextState) {
        $this.Brain.Learn($state, $action, $reward, $nextState)
        $this.TotalReward += $reward
        $this.CastlesGenerated++
        $this.RewardHistory.Add($reward) | Out-Null
        $this.CastleHistory.Add($action) | Out-Null
    }
    
    # Get agent stats
    [hashtable] GetStats() {
        $avgReward = if ($this.CastlesGenerated -gt 0) {
            $this.TotalReward / $this.CastlesGenerated
        } else {
            0.0
        }
        
        $recentAvg = 0.0
        if ($this.RewardHistory.Count -gt 0) {
            $recentCount = [Math]::Min(10, $this.RewardHistory.Count)
            $start = $this.RewardHistory.Count - $recentCount
            $sum = 0.0
            for ($i = $start; $i -lt $this.RewardHistory.Count; $i++) {
                $sum += [double]$this.RewardHistory[$i]
            }
            $recentAvg = $sum / $recentCount
        }
        
        return @{
            Name = $this.Name
            Type = $this.AgentType
            CastlesGenerated = $this.CastlesGenerated
            TotalReward = $this.TotalReward
            AverageReward = $avgReward
            RecentAverage = $recentAvg
            Epsilon = $this.Brain.Epsilon
        }
    }
}

# ==================== COMPETITION ENVIRONMENT ====================

class CompetitionEnvironment {
    [object]$ClassicAgent
    [object]$WhimsicalAgent
    [object]$ModernAgent
    [object]$Rewarding  # AestheticReward
    
    [System.Collections.ArrayList]$AllCastles
    [System.Collections.ArrayList]$RecentCastles
    [int]$Episode
    [int]$Step
    [int]$MaxStepsPerEpisode
    
    [System.Collections.ArrayList]$EpisodeTotalRewards
    [System.Collections.ArrayList]$CoordinationScores
    
    CompetitionEnvironment() {
        # Create three competing agents
        $this.ClassicAgent = New-Object CastleAgent -ArgumentList "Classic", "Classic", `
            ([System.Drawing.Color]::FromArgb(139, 69, 19)), `
            @("Gothic", "Cathedral", "Fortress")
        
        $this.WhimsicalAgent = New-Object CastleAgent -ArgumentList "Whimsical", "Whimsical", `
            ([System.Drawing.Color]::FromArgb(255, 105, 180)), `
            @("FairyTale", "Wizard", "Palace")
        
        $this.ModernAgent = New-Object CastleAgent -ArgumentList "Modern", "Modern", `
            ([System.Drawing.Color]::FromArgb(64, 224, 208)), `
            @("Oriental", "Ruins", "Fortress")
        
        # Create aesthetic reward system
        $this.Rewarding = New-Object AestheticReward
        
        # Initialize tracking
        $this.AllCastles = New-Object System.Collections.ArrayList
        $this.RecentCastles = New-Object System.Collections.ArrayList
        $this.Episode = 0
        $this.Step = 0
        $this.MaxStepsPerEpisode = 30
        
        $this.EpisodeTotalRewards = New-Object System.Collections.ArrayList
        $this.CoordinationScores = New-Object System.Collections.ArrayList
    }
    
    # Run one step of competition
    [hashtable] RunStep() {
        $this.Step++
        
        # Build context
        $context = @{
            RecentCastles = $this.RecentCastles
            CurrentCastleCount = [Math]::Min(20, $this.RecentCastles.Count)
        }
        
        # Randomly select which agent generates this castle
        $agentChoice = Get-Random -Minimum 0 -Maximum 3
        
        if ($agentChoice -eq 0) {
            $agent = $this.ClassicAgent
        } elseif ($agentChoice -eq 1) {
            $agent = $this.WhimsicalAgent
        } else {
            $agent = $this.ModernAgent
        }
        
        # Agent decides castle type
        $state = $agent.GetState($context)
        $castleType = $agent.DecideCastle($context)
        
        # Calculate reward
        $rewardBreakdown = $this.Rewarding.CalculateReward($castleType, $context)
        $reward = $rewardBreakdown.TotalReward
        
        # Add to history
        $this.RecentCastles.Add($castleType) | Out-Null
        if ($this.RecentCastles.Count -gt 20) {
            $this.RecentCastles.RemoveAt(0)
        }
        
        $castleRecord = @{
            Agent = $agent.Name
            Type = $castleType
            Reward = $reward
            RewardBreakdown = $rewardBreakdown
            Step = $this.Step
            Color = $agent.Color
        }
        $this.AllCastles.Add($castleRecord) | Out-Null
        
        # Next state
        $nextContext = @{
            RecentCastles = $this.RecentCastles
            CurrentCastleCount = [Math]::Min(20, $this.RecentCastles.Count)
        }
        $nextState = $agent.GetState($nextContext)
        
        # Agent learns
        $agent.Learn($state, $castleType, $reward, $nextState)
        
        return $castleRecord
    }
    
    # End episode
    [void] EndEpisode() {
        # Calculate total reward for this episode
        $totalReward = 0.0
        $startIndex = [Math]::Max(0, $this.AllCastles.Count - $this.MaxStepsPerEpisode)
        
        for ($i = $startIndex; $i -lt $this.AllCastles.Count; $i++) {
            $totalReward += [double]$this.AllCastles[$i].Reward
        }
        
        $this.EpisodeTotalRewards.Add($totalReward) | Out-Null
        
        # Calculate coordination score (how well agents cooperated)
        $coordScore = $this.CalculateCoordination()
        $this.CoordinationScores.Add($coordScore) | Out-Null
        
        # Decay epsilon for all agents
        $this.ClassicAgent.Brain.EndEpisode($totalReward / 3.0)
        $this.WhimsicalAgent.Brain.EndEpisode($totalReward / 3.0)
        $this.ModernAgent.Brain.EndEpisode($totalReward / 3.0)
        
        $this.Episode++
        $this.Step = 0
    }
    
    # Calculate how well agents coordinated
    [double] CalculateCoordination() {
        if ($this.AllCastles.Count -lt 10) { return 0.5 }
        
        # Look at variety in recent castles
        $recent = New-Object System.Collections.ArrayList
        $startIndex = [Math]::Max(0, $this.AllCastles.Count - 10)
        
        for ($i = $startIndex; $i -lt $this.AllCastles.Count; $i++) {
            $recent.Add($this.AllCastles[$i].Type) | Out-Null
        }
        
        # Count unique types
        $unique = @{}
        foreach ($type in $recent) {
            $unique[$type] = $true
        }
        
        # More variety = better coordination
        $varietyScore = $unique.Count / 8.0  # 8 total types
        
        # Check agent distribution
        $agentCounts = @{ "Classic" = 0; "Whimsical" = 0; "Modern" = 0 }
        for ($i = $startIndex; $i -lt $this.AllCastles.Count; $i++) {
            $agentName = $this.AllCastles[$i].Agent
            $agentCounts[$agentName]++
        }
        
        # Balanced distribution = good coordination
        $balanced = 1.0
        foreach ($count in $agentCounts.Values) {
            $ratio = $count / 10.0
            if ($ratio -lt 0.2 -or $ratio -gt 0.5) {
                $balanced -= 0.2
            }
        }
        $balanced = [Math]::Max(0.0, $balanced)
        
        return ($varietyScore + $balanced) / 2.0
    }
    
    # Get statistics
    [hashtable] GetStats() {
        return @{
            Episode = $this.Episode
            Step = $this.Step
            TotalCastles = $this.AllCastles.Count
            Classic = $this.ClassicAgent.GetStats()
            Whimsical = $this.WhimsicalAgent.GetStats()
            Modern = $this.ModernAgent.GetStats()
            EpisodeTotalRewards = $this.EpisodeTotalRewards
            CoordinationScores = $this.CoordinationScores
        }
    }
}

# ==================== VISUALIZATION ====================

$global:env = New-Object CompetitionEnvironment
$global:running = $false

function Draw-CompetitionDashboard {
    param($g, $width, $height)
    
    # Background
    $bgBrush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::FromArgb(20, 20, 20))
    $g.FillRectangle($bgBrush, 0, 0, $width, $height)
    $bgBrush.Dispose()
    
    # Title
    $titleFont = New-Object System.Drawing.Font("Segoe UI", 16, [System.Drawing.FontStyle]::Bold)
    $titleBrush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::Gold)
    $g.DrawString("?? CASTLE COMPETITION - Three Agents Battle for Aesthetic Space ??", $titleFont, $titleBrush, 10, 10)
    $titleFont.Dispose()
    $titleBrush.Dispose()
    
    # Stats
    $stats = $global:env.GetStats()
    $font = New-Object System.Drawing.Font("Consolas", 9)
    
    # Agent stats (top section)
    $y = 50
    
    # Classic Agent
    $classicBrush = New-Object System.Drawing.SolidBrush($global:env.ClassicAgent.Color)
    $text = "CLASSIC: Castles: $($stats.Classic.CastlesGenerated) | Avg: $([Math]::Round($stats.Classic.RecentAverage, 2)) | e: $([Math]::Round($stats.Classic.Epsilon, 3))"
    $g.DrawString($text, $font, $classicBrush, 10, $y)
    $classicBrush.Dispose()
    
    # Whimsical Agent
    $whimBrush = New-Object System.Drawing.SolidBrush($global:env.WhimsicalAgent.Color)
    $text = "WHIMSICAL: Castles: $($stats.Whimsical.CastlesGenerated) | Avg: $([Math]::Round($stats.Whimsical.RecentAverage, 2)) | e: $([Math]::Round($stats.Whimsical.Epsilon, 3))"
    $g.DrawString($text, $font, $whimBrush, 10, $y + 20)
    $whimBrush.Dispose()
    
    # Modern Agent
    $modernBrush = New-Object System.Drawing.SolidBrush($global:env.ModernAgent.Color)
    $text = "MODERN: Castles: $($stats.Modern.CastlesGenerated) | Avg: $([Math]::Round($stats.Modern.RecentAverage, 2)) | e: $([Math]::Round($stats.Modern.Epsilon, 3))"
    $g.DrawString($text, $font, $modernBrush, 10, $y + 40)
    $modernBrush.Dispose()
    
    # Episode info
    $whiteBrush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::White)
    $text = "Episode: $($stats.Episode) | Step: $($stats.Step)/$($global:env.MaxStepsPerEpisode) | Total Castles: $($stats.TotalCastles)"
    $g.DrawString($text, $font, $whiteBrush, 10, $y + 70)
    
    # Recent castles parade (visual representation)
    $paradeY = 150
    $castleWidth = 40
    $castleHeight = 60
    $spacing = 5
    
    $labelFont = New-Object System.Drawing.Font("Arial", 7)
    $blackBrush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::Black)
    
    $recentCount = [Math]::Min(30, $global:env.AllCastles.Count)
    $startIndex = [Math]::Max(0, $global:env.AllCastles.Count - $recentCount)
    
    for ($i = 0; $i -lt $recentCount; $i++) {
        $castle = $global:env.AllCastles[$startIndex + $i]
        $x = 10 + ($i * ($castleWidth + $spacing))
        
        # Draw castle rectangle with agent color
        $agentBrush = New-Object System.Drawing.SolidBrush($castle.Color)
        $g.FillRectangle($agentBrush, $x, $paradeY, $castleWidth, $castleHeight)
        $agentBrush.Dispose()
        
        # Border
        $borderPen = New-Object System.Drawing.Pen([System.Drawing.Color]::White, 1)
        $g.DrawRectangle($borderPen, $x, $paradeY, $castleWidth, $castleHeight)
        $borderPen.Dispose()
        
        # Type label
        $shortType = $castle.Type.Substring(0, [Math]::Min(4, $castle.Type.Length))
        $g.DrawString($shortType, $labelFont, $blackBrush, $x + 2, $paradeY + 5)
        
        # Reward indicator (height bar)
        $rewardHeight = [Math]::Min(50, $castle.Reward * 5)
        $rewardBrush = New-Object System.Drawing.SolidBrush([System.Drawing.Color]::Lime)
        $g.FillRectangle($rewardBrush, $x + 2, $paradeY + $castleHeight - $rewardHeight - 2, 8, $rewardHeight)
        $rewardBrush.Dispose()
    }
    
    $labelFont.Dispose()
    $blackBrush.Dispose()
    
    # Reward history graph
    if ($stats.EpisodeTotalRewards.Count -gt 1) {
        $graphY = 250
        $graphHeight = 150
        $graphWidth = $width - 40
        
        # Grid
        $gridPen = New-Object System.Drawing.Pen([System.Drawing.Color]::FromArgb(40, 40, 40))
        for ($i = 0; $i -le 5; $i++) {
            $gridY = $graphY + ($graphHeight * $i / 5)
            $g.DrawLine($gridPen, 20, $gridY, 20 + $graphWidth, $gridY)
        }
        $gridPen.Dispose()
        
        # Reward line
        $points = New-Object System.Collections.ArrayList
        $maxReward = ($stats.EpisodeTotalRewards | Measure-Object -Maximum).Maximum
        if ($maxReward -eq 0) { $maxReward = 1 }
        
        for ($i = 0; $i -lt $stats.EpisodeTotalRewards.Count; $i++) {
            $x = 20 + ($i / [Math]::Max(1, $stats.EpisodeTotalRewards.Count - 1)) * $graphWidth
            $y = $graphY + $graphHeight - (($stats.EpisodeTotalRewards[$i] / $maxReward) * $graphHeight)
            $point = New-Object System.Drawing.PointF($x, $y)
            $points.Add($point) | Out-Null
        }
        
        if ($points.Count -gt 1) {
            $pen = New-Object System.Drawing.Pen([System.Drawing.Color]::Gold, 3)
            $g.DrawLines($pen, $points.ToArray([System.Drawing.PointF]))
            $pen.Dispose()
        }
        
        $g.DrawString("Episode Total Rewards (Learning Progress)", $font, $whiteBrush, 20, $graphY - 20)
    }
    
    # Coordination score graph
    if ($stats.CoordinationScores.Count -gt 1) {
        $graphY = 430
        $graphHeight = 100
        $graphWidth = $width - 40
        
        # Grid
        $gridPen = New-Object System.Drawing.Pen([System.Drawing.Color]::FromArgb(40, 40, 40))
        for ($i = 0; $i -le 4; $i++) {
            $gridY = $graphY + ($graphHeight * $i / 4)
            $g.DrawLine($gridPen, 20, $gridY, 20 + $graphWidth, $gridY)
        }
        $gridPen.Dispose()
        
        # Coordination line
        $points = New-Object System.Collections.ArrayList
        
        for ($i = 0; $i -lt $stats.CoordinationScores.Count; $i++) {
            $x = 20 + ($i / [Math]::Max(1, $stats.CoordinationScores.Count - 1)) * $graphWidth
            $y = $graphY + $graphHeight - (($stats.CoordinationScores[$i]) * $graphHeight)
            $point = New-Object System.Drawing.PointF($x, $y)
            $points.Add($point) | Out-Null
        }
        
        if ($points.Count -gt 1) {
            $pen = New-Object System.Drawing.Pen([System.Drawing.Color]::Cyan, 2)
            $g.DrawLines($pen, $points.ToArray([System.Drawing.PointF]))
            $pen.Dispose()
        }
        
        $g.DrawString("Coordination Score (Emergent Cooperation)", $font, $whiteBrush, 20, $graphY - 20)
    }
    
    $font.Dispose()
    $whiteBrush.Dispose()
}

# ==================== MAIN FORM ====================

Write-Host "`n - oo00oo - " -ForegroundColor Yellow
Write-Host "?? CASTLE COMPETITION - WEEK 8 GRAND FINALE ??" -ForegroundColor Cyan
Write-Host " - oo00oo - `n" -ForegroundColor Yellow
Write-Host "Three agents compete for aesthetic space:" -ForegroundColor Green
Write-Host " � Classic Agent (Brown) - Prefers Gothic, Cathedral, Fortress" -ForegroundColor DarkYellow
Write-Host " � Whimsical Agent (Pink) - Prefers FairyTale, Wizard, Palace" -ForegroundColor Magenta
Write-Host " � Modern Agent (Cyan) - Prefers Oriental, Ruins, Fortress" -ForegroundColor Cyan
Write-Host "`nWatch them learn to coordinate! ??`n" -ForegroundColor Yellow

$form = New-Object System.Windows.Forms.Form
$form.Text = "VBAF Castle Competition - Multi-Agent RL Arena"
$form.Width = 1400
$form.Height = 650
$form.BackColor = [System.Drawing.Color]::Black
$form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedDialog
$form.MaximizeBox = $false
$form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
$form.TopMost = $true

# Main panel
$mainPanel = New-Object System.Windows.Forms.Panel
$mainPanel.Left = 0
$mainPanel.Top = 50
$mainPanel.Width = 1400
$mainPanel.Height = 560
$mainPanel.BackColor = [System.Drawing.Color]::FromArgb(20, 20, 20)
$form.Controls.Add($mainPanel)

# Enable double buffering
$prop = $mainPanel.GetType().GetProperty("DoubleBuffered", [System.Reflection.BindingFlags]"Instance,NonPublic")
$prop.SetValue($mainPanel, $true, $null)

$mainPanel.Add_Paint({
    param($sender, $e)
    Draw-CompetitionDashboard $e.Graphics $sender.Width $sender.Height
})

# Start button
$startButton = New-Object System.Windows.Forms.Button
$startButton.Text = "? Start Competition"
$startButton.Left = 10
$startButton.Top = 10
$startButton.Width = 160
$startButton.Height = 30
$startButton.BackColor = [System.Drawing.Color]::FromArgb(0, 200, 80)
$startButton.ForeColor = [System.Drawing.Color]::White
$startButton.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
$startButton.Font = New-Object System.Drawing.Font("Segoe UI", 8, [System.Drawing.FontStyle]::Bold)
$startButton.Add_Click({
    if (-not $global:running) {
        $global:running = $true
        $startButton.Text = "? Pause"
        $startButton.BackColor = [System.Drawing.Color]::FromArgb(255, 140, 0)
    } else {
        $global:running = $false
        $startButton.Text = "? Resume"
        $startButton.BackColor = [System.Drawing.Color]::FromArgb(0, 200, 80)
    }
})
$form.Controls.Add($startButton)

# Reset button
$resetButton = New-Object System.Windows.Forms.Button
$resetButton.Text = "?? Reset"
$resetButton.Left = 180
$resetButton.Top = 10
$resetButton.Width = 100
$resetButton.Height = 30
$resetButton.BackColor = [System.Drawing.Color]::FromArgb(220, 50, 50)
$resetButton.ForeColor = [System.Drawing.Color]::White
$resetButton.FlatStyle = [System.Windows.Forms.FlatStyle]::Flat
$resetButton.Font = New-Object System.Drawing.Font("Segoe UI", 8, [System.Drawing.FontStyle]::Bold)
$resetButton.Add_Click({
    $global:running = $false
    $global:env = New-Object CompetitionEnvironment
    $startButton.Text = "? Start Competition"
    $startButton.BackColor = [System.Drawing.Color]::FromArgb(0, 200, 80)
    $mainPanel.Invalidate()
})
$form.Controls.Add($resetButton)

# Status label
$statusLabel = New-Object System.Windows.Forms.Label
$statusLabel.Text = "Ready - Press Start to begin the competition!"
$statusLabel.Left = 300
$statusLabel.Top = 15
$statusLabel.Width = 1000
$statusLabel.ForeColor = [System.Drawing.Color]::Gold
$statusLabel.Font = New-Object System.Drawing.Font("Consolas", 9, [System.Drawing.FontStyle]::Bold)
$form.Controls.Add($statusLabel)

# Update timer
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 100  # 10 steps per second
$timer.Add_Tick({
    if ($global:running) {
        # Run one step
        $result = $global:env.RunStep()
        
        # Check if episode complete
        if ($global:env.Step -ge $global:env.MaxStepsPerEpisode) {
            $global:env.EndEpisode()
        }
        
        # Update status
        $stats = $global:env.GetStats()
        $statusLabel.Text = "Episode: $($stats.Episode) | Step: $($stats.Step) | Last: $($result.Agent) ? $($result.Type) (Reward: $([Math]::Round($result.Reward, 2)))"
    }
    
    # Redraw
    $mainPanel.Invalidate()
})
$timer.Start()

Write-Host " ? Competition arena ready!" -ForegroundColor Green
Write-Host " - oo00oo - `n" -ForegroundColor Yellow

$form.ShowDialog()
$timer.Stop()