Created
April 23, 2025 07:25
-
-
Save consti/0c651ad643a0fca52353e0a908c1d08b to your computer and use it in GitHub Desktop.
ExponenTile Auto-Play - with strategies
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
(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