Crosswords

Structures

Enigmistics.CrosswordWordType
CrosswordWord

Structure for a word placed in the crossword.

Fields

  • word::String: the actual word
  • row::Int: starting row
  • col::Int: starting column
  • direction::Symbol: either :vertical or :horizontal
source
Enigmistics.CrosswordBlackCellType
CrosswordBlackCell

Structure for a black cell placed in the crossword.

Fields

  • count::Float64: number of words that share that black cell (or Inf64 if it was manually placed)
  • manual::Bool: if the cell was manually set by user or automatically derived based on surrounding words
source
Enigmistics.CrosswordPuzzleType
CrosswordPuzzle

Structure for a crossword puzzle.

Fields

  • grid::Matrix{Char}: the actual crossword grid
  • words::Vector{CrosswordWord}: vector containing all the words
  • black_cells::Dict{Tuple{Int,Int}, CrosswordBlackCell}: dictionary storing the black cells information
source

Here are some useful constructors:

Enigmistics.CrosswordPuzzleMethod
CrosswordPuzzle(rows::Int, cols::Int)

Construct a crossword with an empty grid of the given dimensions.

Examples

julia> cw = CrosswordPuzzle(4,5)
    1  2  3  4  5 
  ┌───────────────┐
1 │ ⋅  ⋅  ⋅  ⋅  ⋅ │
2 │ ⋅  ⋅  ⋅  ⋅  ⋅ │
3 │ ⋅  ⋅  ⋅  ⋅  ⋅ │
4 │ ⋅  ⋅  ⋅  ⋅  ⋅ │
  └───────────────┘
source
Enigmistics.CrosswordPuzzleMethod
CrosswordPuzzle(rows::Int, cols::Int, words::Vector{CrosswordWord})

Construct a crossword with the given dimensions and words. Return an error if words intersections are not compatible.

Examples

julia> words = [CrosswordWord("CAT",2,2,:horizontal), CrosswordWord("MAP",1,3,:vertical),
                CrosswordWord("SIR",4,4,:horizontal)];

julia> CrosswordPuzzle(5,6,words)
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ ⋅  ⋅  M  ⋅  ⋅  ⋅ │
2 │ ■  C  A  T  ■  ⋅ │
3 │ ⋅  ⋅  P  ⋅  ⋅  ⋅ │
4 │ ⋅  ⋅  ■  S  I  R │
5 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
  └──────────────────┘

julia> words = [CrosswordWord("CAT",2,2,:horizontal), CrosswordWord("MAP",1,3,:vertical),
                CrosswordWord("SIR",4,4,:horizontal), CrosswordWord("DOG",1,2,:vertical)];

julia> CrosswordPuzzle(5,6,words)
┌ Warning: Cannot place word 'DOG' at (1, 2) vertically due to conflict at cell (2, 2); found when checking the inner cells.
┌ Error: Words intersections are not compatible.
source
Enigmistics.CrosswordPuzzleMethod
CrosswordPuzzle(words::Vector{CrosswordWord})

Construct a crossword with the given words (dimensions are inferred from words positions). Return an error if words intersections are not compatible.

Examples

julia> words = [CrosswordWord("CAT",2,2,:horizontal), CrosswordWord("MAP",1,3,:vertical),
                CrosswordWord("SIR",4,4,:horizontal)];

julia> CrosswordPuzzle(words)
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ ⋅  ⋅  M  ⋅  ⋅  ⋅ │
2 │ ■  C  A  T  ■  ⋅ │
3 │ ⋅  ⋅  P  ⋅  ⋅  ⋅ │
4 │ ⋅  ⋅  ■  S  I  R │
  └──────────────────┘

julia> words = [CrosswordWord("CAT",2,2,:horizontal), CrosswordWord("MAP",1,3,:vertical),
                CrosswordWord("SIR",4,4,:horizontal), CrosswordWord("DOG",1,2,:vertical)];

julia> CrosswordPuzzle(words)
┌ Warning: Cannot place word 'DOG' at (1, 2) vertically due to conflict at cell (2, 2); found when checking the inner cells.
┌ Error: Words intersections are not compatible.
source

Interface

Enigmistics.show_crosswordFunction
show_crossword(cw::CrosswordPuzzle; 
	words_details=true, black_cells_details=false, plot_details=true)

Print the crossword grid, possibly along with the words details and black cells details.

Examples

julia> cw = example_crossword() # normal output
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  O  ■  A │
3 │ T  ■  S  O  U  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ W  I  N  D  O  W │
  └──────────────────┘

julia> show_crossword(cw, black_cells_details=true) # extended details output
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  O  ■  A │
3 │ T  ■  S  O  U  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ W  I  N  D  O  W │
  └──────────────────┘

Grid size: (6, 6)
Black cells density: 19.44%

Horizontal words:
- GOLDEN
- AN
- SOUR
- EVER
- IE
- WINDOW
Vertical words:
- GATE
- ON, VII
- DOOR
- NARROW
- SEEN

Black cells:
 - at (5, 5) was manually placed
 - at (3, 2) was automatically derived (delimiting 3 words)
 - at (4, 5) was automatically derived (delimiting 1 words)
 - at (2, 5) was manually placed
 - at (5, 1) was automatically derived (delimiting 2 words)
 - at (2, 3) was automatically derived (delimiting 2 words)
 - at (5, 4) was automatically derived (delimiting 2 words)

             Words length distribution         
    ┌────────────────────────────────────────┐ 
  2 │■■■■■■■■■■■■■■■■■■■■■■ 3                │ 
  3 │■■■■■■■ 1                               │ 
  4 │■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 5 │ 
  5 │ 0                                      │ 
  6 │■■■■■■■■■■■■■■■■■■■■■■ 3                │ 
    └────────────────────────────────────────┘ 
source
Enigmistics.example_crosswordFunction
example_crossword(; type="full")

Return an example crossword, useful e.g during testing. Available values for now are

  • "full": a complete crossword with all words
  • "partial": an incomplete crossword with some words missing

Examples

julia> cw = example_crossword(type="full")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  O  ■  A │
3 │ T  ■  S  O  U  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ W  I  N  D  O  W │
  └──────────────────┘

julia> example_crossword(type="partial")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘
source
Enigmistics.enlarge!Function
enlarge!(cw::CrosswordPuzzle, how::Symbol, times=1)
enlarge!(cw::CrosswordPuzzle, times=1)

Enlarge the crossword grid in the direction given by how (accepted values are :N, :S, :E, :O) by appropriately inserting times empty rows/columns. If no direction is specified, the grid will be enlarged in all directions by the amount given by times.

Examples

julia> cw = example_crossword()
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  O  ■  A │
3 │ T  ■  S  O  U  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ W  I  N  D  O  W │
  └──────────────────┘

