Thanksgiving week, time to relax and play some games. While I’m not playing Fallout 4, I’ve still got something open!
Cross Set is a recent release on Steam, with a premise similar to Sudoku. These games are always good to relax with, because you can usually go through the mechanical motions to solve them and let your brain idle. The rules for this puzzler are simple:
If you’ve ever played sudoku, the game should look familiar. This is roughly the intermediate stage of solving a sudoku puzzle. I quickly got bored after I cleared the first two levels, but still had the urge to 100% complete the puzzles. Briefly, I considered refunding my purchase if I could write a solver for the game. Then I figured if I wrote the solver, then there’s no point in requesting a refund, as I clearly got my money’s worth. Sounds like a fun programming challenge, so let’s get started! (I also considered writing a tool to help with the two incremental games I open occasionally, but interacting with them wasn’t as simple.)
For each level, the grid can easily be serialized into a text format, delimiting each cell with a space and each row with a newline. From there, it will be parsed into a PuzzleGrid
object.
2-8.txt:
512 736 463 524 241 256 347
756 746 136 346 423 745 614
457 235 267 235 573 673 127
513 156 623 7 625 235 462
735 564 725 137 712 524 251
361 271 413 473 356 412 257
514 513 237 462 246 125 257
The script should print out a solved grid that I can then manually enter into the game. Not great, but much easier than interacting with Tap Tap Infinity or Time Clickers.
Ideally, the script should solve the grid completely. However, I’m not sure if my approach to solving the puzzle will be able to work 100% of the time. If the puzzle can’t actually be solved, I’d like the program to at least return a partially solved puzzle. Absolute worst case is no cells solved, so I probably need to attempt solving it by hand and figure out a new algorithm.
main.py
cross_set/
__init__.py
solver.py - Contains the algorithms
puzzle_grid.py - Contains the objects detailing the puzzle
Simple enough, I want to read cells from a text file. This will be in main.py, so that the cross_set package only needs to deal with one kind of input. I’ll go ahead and define that to be a list of list of sets. If in the future I want to change input methods (maybe a fancy curses TUI?), then I just change main.py .
main.py
The main object will be the PuzzleGrid, representing the grid of cells, with coordinates 0,0 at the top left corner. There are several methods to implement here that I think would be useful for writing the solving algorithms:
__init__(puzzle)
- Takes a list of list of sets
representing the grid__str__()
- Returns a nice serialized version of the grid (which should be equivalent format to what is passed in from the file)column(x)
- Returns a list of cells in Column xrow(y)
- Returns a list of cells in Row ycell(x,y)
- Get the cell located at Column x, Row yset_cell(x,y,val)
- Set the cell located at Column x, Row ysize()
- How big is the puzzle?puzzle_grid.py
At the end of main.py
Simple process of running the solving algorithm and then printing the solved PuzzleGrid
At the end of main.py
Standalone functions I can see use for in the algorithm:
solver.py
My first step when solving a puzzle by hand is to mentally cross off any numbers contradict already locked numbers. This is a good starting point for the solver, so let’s see how far it gets me. I want to keep running this on the puzzle until there are no more changes to the grid (hopefully it’s solved at that point).
solver.py
As it turns out, only one iteration of the algorithm is performed before it’s useless!
125 367 346 245 124 256 347
567 467 136 346 234 457 146
457 235 267 235 357 367 127
135 156 236 7 256 235 246
357 456 257 13 127 245 125
136 127 134 34 356 124 257
145 135 237 246 246 125 257
This is useful to define a minimal form of the puzzle though, so let’s make that a helper function.
solver.py
The next method I use mentally is to look for the cells that contain the only instance of a value in a row/column. That cell can then be finalized to that single value.
solver.py
Not the most functional approach, Python is starting to annoy me about now, but I’m not trying to make a masterpiece the first time through!
11 iterations later, and Level 2-8 was solved.
2 7 4 5 1 6 3
7 4 1 3 2 5 6
4 5 6 2 3 7 1
5 1 2 7 6 3 4
3 6 5 1 7 4 2
6 2 3 4 5 1 7
1 3 7 6 4 2 5
Here are the results after each iteration:
Iteration 1
125 367 346 245 124 256 347
567 467 136 346 2 457 146
4 235 267 235 357 367 1
135 156 236 7 256 235 4
357 6 257 13 127 245 125
136 127 134 34 356 124 257
145 135 237 246 246 125 257
Iteration 2
125 37 346 245 14 256 37
57 47 1 34 2 457 6
4 235 267 235 357 367 1
135 15 236 7 56 235 4
357 6 257 13 17 4 25
136 127 134 34 356 124 257
15 135 237 246 46 125 257
Iteration 3
125 37 346 245 14 256 37
57 47 1 3 2 57 6
4 235 267 235 357 367 1
135 15 236 7 56 235 4
357 6 257 13 17 4 25
136 127 34 34 356 12 257
15 135 237 246 46 125 257
Iteration 4
125 37 346 25 14 256 37
57 4 1 3 2 57 6
4 235 267 25 357 367 1
135 15 236 7 56 235 4
3 6 257 1 7 4 25
136 127 3 4 356 12 257
15 135 237 26 4 125 257
Iteration 5
25 37 4 25 1 256 37
57 4 1 3 2 57 6
4 235 267 25 35 367 1
15 15 26 7 56 3 4
3 6 25 1 7 4 25
16 127 3 4 56 12 257
15 3 27 6 4 125 257
Iteration 6
25 7 4 25 1 6 3
57 4 1 3 2 57 6
4 25 267 25 3 67 1
15 15 2 7 56 3 4
3 6 25 1 7 4 25
16 12 3 4 56 12 7
15 3 27 6 4 125 257
Iteration 7
25 7 4 25 1 6 3
7 4 1 3 2 5 6
4 25 6 25 3 7 1
15 15 2 7 6 3 4
3 6 5 1 7 4 2
16 12 3 4 5 12 7
15 3 7 6 4 125 25
Iteration 8
25 7 4 25 1 6 3
7 4 1 3 2 5 6
4 25 6 25 3 7 1
15 15 2 7 6 3 4
3 6 5 1 7 4 2
6 12 3 4 5 12 7
1 3 7 6 4 2 5
Iteration 9
2 7 4 5 1 6 3
7 4 1 3 2 5 6
4 25 6 25 3 7 1
5 1 2 7 6 3 4
3 6 5 1 7 4 2
6 2 3 4 5 1 7
1 3 7 6 4 2 5
Iteration 10
2 7 4 5 1 6 3
7 4 1 3 2 5 6
4 5 6 2 3 7 1
5 1 2 7 6 3 4
3 6 5 1 7 4 2
6 2 3 4 5 1 7
1 3 7 6 4 2 5
Iteration 11
2 7 4 5 1 6 3
7 4 1 3 2 5 6
4 5 6 2 3 7 1
5 1 2 7 6 3 4
3 6 5 1 7 4 2
6 2 3 4 5 1 7
1 3 7 6 4 2 5
This solves several puzzles up until Level 3-4:
512 534 352 123 53
125 235 1345 125 42
45 53 312 241 412
1234 134 3452 23 24
453 423 1245 4523 4512
Three iterations run before it bails, leaving me with:
125 4 23 123 35
125 235 134 125 24
45 35 123 124 124
1234 13 5 23 24
345 23 124 2345 1245
Inspecting the output, I see some patterns… The rightmost column contains two cells with {2,4}
! Since only those two cells can contain 2 and 4, I can remove that option from other cells in the column. In general, if N
cells hold the exact same N
-tuple of numbers, then you can exclude those numbers from any other cell in their row/column, because those N
cells are the only ones that could contain those numbers.
Fantastic! This solves Level 3-4 with ease in 4 iterations!
Iteration 1
125 4 23 123 35
125 235 134 125 24
45 35 123 124 1
1234 13 5 23 24
345 23 124 2345 15
Iteration 2
5 4 2 1 3
12 235 34 5 24
4 35 3 2 1
123 13 5 3 24
3 23 1 34 5
Iteration 3
5 4 2 1 3
1 3 4 5 2
4 5 3 2 1
2 1 5 3 4
3 2 1 4 5
Iteration 4
5 4 2 1 3
1 3 4 5 2
4 5 3 2 1
2 1 5 3 4
3 2 1 4 5
Between iterations 1 and 2, the right column is stripped of any instances of 2 or 4 that aren’t in rows 2 and 4 (heh?). Works as intended! Let’s see how far this goes.
…
…
All the way to Level 4-5 before it gets stumped:
612 563 513 534 62 462
245 12 462 24 623 261
534 562 23 235 16 145
612 13 456 245 45 241
25 463 645 13 16 43
35 251 341 126 45 613
This is the final output:
16 56 13 345 2 46
5 12 246 24 3 126
4 256 23 235 16 15
16 3 46 245 45 124
2 46 5 13 16 34
3 125 14 126 45 16
If I help the solver along, by removing the 1 from cell 5,5 then it can solve the puzzle, but I’m not sure where to find the algorithm hidden here. Looks like the journey is about to be over. Oh, but hold on now, the far right column only has a single 5 in row 3. Why isn’t it getting locked by the second algorithm? Aha, when I iterate over cell_list
twice, the second time it’s not actually iterating. This is due to iterating over a python generator produced by itertools.chain
, which will act like it’s empty after I’ve iterated over it exactly once. Annoying, but the easy fix is cell_list = list(cell_list)
. Level 4-5 is solved now, so we’re back in business.
6 5 1 3 2 4
5 1 2 4 3 6
4 6 3 2 1 5
1 3 6 5 4 2
2 4 5 1 6 3
3 2 4 6 5 1
While typing in puzzles, I occasionally missed a digit, resulting in some odd looking solutions where some cells had no values! So, I added a sanity check for each iteration.
Only one puzzle left on level 8, this must be a doozy. Three iterations later:
In the end what did I get out of this?
I’d appreciate feedback on the code, which can be found on Github! Especially anything that makes the algorithms more readable.