Build a Competitive Chess Bot with AI
Create a 1600-1800 ELO chess bot in minutes using AI — Complete specifications with proven patterns, performance optimizations, and ready-to-use architecture.
✨ Generate Your Bot with AI →🎯 Platform Overview
Your bot runs in a Docker container with:
- 2 GB memory limit
- Dedicated CPU core (Intel i5-13500)
- 30 second hard timeout (worker killed if exceeded)
- Console.log suppressed (use reportMove timing for debugging)
⚡ Performance with AppleJuice (Rust/NAPI Engine)
| Operation | Avg Time | Calls/Move | Note |
|---|---|---|---|
move() |
~13μs | 5-10 | Extremely fast |
undo() |
~4μs | 5-10 | Extremely fast |
moves() |
~21μs | 10-30 | Get legal moves |
clone() |
~42μs | 1-2 | Create board copy |
get() |
~0.8μs | 100-200 | Get piece at square |
fen() |
~5μs | 1-5 | Position string |
Total chess engine overhead per bot turn: < 0.5ms (insignificant!)
⚠️ IMPORTANT: With AppleJuice, the chess engine is no longer the bottleneck - focus on efficient search algorithms, not avoiding chess operations.
📋 Function Signature
function makeMove(board, timeRemaining, reportMove, getMemory) {
// board: Read-only board interface
// timeRemaining: Milliseconds left (starts at 25000ms)
// reportMove: Callback to report your move (call multiple times!)
// getMemory: Returns {heapUsed, heapTotal, rss} in MB
}
✅ Board API Reference
Read-Only Methods (Main Board)
board.moves() // [{from:'e2', to:'e4', san:'e4', ...}] - Returns move objects
board.get('e4') // {type: 'p', color: 'w'} or null
board.turn() // 'w' or 'b'
board.fen() // Full FEN string
board.pgn() // Full game in PGN format
board.in_check() // Boolean
board.in_checkmate() // Boolean
board.in_stalemate() // Boolean
board.in_draw() // Boolean (50-move, insufficient material)
board.in_threefold_repetition() // Boolean - works but rarely needed
board.game_over() // Boolean
board.clone() // Returns mutable copy with full move history
Mutable Methods (Clone Only)
const clone = board.clone();
const moves = clone.moves(); // Get move objects
clone.move(moves[0]); // ✅ Works on clone - pass move object
clone.undo(); // ✅ Works on clone
clone.in_threefold_repetition();// ✅ Works (clone has history)
board.move(moves[0]); // ❌ ERROR: main board is read-only!
🚨 CRITICAL PERFORMANCE RULES
❌ NEVER Do This:
// ❌ BAD: Creating multiple clones in loops (300+ clones!)
for (let depth = 1; depth <= 10; depth++) {
for (const move of moves) {
const clone = board.clone(); // Creates 300+ clones! Wastes memory and time!
clone.move(move);
}
}
// ❌ BAD: Quiescence without depth limit (stack overflow!)
function quiesce(clone, alpha, beta) {
for (const move of tactical) {
const score = -quiesce(clone, -beta, -alpha); // Infinite recursion!
}
}
// ❌ BAD: Not reporting move immediately (timeout = forfeit!)
function makeMove(board, timeRemaining, reportMove) {
// Search for 20 seconds...
// If timeout before reportMove: YOU LOSE!
reportMove(bestMove);
}
✅ CORRECT Patterns:
// ✅ GOOD: Create ONE clone at start, reuse with move/undo
function makeMove(board, timeRemaining, reportMove) {
const searchBoard = board.clone(); // ONE clone for entire search
const moves = board.moves(); // Get move objects once
for (let depth = 1; depth <= 10; depth++) {
for (const move of moves) {
searchBoard.move(move); // Reuse same clone, pass move object
const score = search(searchBoard, depth);
searchBoard.undo(); // Reset for next move
}
}
}
// ✅ GOOD: Filter captures directly from move objects
const moves = board.moves(); // Returns move objects
const captures = moves.filter(m => m.captured); // Filter for captures
const checks = moves.filter(m => m.san.includes('+')); // Filter for checks
// ✅ GOOD: Depth-limited quiescence (prevents stack overflow)
function quiesce(clone, alpha, beta, depth = 0) {
if (depth >= 4) return evaluate(clone); // Hard stop at 4 ply
// ... rest of quiescence
}
// ✅ GOOD: Report move IMMEDIATELY for safety
function makeMove(board, timeRemaining, reportMove) {
const moves = board.moves();
reportMove(moves[0]); // Safety move FIRST - report move object
// Now search for better moves and call reportMove() again when found
}
🏗️ Complete Bot Architecture
1. Piece Values
const PV = {
p: 100, // Pawn
n: 320, // Knight
b: 330, // Bishop
r: 500, // Rook
q: 900, // Queen
k: 20000 // King (high value to prevent trades)
};
2. Piece-Square Tables
Positional bonuses for each square (white perspective, flip for black):
const PST = {
// Pawns: Encourage center control and advancement
p: [
0, 0, 0, 0, 0, 0, 0, 0,
50, 50, 50, 50, 50, 50, 50, 50,
10, 10, 20, 30, 30, 20, 10, 10,
5, 5, 10, 25, 25, 10, 5, 5,
0, 0, 0, 20, 20, 0, 0, 0,
5, -5,-10, 0, 0,-10, -5, 5,
5, 10, 10,-20,-20, 10, 10, 5,
0, 0, 0, 0, 0, 0, 0, 0
],
// Knights: Prefer center, avoid edges
n: [
-50,-40,-30,-30,-30,-30,-40,-50,
-40,-20, 0, 0, 0, 0,-20,-40,
-30, 0, 10, 15, 15, 10, 0,-30,
-30, 5, 15, 20, 20, 15, 5,-30,
-30, 0, 15, 20, 20, 15, 0,-30,
-30, 5, 10, 15, 15, 10, 5,-30,
-40,-20, 0, 5, 5, 0,-20,-40,
-50,-40,-30,-30,-30,-30,-40,-50
],
// Bishops: Prefer center and long diagonals
b: [
-20,-10,-10,-10,-10,-10,-10,-20,
-10, 0, 0, 0, 0, 0, 0,-10,
-10, 0, 5, 10, 10, 5, 0,-10,
-10, 5, 5, 10, 10, 5, 5,-10,
-10, 0, 10, 10, 10, 10, 0,-10,
-10, 10, 10, 10, 10, 10, 10,-10,
-10, 5, 0, 0, 0, 0, 5,-10,
-20,-10,-10,-10,-10,-10,-10,-20
],
// Rooks: Prefer 7th rank and open files
r: [
0, 0, 0, 0, 0, 0, 0, 0,
5, 10, 10, 10, 10, 10, 10, 5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
0, 0, 0, 5, 5, 0, 0, 0
],
// Queen: Slight center preference
q: [
-20,-10,-10, -5, -5,-10,-10,-20,
-10, 0, 0, 0, 0, 0, 0,-10,
-10, 0, 5, 5, 5, 5, 0,-10,
-5, 0, 5, 5, 5, 5, 0, -5,
0, 0, 5, 5, 5, 5, 0, -5,
-10, 5, 5, 5, 5, 5, 0,-10,
-10, 0, 5, 0, 0, 0, 0,-10,
-20,-10,-10, -5, -5,-10,-10,-20
],
// King: Stay safe in opening/middlegame
k: [
-30,-40,-40,-50,-50,-40,-40,-30,
-30,-40,-40,-50,-50,-40,-40,-30,
-30,-40,-40,-50,-50,-40,-40,-30,
-30,-40,-40,-50,-50,-40,-40,-30,
-20,-30,-30,-40,-40,-30,-30,-20,
-10,-20,-20,-20,-20,-20,-20,-10,
20, 20, 0, 0, 0, 0, 20, 20,
20, 30, 10, 0, 0, 10, 30, 20
]
};
3. Evaluation Function
function evaluate(clone) {
let score = 0;
const turn = clone.turn();
// Material + Piece-Square Tables
for (let sq = 0; sq < 64; sq++) {
const file = sq % 8;
const rank = 8 - Math.floor(sq / 8);
const square = String.fromCharCode(97 + file) + rank;
const piece = clone.get(square);
if (!piece) continue;
// Material value
const materialValue = PV[piece.type];
// Positional value (flip for black)
const pstIndex = piece.color === 'w' ? sq : (63 - sq);
const positionalValue = PST[piece.type] ? PST[piece.type][pstIndex] : 0;
const totalValue = materialValue + positionalValue;
// Add if our piece, subtract if opponent's
if (piece.color === turn) {
score += totalValue;
} else {
score -= totalValue;
}
}
return score;
}
4. Move Ordering (MVV-LVA)
// Most Valuable Victim - Least Valuable Attacker
function scoreMove(move) {
let score = 0;
// Captures: high-value victim - low-value attacker
if (move.captured) {
score = 10000 + (10 * PV[move.captured] - PV[move.piece]);
}
// Promotions
if (move.promotion) {
score += 9000 + PV[move.promotion];
}
// Checks and checkmates
if (move.san.includes('#')) score += 8000;
else if (move.san.includes('+')) score += 4000;
return score;
}
5. Quiescence Search (Horizon Effect Prevention)
function quiesce(clone, alpha, beta, depth = 0) {
// CRITICAL: Depth limit to prevent infinite recursion
if (depth >= 4) return evaluate(clone);
// Time check
if (Date.now() >= stopTime) throw 'timeout';
// Stand-pat evaluation
const standPat = evaluate(clone);
if (standPat >= beta) return beta;
if (alpha < standPat) alpha = standPat;
// Only search captures and checks
const moves = clone.moves(); // Returns move objects
const tactical = moves.filter(m =>
m.captured || m.san.includes('+') || m.san.includes('#')
);
// Sort by MVV-LVA
tactical.sort((a, b) => scoreMove(b) - scoreMove(a));
for (const move of tactical) {
clone.move(move);
const score = -quiesce(clone, -beta, -alpha, depth + 1);
clone.undo();
if (score >= beta) return beta;
if (score > alpha) alpha = score;
}
return alpha;
}
6. Alpha-Beta Negamax Search
let nodes = 0;
let stopTime = 0;
function negamax(clone, depth, alpha, beta) {
// Time check every 1024 nodes
nodes++;
if ((nodes & 1023) === 0 && Date.now() >= stopTime) {
throw 'timeout';
}
// Leaf node: quiescence search
if (depth === 0) {
return quiesce(clone, alpha, beta);
}
// Generate moves
const moves = clone.moves(); // Returns move objects
// Terminal nodes
if (moves.length === 0) {
if (clone.in_checkmate()) {
return -100000 - depth; // Prefer faster mates
}
return 0; // Stalemate
}
// Check extension: search deeper when in check
if (clone.in_check() && depth < 10) {
depth++;
}
// Order moves for better pruning
moves.sort((a, b) => scoreMove(b) - scoreMove(a));
let bestScore = -Infinity;
for (const move of moves) {
clone.move(move);
const score = -negamax(clone, depth - 1, -beta, -alpha);
clone.undo();
bestScore = Math.max(bestScore, score);
alpha = Math.max(alpha, score);
// Beta cutoff (prune remaining moves)
if (alpha >= beta) break;
}
return bestScore;
}
7. Main Function with Iterative Deepening
function makeMove(board, timeRemaining, reportMove, getMemory) {
// Time budget: use most of remaining time, leave 2s buffer
const timeToUse = Math.min(timeRemaining, 20000);
stopTime = Date.now() + timeToUse - 2000;
// Get legal moves
const moves = board.moves(); // Returns move objects
// Edge cases
if (moves.length === 0) return;
if (moves.length === 1) return reportMove(moves[0]);
// CRITICAL: Report immediate safety move
reportMove(moves[0]);
// CRITICAL: Create ONE clone for all search operations
const searchBoard = board.clone();
// Reset counters
nodes = 0;
let bestMove = moves[0];
let bestScore = -Infinity;
try {
// Iterative deepening
for (let depth = 1; depth <= 12; depth++) {
// Reserve 1s for next depth
if (Date.now() + 1000 >= stopTime) break;
let depthBestMove = moves[0];
let depthBestScore = -Infinity;
// Search all moves at current depth
for (const move of moves) {
searchBoard.move(move);
const score = -negamax(searchBoard, depth - 1, -Infinity, Infinity);
searchBoard.undo();
if (score > depthBestScore) {
depthBestScore = score;
depthBestMove = move;
}
}
// Update best move if we completed this depth
if (depthBestScore > bestScore) {
bestScore = depthBestScore;
bestMove = depthBestMove;
reportMove(bestMove);
}
// Sort moves by score for next iteration (best-first)
moves.sort((a, b) => {
if (a.san === bestMove.san) return -1;
if (b.san === bestMove.san) return 1;
return 0;
});
}
} catch (e) {
if (e === 'timeout') {
// Timeout is expected - last reportMove() will be used
throw e; // Re-throw to let bot-worker.js handle timing
}
// Other errors should also be thrown
throw e;
}
}
🚀 Advanced Optimizations (Optional)
Transposition Table
Cache previously evaluated positions to avoid re-computing:
const TT = new Map();
const TT_MAX_SIZE = 100000;
function ttStore(fen, depth, score, flag) {
if (TT.size >= TT_MAX_SIZE) {
// Simple eviction: clear oldest 10%
const keysToDelete = Array.from(TT.keys()).slice(0, TT_MAX_SIZE * 0.1);
keysToDelete.forEach(k => TT.delete(k));
}
TT.set(fen, {depth, score, flag});
}
function ttLookup(fen, depth, alpha, beta) {
const entry = TT.get(fen);
if (!entry || entry.depth < depth) return null;
if (entry.flag === 'EXACT') return entry.score;
if (entry.flag === 'ALPHA' && entry.score <= alpha) return alpha;
if (entry.flag === 'BETA' && entry.score >= beta) return beta;
return null;
}
// In negamax, before searching:
const fen = clone.fen();
const ttScore = ttLookup(fen, depth, alpha, beta);
if (ttScore !== null) return ttScore;
// After search:
let flag = 'EXACT';
if (bestScore <= alphaOrig) flag = 'ALPHA';
else if (bestScore >= beta) flag = 'BETA';
ttStore(fen, depth, bestScore, flag);
Killer Moves Heuristic
Remember non-capture moves that caused beta cutoffs:
const killers = Array(20).fill(null).map(() => [null, null]);
function updateKiller(move, ply) {
if (move.captured) return; // Only non-captures
if (killers[ply][0]?.san === move.san) return; // Already stored
killers[ply][1] = killers[ply][0]; // Shift old killer
killers[ply][0] = move; // Store new killer
}
function isKiller(move, ply) {
return killers[ply].some(k => k && k.san === move.san);
}
// In move ordering:
if (isKiller(move, ply)) score += 5000;
// After beta cutoff:
if (alpha >= beta && !move.captured) {
updateKiller(move, ply);
}
🧪 Testing Strategy
Local Performance Test
// Test with time multiplier to simulate Fly.io
const TIME_MULTIPLIER = 10;
const simulatedTimeRemaining = 25000 / TIME_MULTIPLIER; // 2.5s local = 25s on Fly.io
makeMove(board, simulatedTimeRemaining, reportMove, getMemory);
Expected Performance Benchmarks
| Metric | Target | Warning | Critical |
|---|---|---|---|
| Average move time | < 15s | 15-20s | > 20s |
| Depth reached | 6-8 | 4-5 | < 4 |
| Nodes per second | 5000+ | 2000-5000 | < 2000 |
| Clone count/move | 1-2 | 3-10 | > 10 |
🐛 Common Pitfalls
1. Multiple Clones in Loops
// ❌ Creates 120+ clones - wastes memory and time!
for (let depth = 1; depth <= 10; depth++) {
for (const move of moves) {
const clone = board.clone();
// ...
}
}
// ✅ One clone, reuse with move/undo
const searchBoard = board.clone();
for (let depth = 1; depth <= 10; depth++) {
for (const move of moves) {
searchBoard.move(move);
// search...
searchBoard.undo();
}
}
2. Unbounded Quiescence
// ❌ Can cause stack overflow
function quiesce(clone, alpha, beta) {
for (const move of tactical) {
const score = -quiesce(clone, -beta, -alpha); // No limit!
}
}
// ✅ Depth-limited quiescence (4 ply max)
function quiesce(clone, alpha, beta, depth = 0) {
if (depth >= 4) return evaluate(clone);
// ...
}
3. Not Reporting Immediate Move
// ❌ Timeout before first reportMove = forfeit!
function makeMove(board, timeRemaining, reportMove) {
for (let depth = 1; depth <= 10; depth++) {
// If timeout happens here...
reportMove(bestMove); // ...you never reported and LOSE!
}
}
// ✅ Always report immediately for safety
function makeMove(board, timeRemaining, reportMove) {
reportMove(board.moves()[0]); // Safety move FIRST
// Now improve with search...
}
🎯 Generate Your Bot with AI
Copy the prompt below and paste it into Claude or ChatGPT-4 to generate a complete, production-ready chess bot!
📋 Complete AI Generation Prompt
Create a competitive chess bot (target 1600-1800 ELO) for ChessArena.dev using this exact architecture:
IMPORTANT: The 'board' object is ALREADY PROVIDED - do NOT import chess.js!
The board is a chess.js instance passed to your makeMove(board) function.
CODE FORMAT REQUIREMENTS:
❌ NO import statements: import { Chess } from 'chess.js';
❌ NO export statements: export function makeMove(board) { ... }
✅ CORRECT: Just define the function directly:
function makeMove(board) { ... }
The platform executes your code in strict mode without ES6 modules.
PLATFORM CONSTRAINTS:
- Dedicated CPU core (Intel i5-13500)
- 30 second hard timeout
- 2 GB memory limit
- Uses AppleJuice (Rust/NAPI) chess engine - extremely fast!
- Must call reportMove() immediately, then improve with search
═══════════════════════════════════════════════════════════════
🚨 CRITICAL: THE SINGLE-CLONE PATTERN (Most AIs get this WRONG!)
═══════════════════════════════════════════════════════════════
❌ WRONG - Clone created but not used in recursive functions:
function makeMove(board) {
const searchBoard = board.clone(); // ✅ Clone created
// ...
negamax(searchBoard, ...); // ⚠️ Passes searchBoard...
}
function negamax(board, depth, ...) { // ❌ But parameter named 'board'!
board.move(move); // ❌ Uses wrong parameter name
board.undo();
}
✅ CORRECT - Clone used everywhere:
function makeMove(board) {
const searchBoard = board.clone(); // ✅ Clone created
// ...
negamax(searchBoard, ...); // ✅ Passes searchBoard
}
function negamax(searchBoard, depth, ...) { // ✅ Parameter named 'searchBoard'
searchBoard.move(move); // ✅ Uses searchBoard
searchBoard.undo();
}
✅ CORRECT - moves() returns objects, filter directly:
const moves = board.moves(); // Returns move objects [{from:'e2', to:'e4', san:'e4', ...}]
for (let move of moves) {
searchBoard.move(move); // Pass move object
}
// Filter captures directly from move objects:
const captures = moves.filter(m => m.captured);
❌ WRONG - Recreating moves at each depth:
for (let depth = 1; depth <= 8; depth++) {
const moves = board.moves(); // ❌ Regenerated every depth!
for (let move of moves) { /* search */ }
}
✅ CORRECT - Generate once, reorder:
const moves = board.moves(); // ✅ Generate once
for (let depth = 1; depth <= 8; depth++) {
// Reorder existing array (fast)
moves.sort((a, b) => a === bestMove ? -1 : 1);
for (let move of moves) { /* search */ }
}
MANDATORY RULES:
1. Create ONE clone: const searchBoard = board.clone();
2. Name the parameter 'searchBoard' in negamax() and quiesce()
3. Use searchBoard.move() and searchBoard.undo() everywhere
4. board.moves() always returns move objects - filter directly for captures
5. Generate move list once, reorder after each depth
6. Call reportMove() IMMEDIATELY before search
7. Sort initial moves intelligently (central squares first) to avoid playing a3!
═══════════════════════════════════════════════════════════════
🚨 CRITICAL: FUNCTION SIGNATURE (Most AIs get this WRONG!)
═══════════════════════════════════════════════════════════════
❌ WRONG: function makeMove(board) { ... }
✅ CORRECT: function makeMove(board, timeRemaining, reportMove, getMemory) { ... }
PARAMETERS EXPLAINED:
- board: chess.js instance with current position
- timeRemaining: NUMBER in milliseconds (e.g., 30000 for 30 seconds)
- reportMove: FUNCTION to call with your move (e.g., reportMove('e4'))
- getMemory: FUNCTION that returns available memory in MB
TIME MANAGEMENT - CRITICAL:
❌ WRONG: if (board.timeRemaining() < 2000) // board.timeRemaining does NOT exist!
✅ CORRECT:
const startTime = Date.now();
if (Date.now() - startTime > timeRemaining - 2000) throw 'timeout';
AVAILABLE CHESS.JS METHODS - CRITICAL:
The board object uses chess.js. Only use these GUARANTEED available methods:
✅ SAFE TO USE:
board.moves() // Returns array of move objects
board.move(move) // Make a move (clone only!)
board.undo() // Undo last move (clone only!)
board.clone() // Create mutable copy
board.get(square) // Get piece at square
board.turn() // Current player ('w' or 'b')
board.fen() // Get FEN string
board.pgn() // Get PGN string
board.in_check() // Is current player in check?
board.in_checkmate() // Is it checkmate?
board.in_stalemate() // Is it stalemate?
board.in_draw() // Is it a draw?
board.game_over() // Is the game over?
board.in_threefold_repetition() // ⚠️ SLOW! Use sparingly
❌ DO NOT ASSUME OTHER METHODS EXIST:
board.is_insufficient_material() // ❌ May not exist!
board.is_fifty_moves() // ❌ May not exist!
board.history() // ❌ May not exist!
board.ascii() // ❌ May not exist!
IMPORTANT: Stick to the guaranteed methods listed above. Do not use snake_case
variants or assume additional chess.js v1 methods are available.
REPORTMOVE USAGE:
❌ WRONG: try { if (typeof reportMove === 'function') reportMove(move); } catch(_) {}
✅ CORRECT: reportMove(move);
Just call it directly - it's guaranteed to exist!
INITIAL MOVE ORDERING - CRITICAL:
❌ WRONG: let best = moves[0]; // Alphabetical order → may play Na3 or a3!
✅ CORRECT: Sort moves before selecting first:
moves.sort((a, b) => {
let scoreA = 0, scoreB = 0;
// Central squares bonus
if (/e[45]|d[45]/.test(a)) scoreA += 300;
if (/e[45]|d[45]/.test(b)) scoreB += 300;
// Knight development bonus
if (/^N[fc]/.test(a)) scoreA += 200;
if (/^N[fc]/.test(b)) scoreB += 200;
return scoreB - scoreA;
});
let best = moves[0]; // Now e4/d4/Nf3, not a3!
EXACT FUNCTION SIGNATURES TO USE:
function negamax(searchBoard, depth, alpha, beta, startTime, timeLimit, nodesObj) { ... }
function quiesce(searchBoard, alpha, beta, depth, startTime, timeLimit, nodesObj) { ... }
function makeMove(board, timeRemaining, reportMove, getMemory) {
const startTime = Date.now();
const timeLimit = timeRemaining - 2000; // 2s buffer
const searchBoard = board.clone();
let moves = searchBoard.moves();
// CRITICAL: Sort before selecting first move!
moves.sort((a, b) => {
let scoreA = 0, scoreB = 0;
if (/e[45]|d[45]/.test(a)) scoreA += 300;
if (/e[45]|d[45]/.test(b)) scoreB += 300;
if (/^N[fc]/.test(a)) scoreA += 200;
if (/^N[fc]/.test(b)) scoreB += 200;
return scoreB - scoreA;
});
reportMove(moves[0]); // Report immediately
// ... then improve with search
}
REQUIRED ARCHITECTURE:
- Alpha-beta negamax with iterative deepening (target depth 6-8)
- Quiescence search with 4-ply depth limit
- MVV-LVA move ordering for captures
- Piece-square table evaluation
- Time management: 18s search budget + 2s buffer
USE THESE EXACT VALUES:
Piece Values:
const PV = {p: 100, n: 320, b: 330, r: 500, q: 900, k: 20000};
Piece-Square Tables:
[Copy the PST object from section 2 above - all 6 tables]
Evaluation Function:
[Copy the evaluate() function from section 3 above]
Quiescence Search:
[Copy the quiesce() function from section 5 above]
Negamax Search:
[Copy the negamax() function from section 6 above]
Main Function:
[Copy the makeMove() function from section 7 above]
IMPORTANT IMPLEMENTATION NOTES:
- timeRemaining is a NUMBER parameter (milliseconds), NOT a method
- Use Date.now() - startTime to track elapsed time
- Throw 'timeout' when time expires (let platform handle it)
- Check time every 1024 nodes: if ((nodes & 1023) === 0 && Date.now() - startTime > timeRemaining - 2000)
- Leave 2 second buffer: stop when elapsed > timeRemaining - 2000ms
- Check extension: add 1 depth when in check
- Prefer faster mates: return -100000 - depth for checkmate
- Sort moves by best-first after each completed depth
- Sort initial root moves intelligently (central > development > rest)
Generate complete, production-ready code with:
- All helper functions integrated
- Proper time management with Date.now() - startTime
- Move ordering with MVV-LVA
- Initial move sorting (central squares first)
- Edge case handling (0 moves, 1 move, etc.)
- NO console.log statements (suppressed on platform)
- NO import or export statements (plain JavaScript only)
- Correct signature: function makeMove(board, timeRemaining, reportMove, getMemory)
- Comments explaining key decisions
CODE MUST BE PLAIN JAVASCRIPT (no ES6 modules):
❌ WRONG: import { Chess } from 'chess.js'; export function makeMove...
❌ WRONG: function makeMove(board) { ... }
✅ CORRECT: function makeMove(board, timeRemaining, reportMove, getMemory) { ... }
CRITICAL CHECKLIST BEFORE SUBMITTING CODE:
✅ Signature has 4 parameters: board, timeRemaining, reportMove, getMemory
✅ Time management uses Date.now() - startTime, NOT board.timeRemaining()
✅ reportMove() called directly without try/catch
✅ Initial moves sorted (e4/d4/Nf3 first, NOT a3/Na3)
✅ Single clone pattern with searchBoard
✅ No import/export statements
The code should be ready to paste directly into ChessArena.dev registration!
🔗 How to Use:
- Copy the entire prompt above (click the copy button in the code block)
- Open Claude (https://claude.ai) or ChatGPT (https://chat.openai.com)
- Paste the prompt and press Enter
- Review the generated code (check for the critical patterns!)
- Test locally if possible (with 10x time multiplier)
- Register at https://chessarena.dev/register.html
💡 Tips for Success
Performance Optimization Checklist
- Single clone pattern: One
board.clone()at start of makeMove - No threefold checks: Removed all
in_threefold_repetition()calls - Immediate reportMove: Called before search begins
- Depth-limited quiescence: Hard stop at 4 ply
- Time management: 18s search + 2s buffer
- Move ordering: MVV-LVA for captures
- Time checking: Every 1024 nodes with
(nodes & 1023) === 0
Common AI Generation Issues
If the generated code has problems:
- Timeout Issues: Check for multiple clones or threefold checks
- Low Depth: Verify single clone pattern is used
- Stack Overflow: Ensure quiescence has depth limit
- Forfeit on Timeout: Verify immediate reportMove() call
- Slow Performance: Check for regenerating moves at each depth
📊 Success Metrics
Your bot should achieve:
- ELO: 1600-1800 after 20 games
- Search Depth: 6-8 ply per move
- Move Time: 10-15 seconds average
- Timeout Rate: < 1%
Ready to dominate the leaderboard? Use the AI generation prompt above and join the arena! 🏆
Need help? Check the Documentation or view the Leaderboard for inspiration.
Stuck? Make sure you followed the Critical Performance Rules — these are the #1 reason for timeouts!