Resources
Guidance on Methods
The Automaton<S>
interface allows solvers to traverse game states based on a game's rules. Before you do anything, you should at least skim the docstrings on the Automaton<S>
generic trait declaration. Doing this will let you understand what we are trying to do here, and why the semantics/abstractions around state transitions are the way they are.
Side note: In puzzles, we don't really care about player count, so you will not need to encode whose turn it is into state hashes. If we wanted to make this a multiplayer game, we would need to use the turn bitpacking utilities provided in crate::games::utils
.
1. start
This function reads the configuration of the crossteaser game Session
(please see this issue to see how it is initialized) and returns a hash of the starting state of the specified variant.
Since the variant information only defines the amount of empty slots together with the length and width of the game board, there is ambiguity on the following:
- Where the empty slots are
- How the pieces in the non-empty slots are oriented
It should not matter how you choose to disambiguate, as a strong solution should end up exploring all possibilities anyway (and all states within the same variant under our variant definition are reachable from all other states).
To help you do this, a Face
enum is defined with the possible orientations of a puzzle piece. Other than that, it is completely up to you how you encode/decode the board into/from a State
(which is just a 64-bit unsigned integer). It will take some creativity, but make sure you have some degree of performance in mind (a lot of the time we spend during solving will be doing hashing and unhashing). Since boards are essentially matrices, you can take a look at the nalgebra
package (which you will need to use anyway) for performant linear algebra constructs.
However you end up hashing and unhashing, you should probably factor it out into functions of their own, because it is something that you will do in the next two methods as well.
(Chill) warning: This function is called only once per solve.
2. transition
This simply takes a state and returns a list of possible states reachable from that state. This is the most technical part of this implementation, but it introduces no new concepts. This can definitely be made super slow, so as a baseline, make sure that the runtime and memory usage of whichever algorithm you use is in O(LW), where L
is the length of the board and W
is the width.
If you are feeling extra, you can encode the location of the empty slots into the state hash to speed this up significantly for variants with considerably fewer empty slots than pieces. Clever use of the turn bitpacking utilities should allow you to do this. Note that the hash
and unhash
functions you (supposedly) made for the implementation of start
should come in pretty handy here.
Warning: This function is called once for every state in the puzzle!
3. accepts
Unhash the input state and return true iff the puzzle is solved. This should be done in O(LW) time, where L
is the length of the board and W
is the width. Again, if you are feeling extra, encoding some statistics about the orientation of all pieces into the State
hash should allow you to do this in constant time with relation to the board size (at no additional cost to the runtime of transition
, because you can just update these statistics based on each type of move).
Warning: This function is also called once for every state in the puzzle!
Unhash and Hash Function Techniques
I know this is a lot, so here are three options of unhash
and hash
function signatures you can use, in order of increasing difficulty:
fn hash(representation: Vec<Vec<Face>>) -> State
and fn unhash(state: State) -> Vec<Vec<Face>>
: This is slow because you need to check every slot in the representation
to get the information you need for transition
and accepts
, and because you need to do a ton of memory requests to the operating system (because of the nested Vec<_>
).
- Recommended:
fn hash(representation: Vec<Face>, empty_locations: u64) -> State
and fn unhash(state: State) -> (Vec<Face>, u64)
: This is (a lot) faster because it flattens the representation matrix into a single contiguous piece of memory, and because it keeps track of the locations of the empty slots in the board (meaning that transition
can simply take a look at the pieces around them to know what moves can be made).
fn hash(representation: Vec<Face>, empty_locations: u64, face_counts: EfficientEncoding) -> State
and an inverse unhash
: This somehow adds the information of how many of each type of face is currently facing the player, which would make the accepts
function a constant-time operation. I don't know exactly how to do this, so I will be super impressed if you do it.
- Choose some combination of the above techniques based on the variant parameters for peak performance.