Last active
January 21, 2026 00:31
-
-
Save strayge/67aa73e55e1454b22c57a150aec16680 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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