X-Git-Url: https://harishankar.org/repos/?p=getaclue.git;a=blobdiff_plain;f=crosswordpuzzle.py;h=ffdd9157ba0608294b283d3927ea6dcbbad93760;hp=012450f07e320e1a989cf77e9f380f4baf0bf4ae;hb=d7084b7094c99003960b33d4bd4fb655248c6ab3;hpb=2f775ee1c4d2c260221155494b2c7856bc38dafd diff --git a/crosswordpuzzle.py b/crosswordpuzzle.py index 012450f..ffdd915 100644 --- a/crosswordpuzzle.py +++ b/crosswordpuzzle.py @@ -4,13 +4,19 @@ # 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, - occupied_across = False, occupied_down = False, num = 0, - clue_across = None, clue_down = None, revealed = False): + 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? @@ -28,6 +34,59 @@ class GridItem: # 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): @@ -50,6 +109,11 @@ 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): @@ -73,24 +137,113 @@ 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 + 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 ('puzzle') + + 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 frozen - if self.frozen_grid is False: - raise FrozenGridException + # don't export if grid is not frozen + self.assert_frozen_grid () across_data = [] - across_data.append ("\n") - across_data.append ("\n") - across_data.append (title + "\n") - across_data.append ("<AUTHOR>\n") - across_data.append (author + "\n") - across_data.append ("<COPYRIGHT>\n") - across_data.append (copyright + "\n") - across_data.append ("<SIZE>\n") + 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 + "\n") - across_data.append ("<GRID>\n") + 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 @@ -98,23 +251,23 @@ class CrosswordPuzzle: across_data.append (self.data[row][col].char) else: across_data.append (".") - across_data.append ("\n") + across_data.append ("\r\n") - across_data.append ("<ACROSS>\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 + "\n") + across_data.append (clue + "\r\n") else: - across_data.append ("(No clue yet)\n") + across_data.append ("(No clue yet)\r\n") - across_data.append ("<DOWN>\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 + "\n") + across_data.append (clue + "\r\n") else: - across_data.append ("(No clue yet\n") + across_data.append ("(No clue yet\r\n") acrosslite_str = "".join (across_data) return acrosslite_str @@ -151,6 +304,53 @@ class CrosswordPuzzle: 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 @@ -223,9 +423,8 @@ class CrosswordPuzzle: # 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 grid is frozen then abort + self.assert_unfrozen_grid () # if the word length greater than totalrows - startrow if len(word) > self.rows - row: @@ -242,6 +441,11 @@ class CrosswordPuzzle: 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 @@ -273,9 +477,8 @@ class CrosswordPuzzle: # 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 + # if the grid is frozen then abort + self.assert_unfrozen_grid () # is the word length greater than totalcols - startcol? if len(word) > self.cols - col: @@ -292,6 +495,11 @@ class CrosswordPuzzle: 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 @@ -351,3 +559,88 @@ class CrosswordPuzzle: 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 + +