Category Archives: Uncategorized

Mother Box prop

Building a little tough screen prop that can run html for a Mother Box, designed to run as a phone “wallpaper” and part of a standalone esp32 “cheap yellow display” device in a case, that uses touch screen, and internal speakers.

https://svonberg.org/wp-content/uploads/2025/11/motherbox10.html

Core Functionality


* Kirby-Styled Interface (The Mother Box):

* The interface is a grid of 4×5 metal plates rendered on an HTML <canvas>.

* The entire design uses a bold, limited color palette (gold, red, black, white, magenta/cyan) with heavy outlines and deep shadows, mimicking the look of 1970s comic book printing. 

* The center four plates are replaced by a large, singular “Eye” component that tracks an imaginary, slow-moving target, simulating constant internal thought.

* Diverse Greebles and Tech (Visual Components):
* Each grid section randomly generates one of several distinct Kirby Tech patterns (or “greebles”) to maximize visual variety and complexity:
* Pipes: Horizontal gold tubes with simulated energy pulses flowing through them.
* Circuit: Black panels with angular, glowing lines of gold and cyan data flow.
* Vents: Blue vents with subtle energy (Krackle) escaping.
* Krackle Pit: A recessed area of raw, magenta energy densely packed with black “Kirby Krackle” dots.
* Machinery: Heavy, blocky clusters of gold and red industrial shapes.
* Logic Node: A central blue core with data lines radiating outwards to the corners.
* Energy Fins: Vertical gold fins with glowing energy pulsing between them.
* Omega Block: Large, complex black geometric glyphs, inspired by New Genesis architecture.

Audio System and Interaction:
   * Autonomous Hum: The box maintains a constant, low, throbbing sawtooth oscillator hum (using the Web Audio API) with a slight frequency modulation, suggesting deep, ongoing machine thought.
   * Manual Ping: The powerful “PING!” sound (a high-frequency sine bell tone) and the accompanying visual burst are only triggered by the user when they click or touch the screen.
* Autonomous Animation:
   * The machine operates on its own loop, periodically and randomly activating certain tech tiles (making them briefly pulse with light or energy) and making the central eye slowly scan across the screen, ensuring the box appears perpetually alive and thinking, even without user input.

https://svonberg.org/wp-content/uploads/2025/11/motherbox10.html

Day 20,750

Out on the corner today, just past the hedges by the Firehouse Subs and the bank with the cold brick face, I had my first sighting of a Santa’s helper this season. Not the mall kind, not the glossy catalog kind, but the roadside variety, wind tousling the beard, boots planted on the curb like deep roots. A flash of red against the winter gray, waving at traffic as if every passing car needed a little nudge toward cheer.

His ride was propped beside him, a bright red bike decked out with its own tiny Santa hats, white beard beard and a flag fluttering like it had holiday opinions. I liked that. The whole scene felt like catching a glimpse of a migrating bird you only see once a year. Not quite Santa proper, but a field agent of joy, reporting in from across the median strip.

The Blue Ridge behind him was a smoky blur, trees bare, sky the color of unlit snow. Some folks in their cars probably thought they were just passing a bit of holiday whimsy. Me? I felt like I’d stumbled on the first sign that the season had officially woken up.

A small moment, waving arms and all, but enough to shift the day a little sideways into wonder.

#santaclaus #santashelper #randomjoy #roanokeva #tistheseason🎄

Introduce yourself with four starships.

Bluesky trend translated to my blog

The image shows the Heart of Gold starship from the 2005 film adaptation of The Hitchhiker’s Guide to the Galaxy.

The ship is the first to use the revolutionary Infinite Improbability Drive, which allows instantaneous travel across vast distances.

The appearance of the ship differs significantly across various adaptations; the book describes it as a sleek running shoe, while the film version is a large, smooth orb with a concave rear section for the drive.
The image shows the spaceship Bebop, the primary setting and transport for the main characters of the anime series Cowboy Bebop

The Bebop is a converted interplanetary fishing trawler owned and captained by Jet Black

It serves as the mobile base of operations for a group of bounty hunters (Spike Spiegel, Jet Black, Faye Valentine, Ed, and Ein) in the year 2071

The ship itself does not have any weapons systems or shields.
The image shows the TARDIS, the iconic time machine and spacecraft from the British science fiction television series Doctor Who.

TARDIS is an acronym for Time And Relative Dimension(s) In Space.
It is “dimensionally transcendental,” meaning its interior is a separate dimension and much larger than its exterior appears.

