Example #1
0
def puzzle_redo():
    """ Redoes the last puzzle action then redirects to puzzle screen """

    jsonstr = session.get('puzzle', None)
    puzzle = Puzzle.from_json(jsonstr)
    puzzle.redo()
    jsonstr = puzzle.to_json()
    session['puzzle'] = jsonstr
    return redirect(url_for('uipuzzle.puzzle_screen'))
    def __init__(self, data):

        self.data = data
        self.offset = 0

        # Get the size
        self.offset = self.OFFSET_WIDTH
        self.n = n = self.read_byte()
        self.grid = grid = Grid(n)

        # Read the solution to get the black cell locations
        self.offset = self.OFFSET_WORDS
        for r in range(1, n+1):
            line = self.read_chunk(n)
            for c in range(1, n+1):
                letter = chr(line[c-1])
                if letter == self.BLACK_CELL:
                    grid.add_black_cell(r, c)

        # Create the puzzle, then go back and read the words
        self.puzzle = puzzle = Puzzle(grid)

        self.offset = self.OFFSET_WORDS
        for r in range(1, n+1):
            line = self.read_chunk(n)
            for c in range(1, n + 1):
                letter = chr(line[c-1])
                if not grid.is_black_cell(r, c):
                    puzzle.set_cell(r, c, letter)

        # Skip over the solution work area
        self.offset += n*n

        # Read the title and set it in the puzzle

        title = self.read_string()
        if title != "":
            puzzle.title = title

        # Skip the author and copyright lines
        s = self.read_string()     # author
        s = self.read_string()     # copyright

        # Read the clues
        for nc in puzzle.numbered_cells:
            if nc.a:        # Across word
                clue = self.read_string()
                puzzle.set_clue(nc.seq, Word.ACROSS, clue)
            if nc.d:        # Down word
                clue = self.read_string()
                puzzle.set_clue(nc.seq, Word.DOWN, clue)

        # Done
        self.jsonstr = puzzle.to_json()
Example #3
0
def puzzle_title():
    """ Changes the puzzle title and redirects back to the puzzle screen """

    title = request.form.get('ib-input', None)
    if title:
        jsonstr = session['puzzle']
        puzzle = Puzzle.from_json(jsonstr)
        puzzle.title = title
        jsonstr = puzzle.to_json()
        session['puzzle'] = jsonstr
        flash(f"Puzzle title set to {puzzle.title}")

    # Show the puzzle screen
    return redirect(url_for('uipuzzle.puzzle_screen'))
Example #4
0
def puzzle_save_common(puzzlename):
    """ Common method used by both puzzle_save and puzzle_save_as """

    # Recreate the puzzle from the JSON in the session
    # and validate it
    jsonstr = session.get('puzzle', None)
    puzzle = Puzzle.from_json(jsonstr)
    jsonstr = puzzle.to_json()
    ok, messages = puzzle.validate()
    if not ok:
        flash("Puzzle not saved")
        for message_type in messages:
            message_list = messages[message_type]
            if len(message_list) > 0:
                flash(f"*** {message_type} ***")
                for message in message_list:
                    flash("   " + message)
    else:
        # Save the file
        userid = 1  # TODO Replace hard coded user id
        query = DBPuzzle.query.filter_by(userid=userid, puzzlename=puzzlename)
        if not query.all():
            # No puzzle in the database. This is an insert
            logging.debug(f"Inserting puzzle {puzzlename} into puzzles table")
            created = modified = datetime.now().isoformat()
            newpuzzle = DBPuzzle(userid=userid,
                                 puzzlename=puzzlename,
                                 created=created,
                                 modified=modified,
                                 jsonstr=jsonstr)
            db.session.add(newpuzzle)
            db.session.commit()
        else:
            # Existing puzzle. This is an update
            logging.debug(f"Updating puzzle {puzzlename} in puzzles table")
            oldpuzzle = query.first()
            oldpuzzle.modified = datetime.now().isoformat()
            oldpuzzle.jsonstr = jsonstr
            db.session.commit()

        # Send message about the save
        flash(f"Puzzle saved as {puzzlename}")

        # Store the sha of the saved version of the puzzle
        # in the session as 'puzzle.initial.sha' so that we can
        # detect whether it has been changed since it was last saved
        session['puzzle.initial.sha'] = sha256(puzzle.to_json())

    # Show the puzzle screen
    return redirect(url_for('uipuzzle.puzzle_screen'))
Example #5
0
    def run_csv(self):
        """ Uploads puzzle clues from CSV input """
        csvstr = self.data
        user = self.user
        userid = user.id
        puzzlename = self.puzzlename
        puzzle = DBPuzzle.query.filter_by(userid=userid,
                                          puzzlename=puzzlename).first()
        if not puzzle:
            errmsg = f"Puzzle '{puzzlename}' does not exist"
            raise ValueError(errmsg)
        jsonstr = puzzle.jsonstr
        puzzle = Puzzle.from_json(jsonstr)

        with StringIO(csvstr) as fp:
            cfp = csv.reader(fp)
            next(cfp)  # Skip column headings
            for row in cfp:
                seq = int(row[0])
                direction = row[1]
                text = row[2]
                clue_text = row[3]
                if direction == 'across':
                    word = puzzle.get_across_word(seq)
                    if not word:
                        errmsg = f'{seq} across is not defined'
                        raise RuntimeError(errmsg)
                    previous_text = word.get_text()
                    if text != previous_text:
                        errmsg = f'Word at {seq} across should be "{previous_text}", not "{text}"'
                        raise RuntimeError(errmsg)
                    word.set_clue(clue_text)
                elif direction == 'down':
                    word = puzzle.get_down_word(seq)
                    if not word:
                        errmsg = f'{seq} down is not defined'
                        raise RuntimeError(errmsg)
                    previous_text = word.get_text()
                    if text != previous_text:
                        errmsg = f'Word at {seq} down should be "{previous_text}", not "{text}"'
                        raise RuntimeError(errmsg)
                    word.set_clue(clue_text)
                else:
                    errmsg = f'Direction is "{direction}", not "across" or "down"'
                    raise RuntimeError(errmsg)

        return puzzle
