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>

Pente experiment

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

<!DOCTYPE html>
<html lang=”en”>
<head>
    <meta charset=”UTF-8″>
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
    <title>Pente Master</title>
    <style>
        :root {
            –bg-color: #f0f4f8;
            –board-color: #e3c076;
            –board-line: #4b3621;
            –p1-color: #1a1a1a;    /* Black */
            –p2-color: #f2f2f2;    /* White */
            –accent: #3b82f6;
            –text-main: #1f2937;
        }

        * { box-sizing: border-box; margin: 0; padding: 0; user-select: none; -webkit-user-select: none; }

        body {
            font-family: -apple-system, BlinkMacSystemFont, “Segoe UI”, Roboto, Helvetica, Arial, sans-serif;
            background-color: var(–bg-color);
            color: var(–text-main);
            display: flex;
            flex-direction: column;
            align-items: center;
            min-height: 100vh;
            overflow: hidden;
        }

        header {
            width: 100%;
            padding: 1rem;
            background: white;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            text-align: center;
            z-index: 10;
            display: flex;
            flex-direction: column;
            gap: 10px;
            align-items: center;
            max-width: 900px;
        }

        h1 { font-size: 1.5rem; font-weight: 700; color: var(–accent); }

        .controls {
            display: flex;
            gap: 0.5rem;
            align-items: center;
            flex-wrap: wrap;
            justify-content: center;
        }

        select, button {
            padding: 0.5rem 0.8rem;
            border: 1px solid #e5e7eb;
            border-radius: 0.5rem;
            background: white;
            font-size: 0.9rem;
            cursor: pointer;
            transition: all 0.2s;
        }

        button:hover, select:hover { border-color: var(–accent); color: var(–accent); }
        select:disabled { background: #f3f4f6; color: #9ca3af; cursor: not-allowed; border-color: #e5e7eb; }

        button.primary {
            background: var(–accent);
            color: white;
            border: none;
        }
        button.primary:hover { background: #2563eb; }

        /* Game Info Panel */
        .game-info {
            display: flex;
            width: 100%;
            max-width: 600px;
            justify-content: space-around;
            padding: 1rem;
            font-weight: 600;
        }

        .player-card {
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 0.5rem 1rem;
            border-radius: 0.5rem;
            background: white;
            min-width: 120px;
            border: 2px solid transparent;
            transition: all 0.3s;
        }

        .player-card.active {
            border-color: var(–accent);
            box-shadow: 0 4px 6px rgba(59, 130, 246, 0.1);
            transform: translateY(-2px);
        }

        .stone-indicator {
            width: 24px;
            height: 24px;
            border-radius: 50%;
            box-shadow: 0 2px 4px rgba(0,0,0,0.3);
            margin-bottom: 4px;
            border: 1px solid rgba(0,0,0,0.1);
        }
        .p1-stone { background: var(–p1-color); }
        .p2-stone { background: var(–p2-color); }

        .captures { font-size: 0.8rem; color: #6b7280; margin-top: 4px; }
        .captures span { font-weight: bold; color: var(–text-main); font-size: 1rem; }

        /* Game Area */
        #game-container {
            flex: 1;
            display: flex;
            justify-content: center;
            align-items: center;
            width: 100%;
            padding: 10px;
            position: relative;
        }

        canvas {
            box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
            border-radius: 4px;
            cursor: crosshair;
            background-color: var(–board-color);
        }

        /* Modal & Toast */
        #modal-overlay {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.5); display: none;
            justify-content: center; align-items: center; z-index: 50;
            backdrop-filter: blur(2px);
        }
        .modal {
            background: white; padding: 2rem; border-radius: 1rem;
            text-align: center; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
            max-width: 90%; width: 400px;
        }
        .modal h2 { font-size: 2rem; margin-bottom: 1rem; color: var(–text-main); }
        .modal p { margin-bottom: 1.5rem; color: #4b5563; }

        #toast {
            position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
            background: #374151; color: white; padding: 10px 20px;
            border-radius: 20px; font-size: 0.9rem; opacity: 0;
            transition: opacity 0.3s; pointer-events: none; z-index: 100;
        }

        @media (min-width: 650px) {
            header { flex-direction: row; justify-content: space-between; padding: 1rem 2rem; }
            .controls { justify-content: flex-end; }
        }
    </style>
</head>
<body>

<header>
    <h1>Pente</h1>
    <div class=”controls”>
        <select id=”grid-size” title=”Board Size”>
            <option value=”9″>9×9 (Small)</option>
            <option value=”13″>13×13 (Medium)</option>
            <option value=”19″ selected>19×19 (Standard)</option>
        </select>
       
        <select id=”game-mode” title=”Game Mode”>
            <option value=”hvh”>Human vs Human</option>
            <option value=”hvai” selected>Human vs AI</option>
            <option value=”aiva”>AI vs AI</option>
        </select>

        <select id=”play-as” title=”Play As (For Human vs AI)”>
            <option value=”1″>P1 (Black)</option>
            <option value=”2″>P2 (White)</option>
            <option value=”random”>Random</option>
        </select>

        <button class=”primary” id=”reset-btn”>New Game</button>
    </div>
</header>

<div class=”game-info”>
    <div class=”player-card active” id=”p1-card”>
        <div class=”stone-indicator p1-stone”></div>
        <div id=”p1-name”>Player 1</div>
        <div class=”captures”>Pairs: <span id=”p1-captures”>0</span>/5</div>
    </div>
    <div class=”player-card” id=”p2-card”>
        <div class=”stone-indicator p2-stone”></div>
        <div id=”p2-name”>CPU 1</div>
        <div class=”captures”>Pairs: <span id=”p2-captures”>0</span>/5</div>
    </div>
</div>

<div id=”game-container”>
    <canvas id=”board”></canvas>
</div>

<div id=”toast”>Notification</div>

<div id=”modal-overlay”>
    <div class=”modal”>
        <h2 id=”winner-title”>Wins!</h2>
        <p id=”win-reason”>Reason</p>
        <button class=”primary” id=”modal-reset”>Play Again</button>
    </div>
</div>

<script>
/**
* PENTE GAME LOGIC
*/

let BOARD_SIZE = 19;
const EMPTY = 0;
const P1 = 1; // Black
const P2 = 2; // White

// State
let board = [];
let currentPlayer = P1;
let gameActive = false;
let p1Captures = 0;
let p2Captures = 0;
let winningStones = [];
let lastMove = null;

// Configuration
let p1IsHuman = true;
let p2IsHuman = false;

// Canvas
const canvas = document.getElementById(‘board’);
const ctx = canvas.getContext(‘2d’);
let cellSize = 30;
let boardPadding = 30;

// DOM
const p1Card = document.getElementById(‘p1-card’);
const p2Card = document.getElementById(‘p2-card’);
const p1NameEl = document.getElementById(‘p1-name’);
const p2NameEl = document.getElementById(‘p2-name’);
const p1ScoreEl = document.getElementById(‘p1-captures’);
const p2ScoreEl = document.getElementById(‘p2-captures’);
const modeSelect = document.getElementById(‘game-mode’);
const gridSizeSelect = document.getElementById(‘grid-size’);
const playAsSelect = document.getElementById(‘play-as’);
const resetBtn = document.getElementById(‘reset-btn’);
const modalOverlay = document.getElementById(‘modal-overlay’);
const modalReset = document.getElementById(‘modal-reset’);
const toastEl = document.getElementById(‘toast’);

// AI Config
const AI_DELAY = 600;
let aiTimeout = null;

/* — INITIALIZATION — */

function initGame() {
    // 1. Setup Grid
    BOARD_SIZE = parseInt(gridSizeSelect.value);
    board = Array(BOARD_SIZE).fill().map(() => Array(BOARD_SIZE).fill(EMPTY));
   
    // 2. Setup Roles & Names
    const mode = modeSelect.value;
    let userPref = playAsSelect.value;

    // Handle Random preference
    if (mode === ‘hvai’ && userPref === ‘random’) {
        userPref = Math.random() < 0.5 ? ‘1’ : ‘2’;
        showToast(userPref === ‘1’ ? “Random: You are Player 1” : “Random: You are Player 2”);
    }

    if (mode === ‘hvh’) {
        p1IsHuman = true;
        p2IsHuman = true;
        p1NameEl.innerText = “Player 1”;
        p2NameEl.innerText = “Player 2”;
    } else if (mode === ‘aiva’) {
        p1IsHuman = false;
        p2IsHuman = false;
        p1NameEl.innerText = “CPU 1”;
        p2NameEl.innerText = “CPU 2”;
    } else { // hvai
        if (userPref === ‘1’) {
            p1IsHuman = true;
            p2IsHuman = false;
            p1NameEl.innerText = “Player 1”;
            p2NameEl.innerText = “CPU 2”;
        } else {
            p1IsHuman = false;
            p2IsHuman = true;
            p1NameEl.innerText = “CPU 1”;
            p2NameEl.innerText = “Player 2”;
        }
    }

    // 3. Reset State
    currentPlayer = P1;
    p1Captures = 0;
    p2Captures = 0;
    lastMove = null;
    winningStones = [];
    gameActive = true;
   
    updateScoreUI();
    toggleActiveCard();
    modalOverlay.style.display = ‘none’;
    clearTimeout(aiTimeout);

    resizeCanvas();

    // 4. Check if first player is AI
    checkAiTurn();
}

function checkAiTurn() {
    if (!gameActive) return;
   
    const isHumanTurn = (currentPlayer === P1 && p1IsHuman) || (currentPlayer === P2 && p2IsHuman);
   
    if (!isHumanTurn) {
        clearTimeout(aiTimeout);
        aiTimeout = setTimeout(aiTurn, AI_DELAY);
    }
}

function resizeCanvas() {
    const container = document.getElementById(‘game-container’);
    const size = Math.min(container.clientWidth, container.clientHeight);
    const dpr = window.devicePixelRatio || 1;
   
    const availableSize = size – 20;
    cellSize = Math.floor(availableSize / (BOARD_SIZE + 1));
    if (cellSize < 14) cellSize = 14;
   
    const canvasSize = cellSize * (BOARD_SIZE + 1);
   
    canvas.width = canvasSize * dpr;
    canvas.height = canvasSize * dpr;
    canvas.style.width = `${canvasSize}px`;
    canvas.style.height = `${canvasSize}px`;
   
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    boardPadding = cellSize;
   
    drawBoard();
}

/* — DRAWING — */

function drawBoard() {
    ctx.fillStyle = ‘#e3c076’;
    ctx.fillRect(0, 0, canvas.width, canvas.height); // Clear whole canvas

    // Grid
    ctx.strokeStyle = ‘#4b3621’;
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    for (let i = 0; i < BOARD_SIZE; i++) {
        const pos = boardPadding + i * cellSize;
        ctx.moveTo(pos, boardPadding);
        ctx.lineTo(pos, boardPadding + (BOARD_SIZE – 1) * cellSize);
        ctx.moveTo(boardPadding, pos);
        ctx.lineTo(boardPadding + (BOARD_SIZE – 1) * cellSize, pos);
    }
    ctx.stroke();

    // Stars
    const points = getStarPoints(BOARD_SIZE);
    ctx.fillStyle = ‘#4b3621’;
    for (let r of points) {
        for (let c of points) {
            const x = boardPadding + c * cellSize;
            const y = boardPadding + r * cellSize;
            ctx.beginPath();
            ctx.arc(x, y, cellSize * 0.12, 0, Math.PI * 2);
            ctx.fill();
        }
    }

    // Stones
    for (let r = 0; r < BOARD_SIZE; r++) {
        for (let c = 0; c < BOARD_SIZE; c++) {
            if (board[r][c] !== EMPTY) {
                drawStone(r, c, board[r][c]);
            }
        }
    }

    // Last Move
    if (lastMove) {
        const x = boardPadding + lastMove.c * cellSize;
        const y = boardPadding + lastMove.r * cellSize;
        ctx.fillStyle = ‘rgba(255, 0, 0, 0.5)’;
        ctx.beginPath();
        ctx.arc(x, y, cellSize * 0.15, 0, Math.PI * 2);
        ctx.fill();
    }

    // Win Line
    if (winningStones.length > 0) {
        ctx.strokeStyle = ‘rgba(50, 255, 50, 0.8)’;
        ctx.lineWidth = 4;
        ctx.lineCap = ’round’;
        ctx.beginPath();
        const start = winningStones[0];
        const end = winningStones[winningStones.length – 1];
        ctx.moveTo(boardPadding + start.c * cellSize, boardPadding + start.r * cellSize);
        ctx.lineTo(boardPadding + end.c * cellSize, boardPadding + end.r * cellSize);
        ctx.stroke();
    }
}

function getStarPoints(size) {
    if (size === 19) return [3, 9, 15];
    if (size === 13) return [3, 6, 9];
    if (size === 9) return [2, 4, 6];
    return [];
}

function drawStone(r, c, type) {
    const x = boardPadding + c * cellSize;
    const y = boardPadding + r * cellSize;
    const radius = cellSize * 0.42;

    ctx.beginPath();
    ctx.arc(x, y, radius, 0, Math.PI * 2);
   
    const grad = ctx.createRadialGradient(x – radius/3, y – radius/3, radius/10, x, y, radius);
    if (type === P1) {
        grad.addColorStop(0, ‘#555’);
        grad.addColorStop(1, ‘#000’);
    } else {
        grad.addColorStop(0, ‘#fff’);
        grad.addColorStop(1, ‘#d1d5db’);
    }
   
    ctx.fillStyle = grad;
    ctx.fill();
   
    ctx.shadowColor = ‘rgba(0,0,0,0.2)’;
    ctx.shadowBlur = 2;
    ctx.shadowOffsetX = 1;
    ctx.shadowOffsetY = 1;
    ctx.stroke();
    ctx.shadowColor = ‘transparent’;
}

/* — INPUT & LOGIC — */

canvas.addEventListener(‘mousedown’, handleInput);
canvas.addEventListener(‘touchstart’, (e) => {
    e.preventDefault();
    handleInput(e.touches[0]);
}, {passive: false});

function handleInput(e) {
    if (!gameActive) return;
   
    // Check if it is human turn
    const isHumanTurn = (currentPlayer === P1 && p1IsHuman) || (currentPlayer === P2 && p2IsHuman);
    if (!isHumanTurn) return;

    const rect = canvas.getBoundingClientRect();
    const dpr = window.devicePixelRatio || 1;
   
    const cssX = e.clientX – rect.left;
    const cssY = e.clientY – rect.top;
   
    // Convert to logical coordinates within canvas space
    const x = cssX * (canvas.width / rect.width) / dpr;
    const y = cssY * (canvas.height / rect.height) / dpr;

    const c = Math.round((x – boardPadding) / cellSize);
    const r = Math.round((y – boardPadding) / cellSize);

    if (isValidMove(r, c)) {
        makeMove(r, c);
    }
}

function isValidMove(r, c) {
    return r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && board[r][c] === EMPTY;
}

function makeMove(r, c) {
    board[r][c] = currentPlayer;
    lastMove = {r, c};
   
    // 1. Check Captures
    const captured = checkCaptures(r, c, currentPlayer);
    if (captured > 0) {
        if (currentPlayer === P1) p1Captures += captured;
        else p2Captures += captured;
        updateScoreUI();
        showToast(`${getName(currentPlayer)} captured ${captured} pair(s)!`);
       
        if ((currentPlayer === P1 && p1Captures >= 5) || (currentPlayer === P2 && p2Captures >= 5)) {
            endGame(currentPlayer, “5 Pairs Captured”);
            drawBoard();
            return;
        }
    }

    // 2. Check 5-in-a-row
    const winningLine = checkWin(r, c, currentPlayer);
    if (winningLine) {
        winningStones = winningLine;
        endGame(currentPlayer, “5 Stones in a Row”);
        drawBoard();
        return;
    }

    drawBoard();

    // Switch Turn
    currentPlayer = currentPlayer === P1 ? P2 : P1;
    toggleActiveCard();

    // Trigger Next
    checkAiTurn();
}

function getName(player) {
    return player === P1 ? p1NameEl.innerText : p2NameEl.innerText;
}

function toggleActiveCard() {
    if (currentPlayer === P1) {
        p1Card.classList.add(‘active’);
        p2Card.classList.remove(‘active’);
    } else {
        p1Card.classList.remove(‘active’);
        p2Card.classList.add(‘active’);
    }
}

function updateScoreUI() {
    p1ScoreEl.innerText = p1Captures;
    p2ScoreEl.innerText = p2Captures;
}

function showToast(msg) {
    toastEl.innerText = msg;
    toastEl.style.opacity = 1;
    setTimeout(() => { toastEl.style.opacity = 0; }, 2000);
}

function endGame(winner, reason) {
    gameActive = false;
    const winnerName = getName(winner);
    document.getElementById(‘winner-title’).innerText = `${winnerName} Wins!`;
    document.getElementById(‘win-reason’).innerText = reason;
    modalOverlay.style.display = ‘flex’;
}

/* — MECHANICS — */

const DIRECTIONS = [
    {dr: 0, dc: 1}, {dr: 1, dc: 0}, {dr: 1, dc: 1}, {dr: 1, dc: -1}
];

function checkCaptures(r, c, player) {
    const opponent = player === P1 ? P2 : P1;
    let totalCaptured = 0;

    for (let d of DIRECTIONS) {
        // Forward
        if (getCell(r + d.dr, c + d.dc) === opponent &&
            getCell(r + 2 * d.dr, c + 2 * d.dc) === opponent &&
            getCell(r + 3 * d.dr, c + 3 * d.dc) === player) {
            board[r + d.dr][c + d.dc] = EMPTY;
            board[r + 2 * d.dr][c + 2 * d.dc] = EMPTY;
            totalCaptured++;
        }
        // Backward
        if (getCell(r – d.dr, c – d.dc) === opponent &&
            getCell(r – 2 * d.dr, c – 2 * d.dc) === opponent &&
            getCell(r – 3 * d.dr, c – 3 * d.dc) === player) {
            board[r – d.dr][c – d.dc] = EMPTY;
            board[r – 2 * d.dr][c – 2 * d.dc] = EMPTY;
            totalCaptured++;
        }
    }
    return totalCaptured;
}

function checkWin(r, c, player) {
    for (let d of DIRECTIONS) {
        let stones = [{r,c}];
        let i = 1;
        while (getCell(r + i * d.dr, c + i * d.dc) === player) {
            stones.push({r: r + i * d.dr, c: c + i * d.dc}); i++;
        }
        let j = 1;
        while (getCell(r – j * d.dr, c – j * d.dc) === player) {
            stones.push({r: r – j * d.dr, c: c – j * d.dc}); j++;
        }
        if (stones.length >= 5) return stones;
    }
    return null;
}

function getCell(r, c) {
    if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE) return -1;
    return board[r][c];
}

/* — AI — */

// Scores
const S_WIN = 1000000;
const S_BLK_WIN = 500000;
const S_WIN_CAP = 200000;
const S_BLK_WIN_CAP = 150000;
const S_CAP = 10000;
const S_BLK_CAP = 8000;
const S_OPEN4 = 5000;
const S_OPEN3 = 1000;

function aiTurn() {
    if (!gameActive) return;
   
    const center = Math.floor(BOARD_SIZE / 2);
   
    // First move optimization
    let stones = 0;
    for(let r=0;r<BOARD_SIZE;r++) for(let c=0;c<BOARD_SIZE;c++) if(board[r][c]) stones++;
    if (stones === 0) { makeMove(center, center); return; }

    const moves = filterRelevantMoves();
    let bestMove = null;
    let maxScore = -Infinity;

    // If no relevant moves (rare), pick random empty
    if (moves.length === 0) {
        const all = getPossibleMoves();
        if (all.length > 0) makeMove(all[0].r, all[0].c);
        return;
    }

    for (let move of moves) {
        const score = evaluateMove(move.r, move.c, currentPlayer, center);
        const finalScore = score + Math.random() * 10; // Variation

        if (finalScore > maxScore) {
            maxScore = finalScore;
            bestMove = move;
        }
    }

    if (bestMove) makeMove(bestMove.r, bestMove.c);
}

function filterRelevantMoves() {
    const occupied = [];
    for(let r=0; r<BOARD_SIZE; r++) {
        for(let c=0; c<BOARD_SIZE; c++) {
            if(board[r][c] !== EMPTY) occupied.push({r,c});
        }
    }
   
    const relevantSet = new Set();
    const range = 2;

    for (let stone of occupied) {
        for (let dr = -range; dr <= range; dr++) {
            for (let dc = -range; dc <= range; dc++) {
                const nr = stone.r + dr;
                const nc = stone.c + dc;
                if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE && board[nr][nc] === EMPTY) {
                    relevantSet.add(`${nr},${nc}`);
                }
            }
        }
    }
    return Array.from(relevantSet).map(s => {
        const [r, c] = s.split(‘,’).map(Number);
        return {r, c};
    });
}

function getPossibleMoves() {
    let m = [];
    for (let r=0;r<BOARD_SIZE;r++) for (let c=0;c<BOARD_SIZE;c++) if (board[r][c]===EMPTY) m.push({r,c});
    return m;
}

function evaluateMove(r, c, me, center) {
    const opp = me === P1 ? P2 : P1;
    let score = 0;

    const dist = Math.abs(r – center) + Math.abs(c – center);
    score -= dist * 2;

    // Offensive
    board[r][c] = me;
    if (checkWin(r, c, me)) score += S_WIN;
    const caps = countCaptures(r, c, me);
    if (caps > 0) {
        score += S_CAP * caps;
        const myCaps = me === P1 ? p1Captures : p2Captures;
        if (myCaps + caps >= 5) score += S_WIN_CAP;
    }
    score += analyzePatterns(r, c, me);
    board[r][c] = EMPTY;
   
    // Defensive
    board[r][c] = opp;
    if (checkWin(r, c, opp)) score += S_BLK_WIN;
    const oppCaps = countCaptures(r, c, opp);
    if (oppCaps > 0) {
        score += S_BLK_CAP * oppCaps;
        const enCaps = opp === P1 ? p1Captures : p2Captures;
        if (enCaps + oppCaps >= 5) score += S_BLK_WIN_CAP;
    }
    score += analyzePatterns(r, c, opp) * 0.9;
    board[r][c] = EMPTY;
   
    return score;
}

function countCaptures(r, c, player) {
    const opponent = player === P1 ? P2 : P1;
    let count = 0;
    for (let d of DIRECTIONS) {
        if (getCell(r + d.dr, c + d.dc) === opponent &&
            getCell(r + 2 * d.dr, c + 2 * d.dc) === opponent &&
            getCell(r + 3 * d.dr, c + 3 * d.dc) === player) count++;
        if (getCell(r – d.dr, c – d.dc) === opponent &&
            getCell(r – 2 * d.dr, c – 2 * d.dc) === opponent &&
            getCell(r – 3 * d.dr, c – 3 * d.dc) === player) count++;
    }
    return count;
}

function analyzePatterns(r, c, player) {
    let score = 0;
    for (let d of DIRECTIONS) {
        let line = [];
        for(let k=-4; k<=4; k++) line.push(getCell(r + k*d.dr, c + k*d.dc));
        const s = line.map(x => x===player?’X’:(x===EMPTY?’.’:’O’)).join(”);
        if (s.includes(‘.XXXX.’)) score += S_OPEN4;
        else if (s.includes(‘OXXXX.’) || s.includes(‘.XXXXO’)) score += S_OPEN4 * 0.5;
        if (s.includes(‘.XXX.’)) score += S_OPEN3;
        if (s.includes(‘.X.XX.’) || s.includes(‘.XX.X.’)) score += S_OPEN3 * 0.9;
    }
    return score;
}

/* — EVENTS — */

// Toggle Play As select based on mode
modeSelect.addEventListener(‘change’, () => {
    if (modeSelect.value === ‘hvai’) {
        playAsSelect.disabled = false;
    } else {
        playAsSelect.disabled = true;
    }
    initGame();
});

gridSizeSelect.addEventListener(‘change’, initGame);
playAsSelect.addEventListener(‘change’, initGame);
resetBtn.addEventListener(‘click’, initGame);
modalReset.addEventListener(‘click’, initGame);
window.addEventListener(‘resize’, resizeCanvas);
window.onload = () => {
    resizeCanvas();
    initGame();
};

</script>
</body>
</html>

Pathfinder html app

Initiative tracker for pathfinder, html format for easy use on phone, laptop, tablet or whatnot.

I goofed on the d20s, will have to revise later

V 0.01 here –

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

Source

<!DOCTYPE html>
<html lang=”en”>
<head>
    <meta charset=”UTF-8″>
    <meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
    <title>Pathfinder Initiative Tracker with Quick Add</title>
    <script src=”https://cdn.tailwindcss.com”></script>
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        ‘primary’: ‘#9f1239’, /* Deep Red for Pathfinder */
                        ‘secondary’: ‘#1f2937’,
                        ‘accent’: ‘#fcd34d’, /* Gold/Amber */
                        ‘background’: ‘#0f172a’, /* Dark Blue/Black */
                    },
                    fontFamily: {
                        sans: [‘Inter’, ‘sans-serif’],
                    },
                }
            }
        }
    </script>
    <style>
        @import url(‘https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap’);
        .scroll-container {
            max-height: 70vh;
            overflow-y: auto;
            scrollbar-width: thin;
            scrollbar-color: #9f1239 #1f2937;
        }
        .scroll-container::-webkit-scrollbar {
            width: 8px;
        }
        .scroll-container::-webkit-scrollbar-thumb {
            background-color: #9f1239;
            border-radius: 10px;
        }
        .scroll-container::-webkit-scrollbar-track {
            background: #1f2937;
        }
        /* Custom D20 styling */
        .d20-icon {
            filter: drop-shadow(0 0 5px rgba(252, 211, 77, 0.7));
            width: 1.5em;
            height: 1.5em;
            fill: currentColor;
        }
    </style>
