Skip to content

Instantly share code, notes, and snippets.

@consti
Created April 23, 2025 07:25
Show Gist options
  • Save consti/0c651ad643a0fca52353e0a908c1d08b to your computer and use it in GitHub Desktop.
Save consti/0c651ad643a0fca52353e0a908c1d08b to your computer and use it in GitHub Desktop.
ExponenTile Auto-Play - with strategies
(function autoPlayExponenTile() {
// Configuration
const config = {
moveDelay: 500, // Milliseconds between moves
maxMoves: 1000, // Safety limit
strategy: 1 // Default strategy (1-4)
};
let moveCount = 0;
let running = false;
// Get the actual game board elements
function getBoardElements() {
// Try different selectors that might match the tile elements
const selectors = [
'.board .tile', // Common class naming
'[data-value]', // Elements with data-value attribute
'[class*="tile"]', // Elements with "tile" in the class name
'.grid-container > div', // Grid container children
'.game-board > div', // Game board children
'[role="button"]', // Interactive elements
];
for (const selector of selectors) {
const elements = document.querySelectorAll(selector);
if (elements.length >= 9) { // Expecting at least a 3x3 grid
console.log(`Found ${elements.length} tiles with selector: ${selector}`);
return Array.from(elements);
}
}
// Fallback: find elements that look like game tiles based on their appearance
const allElements = document.querySelectorAll('div');
const possibleTiles = Array.from(allElements).filter(el => {
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
// Look for square elements with similar dimensions that are visible
return rect.width > 20 &&
rect.height > 20 &&
Math.abs(rect.width - rect.height) < 10 &&
style.display !== 'none' &&
style.visibility !== 'hidden';
});
// Group elements by size to find the most common size (likely the game tiles)
const sizeGroups = {};
possibleTiles.forEach(el => {
const width = Math.round(el.getBoundingClientRect().width);
sizeGroups[width] = sizeGroups[width] || [];
sizeGroups[width].push(el);
});
// Find the size group with the most elements
let maxGroup = [];
Object.values(sizeGroups).forEach(group => {
if (group.length > maxGroup.length) maxGroup = group;
});
console.log(`Found ${maxGroup.length} potential tiles based on size analysis`);
return maxGroup;
}
// Find all possible moves on the board
function findPossibleMoves() {
const tiles = getBoardElements();
const moves = [];
// Create a grid representation
const size = Math.sqrt(tiles.length);
if (size % 1 !== 0) {
console.log(`Warning: Tile count (${tiles.length}) is not a perfect square. Using estimated size: ${Math.floor(size)}`);
}
const gridSize = Math.floor(size);
const grid = [];
for (let i = 0; i < gridSize; i++) {
grid[i] = [];
for (let j = 0; j < gridSize; j++) {
const index = i * gridSize + j;
if (index < tiles.length) {
// Try to get the tile value from different sources
let value = null;
// Try data-value attribute
if (tiles[index].hasAttribute('data-value')) {
value = parseInt(tiles[index].getAttribute('data-value'), 10);
}
// Try innerText
else if (tiles[index].innerText) {
value = parseInt(tiles[index].innerText.trim(), 10);
}
// Try aria-label
else if (tiles[index].hasAttribute('aria-label')) {
const label = tiles[index].getAttribute('aria-label');
const match = label.match(/\d+/);
if (match) value = parseInt(match[0], 10);
}
// If we still can't determine the value, try to get it from the CSS classes
if (isNaN(value) || value === null) {
const classes = tiles[index].className.split(' ');
for (const cls of classes) {
const match = cls.match(/value-(\d+)/);
if (match) {
value = parseInt(match[1], 10);
break;
}
}
}
// If still no value, try to get it from child elements
if (isNaN(value) || value === null) {
const children = tiles[index].querySelectorAll('*');
for (const child of children) {
if (child.innerText && !isNaN(parseInt(child.innerText.trim(), 10))) {
value = parseInt(child.innerText.trim(), 10);
break;
}
}
}
// Default to 0 if still no value
if (isNaN(value) || value === null) {
value = 0;
}
grid[i][j] = {
element: tiles[index],
value: value,
x: j,
y: i,
index: index
};
}
}
}
// Print the grid for debugging
console.log("Grid values:");
const gridValuesStr = grid.map(row => row.map(cell => cell.value).join(' ')).join('\n');
console.log(gridValuesStr);
// Process the grid based on strategy
let processedMoves = [];
// Check all possible swaps
for (let i = 0; i < gridSize; i++) {
for (let j = 0; j < gridSize; j++) {
// Check right swap
if (j < gridSize - 1) {
checkAndAddMove(grid, i, j, i, j + 1, moves);
}
// Check down swap
if (i < gridSize - 1) {
checkAndAddMove(grid, i, j, i + 1, j, moves);
}
}
}
// Apply the selected strategy to sort moves
const strategyDescription = sortMovesByStrategy(moves, config.strategy);
console.log(`Found ${moves.length} possible moves using strategy: ${strategyDescription}`);
return moves;
}
// Sort moves based on the selected strategy
function sortMovesByStrategy(moves, strategy) {
let description = "";
switch (strategy) {
case 1: // Move smallest numbers first
description = "Move smallest numbers first";
moves.sort((a, b) => {
const aMin = Math.min(a.value1, a.value2);
const bMin = Math.min(b.value1, b.value2);
return aMin - bMin;
});
break;
case 2: // Move largest numbers first
description = "Move largest numbers first";
moves.sort((a, b) => {
const aMax = Math.max(a.value1, a.value2);
const bMax = Math.max(b.value1, b.value2);
return bMax - aMax;
});
break;
case 3: // First option from top (top-to-bottom, left-to-right)
description = "First option from top";
moves.sort((a, b) => {
if (a.y1 !== b.y1) return a.y1 - b.y1;
if (a.x1 !== b.x1) return a.x1 - b.x1;
return 0;
});
break;
case 4: // First option from bottom (bottom-to-top, right-to-left)
description = "First option from bottom";
moves.sort((a, b) => {
if (a.y1 !== b.y1) return b.y1 - a.y1;
if (a.x1 !== b.x1) return b.x1 - a.x1;
return 0;
});
break;
default:
description = "Default - Move highest scoring moves first";
moves.sort((a, b) => b.score - a.score);
}
return description;
}
// Check if swapping two tiles creates a valid move
function checkAndAddMove(grid, y1, x1, y2, x2, moves) {
const value1 = grid[y1][x1].value;
const value2 = grid[y2][x2].value;
// Skip if both values are the same (no point swapping identical tiles)
if (value1 === value2 && value1 === 0) return;
// Temporarily swap
grid[y1][x1].value = value2;
grid[y2][x2].value = value1;
// Check for at least 3 in a row or column after swap
if (hasMatch(grid, y1, x1) || hasMatch(grid, y2, x2)) {
moves.push({
from: grid[y1][x1].element,
to: grid[y2][x2].element,
score: calculateMoveScore(grid, y1, x1, y2, x2),
value1: value1,
value2: value2,
y1: y1,
x1: x1,
y2: y2,
x2: x2
});
}
// Swap back
grid[y1][x1].value = value1;
grid[y2][x2].value = value2;
}
// Check if there's a match at the given position
function hasMatch(grid, y, x) {
const value = grid[y][x].value;
if (value === 0) return false;
// Check horizontal matches
let horizontalCount = 1;
// Check left
for (let j = x - 1; j >= 0 && grid[y][j] && grid[y][j].value === value; j--) {
horizontalCount++;
}
// Check right
for (let j = x + 1; j < grid[0].length && grid[y][j] && grid[y][j].value === value; j++) {
horizontalCount++;
}
// Check vertical matches
let verticalCount = 1;
// Check up
for (let i = y - 1; i >= 0 && grid[i] && grid[i][x] && grid[i][x].value === value; i--) {
verticalCount++;
}
// Check down
for (let i = y + 1; i < grid.length && grid[i] && grid[i][x] && grid[i][x].value === value; i++) {
verticalCount++;
}
return horizontalCount >= 3 || verticalCount >= 3;
}
// Calculate a score for a move to prioritize better moves
function calculateMoveScore(grid, y1, x1, y2, x2) {
let score = 0;
const val1 = grid[y1][x1].value;
const val2 = grid[y2][x2].value;
// Check matches after swap in both directions
const directions = [
{ y: y1, x: x1 },
{ y: y2, x: x2 }
];
for (const pos of directions) {
// Check horizontal matches
let hCount = 1;
let hSum = grid[pos.y][pos.x].value;
// Check left
for (let j = pos.x - 1; j >= 0 && grid[pos.y][j] && grid[pos.y][j].value === grid[pos.y][pos.x].value; j--) {
hCount++;
hSum += grid[pos.y][j].value;
}
// Check right
for (let j = pos.x + 1; j < grid[0].length && grid[pos.y][j] && grid[pos.y][j].value === grid[pos.y][pos.x].value; j++) {
hCount++;
hSum += grid[pos.y][j].value;
}
// Check vertical matches
let vCount = 1;
let vSum = grid[pos.y][pos.x].value;
// Check up
for (let i = pos.y - 1; i >= 0 && grid[i] && grid[i][pos.x] && grid[i][pos.x].value === grid[pos.y][pos.x].value; i--) {
vCount++;
vSum += grid[i][pos.x].value;
}
// Check down
for (let i = pos.y + 1; i < grid.length && grid[i] && grid[i][pos.x] && grid[i][pos.x].value === grid[pos.y][pos.x].value; i++) {
vCount++;
vSum += grid[i][pos.x].value;
}
// Add to score if there's a match
if (hCount >= 3) {
// More tiles and higher values are better
score += hCount * hSum;
}
if (vCount >= 3) {
score += vCount * vSum;
}
}
return score;
}
// Perform a move by clicking or simulating swipe
function performMove(move) {
console.log(`Making move ${moveCount}: ${move.y1},${move.x1} (${move.value1}) ↔ ${move.y2},${move.x2} (${move.value2})`);
// Click on the first tile
simulateClick(move.from);
// Wait a bit and click on the second tile
setTimeout(() => {
simulateClick(move.to);
// Wait for animations to complete
setTimeout(() => {
if (running) makeNextMove();
}, config.moveDelay);
}, 100);
}
// Simulate a mouse click on an element
function simulateClick(element) {
const rect = element.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
const mouseDownEvent = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y
});
const mouseUpEvent = new MouseEvent('mouseup', {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y
});
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
clientX: x,
clientY: y
});
element.dispatchEvent(mouseDownEvent);
element.dispatchEvent(mouseUpEvent);
element.dispatchEvent(clickEvent);
}
// Make the next move
function makeNextMove() {
if (!running || moveCount >= config.maxMoves) {
if (moveCount >= config.maxMoves) {
console.log("Reached maximum move limit. Stopping.");
}
running = false;
return;
}
moveCount++;
const possibleMoves = findPossibleMoves();
if (possibleMoves.length > 0) {
// The moves are already sorted by the selected strategy
performMove(possibleMoves[0]);
} else {
console.log("No valid moves found. Game might be over.");
running = false;
}
}
// Change the strategy
function setStrategy(strategyNum) {
if (strategyNum >= 1 && strategyNum <= 4) {
config.strategy = strategyNum;
console.log(`Strategy changed to: ${strategyNum}`);
} else {
console.log(`Invalid strategy: ${strategyNum}. Must be between 1-4.`);
}
}
// Create public API
window.autoPlayExponenTile = {
start: () => {
moveCount = 0;
running = true;
makeNextMove();
},
stop: () => {
running = false;
console.log("Automation stopped.");
},
toggle: () => {
running = !running;
console.log(`ExponenTile automation ${running ? 'started' : 'paused'}`);
if (running) makeNextMove();
},
setStrategy: (num) => {
setStrategy(num);
},
getConfig: () => {
console.log(`Current strategy: ${config.strategy}`);
return config;
}
};
console.log(
"ExponenTile automation ready! Use:\n" +
"- autoPlayExponenTile.setStrategy(1-4) to change strategy:\n" +
" 1) Move the smallest numbers first\n" +
" 2) Move the largest numbers first\n" +
" 3) Try to move the first possible option from the top\n" +
" 4) Try to move the first possible option from the bottom\n" +
"- autoPlayExponenTile.start() to begin playing\n" +
"- autoPlayExponenTile.stop() to stop"
);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment