Scripts/boids-flock.ps1

# Unique Concept: Boids flocking algorithm - simulates bird/fish swarm behavior with cohesion, separation, and alignment.
# Each boid follows three rules creating emergent collective motion patterns colored by velocity and density.


$ErrorActionPreference = 'Stop'
$esc = [char]27
$reset = "$esc[0m"

function Convert-HsvToRgb {
    param(
        [double]$Hue,
        [double]$Saturation,
        [double]$Value
    )
    $h = ($Hue % 1) * 6
    $sector = [math]::Floor($h)
    $fraction = $h - $sector
    $p = $Value * (1 - $Saturation)
    $q = $Value * (1 - $fraction * $Saturation)
    $t = $Value * (1 - (1 - $fraction) * $Saturation)
    switch ($sector) {
        0 { $r = $Value; $g = $t; $b = $p }
        1 { $r = $q; $g = $Value; $b = $p }
        2 { $r = $p; $g = $Value; $b = $t }
        3 { $r = $p; $g = $q; $b = $Value }
        4 { $r = $t; $g = $p; $b = $Value }
        default { $r = $Value; $g = $p; $b = $q }
    }
    return @([int][math]::Round($r * 255), [int][math]::Round($g * 255), [int][math]::Round($b * 255))
}

function Clamp {
    param([double]$Value, [double]$Min, [double]$Max)
    if ($Value -lt $Min) { return $Min }
    if ($Value -gt $Max) { return $Max }
    return $Value
}

$width = 90
$height = 30
$numBoids = 200  # Increased for more detail
$rand = [System.Random]::new(789)

# Initialize boids with random positions and velocities
$boids = for ($i = 0; $i -lt $numBoids; $i++) {
    [pscustomobject]@{
        X  = $rand.NextDouble() * $width
        Y  = $rand.NextDouble() * $height
        Vx = ($rand.NextDouble() - 0.5) * 2.0
        Vy = ($rand.NextDouble() - 0.5) * 2.0
    }
}

# Simulate several steps to create interesting patterns
$steps = 15
for ($step = 0; $step -lt $steps; $step++) {
    foreach ($boid in $boids) {
        # Flocking rules
        $cohesionX = 0.0
        $cohesionY = 0.0
        $separationX = 0.0
        $separationY = 0.0
        $alignmentX = 0.0
        $alignmentY = 0.0
        $nearCount = 0

        foreach ($other in $boids) {
            if ($other -eq $boid) { continue }

            $dx = $other.X - $boid.X
            $dy = $other.Y - $boid.Y
            $dist = [math]::Sqrt($dx * $dx + $dy * $dy)

            if ($dist -lt 15) {
                # Cohesion - move toward average position
                $cohesionX += $other.X
                $cohesionY += $other.Y

                # Alignment - match velocity
                $alignmentX += $other.Vx
                $alignmentY += $other.Vy

                $nearCount++

                # Separation - avoid crowding
                if ($dist -lt 5 -and $dist -gt 0) {
                    $separationX -= $dx / $dist
                    $separationY -= $dy / $dist
                }
            }
        }

        if ($nearCount -gt 0) {
            $cohesionX = ($cohesionX / $nearCount - $boid.X) * 0.01
            $cohesionY = ($cohesionY / $nearCount - $boid.Y) * 0.01
            $alignmentX = ($alignmentX / $nearCount) * 0.08
            $alignmentY = ($alignmentY / $nearCount) * 0.08
        }

        $separationX *= 0.1
        $separationY *= 0.1

        # Update velocity
        $boid.Vx += $cohesionX + $separationX + $alignmentX
        $boid.Vy += $cohesionY + $separationY + $alignmentY

        # Limit speed
        $speed = [math]::Sqrt($boid.Vx * $boid.Vx + $boid.Vy * $boid.Vy)
        if ($speed -gt 3.0) {
            $boid.Vx = ($boid.Vx / $speed) * 3.0
            $boid.Vy = ($boid.Vy / $speed) * 3.0
        }
    }

    # Update positions
    foreach ($boid in $boids) {
        $boid.X += $boid.Vx
        $boid.Y += $boid.Vy

        # Wrap around edges
        if ($boid.X -lt 0) { $boid.X += $width }
        if ($boid.X -ge $width) { $boid.X -= $width }
        if ($boid.Y -lt 0) { $boid.Y += $height }
        if ($boid.Y -ge $height) { $boid.Y -= $height }
    }
}

# Create density and velocity heatmap
$grid = @{}
foreach ($boid in $boids) {
    $gx = [int]$boid.X
    $gy = [int]$boid.Y

    if ($gx -ge 0 -and $gx -lt $width -and $gy -ge 0 -and $gy -lt $height) {
        $key = "$gx,$gy"
        $speed = [math]::Sqrt($boid.Vx * $boid.Vx + $boid.Vy * $boid.Vy)

        if ($grid.ContainsKey($key)) {
            $grid[$key].Count++
            $grid[$key].Speed += $speed
        }
        else {
            $grid[$key] = [pscustomobject]@{
                Count = 1
                Speed = $speed
                Vx    = $boid.Vx
                Vy    = $boid.Vy
            }
        }
    }
}

# Render
for ($row = $height - 1; $row -ge 0; $row--) {
    $sb = [System.Text.StringBuilder]::new()
    for ($col = 0; $col -lt $width; $col++) {
        $key = "$col,$row"

        if ($grid.ContainsKey($key)) {
            $cell = $grid[$key]
            $avgSpeed = $cell.Speed / $cell.Count

            # Color by speed and density
            $hue = (0.55 + $avgSpeed * 0.15) % 1
            $saturation = Clamp -Value (0.5 + 0.4 * ($cell.Count / 5.0)) -Min 0 -Max 1
            $value = Clamp -Value (0.35 + 0.6 * ($avgSpeed / 3.0)) -Min 0.3 -Max 1.0

            $rgb = Convert-HsvToRgb -Hue $hue -Saturation $saturation -Value $value

            # Symbol shows direction
            $angle = [math]::Atan2($cell.Vy, $cell.Vx)
            $octant = [int](($angle + [math]::PI) / (2 * [math]::PI) * 8) % 8

            $symbol = switch ($octant) {
                0 { '→' }
                1 { '↗' }
                2 { '↑' }
                3 { '↖' }
                4 { '←' }
                5 { '↙' }
                6 { '↓' }
                7 { '↘' }
                default { '●' }
            }

            if ($cell.Count -gt 3) { $symbol = '◉' }

            $null = $sb.Append("$esc[38;2;$($rgb[0]);$($rgb[1]);$($rgb[2])m$symbol")
        }
        else {
            $null = $sb.Append("$esc[38;2;10;10;15m ")
        }
    }
    Write-Host ($sb.ToString() + $reset)
}

Write-Host $reset