</head>
<body class=”bg-background text-gray-100 min-h-screen p-4 font-sans”>

    <div id=”app” class=”max-w-4xl mx-auto space-y-8″>
        <header class=”text-center py-4 bg-gray-800 rounded-xl shadow-2xl”>
            <h1 class=”text-3xl font-bold text-accent flex items-center justify-center”>
                <!– Left D20 SVG Icon –>
                <svg class=”d20-icon mr-3 text-accent” viewBox=”0 0 512 512″ xmlns=”http://www.w3.org/2000/svg”>
                    <path d=”M495.9 224.2L255.6 32.5 15.3 224.2l-1.3 268.3 241.6 42.4 240.2-42.4-1.9-268.3zM461 247.4l-64.8 112.5-121.3 210.3-2.6 4.5V230.1l-66.2-114.6 37.9-65.7 151.8 263.3 121.2-210.1 3.5 6.1zM344.2 360l-33.5 58.2-113.8-197.6 147.3-84.9 33.5 58.2 113.8 197.6-147.3 84.9z”/>
                </svg>
               
                Pathfinder Initiative Tracker
               
                <!– Right D20 SVG Icon –>
                <svg class=”d20-icon ml-3 text-accent” viewBox=”0 0 512 512″ xmlns=”http://www.w3.org/2000/svg”>
                    <path d=”M495.9 224.2L255.6 32.5 15.3 224.2l-1.3 268.3 241.6 42.4 240.2-42.4-1.9-268.3zM461 247.4l-64.8 112.5-121.3 210.3-2.6 4.5V230.1l-66.2-114.6 37.9-65.7 151.8 263.3 121.2-210.1 3.5 6.1zM344.2 360l-33.5 58.2-113.8-197.6 147.3-84.9 33.5 58.2 113.8 197.6-147.3 84.9z”/>
                </svg>
            </h1>
            <p class=”text-sm text-gray-400 mt-1″>Uses d20 + Initiative Mod + DEX tiebreaker rules.</p>
        </header>

        <!– Input and Character Management –>
        <div class=”bg-gray-800 p-6 rounded-xl shadow-xl flex flex-col lg:flex-row gap-6″>
           
            <!– Quick Add Section –>
            <div class=”lg:w-1/3 space-y-4″>
                <h2 class=”text-xl font-semibold text-primary”>Quick Add Monster/NPC</h2>
                <div>
                    <label for=”quick-add-select” class=”block text-sm font-medium text-gray-300 mb-1″>Select Preset</label>
                    <select id=”quick-add-select” class=”block w-full rounded-lg border-gray-700 bg-gray-700 text-gray-100 p-2 focus:ring-primary focus:border-primary”>
                        <option value=”” disabled selected>— Choose a Creature —</option>
                    </select>
                </div>
                <button onclick=”addSelectedPreset()” class=”w-full bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg transition duration-200 shadow-md”>
                    Quick Add to Roster
                </button>
                <div class=”border-t border-gray-700 pt-4″>
                    <h2 class=”text-xl font-semibold mb-3 text-primary”>Manual Add</h2>
                    <form id=”add-character-form” class=”space-y-4″>
                        <div>
                            <label for=”char-name” class=”block text-sm font-medium text-gray-300″>Name</label>
                            <input type=”text” id=”char-name” placeholder=”Name or Monster Type” required class=”mt-1 block w-full rounded-lg border-gray-700 bg-gray-700 text-gray-100 p-2 focus:ring-primary focus:border-primary”>
                        </div>
                        <div>
                            <label for=”init-mod” class=”block text-sm font-medium text-gray-300″>Initiative Modifier (e.g., +5)</label>
                            <input type=”number” id=”init-mod” placeholder=”+5″ required class=”mt-1 block w-full rounded-lg border-gray-700 bg-gray-700 text-gray-100 p-2 focus:ring-primary focus:border-primary”>
                        </div>
                        <div>
                            <label for=”dex-mod” class=”block text-sm font-medium text-gray-300″>DEX Modifier (Tiebreaker)</label>
                            <input type=”number” id=”dex-mod” placeholder=”+2″ required class=”mt-1 block w-full rounded-lg border-gray-700 bg-gray-700 text-gray-100 p-2 focus:ring-primary focus:border-primary”>
                        </div>
                        <button type=”submit” class=”w-full bg-primary hover:bg-red-700 text-white font-bold py-2 px-4 rounded-lg transition duration-200 shadow-md”>
                            Add to Roster Manually
                        </button>
                    </form>
                </div>
            </div>

            <!– Current Roster –>
            <div class=”lg:w-2/3″>
                <h2 class=”text-xl font-semibold mb-3 text-primary”>Current Roster</h2>
                <div id=”roster-list” class=”space-y-2 max-h-96 overflow-y-auto pr-2″>
                    <p class=”text-gray-400 text-sm” id=”empty-roster-msg”>Add characters and their modifiers to begin.</p>
                    <!– Character cards will be injected here –>
                </div>
            </div>
        </div>

        <!– Initiative Roll and Turn Order Display –>
        <div class=”bg-gray-800 p-6 rounded-xl shadow-xl”>
            <div class=”flex justify-between items-center mb-4 flex-wrap gap-3″>
                <button onclick=”rollInitiative()” id=”roll-button” class=”flex-grow bg-accent hover:bg-yellow-400 text-gray-900 font-extrabold py-3 px-6 rounded-lg transition duration-200 text-lg shadow-lg”>
                    Roll Initiative! (New Combat)
                </button>
                <button onclick=”resetRoster()” class=”bg-gray-600 hover:bg-gray-700 text-white text-sm font-bold py-3 px-3 rounded-lg transition duration-200″>
                    Reset Roster
                </button>
            </div>

            <h2 class=”text-2xl font-semibold text-accent mt-6 mb-3 border-b border-gray-700 pb-2″>Combat Turn Order</h2>

            <div id=”turn-order-list” class=”scroll-container space-y-2″>
                <p class=”text-center text-gray-400 p-4″>Press “Roll Initiative!” to generate the combat order.</p>
                <!– Turn entries will be injected here –>
            </div>
        </div>
    </div>

    <script>
        // Global State
        let characters = [];
        let initiativeRolled = false;
        const TURNS_TO_SHOW = 20;

        // Pathfinder Monster/NPC Presets (Expanded List)
        const presets = [
            // NEW ADDITION
            { name: “Housecat (CR —)”, initMod: 3, dexMod: 3, cr: ‘—’ },
           
            // CR 1/2 to 1
            { name: “Goblin Warrior (CR 1/3)”, initMod: 2, dexMod: 2, cr: ‘1/3’ },
            { name: “Orc Brute (CR 1/2)”, initMod: 0, dexMod: 0, cr: ‘1/2’ },
            { name: “Skeleton (CR 1/3)”, initMod: 0, dexMod: 0, cr: ‘1/3’ },
            { name: “Zombie (CR 1/2)”, initMod: -1, dexMod: -1, cr: ‘1/2’ },
            { name: “Wolf (CR 1)”, initMod: 2, dexMod: 2, cr: ‘1’ },
            { name: “Axe Beak (CR 1)”, initMod: 1, dexMod: 1, cr: ‘1’ },
           
            // CR 2 to 4
            { name: “Bandit Leader (CR 2 NPC)”, initMod: 3, dexMod: 3, cr: ‘2’ },
            { name: “Ghoul (CR 2)”, initMod: 6, dexMod: 3, cr: ‘2’ },
            { name: “Ogre (CR 3)”, initMod: -1, dexMod: -1, cr: ‘3’ },
            { name: “Giant Spider (CR 3)”, initMod: 7, dexMod: 3, cr: ‘3’ },
            { name: “Wight (CR 4)”, initMod: 4, dexMod: 4, cr: ‘4’ },
            { name: “Young Dragon (CR 4)”, initMod: 2, dexMod: 2, cr: ‘4’ },
           
            // CR 5 to 7
            { name: “Hill Giant (CR 5)”, initMod: -1, dexMod: -1, cr: ‘5’ },
            { name: “Troll (CR 5)”, initMod: 4, dexMod: 1, cr: ‘5’ },
            { name: “Shadow (CR 5)”, initMod: 4, dexMod: 4, cr: ‘5’ },
            { name: “Vampire Spawn (CR 5)”, initMod: 7, dexMod: 4, cr: ‘5’ },
            { name: “Manticore (CR 5)”, initMod: 1, dexMod: 1, cr: ‘5’ },
            { name: “Stone Golem (CR 7)”, initMod: -1, dexMod: -1, cr: ‘7’ },

            // CR 8+
            { name: “Black Pudding (CR 7)”, initMod: -5, dexMod: -5, cr: ‘7’ },
            { name: “Archmage (CR 8 NPC)”, initMod: 10, dexMod: 5, cr: ‘8’ },
            { name: “Drider (CR 8)”, initMod: 5, dexMod: 2, cr: ‘8’ },
            { name: “Frost Giant (CR 9)”, initMod: 0, dexMod: 0, cr: ‘9’ },
            { name: “Efreeti (CR 10)”, initMod: 6, dexMod: 3, cr: ’10’ },
            { name: “Balor (CR 13)”, initMod: 5, dexMod: 2, cr: ’13’ },
            { name: “Ancient Red Dragon (CR 20)”, initMod: 4, dexMod: 4, cr: ’20’ },
           
            // Generic NPCs
            { name: “Commoner (NPC)”, initMod: -1, dexMod: -1, cr: ‘0’ },
            { name: “Acolyte (NPC)”, initMod: 1, dexMod: 1, cr: ‘1/2’ },
            { name: “Guard Captain (NPC)”, initMod: 5, dexMod: 3, cr: ‘3’ },
            { name: “Master Thief (NPC)”, initMod: 9, dexMod: 5, cr: ‘5’ },
        ];

        /**
         * Represents a Pathfinder character’s combat stats.
         */
        class PathfinderCharacter {
            constructor(name, initMod, dexMod) {
                this.id = crypto.randomUUID();
                this.name = name;
                this.initModifier = parseInt(initMod);
                this.dexModifier = parseInt(dexMod);

                // Rolled/Transient values
                this.d20Roll = 0;
                this.d100Tiebreaker = 0;
                this.totalInitiative = 0;
            }

            /**
             * Performs the initiative roll and calculates the total.
             */
            roll() {
                this.d20Roll = Math.floor(Math.random() * 20) + 1; // 1 to 20
                this.d100Tiebreaker = Math.floor(Math.random() * 100) + 1; // 1 to 100 for final tiebreaker
                this.totalInitiative = this.d20Roll + this.initModifier;
            }
        }

        // — DOM Elements —
        const rosterList = document.getElementById(‘roster-list’);
        const turnOrderList = document.getElementById(‘turn-order-list’);
        const emptyRosterMsg = document.getElementById(’empty-roster-msg’);
        const rollButton = document.getElementById(‘roll-button’);
        const quickAddSelect = document.getElementById(‘quick-add-select’);
        const charNameInput = document.getElementById(‘char-name’);
        const initModInput = document.getElementById(‘init-mod’);
        const dexModInput = document.getElementById(‘dex-mod’);

        // — Utility Functions —

        /**
         * Initializes the Quick Add dropdown menu with preset data.
         */
        function initializePresets() {
            // Sort presets alphabetically for easy finding
            presets.sort((a, b) => a.name.localeCompare(b.name));

            presets.forEach(preset => {
                const option = document.createElement(‘option’);
                option.value = preset.name;
                const initSign = preset.initMod >= 0 ? ‘+’ : ”;
                const dexSign = preset.dexMod >= 0 ? ‘+’ : ”;
                option.textContent = `${preset.name} (Init: ${initSign}${preset.initMod}, DEX: ${dexSign}${preset.dexMod})`;
                quickAddSelect.appendChild(option);
            });
        }

        /**
         * Handles adding the selected preset monster to the roster.
         */
        function addSelectedPreset() {
            const selectedName = quickAddSelect.value;
            if (!selectedName) return;

            // Find the original preset data (ignoring the CR in the dropdown text)
            const preset = presets.find(p => p.name === selectedName);
            if (!preset) return;

            // Check if multiple instances of this monster already exist for automatic numbering
            const count = characters.filter(c => c.name.startsWith(preset.name)).length;
           
            // Remove the CR/NPC tag for cleaner numbering, then add it back if it’s the first one
            const baseName = preset.name.replace(/\s*\(CR.*?\)/, ”).replace(/\s*\(NPC\)/, ”).trim();
            const suffix = (preset.name.match(/\((CR.*?)\)/) || preset.name.match(/\((NPC)\)/))?.[0] || ”;
           
            let name;
            if (count === 0) {
                 name = preset.name; // Use the full name with CR/NPC tag if it’s the first one
            } else {
                 // Use the base name + number for subsequent entries
                 name = `${baseName} ${count + 1}`;
            }
           
            addCharacter(name, preset.initMod, preset.dexMod);
            // After adding, reset selection to prevent accidental double adds
            quickAddSelect.selectedIndex = 0;
        }

        /**
         * Sorts the character list based on Pathfinder initiative rules:
         */
        function sortInitiative() {
            return characters.sort((a, b) => {
                // Rule 1: Total Initiative (Highest first)
                if (b.totalInitiative !== a.totalInitiative) {
                    return b.totalInitiative – a.totalInitiative;
                }

                // Rule 2: DEX Modifier (Highest first)
                if (b.dexModifier !== a.dexModifier) {
                    return b.dexModifier – a.dexModifier;
                }

                // Rule 3: Hidden d100 Tiebreaker (Highest first)
                return b.d100Tiebreaker – a.d100Tiebreaker;
            });
        }

        /**
         * Renders the current list of characters in the Roster section.
         */
        function renderRoster() {
            rosterList.innerHTML = ”;
            if (characters.length === 0) {
                emptyRosterMsg.classList.remove(‘hidden’);
                return;
            }
            emptyRosterMsg.classList.add(‘hidden’);

            characters.forEach(char => {
                const initSign = char.initModifier >= 0 ? ‘+’ : ”;
                const dexSign = char.dexModifier >= 0 ? ‘+’ : ”;
                const charEl = document.createElement(‘div’);
                charEl.className = ‘flex justify-between items-center p-3 bg-gray-700 rounded-lg hover:bg-gray-600 transition duration-150’;
                charEl.innerHTML = `
                    <div class=”flex flex-col”>
                        <span class=”font-bold text-lg text-white”>${char.name}</span>
                        <span class=”text-sm text-gray-400″>Init Mod: ${initSign}${char.initModifier} | DEX Mod: ${dexSign}${char.dexModifier}</span>
                    </div>
                    <button data-id=”${char.id}” class=”remove-char-btn text-red-400 hover:text-red-500 transition duration-200 p-1 rounded-full”>
                        <svg xmlns=”http://www.w3.org/2000/svg” class=”h-6 w-6″ fill=”none” viewBox=”0 0 24 24″ stroke=”currentColor” stroke-width=”2″>
                          <path stroke-linecap=”round” stroke-linejoin=”round” d=”M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16″ />
                        </svg>
                    </button>
                `;
                rosterList.appendChild(charEl);
            });

            // Attach removal event listeners
            document.querySelectorAll(‘.remove-char-btn’).forEach(button => {
                button.addEventListener(‘click’, (e) => removeCharacter(e.currentTarget.dataset.id));
            });
        }

        /**
         * Renders the generated turn order list (repeating the same order for multiple rounds).
         */
        function renderTurnOrder() {
            turnOrderList.innerHTML = ”;

            if (characters.length === 0 || !initiativeRolled) {
                turnOrderList.innerHTML = ‘<p class=”text-center text-gray-400 p-4″>Press “Roll Initiative!” to generate the combat order.</p>’;
                return;
            }

            const sortedRoster = sortInitiative();
           
            for (let i = 0; i < TURNS_TO_SHOW; i++) {
                const round = Math.floor(i / sortedRoster.length) + 1;
                const charIndex = i % sortedRoster.length;
                const char = sortedRoster[charIndex];

                // Add Round Header
                if (charIndex === 0) {
                    const roundHeader = document.createElement(‘div’);
                    roundHeader.className = ‘sticky top-0 z-10 bg-primary/90 text-white font-extrabold p-2.5 mt-3 rounded-lg text-center shadow-lg uppercase tracking-wider’;
                    roundHeader.textContent = `— ROUND ${round} —`;
                    turnOrderList.appendChild(roundHeader);
                }

                // Add Turn Entry
                const turnEl = document.createElement(‘div’);
                turnEl.className = `flex items-center p-3 rounded-lg transition duration-150 border-l-4 border-accent/70 ${i === 0 ? ‘bg-green-700/80 shadow-xl’ : ‘bg-gray-700 hover:bg-gray-600’}`;
               
                const initSign = char.initModifier >= 0 ? ‘+’ : ”;
               
                turnEl.innerHTML = `
                    <div class=”w-1/12 text-center font-extrabold text-xl ${i === 0 ? ‘text-white’ : ‘text-accent’}”>${i + 1}</div>
                    <div class=”w-8/12 flex-1 ml-4″>
                        <span class=”font-bold text-lg ${i === 0 ? ‘text-white’ : ‘text-white’}”>${char.name}</span>
                        <span class=”text-sm text-gray-400 block”>Round ${round} | Total Initiative: ${char.totalInitiative}</span>
                    </div>
                    <div class=”w-3/12 text-right”>
                        <span class=”text-xs text-gray-400 block”>d20: ${char.d20Roll} + ${initSign}${char.initModifier}</span>
                        <span class=”text-sm font-semibold block”>Total: ${char.totalInitiative}</span>
                    </div>
                `;
                turnOrderList.appendChild(turnEl);
            }
        }

        /**
         * Main function to update and re-render both lists.
         */
        function updateDisplay() {
            renderRoster();
            renderTurnOrder();
            // Update button text/color based on state
            if (initiativeRolled) {
                rollButton.textContent = “Reroll Initiative (New Combat)”;
                rollButton.classList.remove(‘bg-accent’);
                rollButton.classList.add(‘bg-primary’);
            } else {
                rollButton.textContent = “Roll Initiative! (New Combat)”;
                rollButton.classList.remove(‘bg-primary’);
                rollButton.classList.add(‘bg-accent’);
            }
        }

        // — Core Roster Management —

        /**
         * Adds a character to the roster and updates the display.
         */
        function addCharacter(name, initMod, dexMod) {
            const newChar = new PathfinderCharacter(name, initMod, dexMod);
            characters.push(newChar);
            initiativeRolled = false; // Must re-roll after adding a new character
            updateDisplay();
        }

        /**
         * Removes a character from the roster by ID.
         * @param {string} id – The ID of the character to remove.
         */
        function removeCharacter(id) {
            characters = characters.filter(char => char.id !== id);
            initiativeRolled = false; // Must re-roll after changing the roster
            updateDisplay();
        }

        /**
         * Clears all characters and resets the tracker.
         */
        function resetRoster() {
            characters = [];
            initiativeRolled = false;
            updateDisplay();
        }

        // — Event Handlers —

        /**
         * Rolls initiative for all characters and updates the order.
         */
        function rollInitiative() {
            if (characters.length === 0) {
                turnOrderList.innerHTML = ‘<p class=”text-center text-red-400 p-4 font-bold”>Error: Please add at least one character to the roster before rolling initiative.</p>’;
                initiativeRolled = false;
                updateDisplay();
                return;
            }

            characters.forEach(char => char.roll());
            initiativeRolled = true;
            updateDisplay();
        }

        /**
         * Handles the submission of the “Manual Add” form.
         */
        document.getElementById(‘add-character-form’).addEventListener(‘submit’, function(e) {
            e.preventDefault();

            const name = charNameInput.value.trim();
            const initMod = initModInput.value;
            const dexMod = dexModInput.value;

            if (name && initMod !== ” && dexMod !== ”) {
                addCharacter(name, initMod, dexMod);
               
                // Clear the form
                charNameInput.value = ”;
                initModInput.value = ”;
                dexModInput.value = ”;
                charNameInput.focus();
            } else {
                console.error(“Invalid input: All manual fields must be filled.”);
            }
        });

        // Initial setup on load
        initializePresets();
        updateDisplay();

    </script>
