Completed the HTML exporter
[getaclue.git] / crosswordpuzzle.py
index c1f822b..7d3b270 100644 (file)
@@ -4,6 +4,9 @@
 
 # Class for the puzzle data representation
 
+# for export to PNG image
+import cairo
+
 class GridItem:
        # initialize the item
        def __init__ (self, item_char='.', across_start = False, down_start = False,
@@ -45,18 +48,17 @@ class FrozenGridException (Exception):
        def __init__ (self):
                self.msg = "Grid is frozen and cannot be edited"
 
-class CrosswordPuzzle:
-       # ansi color codes for grid display
-       BRICKRED = '\033[44;1;31m'
-       # bold
-       BOLD = '\033[33m'
-       # blue
-       BLUE = '\033[34m'
-       # grey
-       GREY = '\033[30m'
-       # disable colors
-       ENDCOL = '\033[0m'
+# 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
@@ -74,6 +76,244 @@ class CrosswordPuzzle:
                        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>", "<head>", "<title>"]
+                       html_contents.append (puztitle)
+                       html_contents.append ("</title>")
+                       html_contents.append ("</head>")
+                       html_contents.append ("<body>")
+                       html_contents.append ("<h1>" + puztitle + "</h1>")
+                       html_contents.append ('<img src="' + pngfile + '" alt="puzzle" />')
+
+                       html_contents.append ("<h2>Across clues</h2>")
+                       html_contents.append ("<p>")
+                       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 ("<br />")
+                       html_contents.append ("</p>")
+
+                       html_contents.append ("<h2>Down clues</h2>")
+                       html_contents.append ("<p>")
+                       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 ("<br />")
+                       html_contents.append ("</p>")
+                       html_contents.append ("</body>")
+                       html_contents.append ("</html>")
+
+                       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 ("<ACROSS PUZZLE>\r\n")
+               across_data.append ("<TITLE>\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
@@ -89,14 +329,23 @@ class CrosswordPuzzle:
                        # on the same column
                        if self.data[row+i][col].occupied_down is True:
                                raise IntersectWordException (word, len(word))
-                       # on the previous column
-                       if col > 0 and self.data[row+i][col-1].occupied_down is True:
-                               raise IntersectWordException (word, len(word))
-                       # on the next column
-                       if (col < (len(word) - 1) and
-                               (self.data[row+i][col+1].occupied_down is True or
-                               self.data[row+i][col+1].across_start 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
@@ -127,17 +376,26 @@ class CrosswordPuzzle:
 
                # is the word intersecting any other word?
                for i in range (len(word)):
-                       # on the same line
+                       # on the same row
                        if self.data[row][col+i].occupied_across is True:
                                raise IntersectWordException (word, len(word))
-                       # on a previous line except the last column
-                       if row > 0 and self.data[row-1][col+i].occupied_across is True:
-                               raise IntersectWordException (word, len(word))
-                       # on a next line except the last column
-                       if (row < (self.rows - 1) and
-                        (self.data[row+1][col+i].down_start is True
-                       or self.data[row+1][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
@@ -156,40 +414,6 @@ class CrosswordPuzzle:
                        self.data[row][col+i].char = word[i].upper ()
                        self.data[row][col+i].occupied_across = True
 
-       # display the grid with words
-       def print_grid (self, no_words=False):
-               # get row, col and print them (with grid number if set)
-               for col in range (self.cols):
-                       # print the first row as column headers
-                       print self.BLUE + '  ' + str (col) + self.ENDCOL,
-               print
-
-               for row in range (self.rows):
-                       for col in range (self.cols):
-                               # print the data
-                               # if the cell is numbered i.e. start of a word
-                               if self.data[row][col].numbered != 0:
-                                       print self.BRICKRED + str(self.data[row][col].numbered) + self.ENDCOL,
-                               # print a space
-                               else:
-                                       print ' ',
-                               # if the character is not a blank or a block
-                               if self.data[row][col].char <> "." and self.data[row][col].char <> "#":
-                                       # if words are to be shown regardless of hidden/revealed state
-                                       if no_words is False:
-                                               print self.BOLD + self.data[row][col].char + self.ENDCOL,
-                                       else:
-                                               # display only revealed
-                                               if self.data[row][col].revealed is True:
-                                                       print self.BOLD + self.data[row][col].char + self.ENDCOL,
-                                               # else print a block
-                                               else:
-                                                       print self.GREY + '.' + self.ENDCOL,
-                               else:
-                                       print self.GREY + self.data[row][col].char + self.ENDCOL,
-
-                       print ' ' + self.BLUE + str(row) + self.ENDCOL
-
        # freeze the grid numbers etc.
        def freeze_grid (self):
                # numbering
@@ -208,3 +432,15 @@ class CrosswordPuzzle:
 
                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
+