Skip to content

Instantly share code, notes, and snippets.

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

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

Select an option

Save strayge/67aa73e55e1454b22c57a150aec16680 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name RGG Chess Stats
// @namespace http://tampermonkey.net/
// @version 2026-01-21
// @description Extra stats for each team in RGG Chess
// @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.github.com/strayge/67aa73e55e1454b22c57a150aec16680/raw/rgg_chess_stats.user.js
// @downloadURL https://gist.github.com/strayge/67aa73e55e1454b22c57a150aec16680/raw/rgg_chess_stats.user.js
// ==/UserScript==
(function() {
'use strict';
// Parse player stats from text like "Пройдено 4 игры, дропнуто 0 игр, забрано 5 шашек"
function parsePlayerStats(text) {
const stats = {
completed: 0,
dropped: 0,
captured: 0
};
// Match "Пройдено/Пройдена X игр/игра/игры" (completed games)
// Handles different Russian word forms: игра (1), игры (2-4), игр (0, 5+)
const completedMatch = text.match(/Пройден[ао]\s+(\d+)\s+игр/i);
if (completedMatch) {
stats.completed = parseInt(completedMatch[1], 10);
}
// Match "дропнуто/дропнута X игр/игра/игры" (dropped games)
const droppedMatch = text.match(/дропнут[ао]\s+(\d+)\s+игр/i);
if (droppedMatch) {
stats.dropped = parseInt(droppedMatch[1], 10);
}
// Match "забрано/забрана X шашек/шашка/шашки" (captured checkers)
const capturedMatch = text.match(/забран[ао]\s+(\d+)\s+шаш/i);
if (capturedMatch) {
stats.captured = parseInt(capturedMatch[1], 10);
}
return stats;
}
// Build a map of player name -> stats from the Players section
function buildPlayerStatsMap() {
const playerStats = {};
// Find the "Игроки" (Players) section
const headers = document.querySelectorAll('h6');
let playersSection = null;
for (const header of headers) {
if (header.textContent.includes('Игроки')) {
playersSection = header.parentElement;
break;
}
}
if (!playersSection) {
console.log('RGG Chess Stats: Players section not found');
return playerStats;
}
console.log('RGG Chess Stats: Found players section');
// Search for ALL text nodes containing stats, not just in accordions
// This works even if accordions are collapsed
const allElements = playersSection.querySelectorAll('*');
console.log('RGG Chess Stats: Searching through', allElements.length, 'elements for stats text');
allElements.forEach((el) => {
const text = el.textContent;
// Check if this element contains the stats pattern
// Use 'Пройден' instead of 'Пройдено' to match both Пройдено/Пройдена
// Use 'забран' instead of 'забрано' to match both забрано/забрана
if (text.includes('Пройден') && text.includes('дропнут') && text.includes('забран')) {
// Now find the player name - look upward in the DOM tree
let currentEl = el;
let playerName = '';
// Walk up to find the accordion containing this stats text
while (currentEl && !currentEl.classList.contains('MuiAccordion-root')) {
currentEl = currentEl.parentElement;
}
if (currentEl) {
// Found the accordion, now get player name
const avatar = currentEl.querySelector('.MuiAvatar-img');
const playerNameEl = currentEl.querySelector('.MuiAccordionSummary-content .inline span');
if (avatar && avatar.alt) {
playerName = avatar.alt.toLowerCase();
} else if (playerNameEl) {
playerName = playerNameEl.textContent.trim().toLowerCase();
}
if (playerName && !playerStats[playerName]) {
const stats = parsePlayerStats(text);
playerStats[playerName] = stats;
// console.log('RGG Chess Stats: Player', playerName, '-> completed:', stats.completed, 'dropped:', stats.dropped, 'captured:', stats.captured);
}
}
}
});
console.log('RGG Chess Stats: Built player stats map with', Object.keys(playerStats).length, 'players');
return playerStats;
}
// Count checkers on the board per team
function countBoardCheckers() {
const checkerCounts = {};
// Find all team accordions to get team names and their colors
const headers = document.querySelectorAll('h6');
let teamsSection = null;
for (const header of headers) {
if (header.textContent.includes('Команды')) {
teamsSection = header.parentElement;
break;
}
}
if (!teamsSection) {
console.log('RGG Chess Stats: Teams section not found for board counting');
return checkerCounts;
}
const teamAccordions = teamsSection.querySelectorAll('.MuiAccordion-root');
// Color to team index mapping
// team 0 = blue, team 1 = yellow, team 2 = red, team 3 = green
const colorToIndex = {
'blue': 0,
'green': 3,
'red': 2,
'yellow': 1
};
// Build mapping from team name to team index by extracting crown icon color
const teamNameToIndex = {};
teamAccordions.forEach((teamAccordion) => {
const summaryContent = teamAccordion.querySelector('.MuiAccordionSummary-content');
if (!summaryContent) return;
const teamNameContainer = summaryContent.querySelector('.inline');
if (!teamNameContainer) return;
const teamName = teamNameContainer.textContent.trim();
// Find the crown SVG icon after the team name
const crownSvg = teamNameContainer.querySelector('svg');
if (crownSvg) {
// Get the computed color of the crown
const color = window.getComputedStyle(crownSvg).color;
// console.log('RGG Chess Stats: Team', teamName, 'has crown color:', color);
// Map RGB colors to team indices
// green: rgb(23, 150, 70) = #179646
// red: rgb(213, 32, 32) = #d52020
// blue: rgb(59, 130, 246) = #3b82f6
// yellow: rgb(237, 176, 7) = #edb007
let teamColor = null;
if (color.includes('59, 130, 246') || color.includes('59,130,246')) {
teamColor = 'blue';
} else if (color.includes('237, 176, 7') || color.includes('237,176,7')) {
teamColor = 'yellow';
} else if (color.includes('213, 32, 32') || color.includes('213,32,32')) {
teamColor = 'red';
} else if (color.includes('23, 150, 70') || color.includes('23,150,70')) {
teamColor = 'green';
}
if (teamColor && colorToIndex.hasOwnProperty(teamColor)) {
teamNameToIndex[teamName] = colorToIndex[teamColor];
// console.log('RGG Chess Stats: Team', teamName, 'mapped to index', colorToIndex[teamColor], '(' + teamColor + ')');
}
}
});
// Find all checkers on the board by alt text pattern "шашка команды X" or "дамка команды X"
const allCheckers = document.querySelectorAll('img[alt^="шашка команды"], img[alt^="дамка команды"]');
console.log('RGG Chess Stats: Found', allCheckers.length, 'total checkers on board');
// Count checkers per team index
const teamIndexCounts = {};
allCheckers.forEach(checker => {
const alt = checker.alt;
// Extract team index from "шашка команды 0", "дамка команды 0", etc.
const match = alt.match(/(?:шашка|дамка) команды (\d+)/);
if (match) {
const teamIndex = parseInt(match[1], 10);
teamIndexCounts[teamIndex] = (teamIndexCounts[teamIndex] || 0) + 1;
}
});
// console.log('RGG Chess Stats: Checkers per team index:', teamIndexCounts);
// Map team indices to team names
for (const [teamName, teamIndex] of Object.entries(teamNameToIndex)) {
const count = teamIndexCounts[teamIndex] || 0;
checkerCounts[teamName] = count;
// console.log('RGG Chess Stats: Team', teamName, '(index', teamIndex + ') has', count, 'checkers on board');
}
return checkerCounts;
}
// Find all teams and calculate stats
function calculateTeamStats() {
console.log('RGG Chess Stats: Starting calculateTeamStats');
// First, build a map of all player stats from the Players section
const playerStatsMap = buildPlayerStatsMap();
// Count checkers on the board
const boardCheckerCounts = countBoardCheckers();
// Find the "Команды" section
const headers = document.querySelectorAll('h6');
let teamsSection = null;
for (const header of headers) {
if (header.textContent.includes('Команды')) {
teamsSection = header.parentElement;
break;
}
}
if (!teamsSection) {
console.log('RGG Chess Stats: Teams section not found');
return;
}
// console.log('RGG Chess Stats: Found teams section');
// Find all team accordions within the teams section
const teamAccordions = teamsSection.querySelectorAll('.MuiAccordion-root');
console.log('RGG Chess Stats: Found', teamAccordions.length, 'team accordions');
// Object to collect all team data for console dump
const allTeamsData = {};
teamAccordions.forEach((teamAccordion, teamIdx) => {
// Get team name element (in the accordion summary)
const summaryContent = teamAccordion.querySelector('.MuiAccordionSummary-content');
if (!summaryContent) {
console.log('RGG Chess Stats: No summary content for team', teamIdx);
return;
}
// Find the team name span (contains team name like "Болотная братва")
const teamNameContainer = summaryContent.querySelector('.inline');
if (!teamNameContainer) {
console.log('RGG Chess Stats: No team name container for team', teamIdx);
return;
}
const teamName = teamNameContainer.textContent.trim();
// console.log('RGG Chess Stats: Processing team:', teamName);
// Get board checker count for this team
const boardCheckers = boardCheckerCounts[teamName] || 0;
let totalCompleted = 0;
let totalDropped = 0;
let totalCaptured = 0;
// Get player names from this team (in the accordion details)
const playerLinks = teamAccordion.querySelectorAll('.MuiListItemButton-root');
// console.log('RGG Chess Stats: Team', teamName, 'has', playerLinks.length, 'player links');
playerLinks.forEach((playerLink) => {
// Get player name from avatar alt or text
const avatar = playerLink.querySelector('.MuiAvatar-img');
let playerName = '';
if (avatar && avatar.alt) {
playerName = avatar.alt.toLowerCase();
}
if (playerName && playerStatsMap[playerName]) {
const stats = playerStatsMap[playerName];
totalCompleted += stats.completed;
totalDropped += stats.dropped;
totalCaptured += stats.captured;
// console.log('RGG Chess Stats: Added stats for', playerName, 'to team', teamName);
} else if (playerName) {
console.log('RGG Chess Stats: No stats found for player', playerName, 'in team', teamName);
}
});
console.log('RGG Chess Stats: Team', teamName, 'totals -> completed:', totalCompleted, 'dropped:', totalDropped, 'captured:', totalCaptured);
// Calculate additional statistics
const dropPenalty = Math.round(3 * totalCaptured * totalCompleted / (totalDropped + 1) / (totalDropped + 2));
const lostCheckerPenalty = totalCompleted;
const capturedCheckerBonus = Math.round(3 * totalCompleted / (totalDropped + 1));
const completedGameBonus = Math.round(3 * totalCaptured / (totalDropped + 1) + boardCheckers);
// Extract points from UI (e.g., "2138 очков")
let points = 0;
// Find all spans and look for the one containing "очков"
const allSpans = summaryContent.querySelectorAll('span');
for (const span of allSpans) {
if (span.textContent.includes('очк')) {
const pointsMatch = span.textContent.match(/(\d+)\s*очк/);
if (pointsMatch) {
points = parseInt(pointsMatch[1], 10);
break;
}
}
}
// Store team data for console dump
allTeamsData[teamName] = {
points: points,
onBoard: boardCheckers,
completed: totalCompleted,
dropped: totalDropped,
taken: totalCaptured,
dropCost: dropPenalty,
lostCheckers: lostCheckerPenalty,
takenCheckers: capturedCheckerBonus,
gameClosure: completedGameBonus
};
// Remove existing stats display if present
const existingStats = teamNameContainer.parentElement.querySelector('.rgg-team-stats');
if (existingStats) {
existingStats.remove();
}
// Create stats display element
const statsEl = document.createElement('span');
statsEl.className = 'rgg-team-stats';
statsEl.style.cssText = 'font-size: 1.05em; white-space: nowrap; display: inline-flex; align-items: center; gap: 2px; position: absolute; right: 45px; top: 15px;';
// Create SVG icons
// Base shapes
const oldCheck = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="display: block;"><path d="M13.5 4L6 11.5L2.5 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
const oldCross = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="display: block;"><path d="M12 4L4 12M4 4L12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
// 1. Board Status Icons (Square)
const boardIcon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="display: block;"><rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="8" cy="8" r="3" fill="currentColor"/></svg>`;
const checkIcon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="display: block;"><rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><path d="M11 6L7 10L5 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
const crossIcon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="display: block;"><rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><path d="M10 6L6 10M6 6L10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
const captureIcon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="display: block;"><rect x="2" y="2" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="8" cy="8" r="3" stroke="currentColor" stroke-width="1.5"/><path d="M8 4V12M4 8H12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
// 2. Projected/Bonus Icons (No Square)
const dropPenaltyIcon = oldCross; // Same as old cross icon
const lostCheckerPenaltyIcon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="display: block;"><circle cx="8" cy="8" r="3.5" stroke="currentColor" stroke-width="2"/><path d="M3 3L13 13M13 3L3 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
const capturedBonusIcon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="display: block;"><circle cx="8" cy="8" r="4" stroke="currentColor" stroke-width="2"/><path d="M8 2V14M2 8H14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
const completedBonusIcon = oldCheck; // Same as old check icon
const divStyle = 'display: flex; flex-direction: column; align-items: center; font-size: 0.85em;';
statsEl.innerHTML = `
<div style="color: #a78bfa; min-width: 23px; ${divStyle}" title="Шашек на доске">
${boardIcon}
<span>${boardCheckers}</span>
</div>
<div style="color: #4ade80; min-width: 23px; ${divStyle}" title="Пройдено игр">
${checkIcon}
<span>${totalCompleted}</span>
</div>
<div style="color: #f87171; min-width: 23px; ${divStyle}" title="Дропнуто игр">
${crossIcon}
<span>${totalDropped}</span>
</div>
<div style="color: #72bd9b; min-width: 23px; margin-right: 10px; ${divStyle}" title="Забрано шашек">
${captureIcon}
<span>${totalCaptured}</span>
</div>
<div style="color: #fb923c; min-width: 25px; ${divStyle}" title="Дроп отнимет">
${dropPenaltyIcon}
<span>${dropPenalty}</span>
</div>
<div style="color: #fb923c; min-width: 25px; ${divStyle}" title="Потерянная шашка отнимет">
${lostCheckerPenaltyIcon}
<span>${lostCheckerPenalty}</span>
</div>
<div style="color: #4ade80; min-width: 25px; ${divStyle}" title="Съеденная шашка даст">
${capturedBonusIcon}
<span>${capturedCheckerBonus}</span>
</div>
<div style="color: #4ade80; min-width: 25px; ${divStyle}" title="Закрытая игра даст (без учета времени)">
${completedBonusIcon}
<span>${completedGameBonus}</span>
</div>
`;
const parent = teamNameContainer.parentElement.parentElement;
// Insert after team name
parent.appendChild(statsEl);
});
// Dump all parsed data to console
console.log('=== PARSED TEAM DATA ===');
console.log(allTeamsData);
console.log('RGG Chess Stats: Finished calculateTeamStats');
}
// Run when page is loaded
function init() {
// Wait for content to load (SPA might load content dynamically)
const checkInterval = setInterval(() => {
const teamsHeader = document.querySelector('h6');
if (teamsHeader && teamsHeader.textContent.includes('Команды')) {
clearInterval(checkInterval);
// Wait longer for React hydration to complete
setTimeout(calculateTeamStats, 1000);
}
}, 500);
// Stop checking after 30 seconds
setTimeout(() => clearInterval(checkInterval), 30000);
}
// Start initialization
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment