Jogando com Probabilidades: Como o Wave Function Collapse funciona no Sudoku
🔢

Jogando com Probabilidades: Como o Wave Function Collapse funciona no Sudoku

Created
Jan 4, 2024
Tags
Code
JS
Tutorial

Introdução:

E aí, pessoal! Queria falar sobre uma técnica super legal chamada Wave Function Collapse (WFC). Se você curte criar coisas novas e resolver quebra-cabeças de formas inusitadas, você vai adorar conhecer esse método que vem diretamente do mundo da geração procedural. E para tornar tudo mais interessante, vamos ver como ele pode dar um novo gás na maneira como resolvemos o bom e velho Sudoku.
Ultimamente tenho brincado bastante no https://www.codewars.com e hoje me apareceu um sudoku solver em um dos Katas (https://www.codewars.com/kata/5296bc77afba8baa690002d7/javascript) e me lembro que a muito tempo atras havia brincado de criar um front para fazer um solver basico tambem utilizando WFC em uma das minhas páginas, porem ele não estava 100%.
Amarrei a vontade de codar com o desafio e fui novamente tentar resolver esse sudoku dessa maneira.

O que raios é Wave Function Collapse

Vamos começar do começo. O Wave Function Collapse, ou WFC para os íntimos, é um algoritmo inspirado em ideias malucas da mecânica quântica. Em resumo, ele lida com um monte de possibilidades para cada pedacinho de um grid e vai "colapsando" essas possibilidades até restar apenas uma escolha super legal e única. É tipo jogar com as probabilidades até chegar em algo concreto e totalmente dentro das regras que você definiu.
Agora, você deve estar se perguntando: "O que o Sudoku tem a ver com isso?" Bom, se pensarmos em cada quadradinho do Sudoku como um espaço cheio de números possíveis, podemos usar o WFC para ir "afunilando" essas opções até que cada quadradinho tenha o número perfeito, seguindo as regrinhas clássicas do jogo. Legal, né?
Começamos com um grid de Sudoku meio preenchido, com alguns números já no lugar. O nosso trabalho é preencher o resto. Com o WFC, vamos "colapsando" as células vazias, escolhendo um número que se encaixe até que não sobre mais nenhum espaço vazio. A cada passo, a gente ajusta as possibilidades das células ao redor, para tudo continuar fazendo sentido.
Video preview

Os Bumps na Estrada

Claro, nem tudo são flores. Às vezes, o WFC pode nos levar a um beco sem saída, especialmente em Sudokus mais cabeludos. Aí, pode ser necessário voltar atrás e tentar outro caminho, famozo backtracking. E, dependendo do quebra-cabeça, o WFC pode ser mais rápido ou mais lento.
 
Aqui vai o meu código (comentado) para esse Kata, sem o backtracking pois não foi necessário para os testes apresentados
class SudokuSolver { constructor(initialGrid) { this.grid = initialGrid; this.possibleNumbers = new Map() } initialize() { // Inicializando colocando as possibilidades totais aonde inicialmente não existe numero // no grid, lembrando que 0 no caso é considerado como espaço vazio aqui for (let row = 0; row < 9; row++) { for (let col = 0; col < 9; col++) { if (this.grid[row][col] === 0) { this.possibleNumbers.set(`${row},${col}`, new Set([1,2,3,4,5,6,7,8,9])) } else { this.possibleNumbers.set(`${row},${col}`, new Set([this.grid[row][col]])) } } } // Após inicializado, fiz a primeira leva de propagação, eliminando números repetidos // dentro da regra do jogo, entenda mais na func. de propagação for (let row = 0; row < 9; row++) { for (let col = 0; col < 9; col++) { if (this.grid[row][col] !== 0) { this.propagate(row, col, this.grid[row][col]) } } } } propagate(row, col, number) { // Aqui está basicamente a regra do jogo Sudoku, linhas não podem conter o mesmo número // e nem colunas, alem disso, o jogo 9x9 é dividido em sub grids de 3x3, aonde tambem // não pode ter o número repetido for (let k = 0; k < 9; k++) { this.possibleNumbers.get(`${row},${k}`)?.delete(number) this.possibleNumbers.get(`${k},${col}`)?.delete(number) } // Subgrid let subgridRow = Math.floor(row / 3) * 3 let subgridCol = Math.floor(col / 3) * 3 for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { this.possibleNumbers.get(`${subgridRow + i},${subgridCol + j}`)?.delete(number) } } } collapse() { // Aqui é o momento da mágica, inicialmente eu vejo todas as celulas e verifico a que tem a menor // possibilidade para colocarmos um valor (numa WCF o ideal min. é 1) let minOptions = 10 let cellToCollapse = null for (let row = 0; row < 9; row++) { for (let col = 0; col < 9; col++) { let possibilities = this.possibleNumbers.get(`${row},${col}`) if (possibilities.size >= 1 && possibilities.size < minOptions) { minOptions = possibilities.size cellToCollapse = {row, col} } } } // Encontrado o elemento com menor "entropia", adiciono um valor, propago seguindo a regra // do jogo if (cellToCollapse) { let row = cellToCollapse.row let col = cellToCollapse.col let possibilities = Array.from(this.possibleNumbers.get(`${row},${col}`)) let rowlapseValue = possibilities[0] this.grid[row][col] = rowlapseValue this.possibleNumbers.set(`${row},${col}`, new Set([rowlapseValue])) this.propagate(row, col, rowlapseValue) } } isSolved() { // Enquanto ainda tiver algum valor sem preencher ou com possibilidades, continuo tentando for (let row = 0; row < 9; row++) { for (let col = 0; col < 9; col++) { if (this.grid[row][col] === 0 || this.possibleNumbers.get(`${row},${col}`).size > 1) { return false } } } return true } solve() { // Aqui é simples, tentar resolver até não poder mais (coloquei um tempo de 5 segundos, se passar // paro o solver pois ou o sudoku é daqueles cabeçudos ou alguma coisa deu errado const startTime = Date.now(); const timeLimit = 5000; while (!this.isSolved()) { if (Date.now() - startTime > timeLimit) { throw new Error('Sudoku não resolvido no tempo limite'); } this.collapse() } } showGrid() { // Função simples para verificar como "ta ficando" let grid = '' for (let row = 0; row < 9; row++) { for (let col = 0; col < 9; col++) { grid += `${this.grid[row][col]} ` } grid += `\n` } console.log(grid) } returnSolution() { // O Kata exige um retorno como array const arr = [] for (let row = 0; row < 9; row++) { let line = [] for (let col = 0; col < 9; col++) { line.push(this.grid[row][col]) } arr.push(line) } return arr } }

Conclusão

O Wave Function Collapse é uma ferramenta super bacana que nos mostra como até os quebra-cabeças mais clássicos podem ser vistos por uma nova perspectiva. Com ele, o Sudoku se transforma em um jogo de probabilidades e lógica quântica (quem diria!). O legal é que isso é só a ponta do iceberg, e há um mundo inteiro de possibilidades esperando para ser explorado com o WFC. Então, que tal dar uma chance a essa técnica e ver até onde ela pode te levar?