Este repositorio contiene una implementación impresionante de generadores de movimientos de ajedrez en JavaScript, con dos enfoques principales: x88 y bitboards. La característica más destacada es que genera movimientos estrictamente legales directamente, detectando jaques, jaques mate y ahogados sin necesidad de validación posterior (make-unmake).
movegen/
├── css/ # Estilos CSS
├── img/ # Recursos gráficos
├── js/
│ ├── x88.js # ⭐ Generador x88 (68KB, 1842 líneas)
│ ├── bitboard.js # ⭐ Generador bitboard (1.4MB)
│ ├── engine.js # Motor UCI (437 líneas)
│ ├── chess.js # Librería chess.js
│ └── ... # Otras dependencias
├── index.html # Página principal
└── engine.html # Demo interactiva con tablero
El archivo x88.js utiliza el método 0x88 (hexadecimal 88):
& 0x88 === 0if (sq & 0x88) continue// Constantes de casillas (líneas 7-77)
const a1 = 0, b1 = 1, c1 = 2, ..., h8 = 119
Ventajas del x88:
La clase Board contiene 45 métodos y es extremadamente completa:
class Board {
// Estado de la partida
stm = 0 // Side to move (0=white, 1=black)
counter50 = 0 // Regla de los 50 movimientos
castlingRights = 0 // Derechos de enroque
enpassantSquare = -1 // Casilla de captura al paso
// Tablero
pieceat = new Uint8Array(128) // Pieza en cada casilla
kingsquares = new Uint8Array(2) // Posición de los reyes
piecesquares = [new Array(16), new Array(16)] // Lista de piezas
// Análisis de ataques
attacks = [new Uint8Array(128), new Uint8Array(128)] // Bitfield de ataques
numattacks = [new Uint8Array(128), new Uint8Array(128)] // Contador de ataques
chekingSquares = [new Uint8Array(128), new Uint8Array(128)] // Casillas que dan jaque
matingSquares = [new Uint8Array(128), new Uint8Array(128)] // Casillas que dan mate
pinDirection = [new Uint8Array(128), new Uint8Array(128)] // Dirección de clavadas
inchekValidSquares = [new Uint8Array(128), new Uint8Array(128)] // Casillas válidas en jaque
// Estado de jaque
inCheck = false
inDoubleCheck = false
inCheckMate = false
inStalemate = false
}
El método generateMoves() es extraordinariamente sofisticado. En lugar de generar pseudo-movimientos y validarlos, genera directamente movimientos legales mediante un análisis completo del tablero:
Fases del algoritmo:
// Para cada bando, marca las casillas desde donde se puede dar jaque al rey enemigo
for (let turncolor = white; turncolor <= black; turncolor++) {
let enemyking = this.kingsquares[1 - turncolor]
// Marca ataques de peón que darían jaque
for (const offset of [17, 15]) {
csq = enemyking + (offset * offsetsign)
if (pcolor(this.pieceat[csq]) !== turncolor)
this.chekingSquares[turncolor][csq] |= checkbit[p]
}
// Similar para caballos, alfiles, torres y damas
// Detecta también JAQUES A LA DESCUBIERTA
}
// Calcula todos los ataques del enemigo
// Detecta piezas CLAVADAS mediante el método addSliderAttack()
for (sq of this.piecesquares[turn]) {
// Para cada pieza enemiga
switch (piecetype) {
case b: case r: case q:
// Piezas deslizadoras: detecta clavadas
this.addSliderAttack(turn, piecetype, sq, to, offset)
// ...
}
}
El método addSliderAttack() es brillante:
pinDirection// Si no hay jaque doble, genera movimientos para todas las piezas
if (!this.inDoubleCheck) {
for (sq of this.piecesquares[this.stm]) {
switch (type) {
case p: // Peones con promociones
case n: // Caballos - verifica clavadas
case b: case r: case q: // Deslizadoras con restricciones de clavada
}
}
}
// Movimientos del rey (siempre se generan, incluso en jaque doble)
// El rey SOLO puede ir a casillas NO atacadas
if (!this.inCheck) {
// Verifica que las casillas intermedias estén:
// 1. Vacías
// 2. No atacadas
// 3. La torre esté en su posición
if ((this.pieceat[f1] === empty) &&
(this.numattacks[opposite(this.stm)][f1] === 0) &&
// ... más condiciones
) this.addmove(e1, g1, mask_castling)
}
Este método enriquece cada movimiento con información táctica:
addmove(from, to, maskbits = 0, promotedpiece = 0) {
// 1. Verifica clavadas
if (pindir !== 0) {
if (Math.abs(pindir) !== Math.abs(movedirecction)) return // Pieza clavada
}
// 2. Si estamos en jaque, solo casillas válidas
if (this.inCheck && this.inchekValidSquares[this.stm][to] === 0) return
// 3. Detecta si la jugada da jaque
if (this.chekingSquares[this.stm][to] & checkbit[chekingpiece]) {
maskbits |= mask_check
}
// 4. Detecta jaque a la descubierta
if (this.chekingSquares[this.stm][from] & mask_discovercheck) {
maskbits |= mask_discovercheck
}
// 5. Análisis táctico de la jugada
if (attackbits === 0) {
maskbits |= mask_safe // Casilla segura
} else {
if (lowestSetBit(attackbits) < lowestSetBit(attackbit[movingpiece]))
maskbits |= mask_hanging // Pieza quedaría colgada
}
// 6. Análisis de capturas
if (capturedpiece) {
if (bitCount(attackbits) == 0) maskbits |= mask_freecapture // Captura gratis
if (ptype(capturedpiece) > ptype(movingpiece)) maskbits |= mask_winningcapture
}
}
Máscaras de bits para enriquecer movimientos:
mask_check - Da jaquemask_discovercheck - Jaque a la descubiertamask_safe - Casilla seguramask_hanging - Pieza quedaría colgadamask_freecapture - Captura sin defensamask_winningcapture - Captura ganadora (valor mayor)El código también detecta casillas que dan mate:
// Si el rey enemigo NO tiene escapatorias
if (this.kingescapes[opposite(this.stm)].length === 0) {
// Cualquier jaque seguro es mate
for (let i = 0; i<128; i++) {
if ((numdefenders === 0) && (pcolor(this.pieceat[i]) !== this.stm)) {
if (this.chekingSquares[this.stm][i] & this.attacks[this.stm][i]) {
this.matingSquares[this.stm][i] = this.chekingSquares[this.stm][i]
}
}
}
}
Caso especial increíblemente bien manejado:
// FEN: 7k/8/8/K1pP3r/8/8/8/8 w - c6 0 1
// El rey blanco está en la misma fila que el peón y la torre negra
// Si el peón blanco captura al paso, el rey quedaría en jaque de la torre
if (rank(sq) === rank(kingsquare)) {
let dir = (file(kingsquare) < file(sq)) ? 1 : -1
let next = kingsquare + dir
while (validSquare(next)) {
let piece = this.pieceat[next]
if ((count === 3) && ((ptype(piece) === r) | (ptype(piece) === q)) &&
(pcolor(piece) === opposite(this.stm))) {
ilegalep = true // ¡Captura al paso ilegal!
}
}
}
El historial está altamente optimizado con TypedArrays:
history = {
from: new Uint8Array(MAXPLY),
to: new Uint8Array(MAXPLY),
capturedpiece: new Uint8Array(MAXPLY),
promotedpiece: new Uint8Array(MAXPLY),
counter50: new Uint8Array(MAXPLY),
castlingRights: new Uint8Array(MAXPLY),
enpassantSquare: new Int8Array(MAXPLY),
ply: 0
}
perft(depth) {
this.generateMoves()
if (depth === 1) return this.moves.length
for (var i = 0; i < nmoves; i++) {
this.makemove(moves[i])
nodes += this.perft(depth - 1)
this.undomove()
}
return nodes
}
divide(depth) {
// Variante que muestra nodos por cada movimiento
console.log(this.getMoveStr(moves[i]), movenodes)
}
Soporte completo de notación FEN con validación robusta.
El archivo engine.js implementa un motor UCI en Web Worker:
class UCIChessEngine {
usebb = false
board = (this.usebb) ? new BBBoard() : new Board()
// Comandos UCI soportados:
// - uci, isready, ucinewgame
// - position (startpos, fen, moves)
// - move, undo
// - perft [depth]
}
Características:
engine.htmlDemo completa y funcional con:
// Motor en Web Worker
w = new Worker("js/engine.js")
w.postMessage('uci')
w.postMessage('position fen ' + fen)
w.postMessage('perft 5')
TypedArrays para reducir memoriasq & 0x88)csq, sq, to)Cada movimiento contiene:
move = {
from: e2,
to: e4,
movingpiece: wp,
captured: 0,
promotedpiece: 0,
mask: mask_safe | mask_pawnmove // ¡Información táctica!
}
Esto permite:
attacks[side][square] // Bitfield: qué piezas atacan
numattacks[side][square] // Contador: cuántas piezas atacan
Permite saber instantáneamente:
Las clavadas se detectan durante la generación, no después:
pinDirection[side][square] indica la dirección de la clavadaSegún el código de benchmarking en engine.js:
// Ejemplo de medición
var d = performance.now()
var result = engine.perft(depth)
var d2 = performance.now()
var nps = Math.round(1000 * (result / elapsedtime))
Perft esperado (JavaScript en navegador):
Para mejorar rendimiento:
hello.wasm)hello.rs)// Archivo de 1470 bytes con experimentos
module.wasm, module.wat)// engine.html líneas 155-161
async function fetchAndInstantiate() {
const buffer = await response.arrayBuffer()
const obj = await WebAssembly.instantiate(buffer)
console.log(obj.instance.exports.popcnt64(0b10010010n))
}
Experimento con conteo de bits en WASM para bitboards.
// tests/perft.test.js
describe('Perft Tests', () => {
it('should match perft 5 for startpos', () => {
expect(board.perft(5)).toBe(4865609)
})
})
Algoritmo Único: La generación de movimientos estrictamente legales con detección integrada de jaques/mates es muy impresionante y poco común.
Implementación Completa: No es un “toy project” - tiene FEN, UCI, Web Workers, dos representaciones diferentes.
Código Sofisticado: El manejo de casos especiales (ej. captura al paso con clavada horizontal) demuestra profundo conocimiento de ajedrez.
Arquitectura Moderna: Web Workers, TypedArrays, experimentos con WebAssembly.
Este proyecto es excelente material de estudio y una base sólida para:
Nota Final: Este es un proyecto de muy alta calidad técnica. La generación de movimientos legales directa con análisis táctico integrado es una característica diferenciadora que merece ser documentada y compartida con la comunidad de programación de ajedrez. ¡Excelente trabajo! 🏆
Se ha realizado un trabajo intensivo de depuración y mejora en bitboard.js para asegurar su compatibilidad con Node.js y la corrección de su lógica de generación de movimientos.
magic-tables.js) mediante require en entorno Node.js.filea, rank8, etc.) que impedían la ejecución fuera del navegador.generate_kingmoves donde la verificación de derechos de enroque estaba comentada, permitiendo enroques ilegales.makemove (c1 -> _c1), undomove (f1 -> _f1), y referencias a this faltantes (totalocc, side).js/bitboard.js: Código fuente corregido.js/magic-tables.js: Tablas mágicas externalizadas y exportables.tests/bitboard-node.js: Wrapper para pruebas en Node.js.tests/perft-test.js: Suite de pruebas Perft.