Skip to content

Instantly share code, notes, and snippets.

@Krinkle
Last active July 8, 2025 03:56
Show Gist options
  • Save Krinkle/a170d6720034239a0f53f095d12d58a3 to your computer and use it in GitHub Desktop.
Save Krinkle/a170d6720034239a0f53f095d12d58a3 to your computer and use it in GitHub Desktop.
Matt Gray's 2025 Conga on onemillionchessboards.com
// One time:
// * Open the web inspector.
// * Paste the below code in the console.
//
// Usage:
// 1. Find a white pawn at the front of one of the conga lines in column 3425.
// Matt started around 3425,4464
// and as of 6 Jul 2025 we have migrated north until 3425,3740.
// 2. Click on the white pawn so that it is selected (yellow) and gets a target (blue)
// in front of it.
// 3. Run choochoo() from the console, which will move the pawn north upto 100 squares.
//
// If it stops and is still within view but not selected, simply run choochoo() again.
//
// If it fails and is out of view, find it again, select it, and
// run choochoo().
//
// If your pawn joined the end of another conga line, congrats!
// Time to find another pawn, and go to step 1.
//
// -------
//
// Copyright 2025 Timo Tijhof <https://timotijhof.net/congajs>
// This is free and unencumbered software released into the public domain.
//
// Changelog:
// * v1 (2025-07-05): Initial release at <https://timotijhof.net/congajs>,
// in response to https://chaos.social/@mattgrayyes/114800817697031071.
// * v2 (2025-07-06): Optimization: Reduce DELAY_AFTER_NAVUP from 300ms to 250ms.
// * v3 (2025-07-06): Optimization: Reduce DELAY_AFTER_RESELECT from 300ms to 200ms.
// * v4 (2025-07-06): Add error messages.
// * v5 (2025-07-06): Fix: Increase DELAY_AFTER_NAVUP back up to 300ms because apparently
// on bigger screens (or lower zoom level) it takes longer causing us
// to stop after just a one one northward move.
// * v6 (2025-07-06): Make resuming easier by selecting the inspected pawn first.
// * v7 (2025-07-06): Make script even easier, by inverting the approach. We now auto-inspect
// the selected pawn, instead of auto-selecting the inspected pawn.
// No more reliance on using the Inspector dev in the browser console,
// and thus no more $0 variable.
// * v8 (2025-07-07): Optimization: Batch four moves together to reduce post-navup delays.
function choochoo() {
'use strict';
// A selected pawn has "--axe-transform" set to scale(1.2) instead of scale(1).
const SEL_WHITE_PAWN = 'button.sc-dVBluf.dmuknz[style*="--axe-transform: scale(1.2)"]:has(img[src="/pieces/white-processed/pawn.png"])';
const SEL_TARGET = 'button.sc-fLDLck.hJsSaA';
const SEL_BOARD_UP = 'button:has(svg.lucide-circle-arrow-up)';
const REPEATS = 300;
const DELAY_AFTER_NAVUP = 300;
const DELAY_AFTER_MOVE = 300;
const DELAY_AFTER_RESELECT = 200;
class CongaJsError extends Error {
constructor(message) {
super(message);
this.name = 'CongaJsError';
}
}
class CongaJsNoTarget extends Error {}
const pawn = document.querySelector(SEL_WHITE_PAWN);
if (!pawn) {
throw new CongaJsError('Select a white pawn before running choochoo()');
}
if (!pawn.isConnected) {
throw new CongaJsError('Pawn element has been recycled. Find it, inspect it, and run choochoo() again.');
}
const boardUp = document.querySelector(SEL_BOARD_UP);
if (!boardUp) {
throw new CongaJsError('Failed to locate "board up" button on the screen.');
}
// Rather than scheduling a large number of spaced out timeouts all at once,
// we use a Promise chain. This has a number of benefits:
// - The order is guruanteed.
// - Over time, race conditions and freezes will cause our time predictiom to be off
// eventually causing us to select targets that don't exist yet. By chaining the timeouts
// one at a time, we're never more than a few millisecond out of sync, which we can account for.
// - We can catch errors and thus naturally cancel any remaining actions else.
// This is important because errors will sometimes happen, for example, after each move,
// blue target squares are inserted, which takes a short amount of time. Sometimes this
// takes longer than we expect, and then we can't click the target and so we fail.
// At this point, we don't want to continue as otherwise we'll move the board up too far,
// and we'll loose our pawn $0 reference. The game recycles thus button 1:1 for a given
// logical game piece, but once it is out of view, if we navigate back to it,
// a new button is created to represent that pawn. By catching the errors, you can
// simply run the snippet again to continue (once you've selected the pawn).
// - We can apply the delay to *after* an action, rather then before it,
// so that the loop below is more intuitive (rather than scheduling each action based
// on the mandatory delay from the previous type of action, we can declare them together).
function delayWithCatch(ms, fn) {
return Promise.try(fn)
.then(() => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
});
}
function moveUp() {
// If there are black pawns ahead (north-west or north-east) there will
// be multiple targets. The game draws the northward option first, and
// the any diagonal/alternative options, so just select the first one.
const target = document.querySelectorAll(SEL_TARGET)[0];
if (!target) {
throw new CongaJsError('Pawn has no available targets. Find another and go again!');
}
// If there are only diagnoal/alternative options, that will be first.
// This happens if we reached the back of the next conga line, and there's
// also a black pawn nearby. Don't take that, instead stop here.
// Confirm by comparing x coord of target to x coord of pawn.
const targetX = target?.style?.getPropertyValue('--x');
if (!targetX) {
throw new CongaJsError('Could not find X coord of target');
}
if (!pawn.style.transform.includes(targetX)) {
throw new CongaJsError('Stopped. The only target would abandon our conga line. Find another and go again!');
}
target.click();
}
let pChain = Promise.resolve();
for (let i = 0; i < REPEATS; i++) {
let retry = false;
pChain = pChain.then(delayWithCatch.bind(null, DELAY_AFTER_MOVE, () => moveUp()));
pChain = pChain.then(delayWithCatch.bind(null, DELAY_AFTER_RESELECT, () => pawn.click()));
pChain = pChain.then(delayWithCatch.bind(null, DELAY_AFTER_MOVE, () => moveUp()));
pChain = pChain.then(delayWithCatch.bind(null, DELAY_AFTER_RESELECT, () => pawn.click()));
pChain = pChain.then(delayWithCatch.bind(null, DELAY_AFTER_MOVE, () => moveUp()));
pChain = pChain.then(delayWithCatch.bind(null, DELAY_AFTER_RESELECT, () => pawn.click()));
pChain = pChain.then(delayWithCatch.bind(null, DELAY_AFTER_MOVE, () => moveUp()));
pChain = pChain.then(delayWithCatch.bind(null, DELAY_AFTER_RESELECT, () => pawn.click()));
// Optimization: Reduce post-navup delay by batching four moves together.
pChain = pChain.then(delayWithCatch.bind(null, DELAY_AFTER_NAVUP, () => {
boardUp.click();
boardUp.click();
boardUp.click();
boardUp.click();
}));
}
pChain.catch((e) => {
if (e instanceof CongaJsError) {
console.warn(e.toString());
} else {
console.debug(e.name + ': ' + e.message);
console.warn(new CongaJsError('Stopped. Pawn blocked or lost selection. Find one and go again!').toString());
}
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment