All posts by scottobear

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

Martian Chess ai / atrophy

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

I have updated the game to include the 27-Move Rule (The “Atrophy” Rule).


Changes Implemented
New Game Rule: Added a counter for movesSinceCapture. If it reaches 27, the game ends immediately.


AI Awareness:
The AI now receives the current movesSinceCapture count in its simulation.

Winning Strategy: If the AI is ahead in points and the counter is high (e.g., 25), it will prioritize non-capture moves to force the game to end and lock in its victory.

Losing Strategy: If the AI is behind, it will aggressively look for captures to reset the timer and extend the game.

UI Update: Added a “Stalemate Timer” bar below the score to show how close the game is to ending due to a lack of captures. Also changed graphics slightly to call back to Looney Pyramids.

Martian Chess experiment

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

How to Play


Objective: Score the most points by capturing pieces.


Movement:
Pawn (Small – 1 pt): Moves 1 step diagonally.


Drone (Medium – 2 pts): Moves 1 or 2 steps orthogonally (cannot jump).


Queen (Large – 3 pts): Moves any distance orthogonally or diagonally (cannot jump).


Ownership (Crucial Rule): Color is determined by location. Pieces on the left (Red zone) belong to Player 1. Pieces on the right (Blue zone) belong to Player 2/CPU.


The Canal: If you move a piece from your zone to the opponent’s zone, it immediately changes color and becomes theirs (unless you used it to capture, in which case you remove the captured piece and score points).

Winning: The game ends when a player has no pieces remaining in their territory. High score wins.

<!DOCTYPE html>
<html lang=”en”>
<head>
    <meta charset=”UTF-8″>
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
    <title>Martian Chess</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>
   
    <!– Phosphor Icons –>
    <script src=”https://unpkg.com/@phosphor-icons/web”></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: manipulation;
        }
       
        .font-display {
            font-family: ‘Orbitron’, sans-serif;
        }

        .perspective-board {
            transform: perspective(1000px) rotateX(5deg);
            transition: transform 0.3s ease;
        }
    </style>
