refactor: Chess logic

- improve app styling and layout
This commit is contained in:
2026-06-10 20:27:39 +05:30
parent ea99626784
commit 8c18901bb4
3 changed files with 676 additions and 636 deletions

344
src/lib/chess.js Normal file
View 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 };
}

View File

@@ -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