# Get A Clue (C) 2010 V. Harishankar # Crossword puzzle maker program # Licensed under the GNU GPL v3 # Class for the puzzle data representation # for export to PNG image import cairo class GridItem: # initialize the item def __init__ (self, item_char='.', item_guess = None, across_start = False, down_start = False, occupied_across = False, occupied_down = False, num = 0, clue_across = None, clue_down = None, revealed = False): # character in the cell self.char = item_char # guess of character in cell self.guess = item_guess # is the cell the start of an across word? self.across_start = across_start # is the cell the start of a down word? self.down_start = down_start # is the cell occupied by a letter in an across word? self.occupied_across = occupied_across # is the cell occupied by a letter in a down word? self.occupied_down = occupied_down # numbering of the cell if it is the start of a word self.numbered = num # clue across if the cell is the start of an across word self.clue_across = clue_across # clue down if the cell is the start of a down word self.clue_down = clue_down # is the letter revealed or hidden? self.revealed = revealed # clear the across data def clear_across_data (self): self.across_start = False self.occupied_across = False self.clue_across = None # if no down word starting at item if self.down_start is False: self.numbered = 0 # if no down word at the item if self.occupied_down is False: self.char = '.' self.revealed = False self.guess = None # clear the down data def clear_down_data (self): self.down_start = False self.occupied_down = False self.clue_down = None # if no across word starting at item if self.across_start is False: self.numbered = 0 # if no across word at the item if self.occupied_across is False: self.char = '.' self.revealed = False self.guess = None # reset a grid item completely - use only to destroy whole grid, otherwise # use either clear_across_data () or clear_down_data () for erasing single # words def reset (self): # character in the cell self.char = '.' # guess of character in cell self.guess = None # is the cell the start of an across word? self.across_start = False # is the cell the start of a down word? self.down_start = False # is the cell occupied by a letter in an across word? self.occupied_across = False # is the cell occupied by a letter in a down word? self.occupied_down = False # numbering of the cell if it is the start of a word self.numbered = 0 # clue across if the cell is the start of an across word self.clue_across = None # clue down if the cell is the start of a down word self.clue_down = None # is the letter revealed or hidden? self.revealed = False # exception for too long words class TooLongWordException (Exception): def __init__ (self, word, length): self.word = word self.length = length # exception for intersecting words class IntersectWordException (Exception): def __init__ (self, word, length): self.word = word self.length = length # exception when grid is sought to be changed when frozen class FrozenGridException (Exception): def __init__ (self): self.msg = "Grid is frozen and cannot be edited" # exception when no word is found at a position class NoWordException (Exception): def __init__ (self, row, col): self.pos = (row, col) # exception when no words are present in the grid class NoWordsException (Exception): def __init__ (self): self.msg = "No words in grid" class CrosswordPuzzle: def __init__ (self, rows, cols): # define number of rows and columns self.rows = rows self.cols = cols # initialize the list to hold the grid self.data = [] # initial state of the grid is unfrozen self.frozen_grid = False # create the grid data for i in range (rows): self.data.append ([]) for j in range (cols): self.data[i].append (GridItem ()) # export to an image def export_image (self, pngfile, htmlfile=None, puztitle="Crossword Puzzle", solution=True): # don't export if grid is not frozen if self.frozen_grid is False: raise FrozenGridException # create cairo image surface and context px = 30 surf = cairo.ImageSurface (cairo.FORMAT_RGB24, self.cols*px, self.rows*px) ctx = cairo.Context (surf) ctx.set_source_rgb (1, 1, 1) ctx.rectangle (0, 0, self.cols*px, self.rows*px) ctx.fill () # get the clues across and down clues_across = self.get_clues_across () clues_down = self.get_clues_down () # traverse through the grid for row in range (self.rows): for col in range (self.cols): # if grid is un-occupied if (self.data[row][col].occupied_across is False and self.data[row][col].occupied_down is False): ctx.set_source_rgb (0, 0, 0) ctx.rectangle (col*px, row*px, px, px) ctx.fill () # grid is occupied else: ctx.set_source_rgb (1, 1, 1) ctx.rectangle (col*px, row*px, px, px) ctx.fill () ctx.set_source_rgb (0, 0, 0) ctx.rectangle (col*px, row*px, px, px) ctx.stroke () # if solution is not to be provided, number the grid if solution is False: if self.data[row][col].numbered <> 0: ctx.select_font_face ("Serif") ctx.set_font_size (10) ctx.move_to (col*px+5, row*px+10) ctx.show_text (str(self.data[row][col].numbered)) # display the words else: ctx.select_font_face ("Serif") ctx.set_font_size (16) ctx.move_to (col*px+10, row*px+20) ctx.show_text (self.data[row][col].char) surf.write_to_png (open (pngfile, "wb")) # if solution is false, publish the clues and the image in a HTML file if htmlfile and solution is False: html_contents = ["", "", ""] html_contents.append (puztitle) html_contents.append ("") html_contents.append ("") html_contents.append ("") html_contents.append ("

" + puztitle + "

") html_contents.append ('puzzle') html_contents.append ("

Across clues

") html_contents.append ("

") for word, clue in clues_across: clue_str = str (self.data[word[1]][word[2]].numbered) + " - " \ + clue html_contents.append (clue_str) html_contents.append ("
") html_contents.append ("

") html_contents.append ("

Down clues

") html_contents.append ("

") for word, clue in clues_down: clue_str = str (self.data[word[1]][word[2]].numbered) + " - " \ + clue html_contents.append (clue_str) html_contents.append ("
") html_contents.append ("

") html_contents.append ("") html_contents.append ("") html_str = "\r\n".join (html_contents) fhtml = open (htmlfile, "wb") fhtml.write (html_str) fhtml.close () # get the AcrossLite(TM) data for exporting def export_acrosslite (self, title, author, copyright): # don't export if grid is not frozen if self.frozen_grid is False: raise FrozenGridException across_data = [] across_data.append ("\r\n") across_data.append ("\r\n") across_data.append (title + "\r\n") across_data.append ("<AUTHOR>\r\n") across_data.append (author + "\r\n") across_data.append ("<COPYRIGHT>\r\n") across_data.append (copyright + "\r\n") across_data.append ("<SIZE>\r\n") str_size = str (self.cols) + "x" + str (self.rows) across_data.append (str_size + "\r\n") across_data.append ("<GRID>\r\n") for row in range (self.rows): for col in range (self.cols): if (self.data[row][col].occupied_across is True or self.data[row][col].occupied_down is True): across_data.append (self.data[row][col].char) else: across_data.append (".") across_data.append ("\r\n") across_data.append ("<ACROSS>\r\n") clues_across = self.get_clues_across () for word, clue in clues_across: if clue: across_data.append (clue + "\r\n") else: across_data.append ("(No clue yet)\r\n") across_data.append ("<DOWN>\r\n") clues_down = self.get_clues_down () for word, clue in clues_down: if clue: across_data.append (clue + "\r\n") else: across_data.append ("(No clue yet\r\n") acrosslite_str = "".join (across_data) return acrosslite_str # get all the clues for across def get_clues_across (self): clues = [] # traverse the grid for row in range (self.rows): for col in range (self.cols): if (self.data[row][col].occupied_across is True and self.data[row][col].across_start is True): word_across = self.get_word_across (row, col) clues.append ((word_across, self.data[row][col].clue_across)) # if no across words are found at all if not clues: raise NoWordsException return clues # get all the clues for down def get_clues_down (self): clues = [] # traverse the grid for row in range (self.rows): for col in range (self.cols): if (self.data[row][col].occupied_down is True and self.data[row][col].down_start is True): word_down = self.get_word_down (row, col) clues.append ((word_down, self.data[row][col].clue_down)) # if no down words are found at all if not clues: raise NoWordsException return clues # getting a down word at a position def get_word_down (self, row, col): # if index is out of bounds if row >= self.rows or col >= self.cols: raise NoWordException (row, col) # if there is no occupied down letter at that position if self.data[row][col].occupied_down is False: raise NoWordException (row, col) # now traverse the grid to find the beginning of the word i = row while i >= 0: # if it is occupied down and is the beginning of the word if (self.data[i][col].occupied_down is True and self.data[i][col].down_start is True): start_row = i break i -= 1 i = start_row word_chars = [] # now seek the end of the word while i < self.rows: if self.data[i][col].occupied_down is True: word_chars.append (self.data[i][col].char) else: break i += 1 word = "".join (word_chars) # return the word, starting row, column and length as a tuple return (word, start_row, col, len(word)) # getting an across word at a position def get_word_across (self, row, col): # if index is out of bounds if row >= self.rows or col >= self.cols: raise NoWordException (row, col) # if there is no occupied across letter at that position if self.data[row][col].occupied_across is False: raise NoWordException (row, col) # now traverse the grid to look for the beginning of the word i = col while i >= 0: # if it is occupied across and is the beginning of the word if (self.data[row][i].occupied_across is True and self.data[row][i].across_start is True): start_col = i break i -= 1 i = start_col word_chars = [] # now seek the end of the word while i < self.cols: if self.data[row][i].occupied_across is True: word_chars.append (self.data[row][i].char) else: break i += 1 word = "".join (word_chars) # return the word, starting column, row and length as a tuple return (word, row, start_col, len(word)) # setting a down word def set_word_down (self, row, col, word): # if the grid is frozen the abort if self.frozen_grid is True: raise FrozenGridException # if the word length greater than totalrows - startrow if len(word) > self.rows - row: raise TooLongWordException (word, len(word)) # is the word intersecting any other word? for i in range (len(word)): # on the same column if self.data[row+i][col].occupied_down is True: raise IntersectWordException (word, len(word)) # on the previous column except first column if col > 0: # except the first and last col if i > 0 and i < len(word) - 1: if self.data[row+i][col-1].occupied_down is True: raise IntersectWordException (word, len(word)) # on the next column except last column if col < len(word) - 1: # except the first and last row check if there is any # down word in previous column if i > 0 and i < len(word) - 1: if self.data[row+i][col+1].occupied_down is True: raise IntersectWordException (word, len(word)) # check if there is any across word starting in the # next column if self.data[row+i][col+1].across_start is True: raise IntersectWordException (word, len(word)) # also check the character before and after if (row > 0 and self.data[row-1][col].occupied_down is True and self.data[row-1][col].occupied_across is True): raise IntersectWordException (word, len(word)) if (row + len(word) < self.rows and self.data[row+len(word)][col].occupied_across is True and self.data[row+len(word)][col].occupied_down is True): raise IntersectWordException (word, len(word)) # set the down start to true self.data[row][col].down_start = True # set the word for i in range (len(word)): self.data[row+i][col].occupied_down = True self.data[row+i][col].char = word[i].upper () # setting an across word def set_word_across (self, row, col, word): # if the grid is frozen the abort if self.frozen_grid is True: raise FrozenGridException # is the word length greater than totalcols - startcol? if len(word) > self.cols - col: raise TooLongWordException (word, len(word)) # is the word intersecting any other word? for i in range (len(word)): # on the same row if self.data[row][col+i].occupied_across is True: raise IntersectWordException (word, len(word)) # on a previous row except first row if row > 0: # if not the first or last col if i > 0 and i < len(word) - 1: if self.data[row-1][col+i].occupied_across is True: raise IntersectWordException (word, len(word)) # on a next row if (row < (self.rows - 1)): # except the first and last letter check if there is # any across intersection if i > 0 and i < len (word) - 1: if self.data[row+1][col+i].occupied_across is True: raise IntersectWordException (word, len(word)) # if a down word is starting at any column below the # word if self.data[row+1][col+i].down_start is True: raise IntersectWordException (word, len(word)) # also check the character beyond and before and after if (col > 0 and (self.data[row][col-1].occupied_across is True or self.data[row][col-1].occupied_down is True)): raise IntersectWordException (word, len(word)) if (col + len(word) < self.cols and (self.data[row][col+len(word)].occupied_across is True or self.data[row][col+len(word)].occupied_down is True)): raise IntersectWordException (word, len(word)) # set across start to true self.data[row][col].across_start = True # set the word for i in range (len(word)): self.data[row][col+i].char = word[i].upper () self.data[row][col+i].occupied_across = True # freeze the grid numbers etc. def freeze_grid (self): # numbering numbering = 1 # run through the grid for row in range (self.rows): for col in range (self.cols): # if grid is blank set the character to # if (self.data[row][col].occupied_across is False and self.data[row][col].occupied_down is False): self.data[row][col].char = "#" elif (self.data[row][col].across_start is True or self.data[row][col].down_start is True): self.data[row][col].numbered = numbering numbering += 1 self.frozen_grid = True # unfreeze the grid numbers etc. def unfreeze_grid (self): # run through the grid for row in range (self.rows): for col in range (self.cols): self.data[row][col].numbered = 0 if (self.data[row][col].occupied_across is False and self.data[row][col].occupied_down is False): self.data[row][col].char = '.' self.frozen_grid = False # reset the entire grid def reset_grid (self): # run through the grid for row in range (self.rows): for col in range (self.cols): # re-initialize all data self.data[row][col].reset () self.frozen_grid = False