</body>
</html>

Critters

A few visitors dropped by the backyard today, each stopping in at their own hour like they were checking into a tiny woodland hotel with flexible scheduling.

Before dawn, the tailless mouse made the first appearance of the day. A small, round shadow skittering across the dim camera view, moving with that cautious little hop-hop-hop that only nighttime creatures seem to master. No tail, but plenty of determination. It lingered just long enough to be seen, then slipped back into the pre-dawn hush, leaving only a faint ripple in the stillness.

Hours later, once the sun had rubbed the sleep out of its eyes, a chipmunk arrived around 8:30. This one had attitude. It zipped up onto the concrete like it was clocking in for mischief, then made a very deliberate show of glancing toward the house. There’s a good chance it was taunting the cat, who watched from behind the glass with that slow-tail-swishing, offended dignity only a cat can manage. The chipmunk seemed pleased with itself before dashing off to whatever important acorn diplomacy awaited.

And then, a final visitor: a deer, calm and quiet, wandering into the frame just long enough to gather itself. One moment there, the next leaping up the hill in a single smooth arc, like gravity was only a suggestion. A brief cameo, elegant and gone.

Three tiny chapters, stitched into the morning. I love catching these fragments of the world going about its business while we rub the sleep from our eyes.

Tags: #backyardzoo #taillessmouse #chipmunkantics #catdrama #morningdeer #salemva #littlewildthings