Example #6
0
def puzzle_publish_nytimes():
    """ Publishes a puzzle in New York Times format (PDF) """

    # Get the chosen puzzle name from the query parameters
    puzzlename = request.args.get('puzzlename')

    # Open the corresponding row in the database, read its JSON
    # and recreate the puzzle from it
    jsonstr = puzzle_load_common(userid, puzzlename)
    puzzle = Puzzle.from_json(jsonstr)

    # Generate the output
    publisher = PuzzlePublishNYTimes(puzzle, puzzlename)

    # SVG
    filename = os.path.join(tempfile.gettempdir(), puzzlename + ".svg")
    svg_filename = filename
    with open(filename, "wt") as fp:
        fp.write(publisher.get_svg() + "\n")

    # HTML
    filename = os.path.join(tempfile.gettempdir(), puzzlename + ".html")
    html_filename = filename
    with open(filename, "wt") as fp:
        fp.write(publisher.get_html() + "\n")

    # JSON
    filename = os.path.join(tempfile.gettempdir(), puzzlename + ".json")
    json_filename = filename
    with open(filename, "wt") as fp:
        fp.write(jsonstr + "\n")

    # Create an in-memory zip file
    with BytesIO() as fp:
        with ZipFile(fp, mode="w", compression=ZIP_DEFLATED) as zf:
            zf.write(svg_filename, puzzlename + ".svg")
            zf.write(html_filename, puzzlename + ".html")
            zf.write(json_filename, puzzlename + ".json")
        zipbytes = fp.getvalue()

    # Return it as an attachment
    zipfilename = f"nytimes-{puzzlename}.zip"
    resp = make_response(zipbytes)
    resp.headers['Content-Type'] = "application/zip"
    resp.headers[
        'Content-Disposition'] = f'attachment; filename="{zipfilename}"'
    return resp
Example #7
0
def puzzle_open():
    """ Opens a new puzzle and redirects to puzzle screen """

    # Get the chosen puzzle name from the query parameters
    puzzlename = request.args.get('puzzlename')

    # Open the corresponding file and read its contents as json
    # and recreate the puzzle from it
    userid = 1  # TODO Replace hard coded user id
    jsonstr = puzzle_load_common(userid, puzzlename)
    puzzle = Puzzle.from_json(jsonstr)

    # Store the puzzle and puzzle name in the session
    session['puzzle'] = jsonstr
    session['puzzle.initial.sha'] = sha256(jsonstr)
    session['puzzlename'] = puzzlename

    return redirect(url_for('uipuzzle.puzzle_screen'))
Example #8
0
def puzzle_changed():
    """ REST method that returns whether the puzzle has changed since it was opened """

    current_puzzle_json = session.get('puzzle', None)
    if current_puzzle_json:
        current_puzzle = Puzzle.from_json(current_puzzle_json)
        jsonstr_initial_sha = session.get('puzzle.initial.sha', sha256(None))
        jsonstr_current_sha = sha256(current_puzzle.to_json())
        changed = not (jsonstr_current_sha == jsonstr_initial_sha)
    else:
        changed = False

    obj = {"changed": changed}

    # Send this back to the client in JSON
    resp = make_response(json.dumps(obj), HTTPStatus.OK)
    resp.headers['Content-Type'] = "application/json"
    return resp
Example #9
0
def puzzle_screen():
    """ Renders the puzzle screen """

    # Get the existing puzzle from the session
    puzzle = Puzzle.from_json(session['puzzle'])
    puzzlename = session.get('puzzlename', None)

    # Get the clues
    clues = {
        "across": [{
            "seq": seq,
            "text": word.get_clue() or ""
        } for seq, word in puzzle.across_words.items()],
        "down": [{
            "seq": seq,
            "text": word.get_clue() or ""
        } for seq, word in puzzle.down_words.items()],
    }

    # Create the SVG
    svg = PuzzleToSVG(puzzle)
    boxsize = svg.boxsize
    svgstr = svg.generate_xml()

    # Set the state to editing puzzle
    session['uistate'] = UIState.EDITING_PUZZLE
    enabled = session['uistate'].get_enabled()
    enabled["puzzle_undo"] = len(puzzle.undo_stack) > 0
    enabled["puzzle_redo"] = len(puzzle.redo_stack) > 0
    enabled["puzzle_delete"] = puzzlename is not None

    # Send puzzle.html to the client
    return render_template('puzzle.html',
                           enabled=enabled,
                           puzzlename=puzzlename,
                           puzzletitle=puzzle.title,
                           n=puzzle.n,
                           clues=clues,
                           scrollTopAcross=session.get('scrollTopAcross',
                                                       None),
                           scrollTopDown=session.get('scrollTopDown', None),
                           boxsize=boxsize,
                           svgstr=svgstr)
Example #10
0
def puzzle_statistics():
    """ Return the grid statistics in a JSON string """

    # Get the grid from the session
    puzzle = Puzzle.from_json(session['puzzle'])
    puzzlename = session.get('puzzlename', None)
    puzzletitle = session.get('puzzletitle', None)
    stats = puzzle.get_statistics()
    enabled = {}

    svgstr = PuzzleToSVG(puzzle).generate_xml()

    # Render with puzzle statistics template
    return render_template("puzzle-statistics.html",
                           enabled=enabled,
                           puzzlename=puzzlename,
                           puzzletitle=puzzletitle,
                           svgstr=svgstr,
                           stats=stats)
Example #11
0
 def test_set_cell(self):
     n = 5
     puzzle = Puzzle(Grid(n))
     puzzle.set_cell(2, 3, 'D')
     cell = puzzle.get_cell(2, 3)
     self.assertEqual('D', cell)
     for r in range(1, n + 1):
         if r == 2:
             continue
         for c in range(1, n + 1):
             if c == 3:
                 continue
             cell = puzzle.get_cell(r, c)
             self.assertEqual(Puzzle.WHITE, cell, f'Mismatch at ({r}, {c})')
Example #12
0
def puzzle_preview():
    """ Creates a puzzle preview and returns it to ??? """
    userid = 1  # TODO Replace hard coded user id

    # Get the chosen puzzle name from the query parameters
    puzzlename = request.args.get('puzzlename')

    # Open the corresponding file and read its contents as json
    # and recreate the puzzle from it
    jsonstr = puzzle_load_common(userid, puzzlename)
    puzzle = Puzzle.from_json(jsonstr)

    # Get the top two word lengths
    heading_list = [f"{puzzle.get_word_count()} words"]
    wlens = puzzle.get_word_lengths()
    wlenkeys = sorted(wlens.keys(), reverse=True)
    wlenkeys = wlenkeys[:min(2, len(wlenkeys))]
    for wlen in wlenkeys:
        entry = wlens[wlen]
        total = 0
        if entry["alist"]:
            total += len(entry["alist"])
        if entry["dlist"]:
            total += len(entry["dlist"])
        heading_list.append(f"{wlen}-letter: {total}")
    heading = f'Puzzle {puzzlename}({", ".join(heading_list)})'

    scale = 0.75
    svgobj = PuzzleToSVG(puzzle, scale=scale)
    width = (svgobj.boxsize * puzzle.n + 32) * scale
    svgstr = svgobj.generate_xml()

    obj = {
        "puzzlename": puzzlename,
        "heading": heading,
        "width": width,
        "svgstr": svgstr
    }
    resp = make_response(json.dumps(obj), HTTPStatus.OK)
    resp.headers['Content-Type'] = "application/json"
    return resp
Example #13
0
def puzzle_publish_cwcompiler():
    """ Publishes a puzzle in the Crossword Compiler XML format """

    # Get the chosen puzzle name from the query parameters
    puzzlename = request.args.get('puzzlename')

    # Open the corresponding row in the database, read its JSON
    # and recreate the puzzle from it
    jsonstr = puzzle_load_common(userid, puzzlename)
    puzzle = Puzzle.from_json(jsonstr)

    # Generate the output
    user = DBUser.query.filter_by(id=userid).first()
    publisher = PuzzleToXML(user, puzzle)

    # XML file
    xml_filename = filename = os.path.join(tempfile.gettempdir(),
                                           puzzlename + ".xml")
    with open(filename, "wt") as fp:
        fp.write(publisher.xmlstr + "\n")

    # JSON
    json_filename = filename = os.path.join(tempfile.gettempdir(),
                                            puzzlename + ".json")
    with open(filename, "wt") as fp:
        fp.write(jsonstr + "\n")

    # Create an in-memory zip file
    with BytesIO() as fp:
        with ZipFile(fp, mode="w", compression=ZIP_DEFLATED) as zf:
            zf.write(xml_filename, puzzlename + ".xml")
            zf.write(json_filename, puzzlename + ".json")
        zipbytes = fp.getvalue()

    # Return it as an attachment
    zipfilename = f"cwcompiler-{puzzlename}.zip"
    resp = make_response(zipbytes)
    resp.headers['Content-Type'] = "application/zip"
    resp.headers[
        'Content-Disposition'] = f'attachment; filename="{zipfilename}"'
    return resp
Example #14
0
 def create_puzzle():
     """
     Creates sample puzzle as:
     +---------+
     | | |*| | |
     | | |*|*| |
     |*|*| |*|*|
     | |*|*| | |
     | | |*| | |
     +---------+
     """
     n = 5
     grid = Grid(n)
     for (r, c) in [
         (1, 3),
         (2, 3),
         (2, 4),
         (3, 1),
         (3, 2),
     ]:
         grid.add_black_cell(r, c)
     return Puzzle(grid)
Example #15
0
 def create_nyt_puzzle():
     """
     Creates a puzzle from a real New York Times crossword of March 15, 2020
     +-----------------------------------------+
     |A|B|B|A|*| | | |*| | | | | |*| | | | | | |
     | | | | |*| | | |*| | | | | |*| | | | | | |
     | | | | | | | | | | | | | | |*| | | | | | |
     | | | | | |*| | | | | |*| | | |*| | | | | |
     | | | |*| | | | | |*|*| | | | | |*|*| | | |
     |*|*|*| | | | | | | | | | | | | | | | | | |
     | | | | | | |*|*| | | | | | |*| | | | | | |
     | | | | |*| | | |*| | | |*|*| | | | | | | |
     | | | | | | | | | | | | | | | | | | |*|*|*|
     | | | |*| | | | | |*| | | | | | |*| | | | |
     | | | | | |*| | | | |*| | | | |*| | | | | |
     | | | | |*| | | | | | |*| | | | | |*| | | |
     |*|*|*| | | | | | | | | | | | | | | | | | |
     | | | | | | | |*|*| | | |*| | | |*| | | | |
     | | | | | | |*| | | | | | |*|*| | | | | | |
     | | | | | | | | | | | | | | | | | | |*|*|*|
     | | | |*|*| | | | | |*|*| | | | | |*| | | |
     | | | | | |*| | | |*| | | | | |*| | | | | |
     | | | | | | |*| | | | | | | | | | | | | | |
     | | | | | | |*| | | | | |*| | | |*| | | | |
     | | | | | | |*| | | | | |*| | | |*| | | | |
     +-----------------------------------------+
     """
     n = 21
     grid = Grid(n)
     for (r, c) in [(1, 5), (1, 9), (1, 15), (2, 5),
                    (2, 9), (2, 15), (3, 15), (4, 6), (4, 12), (4, 16),
                    (5, 4), (5, 10), (5, 11), (5, 17), (5, 18), (6, 1),
                    (6, 2), (6, 3), (7, 7), (7, 8), (7, 15), (8, 5), (8, 9),
                    (8, 13), (8, 14), (9, 19), (9, 20), (9, 21), (10, 4),
                    (10, 10), (11, 6), (11, 11), (12, 5), (13, 1), (13, 2),
                    (13, 3), (14, 8), (15, 7), (17, 4), (17, 5)]:
         grid.add_black_cell(r, c)
     return Puzzle(grid)
Example #16
0
def puzzle_replace_grid():

    # Get the chosen grid name from the query parameters
    gridname = request.args.get('gridname')
    if not gridname:
        return redirect(url_for('uipuzzle.puzzle_screen'))

    # Create the grid
    userid = 1  # TODO replace hard-coded user ID
    query = DBGrid.query.filter_by(userid=userid, gridname=gridname)
    jsonstr = query.first().jsonstr
    grid = Grid.from_json(jsonstr)

    # Get the current puzzle
    puzzle = Puzzle.from_json(session['puzzle'])

    # and replace its grid
    puzzle.replace_grid(grid)

    # Save in the session
    session['puzzle'] = puzzle.to_json()

    return redirect(url_for('uipuzzle.puzzle_screen'))
Example #17
0
    def test_load_atlantic(self):

        # Make sure the saved file is there: create it in /tmp
        self.test_save()
        filename = os.path.join(tempfile.gettempdir(),
                                "test_puzzle.test_save.json")

        # Load the file
        with open(filename, "rt") as fp:
            jsonstr = fp.read()
        puzzle = Puzzle.from_json(jsonstr)

        # Check some contents

        self.assertEqual(9, puzzle.n)

        word = puzzle.get_across_word(8)
        self.assertEqual("SLIM", word.get_text())
        self.assertEqual("Reed-like", word.get_clue())

        word = puzzle.get_down_word(17)
        self.assertEqual("EHRE", word.get_text())
        self.assertEqual("Honor, to Fritz", word.get_clue())
 def test_same_grid(self):
     oldpuzzle = TestPuzzle.create_solved_atlantic_puzzle()
     grid = Grid.from_json(oldpuzzle.to_json())
     newpuzzle = Puzzle.from_json(oldpuzzle.to_json())
     newpuzzle.replace_grid(grid)
     self.assertEqual(oldpuzzle, newpuzzle)
Example #19
0
    def create_puzzle(self, xmlstr):
        ns = {
            'cc': 'http://crossword.info/xml/crossword-compiler',
            'rp': 'http://crossword.info/xml/rectangular-puzzle'
        }
        root = ET.fromstring(xmlstr)
        elem_rp = root.find('rp:rectangular-puzzle', ns)
        elem_crossword = elem_rp.find('rp:crossword', ns)
        elem_grid = elem_crossword.find('rp:grid', ns)

        # Grid size
        n = int(elem_grid.get('height'))
        grid = Grid(n)

        # Black cells
        for elem_cell in elem_grid.findall('rp:cell', ns):
            celltype = elem_cell.get('type')
            if celltype == 'block':
                r = int(elem_cell.get('y'))
                c = int(elem_cell.get('x'))
                grid.add_black_cell(r, c)

        # Title
        elem_title = elem_rp.find('rp:metadata/rp:title', ns)
        title = elem_title.text
        if not title:
            title = None

        # Puzzle
        puzzle = Puzzle(grid, title)

        # Add the cells
        for elem_cell in elem_grid.findall('rp:cell', ns):
            r = int(elem_cell.get('y'))
            c = int(elem_cell.get('x'))
            if puzzle.is_black_cell(r, c):
                continue
            letter = elem_cell.get('solution')
            if letter != ' ':
                puzzle.set_cell(r, c, letter)

        # Map the word ID to numbered cells
        wordmap = {}
        for elem_word in elem_crossword.findall('rp:word', ns):
            wordid = elem_word.get('id')
            r = int(elem_word.get('y').split('-')[0])
            c = int(elem_word.get('x').split('-')[0])
            nc = puzzle.get_numbered_cell(r, c)
            wordmap[wordid] = nc

        # Add the clues
        for elem_clues in elem_crossword.findall('rp:clues', ns):
            elem_b = elem_clues.find('rp:title/rp:b', ns)
            direction = elem_b.text[0]  # A or D
            for elem_clue in elem_clues.findall('rp:clue', ns):
                wordid = elem_clue.get('word')
                nc = wordmap[wordid]
                clue = elem_clue.text
                puzzle.set_clue(nc.seq, direction, clue)

        return puzzle
Example #20
0
 def test_title_set_later(self):
     grid = Grid(11)
     puzzle = Puzzle(grid)
     puzzle.title = "Later"
     self.assertEqual("Later", puzzle.title)
Example #21
0
 def test_title_changed(self):
     grid = Grid(11)
     puzzle = Puzzle(grid, "First Title")
     self.assertEqual("First Title", puzzle.title)
     puzzle.title = "Second Title"
     self.assertEqual("Second Title", puzzle.title)
Example #22
0
def puzzle_click(direction):
    """ Common method used by both puzzle_click_across and puzzle_click_down """

    # Get the seq or row and column clicked from the query parms
    r = request.args.get('r', None)
    c = request.args.get('c', None)
    seq = request.args.get('seq', None)

    # Get the existing puzzle from the session
    puzzle = Puzzle.from_json(session['puzzle'])
    puzzlename = session.get('puzzlename', None)
    puzzletitle = puzzle.title

    if r is not None and c is not None:
        r = int(r)
        c = int(c)
        # Get the numbered cell at (r, c)
        numbered_cell = puzzle.get_numbered_cell(r, c)
        if not numbered_cell:
            errmsg = f"({r},{c}) is not a numbered cell"
            return redirect(url_for('uipuzzle.puzzle_screen'))
        seq = numbered_cell.seq
    elif seq is not None:
        seq = int(seq)

    # Get the word
    if direction.startswith('A'):
        word = puzzle.get_across_word(seq)
        if not word:
            errmsg = f"Not the start of an across word"
            return redirect(url_for('uipuzzle.puzzle_screen'))
    else:
        word = puzzle.get_down_word(seq)
        if not word:
            errmsg = f"Not the start of a down word"
            return redirect(url_for('uipuzzle.puzzle_screen'))
        pass

    length = word.length

    # Save seq, direction, and length in the session
    session['seq'] = seq
    session['direction'] = direction
    session['length'] = length

    # Get the existing text and clue from this word
    # and replace blanks with "."
    text = word.get_text()
    text = re.sub(' ', '.', text)
    clue = word.get_clue()
    if not clue:
        clue = ""

    # Create the SVG
    svg = PuzzleToSVG(puzzle)
    svgstr = svg.generate_xml()

    # Invoke the puzzle word editor
    session['uistate'] = UIState.EDITING_WORD
    enabled = session['uistate'].get_enabled()

    return render_template("word-edit.html",
                           enabled=enabled,
                           puzzlename=puzzlename,
                           puzzletitle=puzzletitle,
                           svgstr=svgstr,
                           seq=seq,
                           direction=direction,
                           text=text,
                           clue=clue,
                           length=length)
Example #23
0
    def create_nyt_daily():
        """ from https://www.nytimes.com/crosswords/game/daily/2016/09/20 """
        grid = Grid(15)
        for r, c in [(1, 5), (1, 11), (2, 5), (2, 11), (3, 5), (3, 11),
                     (4, 14), (4, 15), (5, 7), (5, 12), (6, 1), (6, 2), (6, 3),
                     (6, 8), (6, 9), (7, 4), (8, 5), (8, 6), (8, 10), (8, 11),
                     (11, 4)]:
            grid.add_black_cell(r, c)
        puzzle = Puzzle(grid)
        for seq, text in [(1, "ACTS"), (5, "PLASM"),
                          (10, "EDGY"), (14, "SHIV"), (15, "RUCHE"),
                          (16, "VALE"), (17, "NINE"), (18, "USUAL"),
                          (19, "IRON"), (20, "ENGLISHTRIFLE"), (23, "RASTAS"),
                          (24, "EDNA"), (25, "WAS"), (28, "EMIT"),
                          (30, "DIGEST"), (32, "MAY"), (35, "BAKEDALASKA"),
                          (38, "ALOT"), (40, "ONO"), (41, "BAER"),
                          (42, "PLUMPUDDING"), (47, "YDS"), (48, "LESSON"),
                          (49, "TIOS"), (51, "EYE"), (52, "OHMS"),
                          (55, "CLUMSY"), (59, "NOPIECEOFCAKE"), (62, "UNDO"),
                          (64, "NAOMI"), (65, "KRIS"), (66, "SIMP"),
                          (67, "GLOMS"), (68, "EIRE"), (69, "EXES"),
                          (70, "ESTEE"), (71, "RATS")]:
            puzzle.get_across_word(seq).set_text(text)

        for seq, clue in [
            (1, "___ of the Apostles"),
            (5, "Ending with neo- or proto-"),
            (10, "Pushing conventional limits"),
            (14, "Blade in the pen"),
            (15, "Strip of fabric used for trimming"),
            (16, "Low ground, poetically"),
            (17, "Rock's ___ Inch Nails"),
            (18, "Habitual customer's order, with \"the\""),
            (19, "Clothes presser"),
            (20,
             "Layers of sherry-soaked torte, homemade custard and fruit served chilled in a giant stem glass"
             ),
            (23, "Dreadlocked ones, informally"),
            (24, "Comical \"Dame\""),
            (25, "\"Kilroy ___ here\""),
            (28, "Give off, as vibes"),
            (30, "Summary"),
            (32, "___-December romance"),
            (35,
             "Ice cream and sponge topped with meringue and placed in a very hot oven for a few minutes"
             ),
            (38, "Oodles"),
            (40, "Singer with the site imaginepeace.com"),
            (41, "Boxer Max"),
            (42,
             "Steamed-for-hours, aged-for-months concoction of treacle, brandy, fruit and spices, set afire and served at Christmas"
             ),
            (47, "Fabric purchase: Abbr."),
            (48, "Teacher's plan"),
            (49, "Uncles, in Acapulco"),
            (51, "___ contact"),
            (52, "Units of resistance"),
            (55, "Ham-handed"),
            (59,
             "What a chef might call each dessert featured in this puzzle, literally or figuratively"
             ),
            (62, "Command-Z command"),
            (64, "Actress Watts"),
            (65, "Kardashian matriarch"),
            (66, "Fool"),
            (67, "Latches (onto)"),
            (68, "Land of Blarney"),
            (69, "Ones who are splitsville"),
            (70, "Lauder of cosmetics"),
            (71, "\"Phooey!\""),
        ]:
            puzzle.get_across_word(seq).set_clue(clue)

        for seq, clue in [
            (1, "Ed of \"Up\""),
            (2, "Set traditionally handed down to an eldest daughter"),
            (3, "Tiny bell sounds"),
            (4, "Willowy"),
            (5, "German kingdom of old"),
            (6, "Growing luxuriantly"),
            (7, "Severe and short, as an illness"),
            (8, "Glass fragment"),
            (9, "Gates of philanthropy"),
            (10, "Voldemort-like"),
            (11, "\"Hesitating to mention it, but...\""),
            (12, "Mop & ___"),
            (13, "Itch"),
            (21, "da-DAH"),
            (22, "Pass's opposite"),
            (26, "\"___ and answered\" (courtroom objection)"),
            (27, "Constellation units"),
            (29, "Walloped to win the bout, in brief"),
            (31, "Chew the fat"),
            (32, "Sugar ___"),
            (33, "Locale for urban trash cans"),
            (34, "Sam Cooke's first #1 hit"),
            (36, "Come to a close"),
            (37, "\"I dare you!\""),
            (39, "Designs with ® symbols: Abbr."),
            (43, "Lowdown, in slang"),
            (44, "Drive mad"),
            (45, "Salade ___"),
            (46, "Club game"),
            (50, "Lollipop"),
            (53, "\"Square\" things, ideally"),
            (54, "\"Git!\""),
            (56, "\"West Side Story\" seamstress"),
            (57, "Mini, e.g."),
            (58, "Positive R.S.V.P.s"),
            (60, "Error report?"),
            (61, "J.Lo's daughter with a palindromic name"),
            (62, "Manipulate"),
            (63, "Kill, as an idea"),
        ]:
            puzzle.get_down_word(seq).set_clue(clue)
        return puzzle
