refactor: Chess logic
- improve app styling and layout
This commit is contained in:
344
src/lib/chess.js
Normal file
344
src/lib/chess.js
Normal file
@@ -0,0 +1,344 @@
|
||||
export const PIECES = {
|
||||
wK: '/icons/wK.svg', wQ: '/icons/wQ.svg', wR: '/icons/wR.svg', wB: '/icons/wB.svg', wN: '/icons/wN.svg', wP: '/icons/wP.svg',
|
||||
bK: '/icons/bK.svg', bQ: '/icons/bQ.svg', bR: '/icons/bR.svg', bB: '/icons/bB.svg', bN: '/icons/bN.svg', bP: '/icons/bP.svg'
|
||||
};
|
||||
|
||||
export const INIT = [
|
||||
['bR','bN','bB','bQ','bK','bB','bN','bR'],
|
||||
['bP','bP','bP','bP','bP','bP','bP','bP'],
|
||||
[null,null,null,null,null,null,null,null],
|
||||
[null,null,null,null,null,null,null,null],
|
||||
[null,null,null,null,null,null,null,null],
|
||||
[null,null,null,null,null,null,null,null],
|
||||
['wP','wP','wP','wP','wP','wP','wP','wP'],
|
||||
['wR','wN','wB','wQ','wK','wB','wN','wR'],
|
||||
];
|
||||
|
||||
export function deepClone(b) {
|
||||
return b.map(r => [...r]);
|
||||
}
|
||||
|
||||
export function createInitialPieceIds() {
|
||||
let ids = [];
|
||||
let count = 0;
|
||||
for (let r = 0; r < 8; r++) {
|
||||
let row = [];
|
||||
for (let c = 0; c < 8; c++) {
|
||||
if (INIT[r][c]) {
|
||||
row.push(`piece-${INIT[r][c]}-${count++}`);
|
||||
} else {
|
||||
row.push(null);
|
||||
}
|
||||
}
|
||||
ids.push(row);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
export function buildTree(moves) {
|
||||
let root = { move: null, children: [], note: null };
|
||||
let cur = root;
|
||||
for (let m of moves) {
|
||||
let node = { move: m.san, children: [], note: m.note || null };
|
||||
cur.children.push(node);
|
||||
cur = node;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
export function walkTree(tree, moves) {
|
||||
let cur = tree;
|
||||
for (let m of moves) {
|
||||
let child = cur.children.find(c => c.move === m);
|
||||
if (!child) return null;
|
||||
cur = child;
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
export function getNoteForHistory(historyPrefix, customNotes, currentRep, currentRepertoireObj) {
|
||||
let line = historyPrefix.join(' ');
|
||||
|
||||
if (customNotes[currentRep] && customNotes[currentRep][line] !== undefined) {
|
||||
return customNotes[currentRep][line];
|
||||
}
|
||||
for (let op of currentRepertoireObj.openings) {
|
||||
let opMoves = op.pgn.split(' ').filter(Boolean);
|
||||
if (opMoves.slice(0, historyPrefix.length).join(' ') === line) {
|
||||
let node = walkTree(op.tree, historyPrefix);
|
||||
if (node && node.note) {
|
||||
return node.note;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildTreePreservingNotes(historyMoves, oldTree, customNotes, currentRep, currentRepertoireObj) {
|
||||
let root = { move: null, children: [], note: null };
|
||||
let cur = root;
|
||||
|
||||
for (let i = 0; i < historyMoves.length; i++) {
|
||||
let m = historyMoves[i];
|
||||
let prefix = historyMoves.slice(0, i + 1);
|
||||
|
||||
let existingNote = null;
|
||||
if (oldTree) {
|
||||
let node = walkTree(oldTree, prefix);
|
||||
if (node && node.note) {
|
||||
existingNote = node.note;
|
||||
}
|
||||
}
|
||||
|
||||
if (!existingNote) {
|
||||
existingNote = getNoteForHistory(prefix, customNotes, currentRep, currentRepertoireObj);
|
||||
}
|
||||
|
||||
let node = { move: m, children: [], note: existingNote };
|
||||
cur.children.push(node);
|
||||
cur = node;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
export function getBoardSignature(b, t) {
|
||||
let pieces = [];
|
||||
for (let r = 0; r < 8; r++) {
|
||||
for (let c = 0; c < 8; c++) {
|
||||
pieces.push(b[r][c] || '.');
|
||||
}
|
||||
}
|
||||
return pieces.join(',') + ' ' + t;
|
||||
}
|
||||
|
||||
export function getMoves(board, r, c) {
|
||||
let p = board[r][c];
|
||||
if (!p) return [];
|
||||
let color = p[0], type = p[1];
|
||||
let moves = [];
|
||||
function inBounds(r,c){return r>=0&&r<8&&c>=0&&c<8;}
|
||||
function friendly(r,c){return inBounds(r,c)&&board[r][c]&&board[r][c][0]===color;}
|
||||
function enemy(r,c){return inBounds(r,c)&&board[r][c]&&board[r][c][0]!==color;}
|
||||
function empty(r,c){return inBounds(r,c)&&!board[r][c];}
|
||||
function slide(dirs){for(let [dr,dc] of dirs){let nr=r+dr,nc=c+dc;while(inBounds(nr,nc)){if(friendly(nr,nc))break;moves.push([nr,nc]);if(board[nr][nc])break;nr+=dr;nc+=dc;}}}
|
||||
function jump(drs){for(let [dr,dc] of drs){let nr=r+dr,nc=c+dc;if(inBounds(nr,nc)&&!friendly(nr,nc))moves.push([nr,nc]);}}
|
||||
if(type==='R')slide([[1,0],[-1,0],[0,1],[0,-1]]);
|
||||
else if(type==='B')slide([[1,1],[1,-1],[-1,1],[-1,-1]]);
|
||||
else if(type==='Q')slide([[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]]);
|
||||
else if(type==='N')jump([[2,1],[2,-1],[-2,1],[-2,-1],[1,2],[1,-2],[-1,2],[-1,-2]]);
|
||||
else if(type==='K')jump([[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]]);
|
||||
else if(type==='P'){
|
||||
let dir=color==='w'?-1:1, start=color==='w'?6:1;
|
||||
if(empty(r+dir,c)){moves.push([r+dir,c]);if(r===start&&empty(r+2*dir,c))moves.push([r+2*dir,c]);}
|
||||
if(enemy(r+dir,c-1))moves.push([r+dir,c-1]);
|
||||
if(enemy(r+dir,c+1))moves.push([r+dir,c+1]);
|
||||
}
|
||||
return moves;
|
||||
}
|
||||
|
||||
export function moveToSAN(board, fr, fc, tr, tc) {
|
||||
let p = board[fr][fc];
|
||||
let type = p[1];
|
||||
let files='abcdefgh', target=files[tc]+(8-tr);
|
||||
let capture = board[tr][tc] ? 'x':'';
|
||||
if(type==='P') return capture ? files[fc]+'x'+target : target;
|
||||
if(type==='K') return 'K'+capture+target;
|
||||
return type+capture+target;
|
||||
}
|
||||
|
||||
export function playMoveOnBoard(b, t, san) {
|
||||
for (let r = 0; r < 8; r++) {
|
||||
for (let c = 0; c < 8; c++) {
|
||||
let p = b[r][c];
|
||||
if (!p || p[0] !== t) continue;
|
||||
let moves = getMoves(b, r, c);
|
||||
for (let [tr, tc] of moves) {
|
||||
if (moveToSAN(b, r, c, tr, tc) === san) {
|
||||
b[tr][tc] = p;
|
||||
b[r][c] = null;
|
||||
if (p[1] === 'P' && (tr === 0 || tr === 7)) {
|
||||
b[tr][tc] = p[0] + 'Q';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getBoardSignaturesForOpening(op) {
|
||||
let tempBoard = deepClone(INIT);
|
||||
let tempTurn = 'w';
|
||||
let signatures = [];
|
||||
|
||||
signatures.push({
|
||||
signature: getBoardSignature(tempBoard, tempTurn),
|
||||
history: []
|
||||
});
|
||||
|
||||
let moves = op.pgn.split(' ').filter(Boolean);
|
||||
for (let i = 0; i < moves.length; i++) {
|
||||
let san = moves[i];
|
||||
let moved = playMoveOnBoard(tempBoard, tempTurn, san);
|
||||
if (!moved) break;
|
||||
tempTurn = tempTurn === 'w' ? 'b' : 'w';
|
||||
signatures.push({
|
||||
signature: getBoardSignature(tempBoard, tempTurn),
|
||||
history: moves.slice(0, i + 1)
|
||||
});
|
||||
}
|
||||
return signatures;
|
||||
}
|
||||
|
||||
export function getLegalMovesForTurn(board, turn) {
|
||||
let legal = [];
|
||||
for (let r = 0; r < 8; r++) {
|
||||
for (let c = 0; c < 8; c++) {
|
||||
let p = board[r][c];
|
||||
if (!p || p[0] !== turn) continue;
|
||||
let moves = getMoves(board, r, c);
|
||||
for (let [tr, tc] of moves) {
|
||||
let san = moveToSAN(board, r, c, tr, tc);
|
||||
legal.push({
|
||||
san: san,
|
||||
fr: r,
|
||||
fc: c,
|
||||
tr: tr,
|
||||
tc: tc
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return legal;
|
||||
}
|
||||
|
||||
export function canonicalize(str) {
|
||||
let text = str.toLowerCase().trim();
|
||||
|
||||
// Replace spoken numbers
|
||||
text = text
|
||||
.replace(/\bone\b/g, '1')
|
||||
.replace(/\btwo\b/g, '2')
|
||||
.replace(/\bthree\b/g, '3')
|
||||
.replace(/\bfour\b/g, '4')
|
||||
.replace(/\bfor\b/g, '4')
|
||||
.replace(/\bfive\b/g, '5')
|
||||
.replace(/\bsix\b/g, '6')
|
||||
.replace(/\bseven\b/g, '7')
|
||||
.replace(/\beight\b/g, '8')
|
||||
.replace(/\bate\b/g, '8');
|
||||
|
||||
// Replace spoken letters/files
|
||||
text = text
|
||||
.replace(/\bsee\b/g, 'c')
|
||||
.replace(/\bsea\b/g, 'c')
|
||||
.replace(/\bbe\b/g, 'b')
|
||||
.replace(/\bbee\b/g, 'b')
|
||||
.replace(/\bgee\b/g, 'g')
|
||||
.replace(/\beff\b/g, 'f')
|
||||
.replace(/\baye\b/g, 'a')
|
||||
.replace(/\bay\b/g, 'a');
|
||||
|
||||
// Replace misheard words
|
||||
text = text
|
||||
.replace(/\bbefore\b/g, 'b4')
|
||||
.replace(/\bdetour\b/g, 'd4')
|
||||
.replace(/\bdeeply\b/g, 'd3')
|
||||
.replace(/\bdefended\b/g, 'd5');
|
||||
|
||||
// Standard piece mapping
|
||||
text = text
|
||||
.replace(/knight/g, 'n')
|
||||
.replace(/night/g, 'n')
|
||||
.replace(/bishop/g, 'b')
|
||||
.replace(/rook/g, 'r')
|
||||
.replace(/queen/g, 'q')
|
||||
.replace(/king/g, 'k')
|
||||
.replace(/pawn/g, '');
|
||||
|
||||
// Replace e.g. "d to" -> "d2", then strip remaining "to"
|
||||
text = text.replace(/([a-h])\s+to\b/g, '$12');
|
||||
|
||||
// Movement/capture words
|
||||
text = text
|
||||
.replace(/\bto\b/g, '')
|
||||
.replace(/\bthe\b/g, '')
|
||||
.replace(/\bmove\b/g, '')
|
||||
.replace(/\bplay\b/g, '')
|
||||
.replace(/\bsquare\b/g, '')
|
||||
.replace(/takes/g, 'x')
|
||||
.replace(/take/g, 'x')
|
||||
.replace(/captures/g, 'x')
|
||||
.replace(/capture/g, 'x');
|
||||
|
||||
// Strip spaces and all non-alphanumeric characters
|
||||
return text.replace(/[^a-z0-9]/g, '');
|
||||
}
|
||||
|
||||
export function getSpokenPatternsForMove(san, fr, fc, tr, tc) {
|
||||
let clean = san.replace(/[+#]/g, '');
|
||||
let canon = canonicalize(clean);
|
||||
let patterns = [canon];
|
||||
|
||||
// Add coordinate patterns
|
||||
let files = 'abcdefgh';
|
||||
let startSquare = files[fc] + (8 - fr);
|
||||
let targetSquare = files[tc] + (8 - tr);
|
||||
patterns.push(startSquare + targetSquare); // e.g. "e2e4"
|
||||
|
||||
if (clean === 'O-O') {
|
||||
patterns.push('oo', 'castle', 'castles', 'shortcastle', 'kingsidecastle', 'castlekingside');
|
||||
} else if (clean === 'O-O-O') {
|
||||
patterns.push('ooo', 'longcastle', 'queensidecastle', 'castlequeenside');
|
||||
}
|
||||
|
||||
if (clean.includes('=')) {
|
||||
patterns.push(canonicalize(clean.replace('=', '')));
|
||||
}
|
||||
|
||||
if (clean.includes('x')) {
|
||||
patterns.push(canonicalize(clean.replace('x', '')));
|
||||
}
|
||||
|
||||
return patterns;
|
||||
}
|
||||
|
||||
export function determineParentId(pgnStr, openings) {
|
||||
let moves = pgnStr.split(' ').filter(Boolean);
|
||||
let longestMatchLength = 0;
|
||||
let bestParentId = null;
|
||||
|
||||
for (let op of openings) {
|
||||
let opMoves = op.pgn.split(' ').filter(Boolean);
|
||||
if (opMoves.length > 0 && opMoves.length < moves.length) {
|
||||
let isPrefix = true;
|
||||
for (let i = 0; i < opMoves.length; i++) {
|
||||
if (opMoves[i] !== moves[i]) {
|
||||
isPrefix = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isPrefix && opMoves.length > longestMatchLength) {
|
||||
longestMatchLength = opMoves.length;
|
||||
bestParentId = op.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestParentId;
|
||||
}
|
||||
|
||||
export function validateAndReplayMoves(movesArray) {
|
||||
let tempBoard = deepClone(INIT);
|
||||
let tempTurn = 'w';
|
||||
|
||||
for (let i = 0; i < movesArray.length; i++) {
|
||||
let san = movesArray[i];
|
||||
let moved = playMoveOnBoard(tempBoard, tempTurn, san);
|
||||
if (!moved) {
|
||||
return { valid: false, error: `Move ${i+1} (${san}) is illegal in this position.` };
|
||||
}
|
||||
tempTurn = tempTurn === 'w' ? 'b' : 'w';
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
export * from './chess.js';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user