Expand description

§WASM Crossword Generator

wasm_crossword_generator is a library for generating and operating crossword puzzles with first-class WebAssembly (WASM) support targeting the browser and other WASM environments. While fully functional and ergonomic as a Rust library, the design is WASM-first so some trade-offs are made such as not using const generics, avoiding Option<Option<T>> because of ambiguity during JSON de/serialization, and using structs over tuples.

This library exposes configuration options for specifying the size and density of the crossword. Three stateful implementations of Playmodes are also supported to facilitate different styles of puzzle.

The most basic example of usage in pure Rust looks like:

use wasm_crossword_generator::*;

let words: Vec<Word> = vec![
  Word { text: "library".to_string(), clue: Some("Not a framework.".to_string()) },
  // In a real usage, there would be more entries.
];

// A real SolutionConf would probably want "requirements" to allow for retrying crossword
// generation. Because there is only one word, we know we'll get the world's simplest puzzle in
// one try.
let solution_conf = SolutionConf {
    words: words,
    max_words: 20,
    width: 10,
    height: 10,
    requirements: None,
    initial_placement: None,
};

// A PerWordPuzzle is one where the player only guesses words, not locations. The player is
// immediately informed if the guess is correct or not.
let mut puzzle = PerWordPuzzle::new(solution_conf)?;

let guess = PlacedWord {
  // Because it is a PerWordPuzzle, the placement is ignored, unlike other Playmodes.
  placement: Placement { x: 0, y: 0, direction: rand::random() },

  // NOTE: you don't need to match the "clue" field, it is ignored for purposes of PartialEq
  word: Word { text: "library".to_string(), clue: None }
};

let guess_result = puzzle.guess_word(guess)?;

// Because there is only one word, the guess will result in "Complete" instead of "Correct"
assert_eq!(guess_result, GuessResult::Complete);

A quick tour of the pure Rust structures looks like:

Word a structure containing the field text (the string whose characters make up the answer) and an optional, self-descriptive clue field.

Direction is an enum of either Verticle or Horizontal, used to describe a Word’s orientation.

Placement is a struct containing a pair of coordinates (the fields x and y) and a direction field of type Direction.

PlacedWord is a combination of a Word (field: word) and Placement (field: placement). This is used for the internal representation of the Solution in it’s list of answers.

SolutionRow is a struct with a row field of type Vec<Option<char>>. This is used to to represent a row of the crossword, with each item being of type Some(c: char) in the case of the square being part of a solution or None in the case that the space is blank.

Solution is a struct with a grid field that is a Vec<SolutionRow> and a words field that is a Vec<PlacedWord>, along with a width and height. This is used by the puzzle structs to check player answers and build the stateful game.

SolutionConf is a struct containing a set of options for generating Solutions. It includes optional sub-structs CrosswordReqs and CrosswordInitialPlacement among several other fields.

PuzzleSpace is a struct that represents a stateful space in a game. Contains a bool indicating whether there would be a letter if the puzzle were solved, and a Option<char> representing whether a guess has been made or not.

PuzzleRow is similiar to SolutionRow, but the row field is a Vec<PuzzleSpace>.

Puzzle is a struct representing a stateful crossword puzzle game that responds to user input. It containes a solution field with it’s corresponding Solution, a player_answers field that is a Vec<PlacedWord> representing the player’s (not neccessarily correct) answers, and grid field similar to the one found in Solution, but this time of type Vec<PuzzleRow>.

Three Playmodes exist which make use of the Puzzle struct as their internal state: ClassicPuzzle – a clue-based crossword that doesn’t tell the user if their guess is correct, PlacedWordPuzzle – a puzzle where the player specifies a PlacedWord guess and is told if the guess was correct, and PerWordPuzzle where the player simply guesses a word and if it is present in the puzzle, the player is told that the guess is correct.

Finally, the GuessResult enum encapsulates the possible results from all Playmodes.

Additionally, there are some types used specifically for WASM-based scenerios, mostly wrapper types, specifiers, and types used to return data passed in from the caller back to the caller. These include: PuzzleType, PuzzleContainer, PuzzleCompleteContainer, WrongAnswerPair, WrongAnswersContainer, and PuzzleAndResult.

See the project repo for more details about it’s downstream packaging to NPM.

An example of the library in action is found here, and the source of that web app is found at this repo.