julia> enlarge!(cw, :O, 2); cw
    1  2  3  4  5  6  7  8 
  ┌────────────────────────┐
1 │ ⋅  ■  G  O  L  D  E  N │
2 │ ⋅  ■  A  N  ■  O  ■  A │
3 │ ⋅  ⋅  T  ■  S  O  U  R │
4 │ ⋅  ■  E  V  E  R  ■  R │
5 │ ⋅  ⋅  ■  I  E  ■  ■  O │
6 │ ⋅  ■  W  I  N  D  O  W │
  └────────────────────────┘

julia> enlarge!(cw); cw
    1  2  3  4  5  6  7  8  9 10 
  ┌──────────────────────────────┐
1 │ ⋅  ⋅  ⋅  ■  ■  ⋅  ■  ⋅  ■  ⋅ │
2 │ ⋅  ⋅  ■  G  O  L  D  E  N  ■ │
3 │ ⋅  ⋅  ■  A  N  ■  O  ■  A  ⋅ │
4 │ ⋅  ⋅  ⋅  T  ■  S  O  U  R  ■ │
5 │ ⋅  ⋅  ■  E  V  E  R  ■  R  ⋅ │
6 │ ⋅  ⋅  ⋅  ■  I  E  ■  ■  O  ⋅ │
7 │ ⋅  ⋅  ■  W  I  N  D  O  W  ■ │
8 │ ⋅  ⋅  ⋅  ⋅  ■  ■  ⋅  ⋅  ■  ⋅ │
  └──────────────────────────────┘
source
Enigmistics.shrink!Function
shrink!(cw::CrosswordPuzzle)

Reduce the crossword size to its minimal representation by removing useless rows/columns.

Note that it will not necessarily preserve the connectivity of the crossword. If you want to restore it, you can rely on the enlarge! function.

Examples

julia> cw = example_crossword(type="full"); enlarge!(cw); cw
    1  2  3  4  5  6  7  8 
  ┌────────────────────────┐
1 │ ⋅  ■  ■  ⋅  ■  ⋅  ■  ⋅ │
2 │ ■  G  O  L  D  E  N  ■ │
3 │ ■  A  N  ■  O  ■  A  ⋅ │
4 │ ⋅  T  ■  S  O  U  R  ■ │
5 │ ■  E  V  E  R  ■  R  ⋅ │
6 │ ⋅  ■  I  E  ■  ■  O  ⋅ │
7 │ ■  W  I  N  D  O  W  ■ │
8 │ ⋅  ⋅  ■  ■  ⋅  ⋅  ■  ⋅ │
  └────────────────────────┘

julia> shrink!(cw)
true

julia> cw
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  O  ■  A │
3 │ T  ■  S  O  U  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ W  I  N  D  O  W │
  └──────────────────┘
source
Enigmistics.can_place_wordFunction
can_place_word(cw::CrosswordPuzzle, word::String, row, col, direction::Symbol)
can_place_word(cw::CrosswordPuzzle, cword::CrosswordWord)

Check if a word can be placed in the crossword puzzle cw at the given position and direction.

Return true if the word can be placed, false otherwise.

Examples

julia> cw = example_crossword(type="partial")
    1  2  3  4  5  6
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘

julia> can_place_word(cw, "PEARS", 3, 3, :h)
┌ Warning: Word 'PEARS' does not fit in the grid horizontally at (3, 3).
false

julia> can_place_word(cw, "BEAN", 3, 3, :h)
┌ Warning: Cannot place word 'BEAN' at (3, 3) horizontally due to conflict at cell (3, 6); found when checking the inner cells.
false

julia> can_place_word(cw, "TEA", 3, 3, :h)
┌ Warning: Cannot place word 'TEA' at (3, 3) horizontally due to conflict at cell (3, 6); found when checking the border cells.
false

julia> can_place_word(cw, "DEAR", 1, 4, :v)
true
source
Enigmistics.place_word!Function
place_word!(cw::CrosswordPuzzle, word::String, row, col, direction::Symbol)
place_word!(cw::CrosswordPuzzle, cword::CrosswordWord)

Place a word in the crossword puzzle cw at the given position and direction, also checking if it can actually be placed.

Return true if the word was successfully placed, false otherwise.

Examples

julia> cw = example_crossword(type="partial")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘

julia> place_word!(cw, "cat", 6, 1, :h)
┌ Warning: Cannot place word 'CAT' at (6, 1) horizontally due to conflict at cell (6, 2); found when checking the inner cells.
false

julia> place_word!(cw, "pillow", 6, 1, :h)
true

julia> cw
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ P  I  L  L  O  W │
  └──────────────────┘
source
Enigmistics.remove_word!Function
remove_word!(cw::CrosswordPuzzle, word::String)
remove_word!(cw::CrosswordPuzzle, cword::CrosswordWord)
remove_word!(cw::CrosswordPuzzle, words::Vector{String})
remove_word!(cw::CrosswordPuzzle, cwords::Vector{CrosswordWord})

Remove a word (or multiple words) from the crossword puzzle cw.

Return true if the word was found and removed, false otherwise.

Examples

julia> cw = example_crossword(type="partial")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘

julia> remove_word!(cw, "cat")
┌ Warning: Word 'CAT' not found in the crossword. No changes on the original grid.
false

julia> remove_word!(cw, "narrow")
true

julia> cw
    1  2  3  4  5  6
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  ⋅ │
3 │ T  ■  ⋅  ⋅  ⋅  ⋅ │
4 │ E  V  E  R  ■  ⋅ │
5 │ ■  I  E  ■  ■  ⋅ │
6 │ ⋅  I  ⋅  ⋅  ⋅  ⋅ │
  └──────────────────┘
source
Enigmistics.place_black_cell!Function
place_black_cell!(cw::CrosswordPuzzle, row::Int, col::Int; symmetry=false, double_symmetry=false)

Place a black cell in the crossword puzzle cw at the given position.

Return true if the black cell was successfully placed, false otherwise.

Examples

julia> cw = example_crossword(type="partial")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘

julia> place_black_cell!(cw, 6, 2)
┌ Warning: Cannot place black cell at position (6, 2) since cell is not empty. No changes on the original grid.
false

julia> place_black_cell!(cw, 6, 4)
true

julia> cw
    1  2  3  4  5  6
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ■  ⋅  W │
  └──────────────────┘
source
Enigmistics.remove_black_cell!Function
remove_black_cell!(cw::CrosswordPuzzle, row::Int, col::Int)
remove_black_cell!(cw::CrosswordPuzzle, coords::Vector{<:Union{Tuple{Int, Int}, CartesianIndex{2}}})

Remove a black cell (or multiple black cells) from the crossword puzzle cw at the given position.

Return true if the black cell was successfully removed, false otherwise.

Examples

julia> cw = example_crossword(type="partial"); show_crossword(cw, words_details=false)
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘

Black cells:
 - at (5, 5) was manually placed
 - at (4, 5) was automatically derived (delimiting 1 words)
 - at (3, 2) was automatically derived (delimiting 2 words)
 - at (2, 5) was manually placed
 - at (5, 1) was automatically derived (delimiting 2 words)
 - at (2, 3) was automatically derived (delimiting 1 words)
 - at (5, 4) was automatically derived (delimiting 1 words)

julia> remove_black_cell!(cw, 3, 2)
┌ Warning: Cannot remove automatically placed black cell at position (3, 2) since it's needed as a word delimiter. No changes on the original grid.
false

julia> remove_black_cell!(cw, 5, 5)
true

julia> cw
    1  2  3  4  5  6
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ⋅  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘
source
Enigmistics.clear!Function
clear!(cw::CrosswordPuzzle; deep=false)

Clear all words, possibly preserving manually-placed black cells (with deep=false), from the crossword puzzle cw.

Examples

julia> cw = example_crossword(); show_crossword(cw, words_details=false)
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  O  ■  A │
3 │ T  ■  S  O  U  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ W  I  N  D  O  W │
  └──────────────────┘

Black cells:
 - at (5, 5) was manually placed
 - at (3, 2) was automatically derived (delimiting 3 words)
 - at (4, 5) was automatically derived (delimiting 1 words)
 - at (2, 5) was manually placed
 - at (5, 1) was automatically derived (delimiting 2 words)
 - at (2, 3) was automatically derived (delimiting 2 words)
 - at (5, 4) was automatically derived (delimiting 2 words)

julia> clear!(cw)
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
2 │ ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
3 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
4 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
5 │ ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
6 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
  └──────────────────┘

julia> clear!(cw, deep=true)
    1  2  3  4  5  6
  ┌──────────────────┐
1 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
2 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
3 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
4 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
5 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
6 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
  └──────────────────┘
source

Patterns

Enigmistics.random_patternFunction
random_pattern(nrows, ncols; 
	max_density=0.18, symmetry=true, double_symmetry=true, 
	seed=rand(Int))

Generate a crossword puzzle with black cells placed according to a random pattern which can be totally random, symmetric, or doubly symmetric (i.e. specular).

Arguments

  • nrows::Int, ncols::Int: grid dimensions
  • max_density::Float=0.18: maximum density of black cells
  • symmetry::Bool=true: whether to enforce symmetry
  • double_symmetry::Bool=false: whether to enforce double symmetry (i.e. specularity)
  • seed::Int=rand(Int): random seed for reproducibility

Examples

julia> random_pattern(12, 20, symmetry=false, seed=1)
     1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20
   ┌────────────────────────────────────────────────────────────┐
 1 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ■  ⋅  ⋅  ■ │
 2 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 3 │ ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅ │
 4 │ ■  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ■  ⋅ │
 5 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ■  ⋅  ⋅ │
 6 │ ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 7 │ ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ■  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅ │
 8 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ■  ⋅  ⋅  ⋅  ⋅ │
 9 │ ⋅  ■  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅ │
10 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ■  ⋅  ■  ⋅  ⋅  ■  ⋅ │
11 │ ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ■  ⋅  ■  ⋅  ⋅  ■ │
12 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
   └────────────────────────────────────────────────────────────┘

julia> random_pattern(12, 20, symmetry=true, seed=1)
     1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20
   ┌────────────────────────────────────────────────────────────┐
 1 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■ │
 2 │ ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅ │
 3 │ ■  ⋅  ⋅  ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 4 │ ■  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅ │
 5 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ■  ⋅  ⋅ │
 6 │ ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ■  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 7 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ■  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅ │
 8 │ ⋅  ⋅  ■  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 9 │ ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ■ │
10 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ⋅  ⋅  ■ │
11 │ ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅ │
12 │ ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
   └────────────────────────────────────────────────────────────┘

julia> random_pattern(12, 20, symmetry=true, double_symmetry=true, seed=1)
     1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 
   ┌────────────────────────────────────────────────────────────┐
 1 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 2 │ ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅ │
 3 │ ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■ │
 4 │ ■  ⋅  ■  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ■  ⋅  ■ │
 5 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 6 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ■  ■  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 7 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ■  ■  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 8 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 9 │ ■  ⋅  ■  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ■  ⋅  ■ │
10 │ ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■ │
11 │ ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅ │
12 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
   └────────────────────────────────────────────────────────────┘
source
Enigmistics.striped_patternFunction
striped_pattern(nrows, ncols; 
	max_density = 0.18, 
	min_stripe_dist = 4, keep_stripe_prob = 0.9,
	symmetry = true, double_symmetry = false, 
	seed=rand(Int))

Generate a crossword puzzle with black cells placed according to a striped pattern which can be random, symmetric, or doubly symmetric (i.e. specular).

Arguments

  • nrows::Int, ncols::Int: grid dimensions
  • max_density::Float=0.18: maximum density of black cells
  • min_stripe_dist::Int=4: minimum distance allowed between stripes
  • keep_stripe_prob::Float=0.8: probability of continuing a stripe
  • symmetry::Bool=true: whether to enforce symmetry
  • double_symmetry::Bool=false: whether to enforce double symmetry (i.e. specularity)
  • seed::Int=rand(Int): random seed for reproducibility

Examples

julia> striped_pattern(12, 20, symmetry=false, seed=1)
     1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 
   ┌────────────────────────────────────────────────────────────┐
 1 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 2 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅ │
 3 │ ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅ │
 4 │ ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅ │
 5 │ ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅ │
 6 │ ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 7 │ ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 8 │ ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅ │
 9 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■ │
10 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
11 │ ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅ │
12 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅ │
   └────────────────────────────────────────────────────────────┘

julia> striped_pattern(12, 20, symmetry=true, seed=1)
     1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 
   ┌────────────────────────────────────────────────────────────┐
 1 │ ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 2 │ ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅ │
 3 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅ │
 4 │ ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅ │
 5 │ ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅ │
 6 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 7 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 8 │ ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■ │
 9 │ ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅ │
10 │ ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
11 │ ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅ │
12 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅ │
   └────────────────────────────────────────────────────────────┘

julia> striped_pattern(12, 20, symmetry=true, double_symmetry=true, seed=1)
     1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 
   ┌────────────────────────────────────────────────────────────┐
 1 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 2 │ ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅ │
 3 │ ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅ │
 4 │ ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅ │
 5 │ ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅ │
 6 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 7 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 8 │ ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅ │
 9 │ ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅ │
10 │ ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅ │
11 │ ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅ │
12 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
   └────────────────────────────────────────────────────────────┘
source
Enigmistics.standard_patternFunction
standard_pattern(; style="NewYorkTimes", kwargs...)

Generate a crossword puzzle according to a standard pattern.

