Skip to content

Instantly share code, notes, and snippets.

@strayge
Last active January 21, 2026 00:02
Show Gist options
  • Select an option

  • Save strayge/ebb7253f33f9f36aeac357383e957c0d to your computer and use it in GitHub Desktop.

Select an option

Save strayge/ebb7253f33f9f36aeac357383e957c0d to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name RGG Chess Rewind
// @namespace http://tampermonkey.net/
// @version 2026-01-21
// @description Allow to rewind chess moves on RGG Land
// @author strayge
// @match https://rgg.land/checkers
// @match https://rgg.land/checkers/season/s4
// @icon https://www.google.com/s2/favicons?sz=64&domain=rgg.land
// @grant none
// @updateURL https://gist.githubusercontent.com/strayge/ebb7253f33f9f36aeac357383e957c0d/raw/rgg_chess_rewind.user.js
// @downloadURL https://gist.githubusercontent.com/strayge/ebb7253f33f9f36aeac357383e957c0d/raw/rgg_chess_rewind.user.js
// ==/UserScript==
(function() {
'use strict';
// ============================================
// CONFIGURATION & STATE
// ============================================
let states = []; // Array of board states (index 0 = initial, last = current)
let currentViewIndex = 0; // Currently displayed state index
let moveHistory = []; // Parsed moves from log (newest first as in DOM)
let boardContainer = null; // Reference to board pieces container
let btnBack = null;
let btnForward = null;
let btnCurrent = null;
let moveCounter = null; // Move counter display element
// ============================================
// COORDINATE CONVERSION
// ============================================
// A-S maps to columns 0-18 (left to right)
// 1-19 maps to rows 18-0 (bottom to top, so row 1 = grid index 18)
function coordsToGrid(coordStr) {
const match = coordStr.match(/^([A-S])(\d+)$/i);
if (!match) return null;
const letter = match[1].toUpperCase();
const number = parseInt(match[2], 10);
const col = letter.charCodeAt(0) - 'A'.charCodeAt(0); // A=0, S=18
const row = 19 - number; // 1->18, 19->0
return { col, row };
}
function gridToCoords(col, row) {
const letter = String.fromCharCode('A'.charCodeAt(0) + col);
const number = 19 - row;
return `${letter}${number}`;
}
// ============================================
// BOARD STATE PARSING
// ============================================
function parseCurrentBoardState() {
// Find the board container - it's the div with background-image of the board
const boardWrapper = document.querySelector('div[style*="background-image:url(/images/checkers/board"]');
if (!boardWrapper) {
console.error('RGG Rewind: Board not found');
return null;
}
// The pieces container is the first child div.absolute
boardContainer = boardWrapper.querySelector('div.absolute');
if (!boardContainer) {
console.error('RGG Rewind: Pieces container not found');
return null;
}
const state = new Map();
// Find all piece divs (they have img children with checker piece images)
const pieceDivs = boardContainer.querySelectorAll('div.absolute');
for (const pieceDiv of pieceDivs) {
const img = pieceDiv.querySelector('img[src*="/images/checkers/pieces/"]');
if (!img) continue;
const style = pieceDiv.getAttribute('style');
if (!style) continue;
// Parse position from style: left:calc(100% / 19 * X);top:calc(100% / 19 * Y)
const leftMatch = style.match(/left:\s*calc\(100%\s*\/\s*19\s*\*\s*(\d+)\)/);
const topMatch = style.match(/top:\s*calc\(100%\s*\/\s*19\s*\*\s*(\d+)\)/);
if (!leftMatch || !topMatch) continue;
const col = parseInt(leftMatch[1], 10);
const row = parseInt(topMatch[1], 10);
const coords = gridToCoords(col, row);
// Parse piece type from src
const src = img.getAttribute('src');
const isKing = src.includes('king-');
const colorMatch = src.match(/(man|king)-(\w+)\.webp/);
if (!colorMatch) continue;
const color = colorMatch[2]; // blue, yellow, red, green
state.set(coords, { color, isKing });
}
return state;
}
// Build team color mapping by simulating moves forward from current state
function buildTeamColorMapping(currentBoardState, eventsData) {
const mapping = {};
const boardState = new Map(currentBoardState); // Clone current state
// Process events from newest to oldest (reverse to go backwards in time)
for (let i = 0; i < eventsData.length; i++) {
const event = eventsData[i];
if (event.type === 'move' && event.player?.teamCheckersId !== undefined) {
const teamId = event.player.teamCheckersId;
// Skip if we already know this team's color
if (mapping[teamId]) continue;
// For moves, check what piece is at the destination (current state)
if (event.moveType === 'simple' || event.moveType === 'capture') {
const endPos = event.endCoords?.toUpperCase();
if (endPos && boardState.has(endPos)) {
const piece = boardState.get(endPos);
mapping[teamId] = piece.color;
}
} else if (event.moveType === 'resurrection') {
const pos = event.startCoords?.toUpperCase();
if (pos && boardState.has(pos)) {
const piece = boardState.get(pos);
mapping[teamId] = piece.color;
}
}
}
}
return mapping;
}
// ============================================
// LOG PARSING (from embedded Next.js JSON data)
// ============================================
function parseEventsFromNextData(currentBoardState) {
const moves = [];
// Helper to try parsing JSON with cleanup
const tryParse = (str) => {
try {
return JSON.parse(str);
} catch (e) {
try {
// Handle escaped quotes common in embedded JSON
// This matches \" and replaces with "
// Note: This is a simple heuristic and might break complex nested escaping
return JSON.parse(str.replace(/\\"/g, '"'));
} catch (e2) {
try {
// Double escape?
return JSON.parse(str.replace(/\\"/g, '"').replace(/\\\\/g, '\\'));
} catch (e3) {
return null;
}
}
}
};
// Helper to find events array within a generic object/array
const findEventsArray = (obj) => {
if (!obj || typeof obj !== 'object') return null;
if (Array.isArray(obj)) {
// Check if this array looks like the events list
// It should have objects with 'moveType' or 'startCoords'
if (obj.length > 0 && obj[0] && typeof obj[0] === 'object' && ('moveType' in obj[0] || 'startCoords' in obj[0])) {
return obj;
}
// Recursively check elements
for (const item of obj) {
const found = findEventsArray(item);
if (found) return found;
}
} else {
// Recursively check properties
for (const key in obj) {
const found = findEventsArray(obj[key]);
if (found) return found;
}
}
return null;
};
// Helper to extract a potential JSON/Array string by scanning for brackets around a keyword
const extractArrayAround = (content, keyword) => {
const index = content.indexOf(keyword);
if (index === -1) return null;
const searchStart = Math.max(0, index - 50000);
const searchEnd = Math.min(content.length, index + 50000);
// Find all opening brackets before the keyword
const openBrackets = [];
for (let i = index; i >= searchStart; i--) {
if (content[i] === '[') openBrackets.push(i);
}
// Try closest brackets first (innermost)
for (const start of openBrackets) {
let balance = 1;
let inQuote = false;
let currentPos = start + 1;
while (currentPos < searchEnd) {
const char = content[currentPos];
// Check for unescaped quote
let escapeCount = 0;
let backScan = currentPos - 1;
while (backScan >= start && content[backScan] === '\\') {
escapeCount++;
backScan--;
}
const isEscaped = escapeCount % 2 === 1;
if (char === '"' && !isEscaped) {
inQuote = !inQuote;
} else if (!inQuote) {
if (char === '[') balance++;
else if (char === ']') balance--;
}
if (balance === 0) {
const candidate = content.substring(start, currentPos + 1);
const parsed = tryParse(candidate);
if (parsed) {
const found = findEventsArray(parsed);
if (found) return found;
}
break;
}
currentPos++;
}
}
return null;
};
// Find all script tags with Next.js data
const scripts = document.querySelectorAll('script');
let eventsData = null;
// Strategy 1: Look for "events" key in scripts (existing logic with robust tryParse)
for (const script of scripts) {
const content = script.textContent || '';
const eventsMatch = content.match(/"events":\s*(\[[\s\S]*?\])\s*[,\}]/);
if (eventsMatch) {
eventsData = tryParse(eventsMatch[1]);
if (eventsData) break;
}
const eventsMatchEscaped = content.match(/\\"events\\":\s*(\[[\s\S]*?\])\s*[,\}]/);
if (eventsMatchEscaped) {
eventsData = tryParse(eventsMatchEscaped[1]);
if (eventsData) break;
}
}
// Strategy 2: Look for string literal containing "startCoords" in scripts (Fallback)
if (!eventsData) {
for (const script of scripts) {
const content = script.textContent || '';
// Try regex first for speed
if (content.includes('startCoords')) {
const stringLiteralMatch = content.match(/"((?:\\.|[^"\\])*?startCoords(?:\\.|[^"\\])*?)"/);
if (stringLiteralMatch) {
const parsedRoot = tryParse(stringLiteralMatch[1]);
if (parsedRoot) {
eventsData = findEventsArray(parsedRoot);
if (eventsData) break;
}
}
// Fallback to bracket scanning
if (!eventsData) {
eventsData = extractArrayAround(content, 'startCoords');
if (eventsData) {
console.log('RGG Rewind: Found events via bracket scan in script');
break;
}
}
}
}
}
// Strategy 3: Check self.__next_f (with findEventsArray)
if (!eventsData && typeof self !== 'undefined' && self.__next_f) {
for (const item of self.__next_f) {
if (Array.isArray(item) && item[1]) {
const str = String(item[1]);
const eventsMatch = str.match(/"events":\s*(\[[\s\S]*?\])\s*[,\}]/);
if (eventsMatch) {
eventsData = tryParse(eventsMatch[1]);
if (eventsData) break;
}
if (str.includes('startCoords')) {
const stringLiteralMatch = str.match(/"((?:\\.|[^"\\])*?startCoords(?:\\.|[^"\\])*?)"/);
if (stringLiteralMatch) {
const parsedRoot = tryParse(stringLiteralMatch[1]);
if (parsedRoot) {
eventsData = findEventsArray(parsedRoot);
if (eventsData) break;
}
}
if (!eventsData) {
eventsData = extractArrayAround(str, 'startCoords');
if (eventsData) {
console.log('RGG Rewind: Found events via bracket scan in __next_f');
break;
}
}
}
}
}
}
if (!eventsData || !Array.isArray(eventsData)) {
// Last ditch effort: if we found an object that has an "events" property which is an array
if (eventsData && typeof eventsData === 'object' && Array.isArray(eventsData.events)) {
eventsData = eventsData.events;
} else {
console.error('RGG Rewind: Could not find events data', eventsData);
return null; // Return null to signal retry needed
}
}
console.log('RGG Rewind: Found', eventsData.length, 'events in data');
// Build team color mapping dynamically from current board and moves
const TEAM_COLORS = buildTeamColorMapping(currentBoardState, eventsData);
// Parse events - they are already in newest-first order
for (const event of eventsData) {
if (event.type === 'move') {
const moveType = event.moveType;
// Handle different move types
switch (moveType) {
case 'resurrection':
{
const resurrectEvent = {
type: 'resurrect',
position: event.startCoords?.toUpperCase(),
color: null,
isKing: false
};
// Get color from player's team
if (event.player?.teamCheckersId !== undefined) {
resurrectEvent.color = TEAM_COLORS[event.player.teamCheckersId];
} else {
console.warn('RGG Rewind: Unknown team for resurrection event:', event);
}
if (resurrectEvent.position) {
moves.push(resurrectEvent);
}
}
break;
case 'bomb':
{
const bombEvent = {
type: 'bomb',
position: event.startCoords?.toUpperCase(),
bombedColor: null,
bombedIsKing: false
};
if (bombEvent.position) {
moves.push(bombEvent);
}
}
break;
case 'capture':
{
const move = {
type: 'move',
from: event.startCoords?.toUpperCase(),
to: event.endCoords?.toUpperCase(),
color: null,
capture: event.captureCoords?.toUpperCase() || null,
captureColor: null,
captureIsKing: false
};
// Get color from player's team
if (event.player?.teamCheckersId !== undefined) {
move.color = TEAM_COLORS[event.player.teamCheckersId];
} else {
console.warn('RGG Rewind: Unknown team for capture event:', event);
}
// Get color from captureTeam.checkersId
if (event.captureTeam?.checkersId !== undefined) {
move.captureColor = TEAM_COLORS[event.captureTeam.checkersId];
} else {
console.warn('RGG Rewind: Unknown team for capture event:', event);
}
// Check if it was a king
move.captureIsKing = event.captureIsKing || false;
if (move.from && move.to) {
moves.push(move);
}
}
break;
case 'simple':
{
// Regular move without capture
const move = {
type: 'move',
from: event.startCoords?.toUpperCase(),
to: event.endCoords?.toUpperCase(),
color: null,
capture: null,
captureColor: null,
captureIsKing: false
};
// Get color from player's team
if (event.player?.teamCheckersId !== undefined) {
move.color = TEAM_COLORS[event.player.teamCheckersId];
} else {
console.warn('RGG Rewind: Unknown team for simple move event:', event);
}
if (move.from && move.to) {
moves.push(move);
}
}
break;
default:
console.warn('RGG Rewind: Unknown moveType:', moveType, 'for event:', event);
break;
}
} else if (event.type === 'game') {
// Ignore game start/end events
continue;
} else {
console.warn('RGG Rewind: Unknown event type:', event.type, 'for event:', event);
}
}
// Second pass: enrich bomb events with actual piece information
// by tracking the last known piece at each position
enrichEvents(moves);
return moves;
}
// Helper to check if a move enters the promotion zone
function isPromotionZone(coords, color) {
const grid = coordsToGrid(coords);
if (!grid) return false;
switch (color) {
case 'red': return grid.row === 0;
case 'blue': return grid.row === 18;
case 'green': return grid.col === 18;
case 'yellow': return grid.col === 0;
default: return false;
}
}
// Enrich bomb events with piece info by tracking last known piece at each position
function enrichEvents(moves) {
// Track last known piece at each position
const lastPieceAt = new Map(); // position -> {color, isKing}
// Initialize with starting positions (for pieces bombed before first move)
// Triangle formation for 4-player checkers
const initializeStartingPositions = () => {
// Red pieces (bottom) - triangle pointing up
const redPositions = [
'F1', 'H1', 'J1', 'L1', 'N1',
'G2', 'I2', 'K2', 'M2',
'H3', 'J3', 'L3',
'I4', 'K4',
'J5'
];
// Blue pieces (top) - triangle pointing down
const bluePositions = [
'F19', 'H19', 'J19', 'L19', 'N19',
'G18', 'I18', 'K18', 'M18',
'H17', 'J17', 'L17',
'I16', 'K16',
'J15'
];
// Green pieces (left) - triangle pointing right
const greenPositions = [
'A6', 'A8', 'A10', 'A12', 'A14',
'B7', 'B9', 'B11', 'B13',
'C8', 'C10', 'C12',
'D9', 'D11',
'E10'
];
// Yellow pieces (right) - triangle pointing left
const yellowPositions = [
'S6', 'S8', 'S10', 'S12', 'S14',
'R7', 'R9', 'R11', 'R13',
'Q8', 'Q10', 'Q12',
'P9', 'P11',
'O10'
];
redPositions.forEach(pos => lastPieceAt.set(pos, { color: 'red', isKing: false }));
bluePositions.forEach(pos => lastPieceAt.set(pos, { color: 'blue', isKing: false }));
greenPositions.forEach(pos => lastPieceAt.set(pos, { color: 'green', isKing: false }));
yellowPositions.forEach(pos => lastPieceAt.set(pos, { color: 'yellow', isKing: false }));
};
initializeStartingPositions();
// Process moves in reverse (oldest to newest)
for (let i = moves.length - 1; i >= 0; i--) {
const move = moves[i];
if (move.type === 'move') {
const piece = lastPieceAt.get(move.from);
if (piece) {
// Check for promotion
if (!piece.isKing && isPromotionZone(move.to, piece.color)) {
move.isPromotion = true;
piece.isKing = true;
}
// Move piece
lastPieceAt.delete(move.from);
lastPieceAt.set(move.to, { ...piece });
}
if (move.capture) {
lastPieceAt.delete(move.capture);
}
} else if (move.type === 'resurrect') {
// Remember resurrected piece
lastPieceAt.set(move.position, {
color: move.color,
isKing: move.isKing
});
} else if (move.type === 'bomb') {
// Enrich bomb with last known piece at this position
const piece = lastPieceAt.get(move.position);
if (piece) {
move.bombedColor = piece.color;
move.bombedIsKing = piece.isKing;
lastPieceAt.delete(move.position);
}
}
}
}
// ============================================
// STATE COMPUTATION
// ============================================
function cloneState(state) {
const newState = new Map();
for (const [coords, piece] of state) {
newState.set(coords, { ...piece });
}
return newState;
}
function computeAllStates(currentState, moves) {
const statesArray = [];
let state = cloneState(currentState);
statesArray.push(cloneState(state));
for (const move of moves) {
if (move.type === 'move') {
const piece = state.get(move.to);
if (piece) {
// Update piece if it was a promotion
if (move.isPromotion) {
piece.isKing = false;
}
state.delete(move.to);
state.set(move.from, piece);
if (move.capture && move.captureColor) {
state.set(move.capture, {
color: move.captureColor,
isKing: move.captureIsKing
});
}
}
} else if (move.type === 'resurrect') {
// Unapply resurrect: remove the piece that was added
state.delete(move.position);
} else if (move.type === 'bomb') {
// Unapply bomb: restore the piece that was removed
if (move.bombedColor) {
state.set(move.position, {
color: move.bombedColor,
isKing: move.bombedIsKing
});
} else {
// If we don't know the color, we can't fully restore the state
// This is a limitation - we'd need more data from the event
console.warn('RGG Rewind: Cannot fully restore bomb at', move.position, '- unknown piece color');
}
}
statesArray.push(cloneState(state));
}
statesArray.reverse();
return statesArray;
}
// ============================================
// BOARD RENDERING
// ============================================
function renderState(stateIndex) {
if (!boardContainer || stateIndex < 0 || stateIndex >= states.length) return;
const state = states[stateIndex];
currentViewIndex = stateIndex;
// Remove all existing piece divs (but keep coordinate overlay)
const existingPieces = boardContainer.querySelectorAll('div.absolute');
for (const pieceDiv of existingPieces) {
// Check if this is a piece div (has img with piece image)
const img = pieceDiv.querySelector('img[src*="/images/checkers/pieces/"]');
// Check if this is a highlight overlay
const isHighlight = pieceDiv.classList.contains('highlight-overlay');
if (img || isHighlight) {
pieceDiv.remove();
}
}
// Helper to add highlight
const addHighlight = (coords) => {
const grid = coordsToGrid(coords);
if (!grid) return;
const div = document.createElement('div');
div.className = 'absolute highlight-overlay';
div.style.cssText = `height:calc(100% / 19);left:calc(100% / 19 * ${grid.col});top:calc(100% / 19 * ${grid.row});width:calc(100% / 19);background-color:rgba(255, 235, 59, 0.5);mix-blend-mode:hard-light;pointer-events:none;z-index:0;`;
// Insert before coordinate overlay (last child)
const coordOverlay = boardContainer.querySelector('div.absolute.inset-0');
if (coordOverlay) {
boardContainer.insertBefore(div, coordOverlay);
} else {
boardContainer.appendChild(div);
}
};
// Render highlights for the move that created this state
if (stateIndex > 0 && moveHistory.length > 0) {
// states[0] is initial. states[1] is after move N (where N is moves.length)
// wait, moves are reversed (newest at 0).
// so states[1] is result of oldest move (index moves.length - 1)
// states[states.length - 1] is result of newest move (index 0)
const moveIndex = moveHistory.length - stateIndex;
if (moveIndex >= 0 && moveIndex < moveHistory.length) {
const move = moveHistory[moveIndex];
if (move.type === 'move') {
if (move.from) addHighlight(move.from);
if (move.to) addHighlight(move.to);
} else if (move.type === 'resurrect' || move.type === 'bomb' || move.type === 'promotion') {
if (move.position) addHighlight(move.position);
}
}
}
// Create new piece divs for current state
for (const [coords, piece] of state) {
const grid = coordsToGrid(coords);
if (!grid) continue;
const pieceDiv = document.createElement('div');
pieceDiv.className = 'absolute';
pieceDiv.style.cssText = `height:calc(100% / 19);left:calc(100% / 19 * ${grid.col});top:calc(100% / 19 * ${grid.row});width:calc(100% / 19)`;
const img = document.createElement('img');
const pieceType = piece.isKing ? 'king' : 'man';
img.alt = `шашка`;
img.className = 'object-contain size-full';
img.src = `/images/checkers/pieces/${pieceType}-${piece.color}.webp`;
pieceDiv.appendChild(img);
// Insert before coordinate overlay (last child)
const coordOverlay = boardContainer.querySelector('div.absolute.inset-0');
if (coordOverlay) {
boardContainer.insertBefore(pieceDiv, coordOverlay);
} else {
boardContainer.appendChild(pieceDiv);
}
}
updateButtonStates();
}
// ============================================
// UI CONTROLS
// ============================================
function updateButtonStates() {
if (!btnBack || !btnForward || !btnCurrent) return;
const atStart = currentViewIndex <= 0;
const atEnd = currentViewIndex >= states.length - 1;
btnBack.disabled = atStart;
btnBack.style.opacity = atStart ? '0.4' : '1';
btnBack.style.cursor = atStart ? 'not-allowed' : 'pointer';
btnForward.disabled = atEnd;
btnForward.style.opacity = atEnd ? '0.4' : '1';
btnForward.style.cursor = atEnd ? 'not-allowed' : 'pointer';
btnCurrent.disabled = atEnd;
btnCurrent.style.opacity = atEnd ? '0.4' : '1';
btnCurrent.style.cursor = atEnd ? 'not-allowed' : 'pointer';
// Update move counter
if (moveCounter) {
moveCounter.textContent = `${currentViewIndex} / ${states.length - 1}`;
}
}
function createUI() {
// Find the log title
const headings = document.querySelectorAll('h6.MuiTypography-h6');
let logTitle = null;
for (const h of headings) {
if (h.textContent.includes('Лог событий')) {
logTitle = h;
break;
}
}
if (!logTitle) {
console.error('RGG Rewind: Log title not found');
return;
}
// Create button container
const container = document.createElement('div');
container.style.cssText = `
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 12px;
`;
// Button styles
const buttonStyle = `
padding: 6px 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
background: transparent;
color: #fff;
font-size: 16px;
cursor: pointer;
transition: background-color 0.15s, opacity 0.15s;
`;
// Back button
btnBack = document.createElement('button');
btnBack.innerHTML = '◀';
btnBack.title = 'Предыдущий ход';
btnBack.style.cssText = buttonStyle;
btnBack.onmouseenter = () => { if (!btnBack.disabled) btnBack.style.backgroundColor = 'rgba(255,255,255,0.08)'; };
btnBack.onmouseleave = () => { btnBack.style.backgroundColor = 'transparent'; };
btnBack.onclick = () => {
if (currentViewIndex > 0) {
renderState(currentViewIndex - 1);
}
};
// Forward button
btnForward = document.createElement('button');
btnForward.innerHTML = '▶';
btnForward.title = 'Следующий ход';
btnForward.style.cssText = buttonStyle;
btnForward.onmouseenter = () => { if (!btnForward.disabled) btnForward.style.backgroundColor = 'rgba(255,255,255,0.08)'; };
btnForward.onmouseleave = () => { btnForward.style.backgroundColor = 'transparent'; };
btnForward.onclick = () => {
if (currentViewIndex < states.length - 1) {
renderState(currentViewIndex + 1);
}
};
// Current state button
btnCurrent = document.createElement('button');
btnCurrent.innerHTML = '⏹';
btnCurrent.title = 'Текущее состояние';
btnCurrent.style.cssText = buttonStyle;
btnCurrent.onmouseenter = () => { if (!btnCurrent.disabled) btnCurrent.style.backgroundColor = 'rgba(255,255,255,0.08)'; };
btnCurrent.onmouseleave = () => { btnCurrent.style.backgroundColor = 'transparent'; };
btnCurrent.onclick = () => {
if (currentViewIndex !== states.length - 1) {
renderState(states.length - 1);
}
};
container.appendChild(btnBack);
container.appendChild(btnForward);
container.appendChild(btnCurrent);
// Move counter
moveCounter = document.createElement('div');
moveCounter.style.cssText = `
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
padding: 6px 12px;
display: flex;
align-items: center;
`;
moveCounter.textContent = `${currentViewIndex} / ${states.length - 1}`;
container.appendChild(moveCounter);
// Insert after the log title
logTitle.insertAdjacentElement('afterend', container);
updateButtonStates();
}
// ============================================
// INITIALIZATION
// ============================================
function initialize(retryCount = 0) {
console.log('RGG Rewind: Initializing... (attempt', retryCount + 1, ')');
// Parse current board state
const currentState = parseCurrentBoardState();
if (!currentState || currentState.size === 0) {
console.error('RGG Rewind: Could not parse board state');
return;
}
console.log('RGG Rewind: Parsed', currentState.size, 'pieces');
// Parse move history from embedded data
moveHistory = parseEventsFromNextData(currentState);
// If data not found and we haven't retried too many times, try again
if (moveHistory === null && retryCount < 5) {
console.log('RGG Rewind: Events data not ready, retrying in 1s...');
setTimeout(() => initialize(retryCount + 1), 1000);
return;
}
if (moveHistory === null) {
console.error('RGG Rewind: Could not find events data after multiple retries');
return;
}
console.log('RGG Rewind: Parsed', moveHistory.length, 'moves');
// for (let i = moveHistory.length - 1; i >= 0; i--) {
// console.log('RGG Rewind: Move', moveHistory.length - i, ':', moveHistory[i]);
// }
// Compute all states
states = computeAllStates(currentState, moveHistory);
currentViewIndex = states.length - 1; // Start at current state
console.log('RGG Rewind: Computed', states.length, 'states');
// Create UI
createUI();
console.log('RGG Rewind: Initialization complete');
}
// Wait for page to load
function waitForPage() {
const board = document.querySelector('div[style*="background-image:url(/images/checkers/board"]');
const log = document.querySelector('div[data-testid="virtuoso-item-list"]');
const logTitle = Array.from(document.querySelectorAll('h6.MuiTypography-h6')).find(h => h.textContent.includes('Лог событий'));
if (board && log && logTitle) {
// Additional delay to ensure React hydration is complete
setTimeout(() => {
// Check one more time before initializing
const stillExists = document.querySelector('div[style*="background-image:url(/images/checkers/board"]');
if (stillExists) {
initialize();
} else {
// React re-rendered, wait and try again
setTimeout(waitForPage, 500);
}
}, 1500);
} else {
setTimeout(waitForPage, 500);
}
}
// Start
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', waitForPage);
} else {
waitForPage();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment