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>

Leave a Reply