Available styles are: "NewYorkTimes", "Diamond", "BritishCryptic", "Polar", "Parametric". For each style, the following optional arguments are supported:

  • Diamond: grid_size=(13,13)
  • BritishCryptic: grid_size=(15,15), max_density=0.24, seed=rand(Int)
  • Polar: step_size=0.1, r_function=(θ->0.8*θ), max_theta=2π, round_method=RoundNearest (also RoundUp often produces good results), symmetry=false (argument for the black cell placement function), spread_by=1
  • Parametric: step_size=0.1, x_function=(θ->4*cos(θ)+1), y_function=(θ->4*sin(θ)+1), max_theta=2π, round_method=RoundNearest (also RoundUp often produces good results), symmetry=false (argument for the black cell placement function), spread_by=1

Example

julia> cw = standard_pattern(style="NewYorkTimes")
     1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 
   ┌─────────────────────────────────────────────┐
 1 │ ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅ │
 2 │ ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅ │
 3 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 4 │ ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 5 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ■  ■ │
 6 │ ■  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅ │
 7 │ ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 8 │ ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅ │
 9 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅ │
10 │ ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ■ │
11 │ ■  ■  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
12 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅ │
13 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
14 │ ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅ │
15 │ ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅ │
   └─────────────────────────────────────────────┘

julia> standard_pattern(style="Diamond", grid_size=(9,9))
    1  2  3  4  5  6  7  8  9
  ┌───────────────────────────┐
1 │ ■  ■  ■  ■  ⋅  ■  ■  ■  ■ │
2 │ ■  ■  ■  ⋅  ⋅  ⋅  ■  ■  ■ │
3 │ ■  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ■ │
4 │ ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■ │
5 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
6 │ ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■ │
7 │ ■  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ■ │
8 │ ■  ■  ■  ⋅  ⋅  ⋅  ■  ■  ■ │
9 │ ■  ■  ■  ■  ⋅  ■  ■  ■  ■ │
  └───────────────────────────┘

julia> standard_pattern(style="BritishCryptic", grid_size=(11,13), seed=1234)
     1  2  3  4  5  6  7  8  9 10 11 12 13 
   ┌───────────────────────────────────────┐
 1 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅ │
 2 │ ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅ │
 3 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 4 │ ■  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅ │
 5 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 6 │ ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅ │
 7 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 8 │ ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ■ │
 9 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
10 │ ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅  ■  ⋅ │
11 │ ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
   └───────────────────────────────────────┘

julia> cw = standard_pattern(style="Polar", max_theta=3π)
     1  2  3  4  5  6  7  8  9 10 11 12 13 
   ┌───────────────────────────────────────┐
 1 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 2 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅ │
 3 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅ │
 4 │ ⋅  ⋅  ⋅  ■  ■  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 5 │ ⋅  ⋅  ■  ■  ⋅  ■  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 6 │ ⋅  ■  ■  ⋅  ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 7 │ ⋅  ■  ⋅  ⋅  ⋅  ■  ■  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 8 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 9 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
10 │ ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅ │
11 │ ⋅  ⋅  ■  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ■  ⋅  ⋅ │
12 │ ⋅  ⋅  ⋅  ⋅  ■  ■  ■  ■  ■  ⋅  ⋅  ⋅  ⋅ │
13 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
   └───────────────────────────────────────┘

julia> cw = standard_pattern(style="Parametric")
     1  2  3  4  5  6  7  8  9 10 11 
   ┌─────────────────────────────────┐
 1 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 2 │ ⋅  ⋅  ⋅  ■  ■  ■  ■  ■  ⋅  ⋅  ⋅ │
 3 │ ⋅  ⋅  ■  ■  ⋅  ⋅  ⋅  ■  ■  ⋅  ⋅ │
 4 │ ⋅  ■  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ■  ⋅ │
 5 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 6 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 7 │ ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅ │
 8 │ ⋅  ■  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ■  ⋅ │
 9 │ ⋅  ⋅  ■  ■  ⋅  ⋅  ⋅  ■  ■  ⋅  ⋅ │
10 │ ⋅  ⋅  ⋅  ■  ■  ■  ■  ■  ⋅  ⋅  ⋅ │
11 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
   └─────────────────────────────────┘
source

IO

Enigmistics.save_crosswordFunction
save_crossword(cw::CrosswordPuzzle, filename::String)

Save the crossword puzzle cw to a text file specified by filename.

The grid is saved in a visually readable format, matching the terminal output, complete with borders, row/column indicators, and clear symbols.

Example

julia> cw = example_crossword(type="full")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  O  ■  A │
3 │ T  ■  S  O  U  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ W  I  N  D  O  W │
  └──────────────────┘

julia> save_crossword(cw, "ex1.txt")
file ex1.txt:
     1  2  3  4  5  6 
   ┌──────────────────┐
 1 │ G  O  L  D  E  N │
 2 │ A  N  ■  O  ■  A │
 3 │ T  ■  S  O  U  R │
 4 │ E  V  E  R  ■  R │
 5 │ ■  I  E  ■  ■  O │
 6 │ W  I  N  D  O  W │
   └──────────────────┘
source
Enigmistics.load_crosswordFunction
load_crossword(path::String)

Load a crossword puzzle from a text file specified by path.

The function is capable of parsing both the visually rich format (with borders and numbers) and the classic dense format.

Conventions recognized:

  • or / represent a black cell
  • or . represent an empty cell
  • letters represent filled cells

Example

file ex2.txt:
     1  2  3  4  5  6 
   ┌──────────────────┐
 1 │ G  O  L  D  E  N │
 2 │ A  N  ■  O  ■  ⋅ │
 3 │ T  ■  S  O  U  R │
 4 │ E  V  E  R  ■  ⋅ │
 5 │ ■  I  E  ■  S  ⋅ │
 6 │ ⋅  I  N  ⋅  O  ⋅ │
   └──────────────────┘
julia> cw = load_crossword("ex2.txt"); show_crossword(cw)
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  O  ■  ⋅ │
3 │ T  ■  S  O  U  R │
4 │ E  V  E  R  ■  ⋅ │
5 │ ■  I  E  ■  S  ⋅ │
6 │ ⋅  I  N  ⋅  O  ⋅ │
  └──────────────────┘

Horizontal:
 - 'GOLDEN' at (1, 1)
 - 'AN' at (2, 1)
 - 'SOUR' at (3, 3)
 - 'EVER' at (4, 1)
 - 'IE' at (5, 2)
Vertical:
 - 'GATE' at (1, 1)
 - 'ON' at (1, 2)
 - 'VII' at (4, 2)
 - 'SEEN' at (3, 3)
 - 'DOOR' at (1, 4)
 - 'SO' at (5, 5)
source
Enigmistics.to_latexFunction
to_latex(cw::CrosswordPuzzle, clues::Dict{String, String}=Dict{String, String}(); 
    full_document = true, show_solution = true, solution_on_new_page = false,
    inline_clues = false, title = "")

Generate a string containing the LaTeX code which can be used (thanks to the cwpuzzle package) to represent the crossword puzzle cw.

Keyword Arguments

  • full_document::Bool: If true, includes the \documentclass preamble and \begin{document}. Set to false if you want to paste the output into an existing LaTeX file.
  • show_solution::Bool: If true, generates a second grid at the bottom with the solution.
  • solution_on_new_page::Bool: If true, puts a \newpage before the solution.
  • inline_clues::Bool: If true, formats the clues to run continuously on the same line.
  • title::String: An optional title to be printed above the crossword puzzle.
source

Now we move the focus towards the automation capabilities implemented by this package.

Slots

Enigmistics.SlotType
Slot

Structure for a crossword slot, i.e. a place where a word can be placed.

Fields

  • row::Int, col::Int: slot position
  • direction::Symbol: slot direction (:horizontal or :vertical)
  • length::Int: slot length
  • pattern::String: slot pattern, describing letters or blank spaces in its cells
  • flexible_start::Bool: can this slot be potentially expanded at the start (e.g. is it at the border of the grid)?
  • flexible_end::Bool: can this slot be potentially expanded at the end (e.g. is it at the border of the grid)?
source
Enigmistics.find_constrained_slotsFunction
find_constrained_slots(cw::CrosswordPuzzle)

Find all slots (horizontal and vertical) in the crossword puzzle cw that are constrained, i.e. that have at least one empty cell and length at least 2 characters.

Examples

julia> cw = example_crossword(type="partial")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘

julia> find_constrained_slots(cw)
4-element Vector{Slot}:
 Slot(3, 3, :horizontal, 4, "...R", false, true)
 Slot(6, 1, :horizontal, 6, ".I...W", true, true)
 Slot(3, 3, :vertical, 4, ".EE.", false, true)
 Slot(1, 4, :vertical, 4, "D..R", true, false)
source
Enigmistics.compute_options_simpleFunction
compute_options_simple(cw::CrosswordPuzzle, s::Slot; verbose=false)

Compute the number of fitting words for a slot s of the crossword cw considering its pattern and length.

Return a tuple (n_options, candidates), where n_options is the number of fitting words and candidates is a vector containing the list of fitting words.

Examples

julia> cw = example_crossword(type="partial")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘

julia> slots = find_constrained_slots(cw); slots[2]
Slot(6, 1, :horizontal, 6, ".I...W", true, true)

julia> compute_options_simple(cw, slots[2], verbose=true)
- simple fitting, length: 6 => #options: 19
      some are ["AIMLOW", "BIGWOW", "BILLOW", "JIGSAW", "KIDROW", "LIELOW", "MIDBOW", "MILDEW", "MINNOW", "PILLOW"]
source
Enigmistics.compute_options_splitFunction
compute_options_split(cw::CrosswordPuzzle, s::Slot; verbose=false)

Compute the number of fitting words for a slot s of the crossword cw by simulating the placement of black cells at each internal position of the slot, i.e. possibly splitting the original slot into two smaller slots.

Return a tuple (n_options, candidates), where n_options is a dictionary mapping the internal position of the black cell to the number of fitting words, and candidates is a dictionary mapping that same key to a tuple of two lists of fitting words (left and right sub-slots).

Examples

julia> cw = example_crossword(type="partial")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘

julia> slots = find_constrained_slots(cw); slots[2]
Slot(6, 1, :horizontal, 6, ".I...W", true, true)

julia> compute_options_split(cw, slots[2], verbose=true)
- placing a black cell at (6, 1), pattern: /I...W => #options: 13
      some are ["IDRAW", "IKNEW", "IKNOW", "INDOW", "INLAW", "INLOW", "INNEW", "INTOW", "ISHOW", "ISLEW"]
- placing a black cell at (6, 3), pattern: .I/..W => #options: 23/96
      some are Left: ["AI", "BI", "CI", "DI", "EI", "GI", "HI", "II", "JI", "KI"]
      some are Right: ["ALW", "AWW", "BBW", "BMW", "BOW", "BTW", "CAW", "CCW", "CEW", "COW"]
- placing a black cell at (6, 4), pattern: .I./.W => #options: 341/8
      some are Left: ["AIA", "AIC", "AID", "AIG", "AIL", "AIM", "AIN", "AIR", "AIS", "AIT"]
      they are Right: ["AW", "BW", "EW", "IW", "KW", "NW", "OW", "SW"]
- placing a black cell at (6, 5), pattern: .I../W => #options: 1136
      some are ["AIAI", "AIBO", "AIDA", "AIDE", "AIDS", "AIDY", "AIEA", "AIGU", "AIKO", "AILE"]
source
Enigmistics.compute_options_flexibleFunction
compute_options_flexible(cw::CrosswordPuzzle, s::Slot, k::Int=1; verbose=false)

Compute the number of fitting words for a slot s of the crossword cw considering its flexibility at the start and/or at the end, i.e. simulating the potential expansion of k cells in length which could happen by enlarging the grid.

Return a tuple (n_options, candidates), where n_options is a dictionary mapping the flexibility simulated (increment at the start and/or at the end) to the number of fitting words, and candidates is a dictionary mapping that same key to the list of fitting words.

Examples

julia> cw = example_crossword(type="partial")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘

julia> slots = find_constrained_slots(cw); slots[2]
Slot(6, 1, :horizontal, 6, ".I...W", true, true)

julia> compute_options_flexible(cw, slots[2], 1, verbose=true)
- flexible start/end, increment: (0, 1) => pattern .I...W., length: 7 => #options: 43
      some are ["AIMDOWN", "BIGNEWS", "BIGTOWN", "BIGYAWN", "BILLOWS", "BILLOWY", "DIALTWO", "DIDNTWE", "DIEDOWN", "DIGDOWN"]
- flexible start/end, increment: (1, 0) => pattern ..I...W, length: 7 => #options: 15
      some are ["AWILLOW", "BRISTOW", "DOITNOW", "EXITROW", "HAIRBOW", "LAIDLOW", "LAINLOW", "RAINBOW", "SKIDROW", "SKILSAW"]

julia> compute_options_flexible(cw, slots[2], 2, verbose=true)
- flexible start/end, increment: (0, 2) => pattern .I...W.., length: 8 => #options: 59
      some are ["AIRPOWER", "AISLEWAY", "AIWEIWEI", "BIDEAWEE", "BILLOWED", "BILLOWUP", "CIVILWAR", "DIEDAWAY", "DIESAWAY", "DIRTYWAR"]
- flexible start/end, increment: (1, 1) => pattern ..I...W., length: 8 => #options: 35
      some are ["BOILDOWN", "CHICHEWA", "CHIPBOWL", "CHIPPEWA", "EDITDOWN", "EXITROWS", "FAIRLAWN", "GOINDOWN", "HAILDOWN", "HAIRBOWS"]
- flexible start/end, increment: (2, 0) => pattern ...I...W, length: 8 => #options: 19
      some are ["ALLIKNOW", "BASICLAW", "BUYITNOW", "BYWINDOW", "CAPITALW", "CHAINSAW", "CIVILLAW", "DAVIDLOW", "DIPITLOW", "GETITNOW"]
source
Enigmistics.fitting_wordsFunction
fitting_words(pattern::Regex, min_len::Int, max_len::Int)
fitting_words(pattern::String, min_len::Int, max_len::Int)

Given a pattern and a range of word lengths, return a dictionary mapping each length to the list of matching words having that pattern and length.

Examples

julia> pattern="^pe.*ce$"
"^pe.*ce$"

julia> fitting_words(pattern, 5, 8) # with default english dictionary 
Dict{Int64, Vector{String}} with 4 entries:
  5 => ["PEACE", "PENCE", "PERCE", "PESCE"]
  6 => ["PEARCE", "PEERCE", "PEIRCE"]
  7 => ["PENANCE", "PETMICE"]
  8 => ["PEEDANCE", "PENZANCE", "PERFORCE", "PETFENCE"]
source

Dictionary

Enigmistics.set_dictionaryFunction
set_dictionary(language="en", words_customization=(words->words); allowed_chars = r"[^A-Z]")

Load the dictionary associated to the specified language.

Current supported languages are:

  • "en", english
  • "it", italian
  • "bs", bresciano (italian dialect spoken in the province of Brescia, northern Italy)
  • "lt", latin

Example

julia> set_dictionary("en")
Dictionary set to "en"
Loading file EN_/oxford/mix/WORDS.txt...
14138 words successfully loaded into DICTIONARY_WORDS
Maximum length: 18 letters

              Words length distribution         
     ┌────────────────────────────────────────┐ 
   2 │■■■■■■ 423                              │ 
   3 │■■■■■■■■■■■■■ 874                       │ 
   4 │■■■■■■■■■■■■■■■■■■■■■■■■■■ 1 728        │ 
   5 │■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 2 027   │ 
   6 │■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 2 185 │ 
   7 │■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 2 087  │ 
   8 │■■■■■■■■■■■■■■■■■■■■■■■■■ 1 641         │ 
   9 │■■■■■■■■■■■■■■■■■■■ 1 268               │ 
  10 │■■■■■■■■■■■■■ 881                       │ 
  11 │■■■■■■■■ 535                            │ 
  12 │■■■■ 279                                │ 
  13 │■■ 141                                  │ 
  14 │■ 52                                    │ 
  15 │ 12                                     │ 
  16 │ 4                                      │ 
  17 │ 0                                      │ 
  18 │ 1                                      │ 
     └────────────────────────────────────────┘

Words customization

Words can be manipulated with the words_customization function (applied after the uppercase function in the code, following the convention chosen in this package) in order to create variants of the classic crosswords. Some examples could be:

  • Stenographic Crosswords, where shorthand versions of the words, i.e. ignoring vowels, are used:
# "WORD" becomes "WRD"
words_customization = words -> unique([replace(w, r"[AEIOU]" => "") for w in words])
  • Two-faced (or Bifrontal) Crosswords, where words may be positioned either forwards or backwards:
# "WORD" doubles into "WORD" and "DROW"
words_customization = words -> unique([words; reverse.(words)])
  • Sorted (or Scrambled) Crosswords, where the grid isn't filled with the words themselves but with their letters sorted alphabetically:
# "WORD" becomes "DORW"
words_customization = words -> unique(map(w -> join(sort(collect(w))), words))
  • Alphanumeric Substitution (or Rebus) Crosswords, where numbers occurring in words are replaced by their corresponding digits:
# "HEIGHT" becomes "H8"
words_customization = words -> begin
    push!(ALPHABET,collect('0':'9')...) # to pass word-checks for words which include digits
    digit_map = [
        "ZERO"  => "0", "ONE"   => "1",
        "TWO"   => "2", "THREE" => "3",
        "FOUR"  => "4", "FIVE"  => "5",
        "SIX"   => "6", "SEVEN" => "7",
        "EIGHT" => "8", "NINE"  => "9"
    ]
    return unique(map(w -> replace(w, digit_map...), words))
end
# or this one in italian and with more symbols translated
words_customization = words -> begin
    push!(ALPHABET,collect('0':'9')...) # to pass word-checks for words which include digits
    push!(ALPHABET,['+','-','×']...) # additional symbols
    digit_map = [
        "ZERO"    => "0", "UNO"    => "1",
        "DUE"     => "2", "TRE"    => "3",
        "QUATTRO" => "4", "CINQUE" => "5",
        "SEI"     => "6", "SETTE"  => "7",
        "OTTO"    => "8", "NOVE"   => "9",
        "PIU"     => "+", "MENO"   => "-",
        "PER"     => "×"
    ]
    return unique(map(w -> replace(w, digit_map...), words))
end

Other applications of this function could be to filter only some kind of words, for example:

  • "only include words with no repeating letters" (e.g., "DIALECT" is okay, "DATABASE" is not).
words_customization = words -> filter(w -> allunique(w), words)
source

How to setup a language

  • create a folder which will contain all the language files, for example src/Crosswords/dictionaries/LATIN.

  • create a file containing the words of the dictionary, for example src/Crosswords/dictionaries/LATIN/latinlexicon/WORDS.txt. It should contain just a list of words, one per line. Lowercase or uppercase is indifferent.

AB
ABACTUS
ABACUS
ABALIENATIO
ABALIENO
ABANTEUS
ABANTIADES
ABAVUS
ABCIDO
...
  • optionally, create a file containing the clues for the dictionary words, for example src/Crosswords/dictionaries/LATIN/latinlexicon/CLUES.csv. Note that the extension of the file is now .csv, not .txt, as it should be in the format "WORD,clue".
AB,[ab] {preposition} [space] from
ABACTUS,[abāctus] {adjective} driven away
ABACUS,[abacus] {noun} a table of precious material for the display of plate
ABALIENATIO,[abaliēnātiō] {noun} [in law] a transfer of property
ABALIENO,[abaliēnō] {verb} to convey away
ABANTEUS,[Abantēus] {adjective} of Abas (king of Argos)
ABANTIADES,[Abantiadēs] {noun} a son or descendant of Abas (king of Argos)
ABAVUS,[abavus] {noun} a grandfather's grandfather
ABCIDO,[abcīdō] {verb} to cut off
...
  • expand and adjust accordingly the references in the DICTIONARIES_INFO dictionary from src/Crosswords/dictionary.jl, which for example now is currently set to this:
DICTIONARIES_INFO = Dict(
    "lt" => "LATIN/latinlexicon/WORDS.txt", 
    ...
)
  • clues will be automatically derived from the clues file when calling derive_clues function, which is described in the next section.
Enigmistics.derive_cluesFunction
derive_clues(cw::CrosswordPuzzle, filename::String; limit=3)

Given a crossword puzzle and a clues file (in the format "WORD,clue"), return a dictionary mapping each word in the crossword to its clue. If a word has multiple clues, they (up to the specified limit) will be concatenated with " / " as a separator.

Example

julia> cw = example_crossword()
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  O  ■  A │
3 │ T  ■  S  O  U  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ W  I  N  D  O  W │
  └──────────────────┘

julia> clues = derive_clues(cw, "src/Crosswords/dictionaries/EN_/xd/CLUES.csv", limit=2)
Dict{String, String} with 12 entries:
  "ON"     => "In contact with the upper part. / Upon"
  "GATE"   => "Stadium entrance / Airport area"
  "WINDOW" => "Teller's place / Internet Explorer or Microsoft Word, say"
  "EVER"   => "Always / At any time"
  "AN"     => "One or any / Article before words starting with a vowel sound"
  "SEEN"   => "" ... __ and not heard." / Noticed"
  "DOOR"   => "It holds the lock / It may be a folding or a revolving"
  "GOLDEN" => "Brilliant / Like silence"
  "IE"     => "That is, but in Latin"
  "SOUR"   => "Embitter / Ill-humored"
  "VII"    => ""QB __" (Uris novel) / Caesar's lucky number?"
  "NARROW" => "Long and thin / Like Zion National Park's slot canyons"
source

Moves

Enigmistics.SplitAndPlaceType
SplitAndPlace <: Move

A move corresponding to the placement of a black cell in a slot and one or two words in the resulting sub-slots.

source
Enigmistics.apply!Function
apply!(cw::CrosswordPuzzle, m::Place)
undo!(cw::CrosswordPuzzle, m::Place)

Apply/undo a move of type Place.

Example

julia> cw = example_crossword(type="partial")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘

julia> slot = find_constrained_slots(cw)[2]
Slot(6, 1, :horizontal, 6, ".I...W", true, true)

julia> out = generate_moves(cw, slot, allow_split=true, allow_enlarge=true);

julia> out[1]
Place(Slot(6, 1, :horizontal, 6, ".I...W", true, true), "BILLOW")

julia> apply!(cw, out[1])
true

julia> cw
    1  2  3  4  5  6
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ B  I  L  L  O  W │
  └──────────────────┘

julia> undo!(cw, out[1])
true

julia> cw
    1  2  3  4  5  6
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘
source
apply!(cw::CrosswordPuzzle, m::SplitAndPlace)
undo!(cw::CrosswordPuzzle, m::SplitAndPlace)

Apply/undo a move of type SplitAndPlace.

Example

julia> cw = example_crossword(type="partial")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘

julia> slot = find_constrained_slots(cw)[2]
Slot(6, 1, :horizontal, 6, ".I...W", true, true)

julia> out = generate_moves(cw, slot, allow_split=true, allow_enlarge=true);

julia> out[1000]
SplitAndPlace(Slot(6, 1, :horizontal, 6, ".I...W", true, true), 4, "BIM", "XW")

julia> apply!(cw, out[1000])
true

julia> cw
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ B  I  M  ■  X  W │
  └──────────────────┘

julia> undo!(cw, out[1000])
true

julia> cw
    1  2  3  4  5  6
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘
source
apply!(cw::CrosswordPuzzle, m::JustPlaceABlackCell)
undo!(cw::CrosswordPuzzle, m::JustPlaceABlackCell)

Apply/undo a move of type JustPlaceABlackCell.

Example

julia> cw
    1  2  3  4 
  ┌────────────┐
1 │ ⋅  ⋅  T  Z │
2 │ ⋅  ⋅  E  E │
3 │ ⋅  ⋅  S  T │
4 │ ⋅  ⋅  T  A │
  └────────────┘

julia> slot = find_constrained_slots(cw)[1]
Slot(1, 1, :horizontal, 4, "..TZ", true, true)

julia> move = JustPlaceABlackCell(slot,1)
JustPlaceABlackCell(Slot(1, 1, :horizontal, 4, "..TZ", true, true), 1)

julia> apply!(cw, move)
true

julia> cw
    1  2  3  4 
  ┌────────────┐
1 │ ■  ⋅  T  Z │
2 │ ⋅  ⋅  E  E │
3 │ ⋅  ⋅  S  T │
4 │ ⋅  ⋅  T  A │
  └────────────┘

julia> undo!(cw, move)
true

julia> cw
    1  2  3  4 
  ┌────────────┐
1 │ ⋅  ⋅  T  Z │
2 │ ⋅  ⋅  E  E │
3 │ ⋅  ⋅  S  T │
4 │ ⋅  ⋅  T  A │
  └────────────┘
source
apply!(cw::CrosswordPuzzle, m::EnlargeAndPlace)
undo!(cw::CrosswordPuzzle, m::EnlargeAndPlace)

Apply/undo a move of type EnlargeAndPlace.

Example

julia> cw = example_crossword(type="partial")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘

julia> slot = find_constrained_slots(cw)[2]
Slot(6, 1, :horizontal, 6, ".I...W", true, true)

julia> out = generate_moves(cw, slot, allow_split=true, allow_enlarge=true);

julia> out[end]
EnlargeAndPlace(Slot(6, 1, :horizontal, 6, ".I...W", true, true), "WHIPSAW", 1, 0)

julia> apply!(cw, out[end])
true

julia> cw
    1  2  3  4  5  6  7
  ┌─────────────────────┐
1 │ ■  G  O  L  D  E  N │
2 │ ■  A  N  ■  ⋅  ■  A │
3 │ ⋅  T  ■  ⋅  ⋅  ⋅  R │
4 │ ■  E  V  E  R  ■  R │
5 │ ⋅  ■  I  E  ■  ■  O │
6 │ W  H  I  P  S  A  W │
  └─────────────────────┘

julia> undo!(cw, out[end])
true

julia> cw
    1  2  3  4  5  6
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘
source

Algorithms

Enigmistics.solve!Function
solve!(cw::CrosswordPuzzle; seed=rand(Int), kwargs...)

Fill a crossword puzzle cw using a backtracking algorithm with Minimum Remaining Values (MRV) heuristic, considering all types of moves (simple placements, splits with black cells, enlargements).