</head>
<body>
    <div id=”root”></div>

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

        // — Game Constants & Logic —

        const ROWS = 4;
        const COLS = 8;
        const PAWN = 1;   // Moves Diag 1
        const DRONE = 2;  // Moves Ortho 1-2
        const QUEEN = 3;  // Moves Any Slide

        const INITIAL_SETUP = [
            // Player 1 (Red/Left)
            { r: 0, c: 0, type: QUEEN }, { r: 0, c: 1, type: DRONE }, { r: 0, c: 2, type: PAWN },
            { r: 1, c: 0, type: DRONE }, { r: 1, c: 1, type: PAWN },  { r: 1, c: 2, type: PAWN },
            { r: 2, c: 0, type: DRONE }, { r: 2, c: 1, type: PAWN },  { r: 2, c: 2, type: PAWN },
            { r: 3, c: 0, type: QUEEN }, { r: 3, c: 1, type: DRONE }, { r: 3, c: 2, type: PAWN },
           
            // Player 2 (Blue/Right)
            { r: 0, c: 7, type: QUEEN }, { r: 0, c: 6, type: DRONE }, { r: 0, c: 5, type: PAWN },
            { r: 1, c: 7, type: DRONE }, { r: 1, c: 6, type: PAWN },  { r: 1, c: 5, type: PAWN },
            { r: 2, c: 7, type: DRONE }, { r: 2, c: 6, type: PAWN },  { r: 2, c: 5, type: PAWN },
            { r: 3, c: 7, type: QUEEN }, { r: 3, c: 6, type: DRONE }, { r: 3, c: 5, type: PAWN },
        ];

        const getOwner = (c) => c < 4 ? 0 : 1;
        const isValidPos = (r, c) => r >= 0 && r < ROWS && c >= 0 && c < COLS;

        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;
        };

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

            const addMove = (tr, tc) => {
                const targetPiece = grid[tr][tc];
                if (!targetPiece) {
                    moves.push({ r: tr, c: tc, isCapture: false });
                    return true;
                }
                const targetOwner = getOwner(tc);
                if (targetOwner !== owner) {
                    moves.push({ r: tr, c: tc, isCapture: true, value: targetPiece.type });
                    return false;
                }
                return false;
            };

            if (type === PAWN) {
                [[1, 1], [1, -1], [-1, 1], [-1, -1]].forEach(([dr, dc]) => {
                    const nr = r + dr, nc = c + dc;
                    if (isValidPos(nr, nc)) addMove(nr, nc);
                });
            } else if (type === DRONE) {
                [[0, 1], [0, -1], [1, 0], [-1, 0]].forEach(([dr, dc]) => {
                    let nr = r + dr, nc = c + dc;
                    if (isValidPos(nr, nc)) {
                        if (addMove(nr, nc)) {
                            nr += dr; nc += dc;
                            if (isValidPos(nr, nc)) addMove(nr, nc);
                        }
                    }
                });
            } else if (type === QUEEN) {
                [[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;
        };

        // — Components —

        const PieceIcon = ({ type, className }) => {
             const shapeClass = type === PAWN ? “rounded-full” : type === DRONE ? “rounded-sm” : “rounded-lg”;
            
             // Simple Geometric shapes for clarity
             if (type === PAWN) return (
                <svg viewBox=”0 0 24 24″ fill=”currentColor” className={className}>
                   <circle cx=”12″ cy=”12″ r=”8″ />
                </svg>
             );
             if (type === DRONE) return (
                <svg viewBox=”0 0 24 24″ fill=”currentColor” className={className}>
                   <rect x=”4″ y=”4″ width=”16″ height=”16″ rx=”2″ />
                </svg>
             );
             // Queen/Mother
             return (
                <svg viewBox=”0 0 24 24″ fill=”currentColor” className={className}>
                   <path d=”M12 2L2 22h20L12 2z” />
                </svg>
             );
        };

        const App = () => {
            const [pieces, setPieces] = useState(INITIAL_SETUP);
            const [turn, setTurn] = useState(0);
            const [scores, setScores] = useState([0, 0]);
            const [selected, setSelected] = useState(null);
            const [possibleMoves, setPossibleMoves] = useState([]);
            const [gameMode, setGameMode] = useState(‘PVC’); // ‘PVC’, ‘PVP’, ‘CVC’
            const [gameOver, setGameOver] = useState(false);
            const [lastMove, setLastMove] = useState(null);
            const [aiThinking, setAiThinking] = useState(false);

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

            const checkWinCondition = useCallback((currentPieces, playerJustMoved) => {
                const nextPlayer = 1 – playerJustMoved;
                const piecesInNextZone = currentPieces.filter(p => getOwner(p.c) === nextPlayer);
                return piecesInNextZone.length === 0;
            }, []);

            const executeMove = useCallback((from, move) => {
                setPieces(prevPieces => {
                    const newPieces = […prevPieces];
                    const pieceIndex = newPieces.findIndex(p => p.r === from.r && p.c === from.c);
                    if (pieceIndex === -1) return prevPieces;

                    const movingPiece = { …newPieces[pieceIndex] };
                    let capturedValue = 0;

                    if (move.isCapture) {
                        const targetIndex = newPieces.findIndex(p => p.r === move.r && p.c === move.c);
                        if (targetIndex !== -1) {
                            capturedValue = newPieces[targetIndex].type;
                            newPieces.splice(targetIndex, 1);
                        }
                    }

                    // Re-find index in case splice shifted it
                    const finalMoverIndex = newPieces.findIndex(p => p.r === from.r && p.c === from.c);
                    newPieces[finalMoverIndex] = { …movingPiece, r: move.r, c: move.c };

                    if (capturedValue > 0) {
                        setScores(prev => {
                            const newScores = […prev];
                            newScores[turn] += capturedValue;
                            return newScores;
                        });
                    }
                   
                    // Check win condition immediately with new state
                    const isWin = checkWinCondition(newPieces, turn);
                    if (isWin) {
                        setGameOver(true);
                    } else {
                        setTurn(t => 1 – t);
                    }
                   
                    setLastMove({ from, to: { r: move.r, c: move.c } });
                    return newPieces;
                });

                setSelected(null);
                setPossibleMoves([]);
            }, [turn, checkWinCondition]);

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

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

                if (clickedPiece && clickedOwner === turn) {
                    if (selected && selected.r === r && selected.c === c) {
                        setSelected(null);
                        setPossibleMoves([]);
                    } else {
                        setSelected({ r, c });
                        setPossibleMoves(getMoves(grid, clickedPiece));
                    }
                } else if (selected) {
                    const move = possibleMoves.find(m => m.r === r && m.c === c);
                    if (move) {
                        executeMove(selected, move);
                    } else if (clickedPiece && clickedOwner === turn) {
                        // Switch selection
                        setSelected({ r, c });
                        setPossibleMoves(getMoves(grid, clickedPiece));
                    } else {
                        setSelected(null);
                        setPossibleMoves([]);
                    }
                }
            };

            // — AI Engine —
            const makeAiMove = useCallback((activeTurn) => {
                const aiPieces = pieces.filter(p => getOwner(p.c) === activeTurn);
                let allMoves = [];

                aiPieces.forEach(p => {
                    const moves = getMoves(grid, p);
                    moves.forEach(m => {
                        allMoves.push({ from: p, move: m });
                    });
                });

                if (allMoves.length === 0) {
                    setGameOver(true);
                    return;
                }

                const scoredMoves = allMoves.map(am => {
                    let score = 0;
                    const pieceValue = am.from.type;
                    const isCapture = am.move.isCapture;
                   
                    if (isCapture) {
                        score += am.move.value * 12; // High value on capture
                    }

                    const isEnemyZone = activeTurn === 0 ? am.move.c >= 4 : am.move.c < 4;
                   
                    if (isEnemyZone) {
                        // Cost of losing piece
                        score -= pieceValue * 5;
                        // Aggression Bonus: If we are high on score, trade aggressively
                        if (scores[activeTurn] > scores[1-activeTurn]) score += 2;
                    } else {
                        // Stay in zone
                        score += Math.random() * 2;
                       
                        // Prefer moves that don’t leave us stuck
                        // Center control (Cols 3,4)
                        const distToCanal = Math.abs(am.move.c – 3.5);
                        score += (4 – distToCanal) * 0.5;
                    }

                    return { …am, score };
                });

                scoredMoves.sort((a, b) => b.score – a.score);
               
                // Take top move or random of top 3 to avoid loops
                const topMoves = scoredMoves.slice(0, 3);
                const selectedMove = topMoves[Math.floor(Math.random() * topMoves.length)];
               
                executeMove(selectedMove.from, selectedMove.move);
            }, [grid, pieces, scores, executeMove]);

            // — Game Loop —
            useEffect(() => {
                if (gameOver) return;

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

                if (isCpuTurn) {
                    setAiThinking(true);
                    const timer = setTimeout(() => {
                        makeAiMove(turn);
                        setAiThinking(false);
                    }, gameMode === ‘CVC’ ? 1000 : 800); // Faster for CVC

                    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);
            };

            // — Helpers —
            const getSquareColor = (r, c) => {
                const isLeft = c < 4;
                const base = isLeft ? ‘bg-red-900/20’ : ‘bg-blue-900/20’;
                const alt = isLeft ? ‘bg-red-900/10’ : ‘bg-blue-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’;
                }
                return isDark ? base : alt;
            };

            return (
                <div className=”min-h-screen flex flex-col items-center justify-center p-4 relative overflow-hidden bg-slate-950 select-none”>
                   
                    {/* Background */}
                    <div className=”absolute top-0 left-0 w-full h-full pointer-events-none z-0″>
                        <div className=”absolute top-[-20%] left-[-10%] w-[600px] h-[600px] bg-red-600/10 rounded-full blur-3xl”></div>
                        <div className=”absolute bottom-[-20%] right-[-10%] w-[600px] h-[600px] bg-blue-600/10 rounded-full blur-3xl”></div>
                    </div>

                    {/* Header */}
                    <div className=”z-10 text-center mb-4″>
                        <h1 className=”text-4xl md:text-5xl font-display font-bold text-transparent bg-clip-text bg-gradient-to-r from-red-400 to-blue-400″>
                            MARTIAN CHESS
                        </h1>
                        <div className=”flex gap-2 mt-4 justify-center”>
                            {[‘PVC’, ‘PVP’, ‘CVC’].map(mode => (
                                <button
                                    key={mode}
                                    onClick={() => { setGameMode(mode); resetGame(); }}
                                    className={`px-4 py-1 rounded text-xs font-bold transition border border-slate-700 ${gameMode === mode ? ‘bg-slate-700 text-white’ : ‘bg-slate-900 text-slate-500 hover:bg-slate-800’}`}
                                >
                                    {mode === ‘PVC’ ? ‘1 Player’ : mode === ‘PVP’ ? ‘2 Players’ : ‘AI vs AI’}
                                </button>
                            ))}
                        </div>
                    </div>

                    {/* Score Board */}
                    <div className=”z-10 flex justify-between w-full max-w-3xl mb-2 px-4 items-end”>
                        <div className={`text-center transition-opacity ${turn === 0 ? ‘opacity-100’ : ‘opacity-50’}`}>
                            <div className=”text-red-500 font-bold text-sm”>RED</div>
                            <div className=”text-4xl font-mono text-white”>{scores[0]}</div>
                        </div>
                        <div className=”mb-2″>
                            {gameOver ?
                                <span className=”text-yellow-400 font-bold animate-pulse”>GAME OVER</span> :
                                <span className=”text-slate-600 text-xs uppercase tracking-widest”>{aiThinking ? ‘THINKING…’ : `${turn === 0 ? ‘RED’ : ‘BLUE’} TURN`}</span>
                            }
                        </div>
                        <div className={`text-center transition-opacity ${turn === 1 ? ‘opacity-100’ : ‘opacity-50’}`}>
                            <div className=”text-blue-500 font-bold text-sm”>BLUE</div>
                            <div className=”text-4xl font-mono text-white”>{scores[1]}</div>
                        </div>
                    </div>

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

                                    return (
                                        <div
                                            key={`${r}-${c}`}
                                            onClick={() => handleSquareClick(r, c)}
                                            className={`
                                                relative flex items-center justify-center
                                                border-b border-r ${c === 3 ? ‘border-r-yellow-500/30 border-r-2’ : ‘border-slate-800’}
                                                ${getSquareColor(r, c)}
                                                ${isSelected ? ‘bg-yellow-500/20’ : ”}
                                                ${isMove ? ‘cursor-pointer’ : ”}
                                            `}
                                        >
                                            {isMove && !piece && <div className=”w-2 h-2 rounded-full bg-green-400/50 animate-pulse” />}
                                            {isMove && piece && <div className=”absolute inset-0 border-2 border-red-500/50 animate-pulse” />}

                                            {piece && (
                                                <div
                                                    className={`
                                                        w-3/4 h-3/4 transition-all duration-300
                                                        ${isLeft ? ‘text-red-500’ : ‘text-blue-500’}
                                                        ${isSelected ? ‘scale-110 text-yellow-400’ : ”}
                                                        drop-shadow-lg
                                                    `}
                                                >
                                                    <PieceIcon type={piece.type} className=”w-full h-full” />
                                                    <div className=”absolute -bottom-1 -right-1 text-[8px] font-bold opacity-70 bg-slate-900 px-1 rounded”>
                                                        {piece.type === 1 ? ‘1’ : piece.type === 2 ? ‘2’ : ‘3’}
                                                    </div>
                                                </div>
                                            )}
                                        </div>
                                    );
                                })
                            ))}
                        </div>
                    </div>

                    {/* Reset */}
                    <button
                        onClick={resetGame}
                        className=”z-10 mt-6 px-6 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-sm font-bold tracking-wider transition shadow-lg”
                    >
                        RESET
                    </button>

                </div>
            );
        };

        const root = ReactDOM.createRoot(document.getElementById(‘root’));
        root.render(<App />);
    </script>
</body>
</html>