Example #24
0
 def test_title_set_back_to_none(self):
     grid = Grid(11)
     puzzle = Puzzle(grid, "First Title")
     self.assertEqual("First Title", puzzle.title)
     puzzle.title = None
     self.assertIsNone(puzzle.title)
Example #25
0
 def test_title_not_set(self):
     grid = Grid(11)
     puzzle = Puzzle(grid)
     self.assertIsNone(puzzle.title)
Example #26
0
 def test_title_is_set(self):
     grid = Grid(11)
     puzzle = Puzzle(grid, "My Title")
     self.assertEqual("My Title", puzzle.title)
Example #27
0
    def create_puzzle():
        jsonstr = r'''{
  "n": 15,
  "black_cells": [
[1,5],[1,11],[2,5],[2,11],[3,5],[3,11],[4,14],
[4,15],[5,7],[5,12],[6,1],[6,2],[6,3],
[6,8],[6,9],[7,4],[8,5],[8,6],[8,10],
[8,11],[9,12],[10,7],[10,8],[10,13],[10,14],
[10,15],[11,4],[11,9],[12,1],[12,2],[13,5],
[13,11],[14,5],[14,11],[15,5],[15,11]],
  "numbered_cells": [
    { "seq": 1, "r": 1, "c": 1, "a": 4, "d": 5 },
    { "seq": 2, "r": 1, "c": 2, "a": 0, "d": 5 },
    { "seq": 3, "r": 1, "c": 3, "a": 0, "d": 5 },
    { "seq": 4, "r": 1, "c": 4, "a": 0, "d": 6 },
    { "seq": 5, "r": 1, "c": 6, "a": 5, "d": 7 },
    { "seq": 6, "r": 1, "c": 7, "a": 0, "d": 4 },
    { "seq": 7, "r": 1, "c": 8, "a": 0, "d": 5 },
    { "seq": 8, "r": 1, "c": 9, "a": 0, "d": 5 },
    { "seq": 9, "r": 1, "c": 10, "a": 0, "d": 7 },
    { "seq": 10, "r": 1, "c": 12, "a": 4, "d": 4 },
    { "seq": 11, "r": 1, "c": 13, "a": 0, "d": 9 },
    { "seq": 12, "r": 1, "c": 14, "a": 0, "d": 3 },
    { "seq": 13, "r": 1, "c": 15, "a": 0, "d": 3 },
    { "seq": 14, "r": 2, "c": 1, "a": 4, "d": 0 },
    { "seq": 15, "r": 2, "c": 6, "a": 5, "d": 0 },
    { "seq": 16, "r": 2, "c": 12, "a": 4, "d": 0 },
    { "seq": 17, "r": 3, "c": 1, "a": 4, "d": 0 },
    { "seq": 18, "r": 3, "c": 6, "a": 5, "d": 0 },
    { "seq": 19, "r": 3, "c": 12, "a": 4, "d": 0 },
    { "seq": 20, "r": 4, "c": 1, "a": 13, "d": 0 },
    { "seq": 21, "r": 4, "c": 5, "a": 0, "d": 4 },
    { "seq": 22, "r": 4, "c": 11, "a": 0, "d": 4 },
    { "seq": 23, "r": 5, "c": 1, "a": 6, "d": 0 },
    { "seq": 24, "r": 5, "c": 8, "a": 4, "d": 0 },
    { "seq": 25, "r": 5, "c": 13, "a": 3, "d": 0 },
    { "seq": 26, "r": 5, "c": 14, "a": 0, "d": 5 },
    { "seq": 27, "r": 5, "c": 15, "a": 0, "d": 5 },
    { "seq": 28, "r": 6, "c": 4, "a": 4, "d": 0 },
    { "seq": 29, "r": 6, "c": 7, "a": 0, "d": 4 },
    { "seq": 30, "r": 6, "c": 10, "a": 6, "d": 0 },
    { "seq": 31, "r": 6, "c": 12, "a": 0, "d": 3 },
    { "seq": 32, "r": 7, "c": 1, "a": 3, "d": 5 },
    { "seq": 33, "r": 7, "c": 2, "a": 0, "d": 5 },
    { "seq": 34, "r": 7, "c": 3, "a": 0, "d": 9 },
    { "seq": 35, "r": 7, "c": 5, "a": 11, "d": 0 },
    { "seq": 36, "r": 7, "c": 8, "a": 0, "d": 3 },
    { "seq": 37, "r": 7, "c": 9, "a": 0, "d": 4 },
    { "seq": 38, "r": 8, "c": 1, "a": 4, "d": 0 },
    { "seq": 39, "r": 8, "c": 4, "a": 0, "d": 3 },
    { "seq": 40, "r": 8, "c": 7, "a": 3, "d": 0 },
    { "seq": 41, "r": 8, "c": 12, "a": 4, "d": 0 },
    { "seq": 42, "r": 9, "c": 1, "a": 11, "d": 0 },
    { "seq": 43, "r": 9, "c": 5, "a": 0, "d": 4 },
    { "seq": 44, "r": 9, "c": 6, "a": 0, "d": 7 },
    { "seq": 45, "r": 9, "c": 10, "a": 0, "d": 7 },
    { "seq": 46, "r": 9, "c": 11, "a": 0, "d": 4 },
    { "seq": 47, "r": 9, "c": 13, "a": 3, "d": 0 },
    { "seq": 48, "r": 10, "c": 1, "a": 6, "d": 0 },
    { "seq": 49, "r": 10, "c": 9, "a": 4, "d": 0 },
    { "seq": 50, "r": 10, "c": 12, "a": 0, "d": 6 },
    { "seq": 51, "r": 11, "c": 1, "a": 3, "d": 0 },
    { "seq": 52, "r": 11, "c": 5, "a": 4, "d": 0 },
    { "seq": 53, "r": 11, "c": 7, "a": 0, "d": 5 },
    { "seq": 54, "r": 11, "c": 8, "a": 0, "d": 5 },
    { "seq": 55, "r": 11, "c": 10, "a": 6, "d": 0 },
    { "seq": 56, "r": 11, "c": 13, "a": 0, "d": 5 },
    { "seq": 57, "r": 11, "c": 14, "a": 0, "d": 5 },
    { "seq": 58, "r": 11, "c": 15, "a": 0, "d": 5 },
    { "seq": 59, "r": 12, "c": 3, "a": 13, "d": 0 },
    { "seq": 60, "r": 12, "c": 4, "a": 0, "d": 4 },
    { "seq": 61, "r": 12, "c": 9, "a": 0, "d": 4 },
    { "seq": 62, "r": 13, "c": 1, "a": 4, "d": 3 },
    { "seq": 63, "r": 13, "c": 2, "a": 0, "d": 3 },
    { "seq": 64, "r": 13, "c": 6, "a": 5, "d": 0 },
    { "seq": 65, "r": 13, "c": 12, "a": 4, "d": 0 },
    { "seq": 66, "r": 14, "c": 1, "a": 4, "d": 0 },
    { "seq": 67, "r": 14, "c": 6, "a": 5, "d": 0 },
    { "seq": 68, "r": 14, "c": 12, "a": 4, "d": 0 },
    { "seq": 69, "r": 15, "c": 1, "a": 4, "d": 0 },
    { "seq": 70, "r": 15, "c": 6, "a": 5, "d": 0 },
    { "seq": 71, "r": 15, "c": 12, "a": 4, "d": 0 }
  ],
  "across_words": [
    { "seq": 1, "text": "ACTS", "clue": "____ of the Apostles" },
    { "seq": 5, "text": "PLASM", "clue": "Suffix with ecto- or proto-" },
    { "seq": 10, "text": "ENVY", "clue": "" },
    { "seq": 14, "text": "SHIM", "clue": "Blade in the pen" },
    { "seq": 15, "text": "     ", "clue": null },
    { "seq": 16, "text": "V   ", "clue": null },
    { "seq": 17, "text": "NINE", "clue": "The Nazgul sum" },
    { "seq": 18, "text": "     ", "clue": null },
    { "seq": 19, "text": "I   ", "clue": null },
    { "seq": 20, "text": "ENTERTAINMENT", "clue": "" },
    { "seq": 23, "text": "RASCAL", "clue": "" },
    { "seq": 24, "text": "    ", "clue": null },
    { "seq": 25, "text": "   ", "clue": null },
    { "seq": 28, "text": "H   ", "clue": "" },
    { "seq": 30, "text": "      ", "clue": null },
    { "seq": 32, "text": "   ", "clue": null },
    { "seq": 35, "text": "           ", "clue": null },
    { "seq": 38, "text": "    ", "clue": null },
    { "seq": 40, "text": "   ", "clue": null },
    { "seq": 41, "text": "    ", "clue": null },
    { "seq": 42, "text": "           ", "clue": null },
    { "seq": 47, "text": "   ", "clue": null },
    { "seq": 48, "text": "      ", "clue": null },
    { "seq": 49, "text": "    ", "clue": null },
    { "seq": 51, "text": "   ", "clue": null },
    { "seq": 52, "text": "    ", "clue": null },
    { "seq": 55, "text": "      ", "clue": null },
    { "seq": 59, "text": "             ", "clue": null },
    { "seq": 62, "text": "    ", "clue": null },
    { "seq": 64, "text": "     ", "clue": null },
    { "seq": 65, "text": "    ", "clue": null },
    { "seq": 66, "text": "    ", "clue": null },
    { "seq": 67, "text": "     ", "clue": null },
    { "seq": 68, "text": "    ", "clue": null },
    { "seq": 69, "text": "    ", "clue": null },
    { "seq": 70, "text": "     ", "clue": null },
    { "seq": 71, "text": "    ", "clue": null }
  ],
  "down_words": [
    { "seq": 1, "text": "ASNER", "clue": "Ed of \"Up\"" },
    { "seq": 2, "text": "CHINA", "clue": "" },
    { "seq": 3, "text": "TINTS", "clue": null },
    { "seq": 4, "text": "SMEECH", "clue": null },
    { "seq": 5, "text": "P  TL  ", "clue": null },
    { "seq": 6, "text": "L  A", "clue": null },
    { "seq": 7, "text": "A  I ", "clue": null },
    { "seq": 8, "text": "S  N ", "clue": null },
    { "seq": 9, "text": "M  M   ", "clue": null },
    { "seq": 10, "text": "EVIN", "clue": "" },
    { "seq": 11, "text": "N  T     ", "clue": null },
    { "seq": 12, "text": "V  ", "clue": null },
    { "seq": 13, "text": "Y  ", "clue": null },
    { "seq": 21, "text": "RA  ", "clue": null },
    { "seq": 22, "text": "E   ", "clue": null },
    { "seq": 26, "text": "     ", "clue": null },
    { "seq": 27, "text": "     ", "clue": null },
    { "seq": 29, "text": "    ", "clue": null },
    { "seq": 31, "text": "   ", "clue": null },
    { "seq": 32, "text": "     ", "clue": null },
    { "seq": 33, "text": "     ", "clue": null },
    { "seq": 34, "text": "         ", "clue": null },
    { "seq": 36, "text": "   ", "clue": null },
    { "seq": 37, "text": "    ", "clue": null },
    { "seq": 39, "text": "   ", "clue": null },
    { "seq": 43, "text": "    ", "clue": null },
    { "seq": 44, "text": "       ", "clue": null },
    { "seq": 45, "text": "       ", "clue": null },
    { "seq": 46, "text": "    ", "clue": null },
    { "seq": 50, "text": "      ", "clue": null },
    { "seq": 53, "text": "     ", "clue": null },
    { "seq": 54, "text": "     ", "clue": null },
    { "seq": 56, "text": "     ", "clue": null },
    { "seq": 57, "text": "     ", "clue": null },
    { "seq": 58, "text": "     ", "clue": null },
    { "seq": 60, "text": "    ", "clue": null },
    { "seq": 61, "text": "    ", "clue": null },
    { "seq": 62, "text": "   ", "clue": null },
    { "seq": 63, "text": "   ", "clue": null }
  ]
}'''
        puzzle = Puzzle.from_json(jsonstr)
        return puzzle