The TARDIS has a “chameleon circuit” designed to change its exterior appearance to blend in with its surroundings, but the Doctor’s circuit is broken, leaving it permanently stuck as a 1960s-style London police public call box.

The ship is a living organism, capable of telepathic interaction with its passengers.
The image shows a model of the fictional space freighter Valley Forge from the 1972 film Silent Running.

The ship’s primary purpose in the film was to carry the last remaining forests and plant life from Earth in large, geodesic domes.

Birds and cricket

The backyard camera picked up a little night shift worker again. A single cricket wandered into the glow like it had stepped onto a tiny stage. It paused right in front of the lens, antennae sweeping the air, as if deciding whether the backyard was safe for another round of quiet exploring. These late night check ins always feel like small secrets shared by the dark.

Come daylight, the mood changes but the sense of visiting remains. One tiny bird at a time stops by, hopping onto the concrete slab like it is clocking in for a quick inspection. They do not arrive as a flock, just solo appearances throughout the day. A hop, a pause, a curious tilt of the head. Then they flutter off and a while later another shows up to repeat the same gentle ritual.

There is something soothing about how the yard shifts between its night and day guests. Cricket under the moon, bird under the sun, both taking their turns like the world is following a simple schedule whispered by the seasons. It is easy to forget how many lives trace small paths around us until a quiet camera reminds us.

#backyardzoo #cricket #tinybird #slabhopper #dayandnight #roanokeva

I finally saw Brie Larson act in something where I thought she actually did a good job (The Bear, Season 4). She meshed dynamically with the cast, emoted, and was believable.

I wish she had brought that energy to anything else I’ve ever seen her perform in.

Martian chess classic layout

For folks that want the layout of the original game, with three of each pawns, drones and queens.

Modified pyramid design, made it vertical to look more like original 2 player, and implemented some non-stalemate cpu player action

https://svonberg.org/wp-content/uploads/2025/11/martianchessclassic3.html

<!DOCTYPE html>
<html lang=”en”>
<head>
    <meta charset=”UTF-8″>
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no”>
    <title>Martian Chess – Repetition Avoidance</title>
   
    <!– React & ReactDOM –>
    <script crossorigin src=”https://unpkg.com/react@18/umd/react.production.min.js”></script>
    <script crossorigin src=”https://unpkg.com/react-dom@18/umd/react-dom.production.min.js”></script>
   
    <!– Babel for JSX –>
    <script src=”https://unpkg.com/@babel/standalone/babel.min.js”></script>
   
    <!– Tailwind CSS –>
    <script src=”https://cdn.tailwindcss.com”></script>
   
    <style>
        @import url(‘https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Rajdhani:wght@400;600&display=swap’);
       
        body {
            font-family: ‘Rajdhani’, sans-serif;
            background-color: #0f172a;
            color: #e2e8f0;
            touch-action: none;
            overflow: hidden;
        }
       
        .font-display { font-family: ‘Orbitron’, sans-serif; }

        .perspective-board {
            transform: perspective(1000px) rotateX(5deg);
            transition: transform 0.3s ease;
        }

        .glow-red { filter: drop-shadow(0 0 4px rgba(239, 68, 68, 0.6)); }
        .glow-blue { filter: drop-shadow(0 0 4px rgba(59, 130, 246, 0.6)); }
        .glow-yellow { filter: drop-shadow(0 0 6px rgba(234, 179, 8, 0.8)); }
    </style>
