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 –
