# 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 words containing non-alpha characters
class WordCharsException (Exception):
def __init__ (self, word):
self.word = word
# 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 a word number is not found in the grid
class NoNumberException (Exception):
def __init__ (self, num):
self.num = num
# exception when no words are present in the grid
class NoWordsException (Exception):
def __init__ (self):
self.msg = "No words in grid"
# exception to raise when solution is imcomplete when trying to verify it
class IncompleteSolutionException (Exception):
def __init__ (self):
self.msg = "Solution incomplete"
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
self.assert_frozen_grid ()
# 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 ('')
html_contents.append ("Across clues
")
html_contents.append ("")
for word, clue in clues_across:
# clue should be: - clue text (chars)
clue_str = str (self.data[word[1]][word[2]].numbered) + " - " \
+ clue + " (" + str (word[3]) + ")"
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 + " (" + str (word[3]) + ")"
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
self.assert_frozen_grid ()
across_data = []
across_data.append ("\r\n")
across_data.append ("\r\n")
across_data.append (title + "\r\n")
across_data.append ("\r\n")
across_data.append (author + "\r\n")
across_data.append ("\r\n")
across_data.append (copyright + "\r\n")
across_data.append ("\r\n")
str_size = str (self.cols) + "x" + str (self.rows)
across_data.append (str_size + "\r\n")
across_data.append ("\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 ("\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 ("\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 an across word at a number (note that the grid should be
# frozen for calling this otherwise a FrozenGridException will be raised)
def get_word_across_at_num (self, num):
# assert that the grid is frozen
self.assert_frozen_grid ()
# traverse the grid
for row in range (self.rows):
for col in range (self.cols):
if self.data[row][col].numbered == num:
word = self.get_word_across (row, col)
return word
# if number is not found
raise NoNumberException (num)
# getting a down word at a number (note that the grid should be frozen
# for calling this otherwise a FrozenGridException will be raised)
def get_word_down_at_num (self, num):
# assert that the grid is frozen
self.assert_frozen_grid ()
# traverse the grid
for row in range (self.rows):
for col in range (self.cols):
if self.data[row][col].numbered == num:
word = self.get_word_down (row, col)
return word
# if number is not found
raise NoNumberException (num)
# getting the position of a number on the grid (note that the grid should
# be frozen for calling this otherwise a FrozenGridException will be raised)
def get_position_of_num (self, num):
# assert that the grid is frozen
self.assert_frozen_grid ()
# traverse the grid
for row in range (self.rows):
for col in range (self.cols):
if self.data[row][col].numbered == num:
return (row, col)
# if number is not found
raise NoNumberException (num)
# 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 then abort
self.assert_unfrozen_grid ()
# if the word has non-alphabetic characters
if not word.isalpha ():
raise WordCharsException (word)
# 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))
# if the previous column is the end of an across word
if (self.data[row+i][col-1].occupied_across is True and
self.data[row+i][col].occupied_across is False):
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 then abort
self.assert_unfrozen_grid ()
# if the word has non-alphabetic characters
if not word.isalpha ():
raise WordCharsException (word)
# 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))
# if the previous row is the end of a down word
if (self.data[row-1][col+i].occupied_down is True and
self.data[row][col+i].occupied_down is False):
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
# raise an exception if the grid is frozen
def assert_unfrozen_grid (self):
if self.frozen_grid is True:
raise FrozenGridException
# raise an exception if the grid is NOT frozen
def assert_frozen_grid (self):
if self.frozen_grid is False:
raise FrozenGridException
# 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
# remove an across word at position
def remove_word_across (self, row, col):
# if grid is frozen don't allow removal of word
self.assert_unfrozen_grid ()
word, brow, bcol, l = self.get_word_across (row, col)
# traverse from the beginning to end of the word and erase it
c = bcol
while c < self.cols:
if self.data[brow][c].occupied_across is True:
self.data[brow][c].clear_across_data ()
else:
break
c += 1
# remove a down word at position
def remove_word_down (self, row, col):
# if grid is frozen don't allow removal of word
self.assert_unfrozen_grid ()
word, brow, bcol, l = self.get_word_down (row, col)
# traverse from the beginn to end of the word and erase it
r = brow
while r < self.rows:
if self.data[r][bcol].occupied_down is True:
self.data[r][bcol].clear_down_data ()
else:
break
r += 1
# reveal/unreveal a word at position
def reveal_word_across (self, row, col, revealed=True):
# set the revealed flag for the word at the position
word= self.get_word_across (row, col)
c = word[2]
while c < self.cols:
if self.data[word[1]][c].occupied_across is True:
self.data[word[1]][c].revealed = revealed
else:
break
c += 1
# reveal/unreveal a word at position
def reveal_word_down (self, row, col, revealed=True):
# set the revealed flag for the word at the position
word = self.get_word_down (row, col)
r = word[1]
while r < self.rows:
if self.data[r][word[2]].occupied_down is True:
self.data[r][word[2]].revealed = revealed
else:
break
r += 1
# reveal/hide the entire solution by resetting revealed flag at all cells
def reveal_solution (self, revealed=True):
# run through the grid and set revealed to False
for row in range (self.rows):
for col in range (self.cols):
self.data[row][col].revealed = revealed
# clear the guesses for the board
def clear_guesses (self):
# run through the grid and set the guesses to None
for row in range (self.rows):
for col in range (self.cols):
self.data[row][col].guess = None
# verify the solution - return True if all guessed characters are correct
# return False if some of them are wrong.
# if the board is not completed as yet, raise a IncompleteSolutionException
def is_solution_correct (self):
# run through the grid and check for each character in occupied cells
flag = True
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):
# if there is no guess at a particular location raise
# the incomplete solution exception
if not self.data[row][col].guess:
raise IncompleteSolutionException
# if a character doesn't match, return False
if self.data[row][col].char <> self.data[row][col].guess:
flag = False
# finally return result
return flag