</head>
<body>
    <!– Loading/Error Container –>
    <div id=”loader” style=”position:fixed; inset:0; display:flex; align-items:center; justify-content:center; color:white; background:#0f172a; z-index:9999;”>
        Loading Martian Chess…
    </div>

    <div id=”root”></div>

    <script>
        // Global Error Handler
        window.onerror = function(message, source, lineno, colno, error) {
            const loader = document.getElementById(‘loader’);
            loader.style.display = ‘flex’;
            loader.style.flexDirection = ‘column’;
            loader.style.padding = ’20px’;
            loader.innerHTML = `
                <h2 style=”color: #ef4444; font-size: 20px; margin-bottom: 10px;”>Game Error (Check Console for Details)</h2>
                <pre style=”background: #1e293b; padding: 15px; border-radius: 5px; color: #e2e8f0; overflow: auto; max-width: 100%; font-family: monospace; font-size: 12px;”>${message}\n\nLine: ${lineno}</pre>
                <button onclick=”location.reload()” style=”margin-top: 20px; padding: 10px 20px; background: #3b82f6; color: white; border: none; border-radius: 5px;”>Reload Game</button>
            `;
            return false;
        };
    </script>

    <script type=”text/babel”>
        try {
            const { useState, useEffect, useMemo, useCallback } = React;

            // — Game Constants —
            const ROWS = 8;
            const COLS = 4;
            const PAWN = 1;
            const DRONE = 2;
            const QUEEN = 3;
            const MAX_MOVES_WITHOUT_CAPTURE = 27;

            // — Correct Setup based on Right Image —
            const INITIAL_SETUP = [
                // Blue (Top) – Left Aligned (Cols 0,1,2)
                { r: 0, c: 0, type: QUEEN, id: ‘b_q1’ }, { r: 0, c: 1, type: QUEEN, id: ‘b_q2’ }, { r: 0, c: 2, type: QUEEN, id: ‘b_q3’ },
                { r: 1, c: 0, type: DRONE, id: ‘b_d1’ }, { r: 1, c: 1, type: DRONE, id: ‘b_d2’ }, { r: 1, c: 2, type: DRONE, id: ‘b_d3’ },
                { r: 2, c: 0, type: PAWN, id: ‘b_p1’ },  { r: 2, c: 1, type: PAWN, id: ‘b_p2’ },  { r: 2, c: 2, type: PAWN, id: ‘b_p3’ },

                // Red (Bottom) – Right Aligned (Cols 1,2,3)
                { r: 5, c: 1, type: PAWN, id: ‘r_p1’ },  { r: 5, c: 2, type: PAWN, id: ‘r_p2’ },  { r: 5, c: 3, type: PAWN, id: ‘r_p3’ },
                { r: 6, c: 1, type: DRONE, id: ‘r_d1’ }, { r: 6, c: 2, type: DRONE, id: ‘r_d2’ }, { r: 6, c: 3, type: DRONE, id: ‘r_d3’ },
                { r: 7, c: 1, type: QUEEN, id: ‘r_q1’ }, { r: 7, c: 2, type: QUEEN, id: ‘r_q2’ }, { r: 7, c: 3, type: QUEEN, id: ‘r_q3’ },
            ];

            // 0 = Red (Bottom, Rows 4-7), 1 = Blue (Top, Rows 0-3)
            const getOwner = (r) => r < 4 ? 1 : 0;
            const isValidPos = (r, c) => r >= 0 && r < ROWS && c >= 0 && c < COLS;

            // — Logic Functions —

            const generateGrid = (pieces) => {
                const grid = Array(ROWS).fill(null).map(() => Array(COLS).fill(null));
                pieces.forEach(p => {
                    if(isValidPos(p.r, p.c)) grid[p.r][p.c] = p;
                });
                return grid;
            };

            // Function to serialize the board state for history tracking (only position matters)
            const serializeBoard = (pieces) => {
                return pieces
                    .map(p => `${p.id}:${p.r}${p.c}`)
                    .sort()
                    .join(‘|’);
            };

            const getAllMoves = (grid, piece) => {
                const moves = [];
                const { r, c, type } = piece;
                const owner = getOwner(r);

                const addMove = (tr, tc) => {
                    if (!isValidPos(tr, tc)) return false;

                    const targetPiece = grid[tr][tc];
                    if (!targetPiece) {
                        moves.push({ r: tr, c: tc, isCapture: false });
                        return true;
                    }
                    const targetOwner = getOwner(tr);
                    if (targetOwner !== owner) {
                        // Include the value of the captured piece in the move data
                        moves.push({ r: tr, c: tc, isCapture: true, value: targetPiece.type });
                        return false; // Capture stops movement
                    }
                    return false; // Friendly blocks
                };

                if (type === PAWN) {
                    // Pawns move one step diagonally (in all 4 directions)
                    [[1, 1], [1, -1], [-1, 1], [-1, -1]].forEach(([dr, dc]) => {
                        addMove(r + dr, c + dc);
                    });
                } else if (type === DRONE) {
                    // Drones move one or two steps orthogonally (up, down, left, right)
                    [[0, 1], [0, -1], [1, 0], [-1, 0]].forEach(([dr, dc]) => {
                        let nr = r + dr, nc = c + dc;
                        if (addMove(nr, nc)) {
                            // If first step is free, check the second
                            addMove(r + 2 * dr, c + 2 * dc);
                        }
                    });
                } else if (type === QUEEN) {
                    // Queen moves any number of steps orthogonally or diagonally
                    [[0, 1], [0, -1], [1, 0], [-1, 0], [1, 1], [1, -1], [-1, 1], [-1, -1]].forEach(([dr, dc]) => {
                        let nr = r + dr, nc = c + dc;
                        while (isValidPos(nr, nc)) {
                            if (!addMove(nr, nc)) break;
                            nr += dr; nc += dc;
                        }
                    });
                }
                return moves;
            };

            // — AI Functions —
            const evaluateState = (pieces, scores, aiTurn, movesWithoutCapture, history) => {
                // If the game has ended by atrophy, the outcome is based solely on captured material score.
                if (movesWithoutCapture >= MAX_MOVES_WITHOUT_CAPTURE) {
                    // Large multiplier (10000) makes the captured score the dominant factor in the final result.
                    return (scores[aiTurn] – scores[1-aiTurn]) * 10000;
                }

                // Base score multiplier for captured material (high incentive for capturing)
                let redScore = scores[0] * 100;
                let blueScore = scores[1] * 100;
               
                // Base piece values for positional evaluation
                const PIECE_VALUES = { [PAWN]: 10, [DRONE]: 20, [QUEEN]: 30 };

                pieces.forEach(p => {
                    const owner = getOwner(p.r);
                    const val = PIECE_VALUES[p.type] || 10;
                   
                    // — 1. Piece Value Contribution —
                    let positionalBonus = 0;

                    // — 2. Positional/Aggression Bonus (Encourages forward movement) —
                    if (owner === 0) { // Red (Moves to smaller row index, 7 -> 0)
                        redScore += val;
                        // Calculates progression towards the opponent’s side (row 0)
                        // Bonus scales by base value and is multiplied by 2 for aggression.
                        positionalBonus = val * ((7 – p.r) / 7) * 2;

                        // Extra bonus for controlling the canal boundary (Row 4)
                        if (p.r === 4) positionalBonus += 5;
                    } else { // Blue (Moves to larger row index, 0 -> 7)
                        blueScore += val;
                        // Calculates progression towards the opponent’s side (row 7)
                        // Bonus scales by base value and is multiplied by 2 for aggression.
                        positionalBonus = val * (p.r / 7) * 2;

                        // Extra bonus for controlling the canal boundary (Row 3)
                        if (p.r === 3) positionalBonus += 5;
                    }
                   
                    if (owner === 0) {
                        redScore += positionalBonus;
                    } else {
                        blueScore += positionalBonus;
                    }
                });

                // Calculate the score from the AI’s perspective (maximizing its score, minimizing opponent’s)
                let score = aiTurn === 0 ? redScore – blueScore : blueScore – redScore;

                // — 3. Repetition Penalty (Prevents loops) —
                const serializedState = serializeBoard(pieces);
                const repetitionCount = history.filter(h => h === serializedState).length;
               
                // If a state repeats (third time the position occurs), apply a heavy penalty
                if (repetitionCount >= 2) {
                    score -= 5000; // Very large penalty to ensure the AI avoids repetitive draws
                }
               
                return score;
            };

            const simulateMove = (currentPieces, currentScores, fromPiece, move, activeTurn, currentNoCap) => {
                let nextScores = […currentScores];
                let nextNoCap = currentNoCap + 1;
               
                // 1. Determine target piece
                const targetPiece = currentPieces.find(p => p.r === move.r && p.c === move.c);
               
                // 2. Handle capture by filtering current pieces (removes target if found)
                let nextPieces = currentPieces.filter(p => p.id !== (targetPiece ? targetPiece.id : null));

                if (targetPiece) {
                    nextScores[activeTurn] += targetPiece.type;
                    nextNoCap = 0; // Reset counter on capture
                }
               
                // 3. Move the piece by mapping (finds piece by ID and updates position)
                nextPieces = nextPieces.map(p =>
                    p.id === fromPiece.id ? { …p, r: move.r, c: move.c } : p
                );

                // 4. Check Game Over
                const nextPlayer = 1 – activeTurn;
                const piecesInNextZone = nextPieces.filter(p => getOwner(p.r) === nextPlayer);
                const isGameOver = piecesInNextZone.length === 0 || nextNoCap >= MAX_MOVES_WITHOUT_CAPTURE;

                return { pieces: nextPieces, scores: nextScores, isGameOver, movesWithoutCapture: nextNoCap };
            };

            const minimax = (pieces, scores, depth, alpha, beta, isMaximizing, activeTurn, originalAiTurn, movesWithoutCapture, history) => {
                if (depth === 0 || movesWithoutCapture >= MAX_MOVES_WITHOUT_CAPTURE) {
                    return evaluateState(pieces, scores, originalAiTurn, movesWithoutCapture, history);
                }

                const myPieces = pieces.filter(p => getOwner(p.r) === activeTurn);
                if (myPieces.length === 0) return evaluateState(pieces, scores, originalAiTurn, movesWithoutCapture, history);

                const grid = generateGrid(pieces);
                let bestVal = isMaximizing ? -Infinity : Infinity;

                for (let p of myPieces) {
                    const moves = getAllMoves(grid, p);
                    for (let m of moves) {
                        const result = simulateMove(pieces, scores, p, m, activeTurn, movesWithoutCapture);
                       
                        if (result.isGameOver) {
                             const finalVal = (result.scores[originalAiTurn] – result.scores[1-originalAiTurn]) * 10000;
                             if (isMaximizing) bestVal = Math.max(bestVal, finalVal);
                             else bestVal = Math.min(bestVal, finalVal);
                        } else {
                            // Empty history array is passed for recursion below the top layer
                            const val = minimax(result.pieces, result.scores, depth – 1, alpha, beta, !isMaximizing, 1 – activeTurn, originalAiTurn, result.movesWithoutCapture, []);
                            if (isMaximizing) bestVal = Math.max(bestVal, val);
                            else bestVal = Math.min(bestVal, val);
                        }
                       
                        if (isMaximizing) alpha = Math.max(alpha, bestVal);
                        else beta = Math.min(beta, bestVal);
                       
                        if (beta <= alpha) break;
                    }
                    if (beta <= alpha) break;
                }
                return bestVal;
            };

            // — Component: PieceIcon —
            const PieceIcon = ({ type, className }) => {
                 // Pawn (Small)
                 if (type === PAWN) {
                     return (
                        <svg viewBox=”0 0 24 24″ fill=”currentColor” className={className}>
                            <path d=”M12 6L6 20h12L12 6z” />
                            <path d=”M12 6L12 20L6 20z” fill=”rgba(0,0,0,0.25)” />
                        </svg>
                     );
                 }
                 // Drone (Medium)
                 if (type === DRONE) {
                     return (
                        <svg viewBox=”0 0 24 24″ fill=”currentColor” className={className}>
                            <path d=”M12 2L4 20h16L12 2z” />
                            <path d=”M12 2L12 20L4 20z” fill=”rgba(0,0,0,0.25)” />
                        </svg>
                     );
                 }
                 // Queen (Large – Stacked)
                 return (
                    <svg viewBox=”0 0 24 24″ fill=”currentColor” className={className}>
                        <path d=”M12 1L2 21h20L12 1z” />
                        <path d=”M12 1L12 21L2 21z” fill=”rgba(0,0,0,0.25)” />
                        <circle cx=”12″ cy=”15″ r=”2″ fill=”rgba(255,255,255,0.3)” />
                    </svg>
                 );
            };

            // — Component: App —
            const App = () => {
                const [pieces, setPieces] = useState(INITIAL_SETUP);
                const [turn, setTurn] = useState(0); // 0 = Red (Human/P1), 1 = Blue (AI/P2)
                const [scores, setScores] = useState([0, 0]);
                const [selected, setSelected] = useState(null);
                const [possibleMoves, setPossibleMoves] = useState([]);
                const [gameMode, setGameMode] = useState(‘PVC’);
                const [gameOver, setGameOver] = useState(false);
                const [lastMove, setLastMove] = useState(null);
                const [aiThinking, setAiThinking] = useState(false);
                const [movesWithoutCapture, setMovesWithoutCapture] = useState(0);
                const [history, setHistory] = useState([]);

                // Hide loader when App mounts
                useEffect(() => {
                    const loader = document.getElementById(‘loader’);
                    if (loader) loader.style.display = ‘none’;
                }, []);

                const grid = useMemo(() => generateGrid(pieces), [pieces]);

                const checkWinCondition = (currentPieces, playerJustMoved, currentNoCapCount) => {
                    const nextPlayer = 1 – playerJustMoved;
                    // Check for total destruction
                    const piecesInNextZone = currentPieces.filter(p => getOwner(p.r) === nextPlayer);
                    if (piecesInNextZone.length === 0) return true;
                    // Check for Atrophy (Stalemate Rule)
                    if (currentNoCapCount >= MAX_MOVES_WITHOUT_CAPTURE) return true;
                    return false;
                };

                // ‘from’ piece must contain { id, r, c }
                const executeMove = (from, move) => {
                    setPieces(prevPieces => {
                        let newPieces = prevPieces.map(p => ({…p}));
                       
                        let capturedValue = 0;
                        let nextNoCapCount = movesWithoutCapture + 1;

                        // 1. Handle capture
                        const targetIndex = newPieces.findIndex(p => p.r === move.r && p.c === move.c);
                        if (targetIndex !== -1) {
                            capturedValue = newPieces[targetIndex].type;
                            newPieces.splice(targetIndex, 1);
                            nextNoCapCount = 0; // Reset counter on capture
                        }

                        // 2. Move the piece (safely find and update using map)
                        newPieces = newPieces.map(p => {
                            if (p.id === from.id) {
                                return { …p, r: move.r, c: move.c };
                            }
                            return p;
                        });

                        if (capturedValue > 0) {
                            setScores(prev => {
                                const newScores = […prev];
                                newScores[turn] += capturedValue;
                                return newScores;
                            });
                        }

                        // — History Update —
                        const nextBoardPosition = serializeBoard(newPieces);
                       
                        setHistory(prevHistory => {
                            const newHistory = […prevHistory, nextBoardPosition];
                            // Keep history limited to the last 8 states (4 full moves) for repetition check
                            return newHistory.slice(-8);
                        });
                       
                        setMovesWithoutCapture(nextNoCapCount);
                       
                        const isWin = checkWinCondition(newPieces, turn, nextNoCapCount);
                        if (isWin) {
                            setGameOver(true);
                        } else {
                            setTurn(1 – turn);
                        }
                       
                        setLastMove({ from: { r: from.r, c: from.c }, to: { r: move.r, c: move.c } });
                        return newPieces;
                    });

                    setSelected(null);
                    setPossibleMoves([]);
                };

                const handleSquareClick = (r, c) => {
                    if (gameOver || aiThinking) return;
                    if (gameMode === ‘CVC’) return;
                    if (gameMode === ‘PVC’ && turn === 1) return; // Human is P1 (Red/Bottom)

                    const clickedPiece = grid[r][c];
                    const clickedOwner = getOwner(r);

                    if (clickedPiece && clickedOwner === turn) {
                        if (selected && selected.r === r && selected.c === c) {
                            setSelected(null);
                            setPossibleMoves([]);
                        } else {
                            setSelected(clickedPiece);
                            setPossibleMoves(getAllMoves(grid, clickedPiece));
                        }
                    } else if (selected) {
                        const move = possibleMoves.find(m => m.r === r && m.c === c);
                        if (move) {
                            // Pass the selected piece’s ID and current coordinates
                            executeMove(selected, move);
                        } else if (clickedPiece && clickedOwner === turn) {
                            setSelected(clickedPiece);
                            setPossibleMoves(getAllMoves(grid, clickedPiece));
                        } else {
                            setSelected(null);
                            setPossibleMoves([]);
                        }
                    }
                };

                const makeAiMove = useCallback((activeTurn) => {
                    const myPieces = pieces.filter(p => getOwner(p.r) === activeTurn);
                    let bestScore = -Infinity;
                    let bestMove = null;
                    let bestPiece = null;

                    const gridRef = generateGrid(pieces);

                    for (let p of myPieces) {
                        const moves = getAllMoves(gridRef, p);
                        for (let m of moves) {
                            const result = simulateMove(pieces, scores, p, m, activeTurn, movesWithoutCapture);
                           
                            // To check for repetition, we serialize the resulting state.
                            const nextBoardPosition = serializeBoard(result.pieces);
                            const historyForEvaluation = […history, nextBoardPosition];

                            let score;
                            if (result.isGameOver) {
                                const myFinal = result.scores[activeTurn];
                                const opFinal = result.scores[1-activeTurn];
                                score = (myFinal – opFinal) * 10000;
                            } else {
                                // Increased Minimax Depth to 2 for better lookahead
                                score = minimax(result.pieces, result.scores, 2, -Infinity, Infinity, false, 1 – activeTurn, activeTurn, result.movesWithoutCapture, historyForEvaluation);
                            }
                           
                            score += Math.random() * 0.1; // Small random factor for tie-break

                            if (score > bestScore) {
                                bestScore = score;
                                bestMove = m;
                                bestPiece = p;
                            }
                        }
                    }

                    if (bestMove && bestPiece) {
                        console.log(`AI Move: ${bestPiece.id} (${bestPiece.r},${bestPiece.c}) -> (${bestMove.r},${bestMove.c}) | Score: ${bestScore.toFixed(2)}`);
                        executeMove({ id: bestPiece.id, r: bestPiece.r, c: bestPiece.c }, bestMove);
                    } else {
                        // This means the AI found no legal moves.
                        setGameOver(true);
                        console.log(“AI found no legal moves. Game Over.”);
                    }
                }, [pieces, scores, movesWithoutCapture, history]);

                useEffect(() => {
                    if (gameOver) return;

                    const isCpuTurn = (gameMode === ‘PVC’ && turn === 1) || (gameMode === ‘CVC’);

                    if (isCpuTurn) {
                        setAiThinking(true);
                        const delay = gameMode === ‘CVC’ ? 200 : 500;
                        const timer = setTimeout(() => {
                            requestAnimationFrame(() => {
                                try {
                                    makeAiMove(turn);
                                } catch (err) {
                                    console.error(“AI Move Execution Error:”, err);
                                    setGameOver(true); // Stop the game if AI crashes
                                }
                                setAiThinking(false);
                            });
                        }, delay);
                        return () => clearTimeout(timer);
                    } else {
                        setAiThinking(false);
                    }
                }, [turn, gameMode, gameOver, makeAiMove]);

                const resetGame = () => {
                    setPieces(INITIAL_SETUP);
                    setScores([0, 0]);
                    setTurn(0);
                    setGameOver(false);
                    setSelected(null);
                    setPossibleMoves([]);
                    setLastMove(null);
                    setAiThinking(false);
                    setMovesWithoutCapture(0);
                    setHistory([]); // Reset history on new game
                };

                const getSquareColor = (r, c) => {
                    const isTop = r < 4;
                    const base = isTop ? ‘bg-blue-900/20’ : ‘bg-red-900/20’;
                    const alt = isTop ? ‘bg-blue-900/10’ : ‘bg-red-900/10’;
                    const isDark = (r + c) % 2 === 1;
                   
                    if (lastMove && ((lastMove.from.r === r && lastMove.from.c === c) || (lastMove.to.r === r && lastMove.to.c === c))) {
                        return ‘bg-yellow-500/20 border-yellow-500/50 shadow-[inset_0_0_20px_rgba(234,179,8,0.2)]’;
                    }
                    return isDark ? base : alt;
                };

                const atrophyPercent = (movesWithoutCapture / MAX_MOVES_WITHOUT_CAPTURE) * 100;
                const atrophyColor = atrophyPercent > 75 ? ‘bg-red-500’ : atrophyPercent > 50 ? ‘bg-yellow-500’ : ‘bg-slate-500’;

                return (
                    <div className=”fixed inset-0 flex flex-col bg-slate-950 select-none”>
                       
                        {/* Background */}
                        <div className=”absolute inset-0 pointer-events-none z-0″>
                            <div className=”absolute top-0 left-1/2 -translate-x-1/2 w-[80vw] h-[40vh] bg-blue-600/10 rounded-full blur-3xl”></div>
                            <div className=”absolute bottom-0 left-1/2 -translate-x-1/2 w-[80vw] h-[40vh] bg-red-600/10 rounded-full blur-3xl”></div>
                        </div>

                        {/* Top Bar */}
                        <div className=”relative z-10 flex flex-col items-center pt-2 px-4 shrink-0″>
                             <div className=”flex justify-between w-full max-w-md items-center mb-1″>
                                {/* P2 Blue Score */}
                                <div className={`text-center transition-all ${turn === 1 ? ‘opacity-100 scale-110’ : ‘opacity-50 scale-90’}`}>
                                    <div className=”text-blue-400 text-[10px] font-bold”>BLUE (AI)</div>
                                    <div className=”text-3xl font-mono text-white leading-none drop-shadow-[0_0_8px_rgba(59,130,246,0.6)]”>{scores[1]}</div>
                                </div>
                               
                                <div className=”flex flex-col items-center”>
                                    <h1 className=”text-xl font-display font-bold bg-clip-text text-transparent bg-gradient-to-b from-white to-slate-400″>MARTIAN</h1>
                                    <div className=”flex gap-1 mt-1″>
                                        {[‘PVC’, ‘PVP’, ‘CVC’].map(mode => (
                                            <button key={mode} onClick={() => { setGameMode(mode); resetGame(); }}
                                                className={`px-2 py-0.5 rounded text-[9px] font-bold border border-slate-700 ${gameMode === mode ? ‘bg-slate-700 text-white’ : ‘bg-slate-900 text-slate-500’}`}>
                                                {mode}
                                            </button>
                                        ))}
                                    </div>
                                </div>

                                {/* P1 Red Score */}
                                <div className={`text-center transition-all ${turn === 0 ? ‘opacity-100 scale-110’ : ‘opacity-50 scale-90’}`}>
                                    <div className=”text-red-400 text-[10px] font-bold”>RED (YOU)</div>
                                    <div className=”text-3xl font-mono text-white leading-none drop-shadow-[0_0_8px_rgba(239,68,68,0.6)]”>{scores[0]}</div>
                                </div>
                             </div>
                            
                             {/* Atrophy */}
                             <div className=”w-full max-w-[200px] h-1 bg-slate-800 rounded-full overflow-hidden mt-1″>
                                <div className={`h-full transition-all duration-500 ${atrophyColor}`} style={{ width: `${atrophyPercent}%` }}></div>
                             </div>

                             {/* Status */}
                             <div className=”h-5 mt-1 flex items-center justify-center”>
                                {gameOver ?
                                    <span className=”text-yellow-400 font-bold animate-pulse text-sm tracking-widest”>GAME OVER</span> :
                                    <span className=”text-slate-400 text-[10px] uppercase tracking-widest flex items-center gap-2″>
                                        {aiThinking && <span className=”w-1.5 h-1.5 bg-green-400 rounded-full animate-ping”></span>}
                                        {aiThinking ? ‘COMPUTING…’ : turn === 0 ? <span className=”text-red-400″>RED TURN</span> : <span className=”text-blue-400″>BLUE TURN</span>}
                                    </span>
                                }
                             </div>
                        </div>

                        {/* Board Container */}
                        <div className=”flex-1 relative flex items-center justify-center p-2 z-10 overflow-hidden”>
                            <div className=”perspective-board w-full max-w-[380px] aspect-[4/8] h-auto max-h-[100%]”>
                                <div className=”w-full h-full grid grid-rows-8 grid-cols-4 shadow-2xl border border-slate-700 bg-slate-900/80 backdrop-blur-md rounded-lg overflow-hidden”>
                                    {Array(ROWS).fill(0).map((_, r) => (
                                        Array(COLS).fill(0).map((_, c) => {
                                            const piece = grid[r][c];
                                            const isSelected = selected && selected.r === r && selected.c === c;
                                            const isMove = possibleMoves.find(m => m.r === r && m.c === c);
                                            const isTop = r < 4;
                                            const isCanalBorder = r === 3;

                                            return (
                                                <div
                                                    key={`${r}-${c}`}
                                                    onClick={() => handleSquareClick(r, c)}
                                                    className={`
                                                        relative flex items-center justify-center
                                                        border-r border-slate-800 last:border-r-0
                                                        border-b ${isCanalBorder ? ‘border-b-red-500/50 border-b-2’ : ‘border-b-slate-800’}
                                                        ${getSquareColor(r, c)}
                                                        ${isMove ? ‘cursor-pointer’ : ”}
                                                    `}
                                                >
                                                    {isMove && !piece && <div className=”w-2 h-2 rounded-full bg-green-400/50 animate-pulse shadow-[0_0_5px_#4ade80]” />}
                                                    {isMove && piece && <div className=”absolute inset-0 border-2 border-red-500/50 animate-pulse bg-red-500/10″ />}

                                                    {piece && (
                                                        <div className={`
                                                                w-[85%] h-[85%] transition-all duration-200
                                                                ${isTop ? ‘text-blue-500 glow-blue’ : ‘text-red-500 glow-red’}
                                                                ${isSelected ? ‘scale-110 text-yellow-400 glow-yellow -translate-y-1’ : ”}
                                                            `}>
                                                            <PieceIcon type={piece.type} className=”w-full h-full drop-shadow-md” />
                                                        </div>
                                                    )}
                                                </div>
                                            );
                                        })
                                    ))}
                                </div>
                            </div>
                        </div>

                        {/* Bottom Controls */}
                        <div className=”z-10 pb-6 pt-2 flex justify-center shrink-0″>
                            <button onClick={resetGame} className=”px-6 py-3 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-md text-xs font-bold tracking-widest border border-slate-700 active:scale-95″>
                                NEW GAME
                            </button>
                        </div>
                    </div>
                );
            };

            const root = ReactDOM.createRoot(document.getElementById(‘root’));
            root.render(<App />);
       
        } catch (err) {
            console.error(“Initialization Error:”, err);
            document.getElementById(‘loader’).innerHTML = `<div style=”color:red; padding:20px;”>Init Error: ${err.message}</div>`;
        }
    </script>
</body>
</html>

Based on looney labs martian chess –

https://www.looneylabs.com/content/martian-chess