Last active
October 23, 2020 00:40
-
-
Save quinnlas/af4b03e42b2b364be4b1dc4309d15e70 to your computer and use it in GitHub Desktop.
Solver for https://www.puzzle-binairo.com/
This file contains 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
// an array with 0 for empty, 1 for black, and 2 for white | |
let board = Game.currentState.cellStatus | |
let remainingEmpty = 0 | |
// number should be 1 for black, 2 for white | |
async function makeMove(row, col, number) { | |
for (let i = 0; i < number; i++) { | |
Game.startMove({ row, col }) | |
Game.endMove() | |
} | |
remainingEmpty-- | |
await new Promise(r => setTimeout(r, 1)) // allow the change to be drawn | |
} | |
function invert(color) { | |
return color === 1 ? 2 : 1 | |
} | |
async function fillVerticalTriples() { | |
for (let r = 0; r < board.length - 2; r++) { | |
const row = board[r] | |
for (let c = 0; c < row.length; c++) { | |
const first = row[c] | |
const second = board[r+1][c] | |
const third = board[r+2][c] | |
// pretend these are vertical | |
// ++- | |
if (first && (first === second) && !third) await makeMove(r+2, c, invert(first)) | |
// +-+ | |
if (first && (first === third) && !second) await makeMove(r+1, c, invert(first)) | |
// -++ | |
if (second && (second === third) && !first) await makeMove(r, c, invert(second)) | |
} | |
} | |
} | |
async function fillHorizontalTriples() { | |
for(let rowIndex = 0; rowIndex < board.length; rowIndex++) { | |
row = board[rowIndex] | |
for (let i = 0; i < row.length - 2; i++) { | |
const first = row[i] | |
const second = row[i+1] | |
const third = row[i+2] | |
// ++- | |
if (first && (first === second) && !third) await makeMove(rowIndex, i+2, invert(first)) | |
// +-+ | |
if (first && (first === third) && !second) await makeMove(rowIndex, i+1, invert(first)) | |
// -++ | |
if (second && (second === third) && !first) await makeMove(rowIndex, i, invert(second)) | |
} | |
} | |
} | |
// if there are rows or columns that can only have one color left, fill them | |
async function fillRowsAndColumnsByCounter() { | |
// rows | |
for (let r = 0; r < board.length; r++) { | |
row = board[r] | |
let numBlack = 0, numWhite = 0 | |
row.forEach(cell => { | |
if (cell === 1) numBlack++ | |
else if (cell === 2) numWhite++ | |
}) | |
let fill = 0 | |
if (numBlack === row.length / 2) fill = 2 | |
else if (numWhite === row.length / 2) fill = 1 | |
if (fill) { | |
for (let c = 0; c < row.length; c++) { | |
cell = row[c] | |
if (cell === 0) await makeMove(r, c, fill) | |
} | |
} | |
} | |
// cols | |
for (let c = 0; c < board[0].length; c++) { | |
let numBlack = 0, numWhite = 0 | |
for (let r = 0; r < board.length; r++) { | |
if (board[r][c] === 1) numBlack++ | |
else if (board[r][c] === 2) numWhite++ | |
} | |
let fill = 0 | |
if (numBlack === board.length / 2) fill = 2 | |
else if (numWhite === board.length / 2) fill = 1 | |
if (fill) { | |
for (let r = 0; r < board.length; r++) { | |
if (board[r][c] === 0) await makeMove(r,c,fill) | |
} | |
} | |
} | |
} | |
// given a number of black and white tiles, generate all unique orderings of them | |
function* permute(black, white) { | |
const length = black + white | |
// return all positions of blacks (just fill in whites for the rest) | |
if (black === 0) { | |
yield new Array(length).fill(2) | |
} else if (black === 1) { | |
for (let i = 0; i < length; i++) { | |
const result = new Array(length).fill(2) | |
result[i] = 1 | |
yield result | |
} | |
} else { | |
// move first black around then recurse for the rest | |
// the last possible position of the first black is length - black | |
for (let firstBlackIndex = 0; firstBlackIndex <= length - black; firstBlackIndex++) { | |
const innerBlack = black - 1 | |
const innerWhite = white - firstBlackIndex // index conveniently represents the number of elements to its left | |
for (innerPerm of permute(innerBlack, innerWhite)) { | |
const result = new Array(length).fill(2) | |
result[firstBlackIndex] = 1 | |
result.splice(firstBlackIndex + 1, innerPerm.length, ...innerPerm) | |
yield result | |
} | |
} | |
} | |
} | |
// given an array, return an array with objects for each move, containing index and color | |
// this can logically replace the triples and counter methods but they are faster | |
function singleRowTechnique(row) { | |
// iterate through all possible combinations of remaining colors | |
let remainingBlack = row.length / 2 | |
let remainingWhite = row.length / 2 | |
row.forEach(cell => { | |
if (cell === 1) remainingBlack-- | |
else if (cell === 2) remainingWhite-- | |
}) | |
// too expensive to compute | |
// total number of permutations is actually (remainingBlack + remainingWhite) choose (remainingBlack or remainingWhite) | |
// so this decision could be more sophisticated | |
if (remainingBlack + remainingWhite >= 23) return [] | |
let knownColors | |
let firstSolution = true | |
for (let permutation of permute(remainingBlack, remainingWhite)) { | |
// calculate full row | |
const fullRow = row.slice() | |
// fill based on permutation | |
let j = 0 | |
for (let i = 0; i < fullRow.length; i++) { | |
if (fullRow[i] === 0) { | |
fullRow[i] = permutation[j] | |
j++ | |
} | |
} | |
// determine if it is a solution by checking for triples | |
let isSolution = true | |
for (let i = 0; i < fullRow.length - 2; i++) { | |
if (fullRow[i] === fullRow[i + 1] && fullRow[i] === fullRow[i+2]) { | |
isSolution = false | |
break | |
} | |
} | |
if (!isSolution) continue | |
// keep track of which colors all solutions have in common | |
if (firstSolution) { | |
firstSolution = false | |
knownColors = fullRow | |
} else { | |
// find differences and mark them as 0 | |
for (let i = 0; i < fullRow.length; i++) { | |
if (fullRow[i] !== knownColors[i]) knownColors[i] = 0 | |
} | |
} | |
} | |
// find differences between knownColors and row (those are moves we can make) | |
const result = [] | |
for (let i = 0; i < row.length; i++) { | |
if (row[i] !== knownColors[i]) { | |
result.push({ | |
index: i, | |
color: knownColors[i] | |
}) | |
} | |
} | |
return result | |
} | |
// if all possible solutions of a row or column have some cells in common, fill those | |
async function fillOneRowOrColumnTechniques() { | |
// rows | |
for (let r = 0; r < board.length; r++) { | |
const row = board[r] | |
const moves = singleRowTechnique(row) | |
for (let move of moves) { | |
await makeMove(r, move.index, move.color) | |
} | |
} | |
// columns | |
for (let c = 0; c < board[0].length; c++) { | |
const column = board.map(row => row[c]) | |
const moves = singleRowTechnique(column) | |
for (let move of moves) { | |
await makeMove(move.index, c, move.color) | |
} | |
} | |
} | |
// fill some cells if you can | |
async function findMoves() { | |
await fillRowsAndColumnsByCounter() | |
await fillHorizontalTriples() | |
await fillVerticalTriples() | |
await fillOneRowOrColumnTechniques() | |
} | |
async function main() { | |
board.forEach(row=>{ | |
row.forEach(cell=>{ | |
if (cell === 0) | |
remainingEmpty++ | |
} | |
) | |
} | |
) | |
console.log(remainingEmpty) | |
let previousNumberEmpty = remainingEmpty | |
while (true) { | |
await findMoves() | |
console.log(remainingEmpty) | |
if ((remainingEmpty <= 0) || (remainingEmpty === previousNumberEmpty)) break | |
previousNumberEmpty = remainingEmpty | |
} | |
if (remainingEmpty > 0) | |
console.log('could not find any more moves') | |
else console.log('done') | |
} | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment