# Intro

I put the code for this on github, it's a bit messy but works: https://github.com/jameslyons/crossword_gen.

In previous posts I have been playing with python-constraint, and building crosswords is something that interests me for some reason. I decided to do it in a two step process: step 1 generate the patterns of white and black squares, step 2 fill the patterns with letters. This post will deal with step 1 only, the next post will deal with step 2.

Generating the patterns is well suited to using a constraint solver since you can just specify the things you'd like to be true of the final result, then ask the constraint solver to do it for you. In this case we have e.g. 15 by 15 binary variables, we choose the constraints so that the final pattern looks like a crossword puzzle.

# What do pretty patterns look like

I am interested in automatically generating patterns that look like nice crossword patterns e.g.

Some of the things I'd like to be true of the patterns I generate:

1. I'd like 4-fold symmetry, because I think it looks good.
2. No 2x2 blocks of white squares.
3. No 2x2 blocks of black squares.
4. No length 1 words.
5. No length 2 words.
6. No words longer than MAXLEN.
7. No sequences of black squares longer than 3.
8. White squares must be fully connected.

Some of these conditions are pretty easy to implement with python-constraint, the fully connected constraint I can just apply to the final solutions and discard them at that point. By fully connected I mean you can get from one square to every other square in the pattern, so you can't get disjoint islands.

# The implementation

Examples of what the program outputs:

`3EF9C51EBFAFAAF8FE37577BFB9D5CEFEF75763F8FAAFAFEBC51CFBEgX X . . . . . X . . . . . X X. . . X X X . X . X X X . . .. X . X . . . . . . . X . X .. . . . X . X . X . X . . . .. X X X . . . . . . . X X X .. X . . . X . X . X . . . X .. . . X . . . . . . . X . . .X X . . . X . X . X . . . X X. . . X . . . . . . . X . . .. X . . . X . X . X . . . X .. X X X . . . . . . . X X X .. . . . X . X . X . X . . . .. X . X . . . . . . . X . X .. . . X X X . X . X X X . . .X X . . . . . X . . . . . X X`

The hex string is the binary to hex encoding of the pattern, 'X's are zeros, '.'s are ones. The hex string will be passed to step 2 which will fill in the words using a wordlist.

It's pretty easy to generate a few million example patterns with this program (if you are willing to wait a day), which is what I suggest doing. Once you have a heap of patterns you can further filter them by whatever criteria you want e.g. no more than X 3-letter words, no more than X% black squares etc. Once you have the filtered list, you can try filling them with words.

`from __future__ import print_functionfrom constraint import *from itertools import combinationsfrom math import sqrt,floor# this program will create crossword patterns, but not fill it with words# i.e. just patterns of zeros and ones for the black and white squares# tests if a solution is fully connected i.e. can every point be reached from# any random point. True if fullyconnected, false if disjoint parts.# Could easily be made more efficient, but this works fine.def isfullyconnected(solution):    L = int(sqrt(len(solution)))    # get list of all non-zero points    plist = []    for i in range(L):        for j in range(L):            if solution[i*L + j]: plist.append((i,j))        # start top right corner, try to get to every other point    reached = {plist:1}    rlen = len(reached)    oldrlen = 0    while rlen != oldrlen:      temp = list(reached.keys())      for i,j in temp:        if (i+1,j) in plist: reached[(i+1,j)]=1        if (i-1,j) in plist: reached[(i-1,j)]=1        if (i,j+1) in plist: reached[(i,j+1)]=1        if (i,j-1) in plist: reached[(i,j-1)]=1      oldrlen = rlen      rlen = len(reached)    # if rlen is not eq number of all points, we couldn't reach some points    if rlen < len(plist): return False    return True        from random import randint    def getPattern(sidelen=15,maxlen=9):    problem = Problem()    s = sidelen    maxWordLen = maxlen    problem.addVariables(list(range(s*s)), [0,1])      # ensure there are no 2x2 blocks of ones or zeros    def no4x4block(*x):        if sum(x)==0: return False        if sum(x)==len(x): return False        else: return True    for i in range(s-1):        for j in range(s-1):            problem.addConstraint(no4x4block,[i*s+j,(i*s+j)+1,(i*s+j)+s,(i*s+j)+s+1])        #ensure matrix is symmetric      def eq(a,b):          return a==b    for i in range(s):        for j in range(s):            problem.addConstraint(eq,[i*s+j,(s-i-1)*s+j])            problem.addConstraint(eq,[i*s+j,i*s+(s-j-1)])            problem.addConstraint(eq,[i*s+j,(s-i-1)*s+(s-j-1)])           #ensure no words longer than maxWordLen    def maxlen(*x):        if sum(x) == len(x): return False        else: return True    seq = list(range(maxWordLen+1))    for i in range(s - maxWordLen):        for j in range(s):            problem.addConstraint(maxlen,[(k+i)+j*s for k in seq])            problem.addConstraint(maxlen,[(k+i)*s+j for k in seq])        # no two letter words    def no2letter(*x):        if x == (0,1,1,0): return False        else: return True        seq = list(range(4))    for i in range(s - 3):        for j in range(s):            problem.addConstraint(no2letter,[(k+i)+j*s for k in seq])            problem.addConstraint(no2letter,[(k+i)*s+j for k in seq])    # no two letter words around the edge either    def no2letterEdge(*x):        if x == (1,1,0): return False        else: return True        seq = list(range(4))    for i in range(s):        problem.addConstraint(no2letterEdge,[i,i+s,i+2*s])        problem.addConstraint(no2letterEdge,[i*s,i*s+1,i*s+2])        problem.addConstraint(no2letterEdge,[i*s+s-1,i*s+s-2,i*s+s-3])        problem.addConstraint(no2letterEdge,[i+s*(s-1),i+s*(s-2),i+s*(s-3)])        # no one letter squares    def no1letter(*x):            if sum(x) == 0: return True        if sum(x[1:]) == 0: return False        return True        seq = list(range(4))    for i in range(s):      for j in range(s):        vals = [i*s+j]        if i-1 >= 0: vals.append((i-1)*s+j)         if i+1 < s: vals.append((i+1)*s+j)         if j-1 >= 0: vals.append(i*s+(j-1))         if j+1 < s: vals.append(i*s+(j+1))         problem.addConstraint(no1letter,vals)     # no more than three 0's in a row    def nothreeblank(*x):        if sum(x) == 0: return False        return True        seq = list(range(4))    for i in range(s - 3):        for j in range(s):            problem.addConstraint(nothreeblank,[(k+i)+j*s for k in seq])            problem.addConstraint(nothreeblank,[(k+i)*s+j for k in seq])       for sol in problem.getSolutionIter():          if not isfullyconnected(sol): continue        yield(sol)    from test_bin2hex import bin2heximport sysprint(len(sys.argv))if len(sys.argv)<2: sidelen = 11else: sidelen = int(sys.argv)print('using sidelen',sidelen)f = open('pattern'+str(sidelen)+'.txt','w')for count,sol in enumerate(getPattern(sidelen=sidelen,maxlen=8)):    f.write(bin2hex(''.join([str(sol[a]) for a in range(sidelen*sidelen)]))+'\n')    if count % 100 == 0:       print('count: '+str(count))      print(bin2hex(''.join([str(sol[a]) for a in range(sidelen*sidelen)])))      for i in range(sidelen):        for j in range(sidelen):             if sol[i*sidelen+j]==1: print('.',end=' ')            else: print('X',end=' ')        print('')      print('')    #break`

The hex string is generated using the following python script (test_bin2hex.py). The extra letters are so we can encode binary strings that aren't necessarily divisible by 4.

`bin = ['0000','0001','0010','0011','0100','0101','0110','0111','1000','1001','1010','1011','1100','1101','1110','1111',       '0','1','00','01','10','11','000','001','010','011','100','101','110','111']hex = "0123456789ABCDEFghijklmnopqrst"def bin2hex(binstr):    map = dict(zip(bin,hex))    ret = ''    for i in range(0,len(binstr),4):        sub = binstr[i:i+4]        ret += map[sub]    return ret    def hex2bin(hexstr):    map = dict(zip(hex,bin))    ret = ''    for i in hexstr:        ret += map[i]    return ret`