Structs§

  • ClassicPuzzle Playmode checks that guesses fit the Placement and don’t overwrite existing guesses. It accumulates player answers and can return if it’s complete or not, but does not do so automatically on guess like other Playmodes. It exposes a “remove_answer” function to allow the player to remove guesses they deem as bad when dealing with an incorrect Puzzle.
  • CrosswordInitialPlacement allows the caller to specify where and how large the initial word placed should be.
  • CrosswordReqs is a structure holding requirements the final puzzle must meet including: “max_retries”: how many times to attempt to make a valid puzzle before erroring out, “min_letters_per_word” how small can a word be (if > 3, this value overwrites MIN_LETTER_COUNT) “min_words” the minimum number of words that make up the answer “max_empty_columns” the number of columns that can be completely empty “max_empty_rows” the number of rows that can be completely empty
  • PerWordPuzzle Playmode expects the user to guess a word without a Placement. If the word is present, it is added to the Puzzle at the correct placement. On the last word being correctly guessed, it returns GuessResult::Complete.
  • PlacedWord represents a word which makes up the crossword puzzle’s answers The placement field marks the origin and orientation of the word.
  • PlacedWordPuzzle Playmode expects the guess to a placement and word, then only adds the answer to the puzzle state if the guess is valid. Returns a “Complete” result when the last correct guess is given.
  • Placement describes a location on the crossword puzzle. Used to mark word origins.
  • Puzzle is a stateful game composed of a static solution, a stateful grid, and a list of player submitted answers.
  • PuzzleAndResult is a wrapper type used to deal with ownership issues between the JS/WASM divide. When a JS application calls guess_word, it surrenders ownership of the PuzzleContainer over to the WASM side, which then performs the requested operation. The WASM then uses this wrapper to return the result of the operation along with ownership of the given PuzzleContainer back to the JS side.
  • PuzzleCompleteContainer is used to pass back a puzzle after checking for completeness. This is to allow the JS client to surrender ownership of the puzzle, then have it returned by the WASM function is_puzzle_complete.
  • PuzzleContainer allows both sides of the JS/WASM divide to understand the PuzzleType when the struct is passed over the barrier and back. In normal Rust, each of the Playmode structs are identifiable by their type, but when they are De/Serialized, it is impossible to distinguish between them, since they are all of the same shape: { puzzle: Puzzle }.
  • PuzzleRow contains a vector of PuzzleSpaces at “row”, represetning a stateful row of the crossword’s grid.
  • PuzzleSpace functions as a stateful structure representing whether a char has been guessed for this space. Contains a boolean representing whether it can take a char (true) or is an empty space (false), and has a char_slot that is an Option<char>. This could be represented as an Option<Option<char>> if this were only targeting Rust, but that approach becomes ambiguous when De/Serializing from JS.
  • Solution represents a complete crossword structure. Does not include stateful constructs for user input.
  • SolutionConf is the structure used to configure the generation crossword solutions.
  • SolutionRow represents of one row of a crossword solution. Within the interior “row” vector, there is either a None for a blank space or a Some(c) where c: char which would be a component of the crossword solution. Constructed by passing in a “width” param.
  • Word is a record containing a potential portion of the Crossword answer at “text” along with an optional “clue” field. The “text” field will be split using .chars() with all the implications that brings.
  • WrongAnswerPair is used to get around the inability to use tuples in WASM by converting a tuple of (got, wanted) to a struct with those labeled fields.
  • WrongAnswersContainer is a wrapper used to deal with ownership issues between the JS/WASM divide. When the JS client calls wrong_answers_and_solutions, it passes ownership of the PuzzleContainer that it wants the wrong answers of to WASM. WASM performs the operation, and returns the given PuzzleContainer in this wrapper (along with the requested data) back to the caller.

Enums§

  • CrosswordError is what it says on the tin. In generation, these are ellided over in favor of retrying, which if fails, bubbles the CrosswordError::MaxRetries error.
  • CrosswordInitialPlacementStrategy allows the caller to specifiy how to place the first word of the puzzle. Can be randomly generated using local RNG, which is how default is impl’d.
  • Direction is used to determine the orientation of the word. Includes an “other” function to get a given direction’s inverse and impls Distribution (50/50) so that it can be generated using the local RNG.
  • GuessResult encompasses the possible outcomes of a player guess. Not all results are used with all Playmodes. This allows a calling application to distiguish between a bad answer from a player and a bad state in the Puzzle by returning an error in the case of the latter.
  • PuzzleType allows both sides of the JS/WASM divide to reference different types of Playmode.

Traits§

  • Playmode is an abstraction over the act of the player guessing a word, depending on the playmode different checks and results will occur.

Functions§

  • guess_word is similar to the native Rust’s PlayMode.guess_word(guess) but uses the PuzzleAndResult wrapper type to return ownership of the passed in PuzzleContainer to the JS side.
  • Takes a PuzzleContainer and returns a PuzzleCompleteContainer with a bool at “is_complete” describing puzzle state. The use of these wrapper containers is to get around ownership issues over the JS/WASM divide. JS passes ownership of the PuzzleContainer to WASM and WASM returns the given PuzzleContainer inside the PuzzleCompleteContainer back to the JS caller.
  • new_puzzle is the only way JS/WASM applications can construct Puzzle structs. Requires a PuzzleType which will determine the Puzzle’s Playmode and act as the label of the returned Puzzle struct.
  • new_solution is the only way JS/WASM applications can construct Solution structs
  • remove_answer calls puzzle_container.puzzle.remove_answer(&placement), then returns ownership of the PuzzleContainer back to the calling JS side.
  • set_panic_hook is a debug feature that is called from npm_pkg/src/crossword_generator_wrapper.ts It improves the quality of error messages that are printed to the dev console For more details see <https://github.com/rustwasm/console_error_panic_hook#readme>
  • wrong_answers_and_solutions acts as calling puzzle_container.puzzle.wrong_answers_and_solutions()? but formats the output in structs rather than tuples for the calling JS application and returns ownership of the passed-in PuzzleContainer to the JS side.