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

Leave a Reply