Deer visit

Just one deer on the backyard camera last night, stepping into frame like it wasn’t sure if it was allowed to be there. It came up to the concrete slab out back, gave everything a long, thoughtful once-over, ears perked and tail flicking like Morse code to the darkness.

It lingered just long enough to think about taking another step… then changed its mind and drifted away, soft as breath, back into the shadows. A tiny moment, but it feels like a small blessing every time the wild edges of the world brush up against the house.

Nice to have that little heartbeat of nature checking in before dawn.

#deer #backyardzoo  #roanokeva

How many times has he been rightfully called a pig?

Bubba’s eye view?

Answer: every time

‘Quiet, piggy’: Trump responds to reporter after Epstein question

When speaking to reporters on board Air Force One on 14 November, President Donald Trump was asked about recently released emails from Jeffrey Epstein which mentioned him.

Trump said he knew nothing about that and said the focus should be on other people named in those emails, including former President Bill Clinton.

After a journalist from Bloomberg News tried to ask a follow-up question on Epstein, the president turned to her and said: “Quiet. Quiet, piggy.”

The exchange occurred as calls grew for the US Department of Justice to release files related to its investigation into Epstein, the late financier and convicted sex offender who died in prison in 2019.

On Sunday, President Trump reversed his position and called for Republicans to vote for a bill that forces the Justice Department to release all of its files related to the Epstein case.

#doodle #politics #piggy #ochrejelly

Welcome to my wall scrawls.