chess.mjs
Version:
It's just the ES6 module version of chess.js
395 lines (359 loc) • 18.2 kB
JavaScript
/**
* @author Stefan Haack (https://shaack.com)
*/
import {describe, it, assert} from "teevi/src/teevi.js"
import {Chess} from "../src/Chess.js"
import {chess960StartPositions} from "./chess960StartPositions.js"
import {Chess960} from "../src/Chess960.js"
describe("TestChess960", function () {
it("should generate a valid FEN for Chess 960 based on the list in chess960StartPositions.js", () => {
const chess = new Chess()
chess960StartPositions.forEach((position, index) => {
const fen = Chess960.generateStartPosition(index)
// console.log(index, position, fen);
assert.equal(fen, position)
const chess960 = new Chess(fen, {chess960: true})
assert.equal(chess960.fen(), position)
})
})
it("should load a chess960 game from a valid chess960 fen and do castling", function () {
const fen = "nrkbrnbq/pppppppp/8/8/8/8/PPPPPPPP/NRKBRNBQ w KQkq - 0 1"
const chess = new Chess(fen, {chess960: true})
assert.equal(chess.fen(), fen)
assert.true(chess.move("f4"))
assert.true(chess.move("Nb6"))
assert.true(chess.move("Nb3"))
assert.true(chess.move("f6"))
assert.true(chess.move("e4"))
assert.true(chess.move("e5"))
assert.true(chess.move("f5"))
assert.true(chess.move("d5"))
assert.true(chess.move("g4"))
assert.true(chess.move("Nfd7"))
assert.true(chess.move("exd5"))
assert.true(chess.move("Bxd5"))
assert.true(chess.move("Bf3"))
assert.true(chess.move("c6"))
assert.true(chess.move("Ng3"))
assert.true(chess.move("Be7"))
assert.true(chess.move("O-O-O"))
assert.true(chess.move("Qg8"))
assert.equal(chess.fen(), "1rk1r1q1/pp1nb1pp/1np2p2/3bpP2/6P1/1N3BN1/PPPP3P/2KRR1BQ w kq - 4 10")
})
it("should load a chess960 game from a valid chess960 pgn", function () {
const pgn = `[Event "Freestyle Weissenhaus KO"]
[Site "Wangels GER"]
[Date "2025.02.09"]
[EventDate "2025.02.09"]
[Round "1.1"]
[Result "1-0"]
[White "Levon Aronian"]
[Black "Vladimir Fedoseev"]
[ECO "?"]
[WhiteElo "?"]
[BlackElo "?"]
[PlyCount "72"]
[SetUp "1"]
[FEN "nrkbrnbq/pppppppp/8/8/8/8/PPPPPPPP/NRKBRNBQ w KQkq - 0 1"]
1. f4 Nb6 2. Nb3 f6 3. e4 e5 4. f5 d5 5. g4 Nfd7 6. exd5 Bxd5 7. Bf3 c6 8.
Ng3 Be7 9. O-O-O Qg8 10. d3 Bb4 11. c3 Be7 12. c4 Bxf3 13. Qxf3 Qf7 14. Ne4
O-O 15. Be3 Na4 16. Rg1 b5 17. c5 b4 18. Qf2 Rb5 19. Qc2 a5 20. d4 Ra8 21.
g5 Kh8 22. gxf6 Bxf6 23. d5 cxd5 24. Nd6 Qf8 25. Nxb5 d4 26. c6 Rc8 27.
N3xd4 exd4 28. Bxd4 Nac5 29. Bxf6 Nxf6 30. c7 b3 31. axb3 Na6 32. Kb1 Nb4
33. Qc4 Ne8 34. Rd8 Qxf5+ 35. Ka1 Nc2+ 36. Qxc2 1-0`
const chess = new Chess(null, {chess960: true})
chess.load_pgn(pgn)
assert.equal(chess.fen(), "2rRn2k/2P3pp/8/pN3q2/8/1P6/1PQ4P/K5R1 b - - 0 36")
})
it("should not have isolation problems with the rooks", function () {
// First create a Chess960 game with rooks in non-standard positions
const chess960 = new Chess("qbnnbrkr/pppppppp/8/8/8/8/PPPPPPPP/QBNNBRKR w KQkq - 0 1", {chess960: true})
assert.equal(chess960.fen(), "qbnnbrkr/pppppppp/8/8/8/8/PPPPPPPP/QBNNBRKR w KQkq - 0 1")
assert.equal(chess960.chess960(), true)
// Now create a regular chess game
const regularChess = new Chess("qbnnbrkr/pppppppp/8/8/8/8/PPPPPPPP/QBNNBRKR w KQkq - 0 1")
assert.equal(regularChess.fen(), "qbnnbrkr/pppppppp/8/8/8/8/PPPPPPPP/QBNNBRKR w KQkq - 0 1")
assert.equal(regularChess.chess960(), false)
})
it("should do a queen side castling in chess960", function () {
const chess960 = new Chess("nqnrkbbr/pppppppp/8/8/8/8/PPPPPPPP/NQNRKBBR w KQkq - 0 1", {chess960: true})
assert.true(chess960.chess960())
assert.true(chess960.move("Nd3"))
assert.true(chess960.move("a5"))
// should castle in a chess960 variant
assert.true(chess960.move("O-O-O"))
// The rook should remain at d1 after castling (king moves from e1 to c1, rook stays at d1)
assert.equal(chess960.fen(), "nqnrkbbr/1ppppppp/8/p7/8/3N4/PPPPPPPP/NQKR1BBR b kq - 1 2")
})
it("should do a king side castling in chess960", function () {
const chess960_2 = new Chess("bnrqnkrb/pppppppp/8/8/8/8/PPPPPPPP/BNRQNKRB w KQkq - 0 1", {chess960: true})
assert.true(chess960_2.chess960())
assert.true(chess960_2.move("O-O"))
// King and rook swap positions: king f1->g1, rook g1->f1
assert.equal(chess960_2.fen(), "bnrqnkrb/pppppppp/8/8/8/8/PPPPPPPP/BNRQNRKB b kq - 1 1")
})
it("should do a queen side castling in chess960 with start position 604", function () {
const fen = Chess960.generateStartPosition(604)
assert.equal(fen, "rbqnkrbn/pppppppp/8/8/8/8/PPPPPPPP/RBQNKRBN w KQkq - 0 1")
const chess960 = new Chess(fen, {chess960: true})
assert.true(chess960.chess960())
assert.true(chess960.move("d3"))
assert.true(chess960.move("d6"))
assert.true(chess960.move("Ne3")) // Nd1-e3, clears d1
assert.true(chess960.move("e6"))
assert.true(chess960.move("c3"))
assert.true(chess960.move("c6"))
assert.true(chess960.move("Bc2")) // Bb1-c2, clears b1
assert.true(chess960.move("f6"))
assert.true(chess960.move("Qd2")) // Qc1-d2, clears c1
assert.true(chess960.move("g6"))
// White castles queenside: King e1→c1, Rook a1→d1
assert.true(chess960.move("O-O-O"))
assert.equal(chess960.fen(), "rbqnkrbn/pp5p/2ppppp1/8/8/2PPN3/PPBQPPPP/2KR1RBN b kq - 1 6")
// verify game continues after castling
assert.true(chess960.move("b6"))
assert.true(chess960.move("a3"))
assert.equal(chess960.fen(), "rbqnkrbn/p6p/1pppppp1/8/8/P1PPN3/1PBQPPPP/2KR1RBN b kq - 0 7")
})
it("should detect the correct start position id from a FEN", () => {
for (let id = 0; id < 960; id++) {
const fen = Chess960.generateStartPosition(id)
assert.equal(Chess960.detectStartPosition(fen), id)
}
})
it("should throw when black pieces don't mirror white pieces", () => {
// White back rank is RNBQKBNR but black back rank is rnbkqbnr (swapped king/queen)
const fen = "rnbkqbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
assert.throws(() => {
Chess960.detectStartPosition(fen)
})
})
it("should throw when white pawns are not on second rank", () => {
const fen = "rnbqkbnr/pppppppp/8/8/8/PPPPPPPP/8/RNBQKBNR w KQkq - 0 1"
assert.throws(() => {
Chess960.detectStartPosition(fen)
})
})
it("should throw when black pawns are not on seventh rank", () => {
const fen = "rnbqkbnr/8/pppppppp/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
assert.throws(() => {
Chess960.detectStartPosition(fen)
})
})
it("should not castle in standard chess", function () {
const standard = new Chess("nqnrkbbr/pppppppp/8/8/8/8/PPPPPPPP/NQNRKBBR w KQkq - 0 1")
assert.false(standard.chess960())
assert.true(standard.move("Nd3"))
assert.true(standard.move("a5"))
// should not castle in a standard variant
assert.false(standard.move("O-O-O"))
})
// --- Comprehensive castling tests ---
it("should return castling moves with to=rook square in verbose mode", function () {
// Position 604: RBQNKRBN, king e1, rooks a1 and f1
const chess = new Chess("rbqnkrbn/pppppppp/8/8/2PPN3/8/PPBQPPPP/R3KRBN w KQkq - 0 1", {chess960: true})
const kingMoves = chess.moves({square: "e1", verbose: true})
const castlingMoves = kingMoves.filter(m => m.flags.includes("q") || m.flags.includes("k"))
// O-O-O should target the queenside rook at a1
const ooo = castlingMoves.find(m => m.flags.includes("q"))
assert.true(!!ooo)
assert.equal(ooo.from, "e1")
assert.equal(ooo.to, "a1") // rook's square, not king destination c1
assert.equal(ooo.san, "O-O-O")
})
it("should return kingside castling with to=rook square in verbose mode", function () {
// Position 131: BNRQNKRB, king f1, rook g1 — can castle immediately
const chess = new Chess("bnrqnkrb/pppppppp/8/8/8/8/PPPPPPPP/BNRQNKRB w KQkq - 0 1", {chess960: true})
const kingMoves = chess.moves({square: "f1", verbose: true})
const oo = kingMoves.find(m => m.flags.includes("k"))
assert.true(!!oo)
assert.equal(oo.from, "f1")
assert.equal(oo.to, "g1") // rook's square
assert.equal(oo.san, "O-O")
})
it("should castle via from/to object (king captures own rook)", function () {
// Position 604: after clearing queenside, castle O-O-O by moving king to rook
const chess = new Chess("rbqnkrbn/pp5p/2ppppp1/8/8/2PPN3/PPBQPPPP/R3KRBN w KQkq - 0 1", {chess960: true})
// Castle queenside by providing {from: king, to: rook}
const result = chess.move({from: "e1", to: "a1"})
assert.true(!!result)
assert.equal(result.san, "O-O-O")
// King should be on c1, rook on d1
assert.equal(chess.get("c1").type, "k")
assert.equal(chess.get("d1").type, "r")
assert.equal(chess.get("e1"), null)
assert.equal(chess.get("a1"), null)
})
it("should castle kingside via from/to object", function () {
// Position 131: BNRQNKRB, king f1, rook g1
const chess = new Chess("bnrqnkrb/pppppppp/8/8/8/8/PPPPPPPP/BNRQNKRB w KQkq - 0 1", {chess960: true})
const result = chess.move({from: "f1", to: "g1"})
assert.true(!!result)
assert.equal(result.san, "O-O")
// King should be on g1, rook on f1
assert.equal(chess.get("g1").type, "k")
assert.equal(chess.get("f1").type, "r")
})
it("should undo castling and restore original position", function () {
// Position 604: O-O-O
const fen = "rbqnkrbn/pp5p/2ppppp1/8/8/2PPN3/PPBQPPPP/R3KRBN w KQkq - 0 1"
const chess = new Chess(fen, {chess960: true})
assert.true(!!chess.move("O-O-O"))
// After castling: king c1, rook d1
assert.equal(chess.get("c1").type, "k")
assert.equal(chess.get("d1").type, "r")
// Undo
chess.undo()
// Position should be fully restored
assert.equal(chess.fen(), fen)
assert.equal(chess.get("e1").type, "k")
assert.equal(chess.get("a1").type, "r")
})
it("should undo kingside castling (king-rook swap)", function () {
// Position 131: BNRQNKRB, king f1, rook g1 — swap positions
const fen = "bnrqnkrb/pppppppp/8/8/8/8/PPPPPPPP/BNRQNKRB w KQkq - 0 1"
const chess = new Chess(fen, {chess960: true})
assert.true(!!chess.move("O-O"))
assert.equal(chess.get("g1").type, "k")
assert.equal(chess.get("f1").type, "r")
// Undo
chess.undo()
assert.equal(chess.fen(), fen)
assert.equal(chess.get("f1").type, "k")
assert.equal(chess.get("g1").type, "r")
})
it("should handle black castling in chess960", function () {
// Position 131: BNRQNKRB — let black castle kingside
const chess = new Chess("bnrqnkrb/pppppppp/8/8/8/8/PPPPPPPP/BNRQNKRB w KQkq - 0 1", {chess960: true})
assert.true(chess.move("d4"))
// Black castles kingside: king f8, rook g8
assert.true(chess.move("O-O"))
assert.equal(chess.get("g8").type, "k")
assert.equal(chess.get("f8").type, "r")
})
it("should handle castling where rook is already at destination (d1 for O-O-O)", function () {
// Position with king e1, rook d1: O-O-O means king→c1, rook stays at d1
const chess = new Chess("nqnrkbbr/pppppppp/8/8/8/8/PPPPPPPP/NQNRKBBR w KQkq - 0 1", {chess960: true})
assert.true(chess.move("Nd3"))
assert.true(chess.move("a6"))
assert.true(chess.move("O-O-O"))
// King to c1, rook stays at d1
assert.equal(chess.get("c1").type, "k")
assert.equal(chess.get("d1").type, "r")
})
it("should handle castling where king is already at destination (g1 for O-O)", function () {
// King already on g1 (O-O destination), rook on h1
// O-O: king stays at g1, rook h1→f1
const fen = "4k3/8/8/8/8/8/8/6KR w K - 0 1"
const chess = new Chess(fen, {chess960: true})
assert.true(chess.move("O-O"))
assert.equal(chess.get("g1").type, "k")
assert.equal(chess.get("f1").type, "r")
assert.equal(chess.get("h1"), null)
})
it("should handle castling in standard position (518) in chess960 mode", function () {
// Position 518 is the standard starting position
const fen = Chess960.generateStartPosition(518)
assert.equal(fen, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
const chess = new Chess(fen, {chess960: true})
// Standard opening to enable kingside castling
assert.true(chess.move("e4"))
assert.true(chess.move("e5"))
assert.true(chess.move("Nf3"))
assert.true(chess.move("Nc6"))
assert.true(chess.move("Bc4"))
assert.true(chess.move("Bc5"))
assert.true(chess.move("O-O"))
// Standard castling result: king g1, rook f1
assert.equal(chess.get("g1").type, "k")
assert.equal(chess.get("f1").type, "r")
assert.equal(chess.get("e1"), null)
assert.equal(chess.get("h1"), null)
})
it("should not set captured field on castling moves", function () {
// In chess960, castling to=rook square, but the move should not have captured set
const chess = new Chess("bnrqnkrb/pppppppp/8/8/8/8/PPPPPPPP/BNRQNKRB w KQkq - 0 1", {chess960: true})
const result = chess.move("O-O")
assert.true(!!result)
assert.equal(result.captured, undefined)
})
it("should handle both white and black castling in the same game", function () {
// Position 131: BNRQNKRB — both sides can castle kingside immediately
const chess = new Chess("bnrqnkrb/pppppppp/8/8/8/8/PPPPPPPP/BNRQNKRB w KQkq - 0 1", {chess960: true})
// White castles kingside
assert.true(chess.move("O-O"))
assert.equal(chess.fen(), "bnrqnkrb/pppppppp/8/8/8/8/PPPPPPPP/BNRQNRKB b kq - 1 1")
// Black castles kingside
assert.true(chess.move("O-O"))
assert.equal(chess.get("g8").type, "k")
assert.equal(chess.get("f8").type, "r")
})
it("should remove castling rights after castling", function () {
const chess = new Chess("bnrqnkrb/pppppppp/8/8/8/8/PPPPPPPP/BNRQNKRB w KQkq - 0 1", {chess960: true})
assert.true(chess.move("O-O"))
// White should have no castling rights, FEN should show "kq" not "KQkq"
const fen = chess.fen()
assert.true(fen.includes(" kq "))
})
it("should handle castling with rook on a1 and king far away", function () {
// Position 604: RBQNKRBN — rook a1, king e1 (far apart)
// After clearing b1/c1/d1, queen-side castle should work
const chess = new Chess("rbqnkrbn/pp5p/2ppppp1/8/8/2PPN3/PPBQPPPP/R3KRBN w KQkq - 0 1", {chess960: true})
const moves = chess.moves({verbose: true})
const ooo = moves.find(m => m.san === "O-O-O")
assert.true(!!ooo)
assert.equal(ooo.from, "e1")
assert.equal(ooo.to, "a1")
})
it("should correctly undo multiple moves including castling", function () {
const fen = "bnrqnkrb/pppppppp/8/8/8/8/PPPPPPPP/BNRQNKRB w KQkq - 0 1"
const chess = new Chess(fen, {chess960: true})
assert.true(chess.move("O-O"))
assert.true(chess.move("d5"))
assert.true(chess.move("d4"))
// Undo all three moves
chess.undo()
chess.undo()
chess.undo()
assert.equal(chess.fen(), fen)
})
it("should handle castling where king moves to rook square and rook moves to king square (perfect swap)", function () {
// Position 131: BNRQNKRB — king f1, rook g1
// O-O: king f1→g1, rook g1→f1 — a perfect swap
const chess = new Chess("bnrqnkrb/pppppppp/8/8/8/8/PPPPPPPP/BNRQNKRB w KQkq - 0 1", {chess960: true})
const beforeKing = chess.get("f1")
const beforeRook = chess.get("g1")
assert.equal(beforeKing.type, "k")
assert.equal(beforeRook.type, "r")
assert.true(chess.move("O-O"))
const afterKing = chess.get("g1")
const afterRook = chess.get("f1")
assert.equal(afterKing.type, "k")
assert.equal(afterRook.type, "r")
// Old squares should have the swapped pieces, not be empty
assert.equal(chess.get("f1").type, "r")
assert.equal(chess.get("g1").type, "k")
})
it("should handle castling with rooks at a1 and h1 (corners)", function () {
// Position where rooks are at the standard corners but king is elsewhere
// Position 518 variant: king e1, rooks a1 and h1 (but with different piece arrangement)
// Use a custom position: RNBKQBNR would have king d1, rooks a1 and h1 (but not valid chess960)
// Use actual chess960 position with rooks near corners
// Position 479: RKBBNQNR — king a1? No...
// Let's use a cleared position directly
const fen = "r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1"
const chess = new Chess(fen, {chess960: true})
// Kingside: king e1, rook h1
const moves = chess.moves({square: "e1", verbose: true})
const oo = moves.find(m => m.san === "O-O")
const ooo = moves.find(m => m.san === "O-O-O")
assert.true(!!oo)
assert.true(!!ooo)
assert.equal(oo.to, "h1") // rook's square
assert.equal(ooo.to, "a1") // rook's square
// Execute kingside castling
assert.true(chess.move("O-O"))
assert.equal(chess.get("g1").type, "k")
assert.equal(chess.get("f1").type, "r")
})
})