GHChess / src /components /board /Clock.js
GHHG10's picture
Upload 529 files
f0c8ada verified
Raw
History Blame Contribute Delete
11.1 kB
/**
* Clock utility class for displaying chess clocks from PGN annotations
*/
export class Clock {
/**
* Updates the clock display for both players
* @param {Object} node - The current move tree node
* @param {boolean} isFlipped - Whether the board is flipped
*/
static updateClocks(node, isFlipped = false) {
// Get clock data for current position
const whiteTime = Clock.getClockTime(node, 'white');
const blackTime = Clock.getClockTime(node, 'black');
// Update clock displays
Clock.displayClock('#white-clock .clock-time', whiteTime);
Clock.displayClock('#black-clock .clock-time', blackTime);
// Handle flipped board - clocks should stay with their respective players
// The CSS handles the visual positioning
}
/**
* Gets the clock time for a specific color from the node
* @param {Object} node - The move tree node
* @param {string} color - 'white' or 'black'
* @returns {string|null} The clock time string or null if not available
*/
static getClockTime(node, color) {
// Check if this node has clock data
if (node.clock) {
// Determine which player's clock this represents based on the move
const isWhiteMove = node.move && node.move.color === 'w';
const isBlackMove = node.move && node.move.color === 'b';
if ((color === 'white' && isWhiteMove) || (color === 'black' && isBlackMove)) {
return node.clock;
}
}
return null;
}
/**
* Displays the clock time in the specified element
* @param {string} selector - CSS selector for the clock element
* @param {string|null} timeString - The time string to display
* @param {boolean} isActive - Whether this is the active player's clock
*/
static displayClock(selector, timeString, isActive = false) {
const element = document.querySelector(selector);
if (!element) return;
// Remove all clock-related classes
element.classList.remove('low-time', 'active');
if (timeString) {
const formattedTime = Clock.formatClockTime(timeString);
element.textContent = formattedTime;
// Add low-time class if time is less than 10 seconds
const seconds = Clock.parseTimeToSeconds(timeString);
if (seconds !== null && seconds < 10) {
element.classList.add('low-time');
}
} else {
element.textContent = '--:--';
}
// Add active class if this is the active player's clock
if (isActive) {
element.classList.add('active');
}
}
/**
* Formats a clock time string for display
* @param {string} timeString - Raw time string from PGN (e.g., "0:03:00.9")
* @returns {string} Formatted time string (e.g., "3:00")
*/
static formatClockTime(timeString) {
// Handle different time formats
if (timeString.includes(':')) {
const parts = timeString.split(':');
if (parts.length >= 3) {
// Format: H:MM:SS.s or H:MM:SS
const hours = parseInt(parts[0]);
const minutes = parseInt(parts[1]);
const seconds = parseInt(parts[2].split('.')[0]);
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
} else {
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
} else if (parts.length === 2) {
// Format: MM:SS
const minutes = parseInt(parts[0]);
const seconds = parseInt(parts[1].split('.')[0]);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
}
// Fallback - return as is
return timeString;
}
/**
* Parses a time string to total seconds
* @param {string} timeString - Time string to parse
* @returns {number|null} Total seconds or null if parsing fails
*/
static parseTimeToSeconds(timeString) {
try {
if (timeString.includes(':')) {
const parts = timeString.split(':');
if (parts.length >= 3) {
// Format: H:MM:SS.s
const hours = parseInt(parts[0]);
const minutes = parseInt(parts[1]);
const seconds = parseInt(parts[2].split('.')[0]);
return hours * 3600 + minutes * 60 + seconds;
} else if (parts.length === 2) {
// Format: MM:SS
const minutes = parseInt(parts[0]);
const seconds = parseInt(parts[1].split('.')[0]);
return minutes * 60 + seconds;
}
}
} catch (e) {
console.warn('Failed to parse time string:', timeString);
}
return null;
}
/**
* Resets clock display to default state
*/
static resetClocks() {
Clock.displayClock('#white-clock .clock-time', null, false);
Clock.displayClock('#black-clock .clock-time', null, false);
}
/**
* Updates clocks based on the current move tree position
* @param {Object} moveTree - The move tree instance
* @param {boolean} isFlipped - Whether the board is flipped
* @param {string} pgn - Optional PGN string for TimeControl fallback
*/
static updateFromMoveTree(moveTree, isFlipped = false, pgn = null) {
if (!moveTree || !moveTree.currentNode) {
Clock.resetClocks();
return;
}
const currentNode = moveTree.currentNode;
// Get initial time from TimeControl or first move
const getInitialTime = () => {
// First try to get from TimeControl
if (pgn) {
const timeControlTime = Clock.getTimeControlFromPGN(pgn);
if (timeControlTime) return timeControlTime;
}
// Fallback to first move's clock
if (moveTree.mainline.length > 1) {
const firstMove = moveTree.mainline[1];
if (firstMove.clock) {
return firstMove.clock;
}
}
return null;
};
// For the starting position, show initial time for both players
if (currentNode.id === 'root') {
const initialTime = getInitialTime();
Clock.displayClock('#white-clock .clock-time', initialTime, true); // White to move initially
Clock.displayClock('#black-clock .clock-time', initialTime, false);
return;
}
// Find the most recent clock times for both players
let whiteTime = null;
let blackTime = null;
let nextToMove = 'w'; // Default to white
// Walk backwards through the mainline to find the most recent times
const nodeIndex = moveTree.getNodeIndex(currentNode);
if (nodeIndex !== -1) {
// Determine whose turn it is based on current position
if (currentNode.move) {
nextToMove = currentNode.move.color === 'w' ? 'b' : 'w';
}
// Check current and previous moves for clock data
for (let i = nodeIndex; i >= 1; i--) {
const node = moveTree.mainline[i];
if (node.clock && node.move) {
if (node.move.color === 'w' && !whiteTime) {
whiteTime = node.clock;
} else if (node.move.color === 'b' && !blackTime) {
blackTime = node.clock;
}
}
// Break if we have both times
if (whiteTime && blackTime) break;
}
}
// If we don't have clock data, use initial time
const initialTime = getInitialTime();
if (!whiteTime) whiteTime = initialTime;
if (!blackTime) blackTime = initialTime;
// Update displays with active clock highlighting
const isWhiteActive = nextToMove === 'w';
Clock.displayClock('#white-clock .clock-time', whiteTime, isWhiteActive);
Clock.displayClock('#black-clock .clock-time', blackTime, !isWhiteActive);
}
/**
* Sets both clocks to initial starting time before analysis begins
* @param {Object} moveTree - The move tree instance
* @param {string} pgn - The PGN string to extract TimeControl from
*/
static setInitialClocks(moveTree, pgn) {
if (!moveTree) {
Clock.resetClocks();
return;
}
// Get initial time from PGN TimeControl header
const initialTime = Clock.getTimeControlFromPGN(pgn);
// Set both clocks to initial time, with white active
Clock.displayClock('#white-clock .clock-time', initialTime, true);
Clock.displayClock('#black-clock .clock-time', initialTime, false);
}
/**
* Extracts TimeControl value from PGN header and converts to display format
* @param {string} pgn - The PGN string
* @returns {string|null} Formatted time string or null if not found
*/
static getTimeControlFromPGN(pgn) {
if (!pgn) return null;
// Extract TimeControl header value
const timeControlMatch = pgn.match(/\[TimeControl\s+"([^"]+)"\]/i);
if (!timeControlMatch) return null;
const timeControlValue = timeControlMatch[1];
// Handle different TimeControl formats
if (timeControlValue === '-') {
// Unlimited time
return null;
}
// Handle formats like "600" (seconds), "600+5" (seconds + increment), "1800+30"
const parts = timeControlValue.split('+');
const baseTimeSeconds = parseInt(parts[0]);
if (isNaN(baseTimeSeconds)) return null;
// Convert seconds to time format
return Clock.secondsToTimeString(baseTimeSeconds);
}
/**
* Converts seconds to time string format
* @param {number} seconds - Total seconds
* @returns {string} Formatted time string (e.g., "10:00", "1:30:00")
*/
static secondsToTimeString(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
} else {
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
}
}