Example #28
0
 def test_str(self):
     grid = Grid(3)
     puzzle = Puzzle(grid)
     puzzle_string = str(puzzle)
     self.assertTrue("+-----+" in puzzle_string)
     self.assertTrue("| | | |" in puzzle_string)
Example #29
0
 def run_json(self):
     """ Creates puzzle from JSON input """
     return Puzzle.from_json(self.data)
Example #30
0
    def create_test_puzzle():
        jsonstr = """{
  "n": 9,
  "cells": [
    "+-----------------+",
    "|D|A|B|*|*|E|F|T|S|",
    "|S|L|I|M|*|R|I|O|T|",
    "|L|O|C|A|V|O|R|E|S|",
    "|R|E|U|N|I| |E|D|*|",
    "|*|*|R|A|P|I|D|*|*|",
    "|*|R|I|C|E|C|A|K|E|",
    "|C|O|O|L|R|A|N|C|H|",
    "|C|L|U|E|*| |C|A| |",
    "|R|O|S|S|*|*|E|R|E|",
    "+-----------------+"
  ],
  "black_cells": [
    [ 1, 4 ],
    [ 1, 5 ],
    [ 2, 5 ],
    [ 4, 9 ],
    [ 5, 1 ],
    [ 5, 2 ],
    [ 5, 8 ],
    [ 5, 9 ],
    [ 6, 1 ],
    [ 8, 5 ],
    [ 9, 5 ],
    [ 9, 6 ]
  ],
  "numbered_cells": [
    { "seq": 1, "r": 1, "c": 1, "a": 3, "d": 4 },
    { "seq": 2, "r": 1, "c": 2, "a": 0, "d": 4 },
    { "seq": 3, "r": 1, "c": 3, "a": 0, "d": 9 },
    { "seq": 4, "r": 1, "c": 6, "a": 4, "d": 8 },
    { "seq": 5, "r": 1, "c": 7, "a": 0, "d": 9 },
    { "seq": 6, "r": 1, "c": 8, "a": 0, "d": 4 },
    { "seq": 7, "r": 1, "c": 9, "a": 0, "d": 3 },
    { "seq": 8, "r": 2, "c": 1, "a": 4, "d": 0 },
    { "seq": 9, "r": 2, "c": 4, "a": 0, "d": 8 },
    { "seq": 10, "r": 2, "c": 6, "a": 4, "d": 0 },
    { "seq": 11, "r": 3, "c": 1, "a": 9, "d": 0 },
    { "seq": 12, "r": 3, "c": 5, "a": 0, "d": 5 },
    { "seq": 13, "r": 4, "c": 1, "a": 8, "d": 0 },
    { "seq": 14, "r": 5, "c": 3, "a": 5, "d": 0 },
    { "seq": 15, "r": 6, "c": 2, "a": 8, "d": 4 },
    { "seq": 16, "r": 6, "c": 8, "a": 0, "d": 4 },
    { "seq": 17, "r": 6, "c": 9, "a": 0, "d": 4 },
    { "seq": 18, "r": 7, "c": 1, "a": 9, "d": 3 },
    { "seq": 19, "r": 8, "c": 1, "a": 4, "d": 0 },
    { "seq": 20, "r": 8, "c": 6, "a": 4, "d": 0 },
    { "seq": 21, "r": 9, "c": 1, "a": 4, "d": 0 },
    { "seq": 22, "r": 9, "c": 7, "a": 3, "d": 0 }
  ],
  "across_words": [
    { "seq": 1, "text": "DAB", "clue": null },
    { "seq": 4, "text": "EFTS", "clue": null },
    { "seq": 8, "text": "SLIM", "clue": null },
    { "seq": 10, "text": "RIOT", "clue": null },
    { "seq": 11, "text": "LOCAVORES", "clue": null },
    { "seq": 13, "text": "REUNI ED", "clue": null },
    { "seq": 14, "text": "RAPID", "clue": null },
    { "seq": 15, "text": "RICECAKE", "clue": null },
    { "seq": 18, "text": "COOLRANCH", "clue": null },
    { "seq": 19, "text": "CLUE", "clue": null },
    { "seq": 20, "text": " CA ", "clue": null },
    { "seq": 21, "text": "ROSS", "clue": null },
    { "seq": 22, "text": "ERE", "clue": null }
  ],
  "down_words": [
    { "seq": 1, "text": "DSLR", "clue": null },
    { "seq": 2, "text": "ALOE", "clue": null },
    { "seq": 3, "text": "BICURIOUS", "clue": null },
    { "seq": 4, "text": "ERO ICA ", "clue": null },
    { "seq": 5, "text": "FIREDANCE", "clue": null },
    { "seq": 6, "text": "TOED", "clue": null },
    { "seq": 7, "text": "STS", "clue": null },
    { "seq": 9, "text": "MANACLES", "clue": null },
    { "seq": 12, "text": "VIPER", "clue": null },
    { "seq": 15, "text": "ROLO", "clue": null },
    { "seq": 16, "text": "KCAR", "clue": null },
    { "seq": 17, "text": "EH E", "clue": null },
    { "seq": 18, "text": "CCR", "clue": null }
  ],
  "undo_stack": [],
  "redo_stack": []
}
"""
        puzzle = Puzzle.from_json(jsonstr)
        return puzzle