I like a quick puzzle. Solving riddles keeps your brain in shape, and coming up with solutions to problems also just feels very satisfying to me. I like doing a quick [wordle](https://wordly.org/), but I also like my [Arukone](https://gridgames.app/arukone/) or a quick [Loopy](https://gridgames.app/loopy/). And it's also fun to guess a country [on the globe](https://globle.org) or [by shape](https://worldlegame.io/) every now and then. And I also like playing around with [combining words](https://combinations.org/). So naturally I was immediately attracted by the [Squares](https://squares.org/) game. It works like this: You've got a 4 x 4 grid of letters. Using your cursor you can drag accross the grid to form words. A connection can be made from a letter to any adjacent letters. Pretty easy, right? Not quite. You see: Sometimes there are upwards of 60 intended solutions! Why intended? Because there are also many bonus words you can create. And that's just too much for me. I would claim my English is pretty reasonable (bold statement, right?), but I am not a native speaker. Some of those words I've never heard of. But even if the level of difficulty would be much lower, it's still **a lot** of words to find. It just takes a long long time to do it. Subsequently, as someone who also likes to write computer programs, I had to take on the challenge to solve the squares once and for all. Meet **the squares solver**: ![Figure [solver_first]: Entire run of a simple square.](media/squares_solver_running_first.mp4) This shows an entire run of the solver. It really is just a Python script that moves your mouse for you. And timing is pretty lax. Here is the code: ``` python import sys import time import random from pynput.mouse import Button, Controller def main(): DELAY = 0.2 sequences = [] grid = sys.argv[4].lower() graph = generate_graph(grid) with open("words.txt", "r", encoding="utf-8") as ifile: wordlist = ifile.readlines() wordlist = [word.lower().strip() for word in wordlist] wordlist = [word for word in wordlist if len(word) >= 4] for word in wordlist: sequence = solve_for_word(word, graph, grid) if len(sequence) == 0: continue sequences.append(sequence) # print some statistics print(len(sequences), "words found") duration = 5 + 5 # initialization duration += len(sequences) * 2 * DELAY # press and release duration += sum([len(s) for s in sequences]) * DELAY # dragging time duration += len(sequences) # interval between words t_min = int(duration / 60) t_sec = int(duration - 60 * t_min) print(f"input will take around {t_min}m {t_sec}s") # shuffling for _ in range(len(sequences)): i1, i2 = ( random.randrange(0, len(sequences)), random.randrange(0, len(sequences)), ) sequences[i1], sequences[i2] = sequences[i2], sequences[i1] print("hit any key to start input...") input() run_input(sequences, DELAY) def run_input(sequences, delay): origin = (int(sys.argv[1]), int(sys.argv[2])) size = int(sys.argv[3]) step = size // 4 mouse = Controller() time.sleep(5) # give user time to playe mouse on upper left field mouse.click(Button.left) # focus application time.sleep(5) # give user time to accept input in Wayland for sequence in sequences: sx, sy = sequence[0] % 4, sequence[0] // 4 move_to_field(mouse, sx, sy, origin, step, delay=delay) start_drag(mouse, delay=delay) for i in sequence[1:]: sx, sy = i % 4, i // 4 move_to_field(mouse, sx, sy, origin, step, delay=delay) end_drag(mouse, delay=delay) time.sleep(1) return False # this ends the listener thread def move_to_field(mouse, x, y, origin, step, delay=0.2): mouse.position = (origin[0] + step * x, origin[1] + step * y) time.sleep(delay) def start_drag(mouse, delay=0.2): mouse.press(Button.left) time.sleep(delay) def end_drag(mouse, delay=0.2): mouse.release(Button.left) time.sleep(delay) def solve_for_word(word, graph, grid): paths = [[i] for i, c in enumerate(grid) if c == word[0]] for next_char in word[1:]: next_paths = [] for path in paths: current_field = path[-1] neighbors = graph[current_field] if next_char in neighbors: next_paths.append(path + [neighbors[next_char]]) paths = next_paths paths = [p for p in paths if len(set(p)) == len(p)] if len(paths) > 0: return paths[0] return [] def generate_graph(grid): graph = {} for i, C in enumerate(grid): neighbors = {} for k, N in enumerate(grid): if k == i: continue # check if N is a neighbor of C C_row, C_col = i // 4, i % 4 N_row, N_col = k // 4, k % 4 diff_row = abs(C_row - N_row) diff_col = abs(C_col - N_col) if diff_row <= 1 and diff_col <= 1: neighbors[N] = k graph[i] = neighbors return graph if __name__ == "__main__": main() ``` I know, that code is pretty ugly. But remember: It's just a little Python script that I threw together in about an hour. It doesn't need to be pretty, it just needs to work. And it does work, as you could just see. The idea is really simple: First, I got a list of english words. I found [this one](https://github.com/dwyl/english-words), which is publicly available and seemed to be rather complete. The code then loads these words and reshapes them to make them as identical and consistent as possible. With all words now having a similar form (lowercase letters, no whitespace, etc.) the script iterates over all words and checks if that word could, in theory, be created by dragging accross the square - this yields the candidates to test. The rest of the program is just a lot of boilerplate to make the mouse movements. On my setup I am launching the program using a little bash script, since the Python program itself also takes a few additional parameters for screen size, etc. This is the execution script: ``` bash #!/bin/bash source env/bin/activate # 1st monitor config python good_solve.py 1265 410 530 "$1" # 2nd monitor config (2nd monitor vertical) # python good_solve.py 1265 905 530 "$1" ``` Execution is then really simple (shown for the square above): ``` $ ./run_square_solver domanjwsaedvdtta ``` !!! Tip The Python module `pynput` which is used to move the mouse around is loaded by sourcing the venv environment. Module hygiene is really important these days. The solver doesn't completely solve all words, but I still was able to complete the square by hand: ![Figure [squares_solved]: Fully solved square.](media/squares_solved.png) And yes, I deliberately took that screenshot while the confetti was still drizzling over the square. But that was an easy one! Now what about a difficult square? Well, just watch: ![Figure [solver_second]: Entire run of a difficult square.](media/squares_solver_running_second.mp4) It got 56 words out of 60 - not too bad! Unfortunately I was only able to guess one more word "HERE", leaving me with this unsolved square right here: ![Figure [squares_unsolved]: Unsolved square. Only 3 words are left, but I can't figure out which.](media/squares_unsolved.png) It's a shame. But this square is just too tough for me. Maybe you were able to solve it?