https://svonberg.org/wp-content/uploads/2025/11/martianchessai3.html
https://svonberg.org/wp-content/uploads/2025/11/starcitychessv1.html
https://svonberg.org/wp-content/uploads/2025/11/moonchess1.html
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>Roanoke: Star City vs Blue Ridge</title>
<script src=”https://cdn.tailwindcss.com”></script>
<style>
/* Custom Animations */
@keyframes slideIn {
from { transform: translateY(10px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.animate-slide-in {
animation: slideIn 0.3s ease-out forwards;
}
/* Prevent touch scrolling/zooming on board */
.board-container {
touch-action: none;
}
/* Disable interaction during AI turn */
.ai-thinking {
pointer-events: none;
opacity: 0.9;
cursor: wait;
}
</style>
</head>
<body class=”bg-slate-900 text-slate-200 font-sans min-h-screen flex flex-col items-center justify-center p-4″>
<div class=”max-w-2xl w-full bg-slate-800 shadow-2xl rounded-xl overflow-hidden flex flex-col border border-slate-700″>
<!– Header & Settings –>
<div class=”bg-slate-950 text-white p-4 border-b border-slate-700″>
<div class=”flex justify-between items-center mb-4″>
<div>
<h1 class=”text-xl font-bold text-slate-100 tracking-wider”>ROANOKE <span class=”text-slate-500 text-sm”>VA</span></h1>
<p class=”text-xs text-slate-400″>Star City vs. Blue Ridge (Martian Chess Rules)</p>
</div>
<button onclick=”game.resetGame()” class=”bg-blue-900 hover:bg-blue-800 text-white px-4 py-2 rounded text-sm font-bold transition shadow-md border border-blue-700″>
Reset Game
</button>
</div>
<!– Player Setup –>
<div class=”flex gap-4 text-sm bg-slate-900 p-2 rounded-lg justify-center border border-slate-800″>
<div class=”flex flex-col items-center”>
<label class=”text-xs text-red-400 mb-1 uppercase tracking-widest font-bold”>Star City</label>
<select id=”p2-type” class=”bg-slate-800 text-slate-200 rounded px-2 py-1 border border-slate-600 focus:outline-none focus:border-red-400″>
<option value=”human”>Human</option>
<option value=”ai” selected>AI (The Star)</option>
</select>
</div>
<div class=”flex items-center text-slate-600 font-bold text-xs”>VS</div>
<div class=”flex flex-col items-center”>
<label class=”text-xs text-emerald-500 mb-1 uppercase tracking-widest font-bold”>Blue Ridge</label>
<select id=”p1-type” class=”bg-slate-800 text-slate-200 rounded px-2 py-1 border border-slate-600 focus:outline-none focus:border-emerald-500″>
<option value=”human” selected>Human</option>
<option value=”ai”>AI (The Valley)</option>
</select>
</div>
</div>
</div>
<!– Game Info Bar –>
<div class=”bg-slate-800 p-3 text-sm font-medium border-b border-slate-700 grid grid-cols-3 items-center”>
<div id=”p2-score” class=”flex flex-col items-start pl-2″>
<span class=”text-xs uppercase tracking-wider text-red-400″>Star City</span>
<span class=”text-2xl font-bold text-white leading-none drop-shadow-lg” id=”score-p2″>0</span>
</div>
<div class=”flex flex-col items-center gap-1″>
<div id=”turn-indicator” class=”bg-slate-900 px-4 py-1 rounded-full shadow-inner border border-slate-700 text-emerald-400 font-bold transition-colors duration-300 whitespace-nowrap”>
Blue Ridge Turn
</div>
<!– Deadlock Counter –>
<div class=”text-xs text-slate-500 font-mono bg-slate-900 px-2 py-0.5 rounded border border-slate-800″ title=”Game ends at 27 moves without capture”>
Peaceful Moves: <span id=”deadlock-counter” class=”font-bold text-slate-300″>0</span>/27
</div>
</div>
<div id=”p1-score” class=”flex flex-col items-end pr-2″>
<span class=”text-xs uppercase tracking-wider text-emerald-500″>Blue Ridge</span>
<span class=”text-2xl font-bold text-white leading-none drop-shadow-lg” id=”score-p1″>0</span>
</div>
</div>
<!– Game Board Area –>
<div class=”flex-1 bg-slate-900 p-4 flex justify-center items-center overflow-hidden relative”>
<div id=”board” class=”board-container grid grid-cols-4 gap-0 bg-black border-4 border-black shadow-2xl select-none relative transition-opacity duration-200 ring-1 ring-slate-700″>
<!– Cells generated by JS –>
</div>
</div>
<!– Instructions / Status –>
<div class=”p-4 bg-slate-800 border-t border-slate-700 text-sm text-slate-400″>
<p id=”status-text” class=”font-medium text-center text-slate-300″>Select a piece in your region to move.</p>
<div class=”mt-2 flex gap-4 text-xs text-slate-500 justify-center”>
<span class=”flex items-center gap-1″><div class=”w-2 h-2 bg-slate-400 rounded-full”></div> Pawn (1)</span>
<span class=”flex items-center gap-1″><div class=”w-3 h-3 bg-slate-400 rounded-full”></div> Drone (2)</span>
<span class=”flex items-center gap-1″><div class=”w-4 h-4 bg-slate-400 rounded-full”></div> Queen (3)</span>
</div>
</div>
</div>
<!– Game Logic –>
<script>
// — Constants & Config —
const ROWS = 8;
const COLS = 4;
const MAX_MOVES_WITHOUT_CAPTURE = 27;
const PIECE_TYPES = {
PAWN: { id: ‘pawn’, name: ‘Pawn’, points: 1, size: ‘small’ },
DRONE: { id: ‘drone’, name: ‘Drone’, points: 2, size: ‘medium’ },
QUEEN: { id: ‘queen’, name: ‘Queen’, points: 3, size: ‘large’ }
};
const PLAYERS = {
TOP: 0, // Star City
BOTTOM: 1 // Blue Ridge
};
class MartianChess {
constructor() {
this.board = [];
this.turn = PLAYERS.BOTTOM;
this.scores = { [PLAYERS.TOP]: 0, [PLAYERS.BOTTOM]: 0 };
this.selectedCell = null;
this.validMoves = [];
this.gameActive = false;
this.movesWithoutCapture = 0;
this.lastMove = null;
this.playerTypes = {
[PLAYERS.TOP]: ‘ai’,
[PLAYERS.BOTTOM]: ‘human’
};
// Bind UI selectors
document.getElementById(‘p2-type’).addEventListener(‘change’, (e) => {
this.playerTypes[PLAYERS.TOP] = e.target.value;
if(this.gameActive && this.turn === PLAYERS.TOP && e.target.value === ‘ai’) {
this.playAITurn();
}
});
document.getElementById(‘p1-type’).addEventListener(‘change’, (e) => {
this.playerTypes[PLAYERS.BOTTOM] = e.target.value;
if(this.gameActive && this.turn === PLAYERS.BOTTOM && e.target.value === ‘ai’) {
this.playAITurn();
}
});
this.initBoard();
}
initBoard() {
this.board = Array(ROWS).fill(null).map(() => Array(COLS).fill(null));
this.gameActive = true;
this.movesWithoutCapture = 0;
this.lastMove = null;
// Setup Top Player (Star City)
this.placePiece(0, 0, PIECE_TYPES.QUEEN);
this.placePiece(0, 1, PIECE_TYPES.QUEEN);
this.placePiece(1, 0, PIECE_TYPES.QUEEN);
this.placePiece(0, 2, PIECE_TYPES.DRONE);
this.placePiece(1, 1, PIECE_TYPES.DRONE);
this.placePiece(2, 0, PIECE_TYPES.DRONE);
this.placePiece(1, 2, PIECE_TYPES.PAWN);
this.placePiece(2, 1, PIECE_TYPES.PAWN);
this.placePiece(2, 2, PIECE_TYPES.PAWN);
// Setup Bottom Player (Blue Ridge)
this.placePiece(7, 3, PIECE_TYPES.QUEEN);
this.placePiece(7, 2, PIECE_TYPES.QUEEN);
this.placePiece(6, 3, PIECE_TYPES.QUEEN);
this.placePiece(7, 1, PIECE_TYPES.DRONE);
this.placePiece(6, 2, PIECE_TYPES.DRONE);
this.placePiece(5, 3, PIECE_TYPES.DRONE);
this.placePiece(6, 1, PIECE_TYPES.PAWN);
this.placePiece(5, 2, PIECE_TYPES.PAWN);
this.placePiece(5, 1, PIECE_TYPES.PAWN);
this.turn = PLAYERS.BOTTOM;
this.scores = { 0: 0, 1: 0 };
this.selectedCell = null;
this.validMoves = [];
this.playerTypes[PLAYERS.TOP] = document.getElementById(‘p2-type’).value;
this.playerTypes[PLAYERS.BOTTOM] = document.getElementById(‘p1-type’).value;
this.updateUI();
this.render();
this.checkAI();
}
placePiece(r, c, type) {
this.board[r][c] = { …type };
}
getOwner(row) {
return row < 4 ? PLAYERS.TOP : PLAYERS.BOTTOM;
}
checkAI() {
if (!this.gameActive) return;
const currentPlayerType = this.playerTypes[this.turn];
const boardEl = document.getElementById(‘board’);
if (currentPlayerType === ‘ai’) {
boardEl.classList.add(‘ai-thinking’);
this.updateStatus(`${this.turn === PLAYERS.TOP ? “Star City” : “Blue Ridge”} AI is thinking…`);
setTimeout(() => {
this.playAITurn();
}, 800);
} else {
boardEl.classList.remove(‘ai-thinking’);
this.updateStatus(`${this.turn === PLAYERS.TOP ? “Star City’s” : “Blue Ridge’s”} Turn`);
}
}
// — AI Logic —
playAITurn() {
if (!this.gameActive) return;
const allMoves = this.getAllValidMoves(this.turn);
if (allMoves.length === 0) {
return;
}
let bestMove = null;
let bestScore = -Infinity;
allMoves.sort(() => Math.random() – 0.5);
for (const move of allMoves) {
const score = this.evaluateMove(move);
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
}
if (bestMove) {
this.executeMove(bestMove);
}
}
getAllValidMoves(player) {
const moves = [];
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
if (this.board[r][c] && this.getOwner(r) === player) {
const pieceMoves = this.calculateMoves(r, c);
pieceMoves.forEach(m => {
m.fromR = r;
m.fromC = c;
m.piece = this.board[r][c];
});
moves.push(…pieceMoves);
}
}
}
return moves;
}
evaluateMove(move) {
let score = 0;
if (move.isCapture) {
const targetPiece = this.board[move.r][move.c];
score += (targetPiece.points * 10);
}
const fromOwner = this.getOwner(move.fromR);
const toOwner = this.getOwner(move.r);
if (fromOwner !== toOwner) {
const pieceValue = move.piece.points;
score -= (pieceValue * 8);
} else {
score += 1;
}
let myPiecesCount = 0;
for(let r=0; r<ROWS; r++) {
for(let c=0; c<COLS; c++) {
if(this.board[r][c] && this.getOwner(r) === this.turn) {
myPiecesCount++;
}
}
}
const willEmptyZone = (fromOwner !== toOwner) && (myPiecesCount === 1);
const myCurrentScore = this.scores[this.turn];
const opp = this.turn === PLAYERS.TOP ? PLAYERS.BOTTOM : PLAYERS.TOP;
const oppScore = this.scores[opp];
if (willEmptyZone) {
let finalMyScore = myCurrentScore;
if (move.isCapture) {
const target = this.board[move.r][move.c];
finalMyScore += target.points;
}
if (finalMyScore > oppScore) {
score += 1000;
} else {
score -= 1000;
}
} else {
if (myCurrentScore > oppScore + 3 && fromOwner !== toOwner) {
score += 5;
}
}
if (this.movesWithoutCapture > 20 && myCurrentScore > oppScore) {
if (fromOwner !== toOwner) score += 50;
}
return score;
}
// — Core Logic —
isValidSelection(r, c) {
const piece = this.board[r][c];
if (!piece) return false;
return this.getOwner(r) === this.turn && this.playerTypes[this.turn] === ‘human’;
}
calculateMoves(r, c) {
const piece = this.board[r][c];
if (!piece) return [];
const moves = [];
if (piece.id === ‘pawn’) {
[[1,1], [1,-1], [-1,1], [-1,-1]].forEach(([dr, dc]) => {
this.addMoveIfValid(r, c, dr, dc, 1, moves);
});
} else if (piece.id === ‘drone’) {
[[0,1], [0,-1], [1,0], [-1,0]].forEach(([dr, dc]) => {
this.addMoveIfValid(r, c, dr, dc, 2, moves);
});
} else if (piece.id === ‘queen’) {
[[0,1], [0,-1], [1,0], [-1,0], [1,1], [1,-1], [-1,1], [-1,-1]].forEach(([dr, dc]) => {
this.addMoveIfValid(r, c, dr, dc, 8, moves);
});
}
return moves;
}
addMoveIfValid(startR, startC, dr, dc, maxSteps, moves) {
for (let i = 1; i <= maxSteps; i++) {
const nr = startR + (dr * i);
const nc = startC + (dc * i);
if (nr < 0 || nr >= ROWS || nc < 0 || nc >= COLS) break;
const targetPiece = this.board[nr][nc];
const currentOwner = this.getOwner(startR);
if (!targetPiece) {
moves.push({ r: nr, c: nc });
} else {
const targetZoneOwner = this.getOwner(nr);
if (targetZoneOwner !== currentOwner) {
moves.push({ r: nr, c: nc, isCapture: true });
}
break;
}
}
}
handleCellClick(r, c) {
if (!this.gameActive) return;
if (this.playerTypes[this.turn] === ‘ai’) return;
const move = this.validMoves.find(m => m.r === r && m.c === c);
if (move) {
this.executeMove(move);
return;
}
if (this.isValidSelection(r, c)) {
this.selectedCell = { r, c };
this.validMoves = this.calculateMoves(r, c);
this.render();
this.updateStatus(“Unit selected.”);
} else {
this.selectedCell = null;
this.validMoves = [];
this.render();
}
}
executeMove(move) {
const fromR = move.fromR !== undefined ? move.fromR : this.selectedCell.r;
const fromC = move.fromC !== undefined ? move.fromC : this.selectedCell.c;
const piece = this.board[fromR][fromC];
this.lastMove = {
from: { r: fromR, c: fromC },
to: { r: move.r, c: move.c }
};
if (move.isCapture) {
const targetPiece = this.board[move.r][move.c];
this.scores[this.turn] += targetPiece.points;
this.movesWithoutCapture = 0;
} else {
this.movesWithoutCapture++;
}
this.board[move.r][move.c] = piece;
this.board[fromR][fromC] = null;
this.selectedCell = null;
this.validMoves = [];
this.render();
this.updateUI();
if (this.checkWinCondition()) {
this.gameActive = false;
document.getElementById(‘board’).classList.remove(‘ai-thinking’);
return;
}
this.toggleTurn();
this.render();
this.updateUI();
this.checkAI();
}
toggleTurn() {
this.turn = this.turn === PLAYERS.BOTTOM ? PLAYERS.TOP : PLAYERS.BOTTOM;
}
checkWinCondition() {
if (this.movesWithoutCapture >= MAX_MOVES_WITHOUT_CAPTURE) {
const winner = this.scores[PLAYERS.BOTTOM] > this.scores[PLAYERS.TOP] ? “Blue Ridge” :
this.scores[PLAYERS.BOTTOM] < this.scores[PLAYERS.TOP] ? “Star City” : “Tie”;
const msg = `Stalemate (27 moves)! \n\nWinner: ${winner}\n\nBlue Ridge: ${this.scores[PLAYERS.BOTTOM]}\nStar City: ${this.scores[PLAYERS.TOP]}`;
setTimeout(() => alert(msg), 100);
this.updateStatus(“Game Over: Stalemate Limit Reached”);
return true;
}
let topPieces = 0;
let bottomPieces = 0;
for(let r=0; r<ROWS; r++) {
for(let c=0; c<COLS; c++) {
if (this.board[r][c]) {
if (r < 4) topPieces++;
else bottomPieces++;
}
}
}
if (topPieces === 0 || bottomPieces === 0) {
const winner = this.scores[PLAYERS.BOTTOM] > this.scores[PLAYERS.TOP] ? “Blue Ridge” :
this.scores[PLAYERS.BOTTOM] < this.scores[PLAYERS.TOP] ? “Star City” : “Tie”;
const msg = `Game Over! ${winner} Wins!\n\nFinal Score:\nBlue Ridge: ${this.scores[PLAYERS.BOTTOM]}\nStar City: ${this.scores[PLAYERS.TOP]}`;
setTimeout(() => alert(msg), 100);
this.updateStatus(msg);
return true;
}
return false;
}
resetGame() {
this.initBoard();
}
updateUI() {
document.getElementById(‘score-p1’).textContent = this.scores[PLAYERS.BOTTOM];
document.getElementById(‘score-p2’).textContent = this.scores[PLAYERS.TOP];
const counterEl = document.getElementById(‘deadlock-counter’);
counterEl.textContent = this.movesWithoutCapture;
if (this.movesWithoutCapture > 20) {
counterEl.className = “font-bold text-red-500 animate-pulse”;
} else {
counterEl.className = “font-bold text-slate-300”;
}
const turnInd = document.getElementById(‘turn-indicator’);
const pType = this.playerTypes[this.turn] === ‘ai’ ? ‘(AI)’ : ”;
if (this.turn === PLAYERS.BOTTOM) {
turnInd.textContent = `Blue Ridge Turn ${pType}`;
turnInd.className = `px-4 py-1 rounded-full shadow-inner font-bold whitespace-nowrap bg-emerald-950 text-emerald-400 border border-emerald-900`;
} else {
turnInd.textContent = `Star City Turn ${pType}`;
turnInd.className = `px-4 py-1 rounded-full shadow-inner font-bold whitespace-nowrap bg-red-950 text-red-400 border border-red-900`;
}
}
updateStatus(msg) {
document.getElementById(‘status-text’).textContent = msg;
}
render() {
const boardEl = document.getElementById(‘board’);
boardEl.innerHTML = ”;
for(let r=0; r<ROWS; r++) {
for(let c=0; c<COLS; c++) {
const cell = document.createElement(‘div’);
let classes = “w-16 h-16 flex items-center justify-center relative transition-all duration-200 “;
// Theme Coloring
const isDark = (r + c) % 2 === 1;
let bgColor = “”;
if (r < 4) {
// Star City Zone (Top) – Dark Night Sky / Mill Mountain
bgColor = isDark ? “bg-slate-900” : “bg-slate-800”;
} else {
// Blue Ridge Zone (Bottom) – Misty Valley
bgColor = isDark ? “bg-slate-700” : “bg-slate-600”;
}
// Last Move Highlight (Sunrise Yellow)
const isLastMoveFrom = this.lastMove && this.lastMove.from.r === r && this.lastMove.from.c === c;
const isLastMoveTo = this.lastMove && this.lastMove.to.r === r && this.lastMove.to.c === c;
if (isLastMoveFrom || isLastMoveTo) {
bgColor = “bg-yellow-500/20”;
}
classes += bgColor;
// Canal (The Railroad Tracks line?)
if (r === 3) classes += ” border-b-4 border-black”;
const isSelected = this.selectedCell && this.selectedCell.r === r && this.selectedCell.c === c;
const isValidMove = this.validMoves.find(m => m.r === r && m.c === c);
const isCapture = isValidMove && isValidMove.isCapture;
if (isSelected) classes += ” ring-4 ring-yellow-400 z-10 shadow-lg”;
if (isValidMove) {
if (isCapture) classes += ” ring-4 ring-red-500 bg-red-900/40 cursor-pointer”;
else classes += ” ring-4 ring-emerald-400/50 bg-emerald-900/20 cursor-pointer”;
} else if (this.board[r][c] && this.isValidSelection(r, c)) {
classes += ” cursor-pointer hover:brightness-110″;
}
cell.className = classes;
cell.onclick = () => this.handleCellClick(r, c);
const piece = this.board[r][c];
if (piece) {
cell.innerHTML = this.getPieceSVG(piece, r);
} else if (isValidMove && !isCapture) {
cell.innerHTML = `<div class=”w-2 h-2 bg-emerald-400 rounded-full opacity-50″></div>`;
}
boardEl.appendChild(cell);
}
}
}
getPieceSVG(piece, row) {
const owner = this.getOwner(row);
// — VISUALS —
// Star City (Top): The Roanoke Star. Neon Red body, White Pips.
// Blue Ridge (Bottom): The Mountains. Forest Green body, White/Sky Blue Pips.
let fill = “”;
let stroke = “”;
let pipColor = “”;
let scale = 0.9;
if (piece.size === ‘small’) scale = 0.55;
if (piece.size === ‘medium’) scale = 0.75;
const transform = `transform: scale(${scale}); transform-origin: center;`;
// — SHAPES —
if (owner === PLAYERS.TOP) {
// THE STAR
fill = “fill-red-500”;
stroke = “stroke-white”;
pipColor = “fill-white”;
// 5-Point Star Path
const starPath = “M 50 10 L 61 35 L 88 35 L 66 50 L 75 75 L 50 60 L 25 75 L 34 50 L 12 35 L 39 35 Z”;
// Star Pips (Center cluster)
let pips = “”;
if (piece.id === ‘pawn’) pips = `<circle cx=”50″ cy=”50″ r=”6″ class=”${pipColor}” />`;
else if (piece.id === ‘drone’) pips = `<circle cx=”42″ cy=”50″ r=”5″ class=”${pipColor}” /><circle cx=”58″ cy=”50″ r=”5″ class=”${pipColor}” />`;
else if (piece.id === ‘queen’) pips = `<circle cx=”50″ cy=”40″ r=”5″ class=”${pipColor}” /><circle cx=”40″ cy=”55″ r=”5″ class=”${pipColor}” /><circle cx=”60″ cy=”55″ r=”5″ class=”${pipColor}” />`;
return `
<svg viewBox=”0 0 100 100″ class=”w-full h-full animate-slide-in drop-shadow-lg” style=”${transform}”>
<path d=”${starPath}” class=”${fill} ${stroke}” stroke-width=”4″ stroke-linejoin=”round”/>
${pips}
</svg>
`;
} else {
// THE MOUNTAIN
fill = “fill-emerald-700”;
stroke = “stroke-emerald-300”;
pipColor = “fill-white”;
// Mountain/Triangle shape with snowcap hint style
const mountainPath = “M 50 10 L 90 90 L 10 90 Z”;
// Snowcap detail (optional visual flair)
const snowCap = “M 50 10 L 65 40 L 50 30 L 35 40 Z”;
// Mountain Pips (Lower center)
let pips = “”;
if (piece.id === ‘pawn’) pips = `<circle cx=”50″ cy=”70″ r=”7″ class=”${pipColor}” />`;
else if (piece.id === ‘drone’) pips = `<circle cx=”40″ cy=”70″ r=”6″ class=”${pipColor}” /><circle cx=”60″ cy=”70″ r=”6″ class=”${pipColor}” />`;
else if (piece.id === ‘queen’) pips = `<circle cx=”50″ cy=”55″ r=”6″ class=”${pipColor}” /><circle cx=”35″ cy=”75″ r=”6″ class=”${pipColor}” /><circle cx=”65″ cy=”75″ r=”6″ class=”${pipColor}” />`;
return `
<svg viewBox=”0 0 100 100″ class=”w-full h-full animate-slide-in drop-shadow-lg” style=”${transform}”>
<path d=”${mountainPath}” class=”${fill} ${stroke}” stroke-width=”4″ stroke-linejoin=”round”/>
<path d=”${snowCap}” class=”fill-emerald-100 opacity-30″ />
${pips}
</svg>
`;
}
}
}
const game = new MartianChess();
</script>
</body>
</html>