Arguments:

  • max_trials_per_slot=10: maximum number of candidate words to try for each slot. It gets scaled as the completeness of the grid increases to allow more trials towards the end of the search, when the grid is almost full and therefore also more constrained
  • min_options_to_allow_split=5: if a slot has more than this number of candidate words with the simple method, then we do not consider moves involving splits with black cells when filling that slot
  • min_options_to_allow_enlarge=0: if a slot has more than this number of candidate words with the simple method, then we do not consider moves involving enlarging the grid when filling that slot
  • max_enlarge=1: maximum number of cells by which to enlarge the grid when considering moves of type EnlargeAndPlace
  • verbose=true: print the completeness percentage of the grid during the search (useful also for allowing interrupts through ctrl+c)
  • see_evolution=false: display the status of the grid at each step of the algorithm, therefore allowing to see the grid filling in real time (note: this option will likely produce a slower execution time)

Example

julia> cw = random_pattern(10,14)
     1  2  3  4  5  6  7  8  9 10 11 12 13 14 
   ┌──────────────────────────────────────────┐
 1 │ ■  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ■  ⋅  ⋅ │
 2 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ⋅ │
 3 │ ⋅  ⋅  ⋅  ⋅  ⋅  ■  ⋅  ■  ■  ⋅  ⋅  ⋅  ⋅  ⋅ │
 4 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 5 │ ⋅  ■  ■  ⋅  ■  ⋅  ⋅  ■  ⋅  ⋅  ■  ⋅  ⋅  ⋅ │
 6 │ ⋅  ⋅  ⋅  ■  ⋅  ⋅  ■  ⋅  ⋅  ■  ⋅  ■  ■  ⋅ │
 7 │ ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
 8 │ ⋅  ⋅  ⋅  ⋅  ⋅  ■  ■  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅ │
 9 │ ⋅  ⋅  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅ │
10 │ ⋅  ⋅  ■  ■  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ⋅  ■  ■ │
   └──────────────────────────────────────────┘

julia> solve!(cw, min_options_to_allow_split=0)
true

julia> cw
     1  2  3  4  5  6  7  8  9 10 11 12 13 14 
   ┌──────────────────────────────────────────┐
 1 │ ■  ■  C  A  R  A  C  A  R  A  ■  ■  O  W │
 2 │ B  L  U  E  C  U  R  A  C  A  O  ■  S  R │
 3 │ L  A  R  I  D  ■  E  ■  ■  S  W  A  M  I │
 4 │ I  M  N  O  T  F  E  E  L  I  N  G  I  T │
 5 │ N  ■  ■  U  ■  C  K  ■  A  N  ■  U  N  E │
 6 │ D  K  S  ■  A  K  ■  N  R  ■  A  ■  ■  A │
 7 │ S  C  R  A  T  C  H  A  N  D  C  L  A  W │
 8 │ I  A  I  N  T  ■  ■  I  ■  A  E  C  I  A │
 9 │ D  M  ■  C  A  T  A  R  R  H  A  L  L  Y │
10 │ E  P  ■  ■  R  E  D  N  O  S  E  S  ■  ■ │
   └──────────────────────────────────────────┘
source
Enigmistics.solve_guided!Function
solve_guided!(cw::CrosswordPuzzle; seed=rand(Int), kwargs...)

Fill a crossword puzzle cw using a backtracking algorithm with Minimum Remaining Values (MRV) heuristic, considering all types of moves (simple placements, splits with black cells, enlargements), and allowing the user to have control (i.e. decide) which move to apply at each step.

Arguments are the same as in the solve! function, so look at its documentation for more details.

Example

julia> cw = example_crossword(type="partial")
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘

julia> solve_guided!(cw)
    1  2  3  4  5  6 
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  ⋅  ■  A │
3 │ T  ■  ⋅  ⋅  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘
Select the moves that you like; or none to backtrack with recursion; or quit)
[press: Enter=toggle, a=all, n=none, d=done, q=abort]
   [ ] → quit
   [X] [Place] DEER
   [ ] [Place] DOPR
   [ ] [Place] DURR
   [ ] [Place] DIOR
   [ ] [Place] DOUR
   [ ] [Place] DCOR
   [X] [Place] DOOR
 > [X] [Place] DYER
v  [ ] [Place] DERR

Applying move: [Place] DEER (moves yet to be tested: ["[Place] DOOR", "[Place] DYER"])
    1  2  3  4  5  6
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  E  ■  A │
3 │ T  ■  ⋅  E  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ ⋅  I  ⋅  ⋅  ⋅  W │
  └──────────────────┘
Select the moves that you like; or none to backtrack with recursion; or quit)
[press: Enter=toggle, a=all, n=none, d=done, q=abort]
   [ ] → quit
   [ ] [Place] MIDBOW
   [ ] [Place] PITSAW
   [X] [Place] WINDOW
   [ ] [Place] TILNOW
   [ ] [Place] KIDROW
 > [X] [Place] WINNOW
   [ ] [Place] BIGWOW
   [ ] [Place] AIMLOW
v  [ ] [Place] PITROW

Applying move: [Place] WINDOW (moves yet to be tested: ["[Place] WINNOW"])
    1  2  3  4  5  6
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  E  ■  A │
3 │ T  ■  ⋅  E  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ W  I  N  D  O  W │
  └──────────────────┘
Select the moves that you like; or none to backtrack with recursion; or quit)
[press: Enter=toggle, a=all, n=none, d=done, q=abort]
   [ ] → quit
 > [X] [Place] BEEN
   [X] [Place] SEEN
   [ ] [Place] DEEN
   [ ] [Place] PEEN
   [ ] [Place] WEEN
   [X] [Place] TEEN
   [ ] [Place] KEEN

Applying move: [Place] BEEN (moves yet to be tested: ["[Place] SEEN", "[Place] TEEN"])
    1  2  3  4  5  6
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  E  ■  A │
3 │ T  ■  B  E  ⋅  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ W  I  N  D  O  W │
  └──────────────────┘
Select the moves that you like; or none to backtrack with recursion; or quit)
[press: Enter=toggle, a=all, n=none, d=done, q=abort]
   [ ] → quit
 > [X] [Place] BEER
   [ ] [Place] BEHR
   [ ] [Place] BEOR
   [X] [Place] BEAR
   [ ] [SplitAndPlace] (BE.R → BE/R) BE/nothing

Applying move: [Place] BEER (moves yet to be tested: ["[Place] BEAR"])
true

julia> cw
    1  2  3  4  5  6
  ┌──────────────────┐
1 │ G  O  L  D  E  N │
2 │ A  N  ■  E  ■  A │
3 │ T  ■  B  E  E  R │
4 │ E  V  E  R  ■  R │
5 │ ■  I  E  ■  ■  O │
6 │ W  I  N  D  O  W │
  └──────────────────┘
source