avatarPatrick Kalkman

Summary

Ada and Grace, developers at QuantumQuirk, utilize Python, Pygame, and the OpenAI API to create an intelligent crossword puzzle generator during their annual FedEx Day innovation challenge.

Abstract

In the bustling tech hub of Tallinn, Estonia, two innovative developers named Ada and Grace embark on a creative journey at QuantumQuirk, a tech company renowned for its innovative problem-solving and team-building approach. During a 24-hour innovation sprint known as FedEx Day, they create an AI-powered crossword puzzle generator using Python, Pygame, and the OpenAI API. This generator crafts answers, fits them into the grid, generates clues, and even produces a printable PDF of the puzzle. Their project navigates through the complexities of constraint satisfaction problems to bring to life a modern twist on the classic game, demonstrating the harmonious integration of artificial intelligence and human creativity. The complete source code for their project is available on GitHub, showcasing their pioneering approach and offering a valuable resource for aspiring developers and puzzlers alike.

Opinions

  • The authors present crossword puzzles not just as a form of entertainment but as a universal delight that stimulates minds and has a timeless appeal.
  • British-style and American-style crossword grids are acknowledged for their charm and complexity, with their unique formats and rules reflecting the diversity within crossword puzzles.
  • The use of constraint satisfaction problems in creating a crossword puzzle generator exemplifies a systematic and creative approach to algorithmic challenges.
  • The process of generating clues via the OpenAI API highlights the authors' belief in the power of AI to enhance creativity and efficiency, particularly in tasks that typically require significant human effort.
  • The authors view the PDF generation of the crossword puzzle as an essential step in preserving the traditional experience of solving puzzles on paper.
  • The FedEx Day innovation challenge is esteemed as a valuable company tradition that fosters creativity, teamwork, and the birth of passion projects within QuantumQuirk.
  • The project, from coding journey to the final PDF output, underscores the authors' enthusiasm for leveraging modern technology to innovate timeless classics like the crossword puzzle.
  • Sharing the source code on GitHub demonstrates the authors' commitment to community engagement and support for the open-source movement.

How to Craft Crosswords with Code — A Python, Pygame, and OpenAI API approach

Revolutionizing puzzle-making with the power of artificial intelligence

How to Craft Crosswords with Code. Image generated by Midjourney, prompt by author.

In the lively tech heartbeat of Tallinn, Estonia, tucked among sleek glass skyscrapers and cozy, fragrant coffee shops, lies a beacon of innovation for tech lovers: QuantumQuirk.

QuantumQuirk, a progressive tech company, is a beacon of innovation known for its novel problem-solving and team-building approaches.

Once a year, QuantumQuirk has a tradition known as FedEx Day. No, they’re not delivering packages, but they are delivering innovation. The rules are simple: employees have twenty-four hours to deliver a new, work-related project from concept to creation.

The aim is to promote creativity, foster quick thinking, and spark passion projects. It’s a day that every ‘Quirker’ looks forward to, a day where their imagination has free reign.

This year, amid the symphony of clacking keys and excited chatter, two talented developers, Ada and Grace, huddle in a corner brainstorming their project.

Their eyes shine with determination, and their laptops are poised and ready to turn lines of code into a masterpiece.

“Let’s make a crossword puzzle!” Ada suggests, her fingers tapping rhythmically on her coffee mug.

Grace, always up for a challenge, raises an eyebrow, “Just a crossword puzzle? Doesn’t sound very QuantumQuirky to me.”

“No, no, no,” Ada grins, a twinkle in her eyes. “We use Python, Pygame, and the OpenAI API. We create an intelligent crossword puzzle generator! We design the grid. It fits in the words and generates the clues automatically. We could even add a graphic interface for people to play it in real-time.”

Grace’s eyes light up as she visualizes the concept. A project like this is ambitious, innovative, and just the right amount of quirky. It’s perfect.

Join Ada and Grace as they embark on this thrilling coding journey, navigating the realms of artificial intelligence, game design, and algorithmic problem-solving to create an intelligent crossword puzzle generator.

We’ll break down their steps, codes, triumphs, and challenges, offering an inside look at how you can leverage Python, Pygame, and the OpenAI API to create your crossword conundrums.

Eager to delve deeper into this world of crossword creation? The complete source code for this project is readily accessible to you. You’ll find it housed in this GitHub repository.

Table of contents

· A brief introduction to crossword puzzlesBritish-Style GridsAmerican-Style GridsCrossword sizesClues and hints · Generating the crossword1. Generating answers2. Populating the crossword grid with answers3. Cueing up the clues4. Generating a PDF with the crossword and the cues · 1. Generating answers using the OpenAI API · 2. Populating the Crossword Grid with AnswersThe Crossword and CrosswordGeneratorThe CrosswordEventLoop · 3. Cueing up the clues · 4. Making it printable: Generating a PDF · The FedEx Demo · References and Further Reading:

A brief introduction to crossword puzzles

Crossword puzzles, a delightful mesh of words and wits, have stimulated minds and entertained individuals for over a century. Whether you’re a casual solver who enjoys the challenge over a morning cup of coffee or a seasoned cruciverbalist who relishes the brain-teasing intricacies, there’s no denying the universal appeal of these word puzzles.

Crosswords come in various formats and sizes, each with unique charm and complexity. The two primary formats are the British-style grid and the American-style grid.

British-Style Grids

British-style crossword puzzle, image by Ross Beresford on flickr

British-style or cryptic crosswords typically have a higher black square percentage, alternating shaded and unshaded squares. They’re often symmetrical and contain numbered clues that lead to solutions fitting into the grid horizontally (across) or vertically (down).

American-Style Grids

American-style crosswords have a lower percentage of black squares and are typically designed so that every letter is part of an “across” word and a “down” word. The puzzles are usually themed, with certain longer answers — and a number of the shorter ones — related to the theme somehow.

Crossword sizes

The size of crossword puzzles can vary widely. Smaller daily puzzles in most newspapers range from 13x13 to 15x15 squares, while larger Sunday puzzles may be 21x21 or 25x25. Specific crossword books may even contain “giant” puzzles of 41x41 squares.

Clues and hints

The heart of a crossword puzzle lies in its clues — hints or definitions that solvers must interpret to find the correct word. Depending on the crossword's style, clues can range from straightforward definitions to cryptic or pun-laden hints. A list of clues accompanies each puzzle, categorized as ‘across’ or ‘down’ according to their placement in the grid.

From daily newspapers to specialized crossword books and online platforms to dedicated crossword puzzle apps, these beautiful word puzzles have found their way into various aspects of our daily lives. Not just as a source of entertainment, they serve as a tool to boost vocabulary, improve memory, and even enhance problem-solving skills.

As we journey into creating crossword puzzles with Python, Pygame, and OpenAI API, we must appreciate the charm and complexity of these mind-boggling word grids.

Generating the crossword

In this segment, we’ll sketch the roadmap to bring our crossword puzzle to life, detailing its creation and visualization stages. As we delve deeper into subsequent sections, we’ll illuminate each step, breaking it down and unpacking its intricacies for a better understanding.

1. Generating answers

Our first move in this puzzle-making journey is to create the words that will populate our crossword grid.

To accomplish this, we’ll harness the power of the OpenAI API. By prompting the API, we’ll be able to generate a wealth of words drawn from an array of categories, ensuring a rich diversity in our answers.

Importantly, we’ll aim for a mix of word lengths, producing short, medium, and long answers. This balance is key to crafting a well-formed crossword grid.

Once this step is complete, we’ll have an extensive list of varied words at our disposal, ready to be arranged into a captivating crossword puzzle.

2. Populating the crossword grid with answers

Equipped with diverse words from the preceding step, our focus now shifts to filling in the crossword puzzle. This step is an intricate exercise of arrangement and adjustment, where we carefully fit words into the predetermined grid.

To start with, we select predefined grid structures which we’ve stored in a file. We aim to use these as our foundation and fill the remaining spaces with medium and short words. This task isn’t without its challenges and can require a certain amount of trial and error.

To assist in this process, we employ a backtracking algorithm. This powerful technique places a word in the grid, evaluates if it fits well within the existing structure, and if it doesn’t, the algorithm backtracks to try another word.

It’s important to note that not all generated words will secure a spot in the final grid. Crafting a crossword often demands adaptability and compromise, as fitting words can be akin to solving a puzzle itself. However, the ultimate result is a well-integrated crossword puzzle, prepared and eager for the next stage of its creative journey.

3. Cueing up the clues

With our validated and finalized crossword grid, it’s time to tackle the other half of the puzzle equation — the clues. Just as we did for generating answers, we’ll be engaging the assistance of the OpenAI API for this crucial task.

Our method will involve creating a series of carefully crafted prompts. These will be fed to the API, each one requesting the generation of a crossword clue corresponding to a specific answer from our puzzle.

Ensuring that each clue aligns with the answer’s category or theme is vital, weaving a coherent thread through our puzzle. This cohesion enhances the solver’s experience, immersing them in the puzzle’s overarching theme while keeping them engaged and challenged.

4. Generating a PDF with the crossword and the cues

Upon successfully creating the crossword, the final step is to combine all the pieces in a unified format — a PDF. This PDF includes the completed crossword grid and lists the cues associated with each word. This facilitates easy crossword distribution, printing, or sharing while conveniently compiling all necessary elements in one document.

1. Generating answers using the OpenAI API

“Alright,” Grace begins, pulling up a new Python file on her screen, “the first thing we need to do is generate answers for our crossword puzzle. To do this, we’ll need the power of the OpenAI API.”

Ada, sipping her coffee, peeks over at Grace’s screen. “Shouldn’t we consider using a framework like langchain to connect to the OpenAI API? It might make things a bit simpler.”

Grace considers this for a moment before shaking her head. “The OpenAI Python library is pretty straightforward,” she explains, “We don’t need any additional functionality at this stage. Let’s stick to it.”

“So, we will create a base class, OpenAIResponseProcessor, to encapsulate our interactions with the OpenAI API. We'll start by initializing it with our API key, which we'll retrieve from our environment variables."

class OpenAIResponseProcessor:
    def __init__(self):
        load_dotenv()
        self.api_key = os.getenv("OPENAI_API_KEY")
        openai.api_key = self.api_key

Grace starts coding the OpenAIResponseProcessor class. As she types, she explains the key methods:

  • _create_prompt: This method will create the prompt we send to OpenAI. It will depend on the specific use case, so we’ll leave it unimplemented for now.”
def _create_prompt(self, *args, **kwargs):
    raise NotImplementedError
  • _parse_response: This will extract the data we need from the response returned by OpenAI. Again, this will depend on the specific use-case, so we’ll leave it unimplemented for now.”
def _parse_response(self, response_text):
    raise NotImplementedError
  • _create_cache_filename: We’ll generate several prompts and don’t want to overuse the OpenAI API. To avoid that, we can store the results in a cache file and retrieve them if we need to generate the same prompt again.”
def _create_cache_filename(self, category, word_length, num_words):
    raise NotImplementedError
  • generate: This is where the magic happens. We first check if the cache file for our prompt already exists. If it does, we load the results from there. If not, we send a request to the OpenAI API with our prompt, parse the response, and store it in the cache file.”
def generate(self, *args, **kwargs):
    cache_file = self._create_cache_filename(*args, **kwargs)
    if os.path.exists(cache_file):
        with open(cache_file, "r") as f:
            result = json.load(f)
    else:
        prompt = self._create_prompt(*args, **kwargs)

        response = openai.Completion.create(
            model="text-davinci-003",
            prompt=prompt,
            temperature=0.9,
            max_tokens=4000,
            top_p=1,
            frequency_penalty=0,
            presence_penalty=0.6
        )

        result = self._parse_response(response['choices'][0]['text'])  # type: ignore
        with open(cache_file, "w") as f:
            json.dump(result, f)

    return result

“Once we have this base class,” Grace continues, “we can create a subclass specifically for generating crossword answers. Our CrosswordAnswerGenerator class will provide specific implementations for creating prompts and parsing responses."

Grace turns to Ada, her finger tracing the lines of code on the screen. “Look at the _create_prompt function here," she explains. "It's generating the prompt we'll send to the OpenAI API. Essentially, we're asking the model to generate a list of words.

But not just any words. We want {num_words} words, each with {word_length} syllables, all related to a certain {category}. And to make it even more specific, we tell the model that these words should be suitable for a crossword puzzle. This specificity helps us get the precise output we need for our crossword generator!"

class CrosswordAnswerGenerator(OpenAIResponseProcessor):
    def _create_prompt(self, category, word_length, num_words):
        return (f"Generate a list of {num_words} {word_length}-syllable "
                f"English words related to the category '{category}', suitable"
                "for a crossword puzzle.")

    def _parse_response(self, response_text):
        lines = response_text.strip().split('\n')
        words = [line.split(' ', 1)[1] for line in lines if len(line.split(' ', 1)) > 1]
        words = [word.lower().strip() for word in words]
        return words

    def _create_cache_file(self, category, word_length, num_words):
        return f"cache/{category}_{word_length}_{num_words}.json"

    def get_additional_words(self):
        with open("cache/additional_words.txt", "r") as f:
            words = f.read().splitlines()
        return [word.lower().strip() for word in words]

Ada nods, following Grace’s explanations as she sees the code come to life. “And the get_additional_words method?” she asks.

“That’s just a helper method. If we need to add any extra words manually, we can put them in a file, and this method will load them for us,” Grace answers.

Ada gazes at the code. Her brow furrowed in thought. “Grace,” she inquires, “how many words do we need to generate?”

Grace leans back in her chair, pondering. “Well,” she replies, “that’s a good question, Ada. It’s not a fixed number. The quantity of words we’ll need depends on the size and complexity of our crossword grid. Larger, more intricate grids might require a larger pool of words to choose from. We’ll have to calibrate and fine-tune it as we proceed with our project.

With a final flourish, Grace completes the CrosswordAnswerGenerator class and leans back, satisfied. "Now, we can get our crossword answers directly from OpenAI."

Ada grins, clapping her hands together. “Perfect! Let’s get those words!”

2. Populating the Crossword Grid with Answers

Ada leans back in her chair, her gaze distant as she remembers a specific lesson from the ‘CS50’s Introduction to Artificial Intelligence with Python’ course she had taken. A look of understanding flashes across her face, and she begins explaining to Grace.

“The most complex part of this problem is creating a crossword grid and populating it with the right answers. We have to make sure that the words fit perfectly and follow all the rules of a crossword puzzle,” Ada explains, then realizes that Grace might not understand these terms.

“By the way, when I say ‘fit perfectly,’ I mean that every box in the crossword puzzle that’s supposed to contain a letter does — and no words spill over. And the rules? They’re the guidelines of a crossword puzzle, like how words should be arranged, their direction, and how they cross each other.”

Grace listens intently, “I see. But how can we model this? Is there a systematic way to approach it?”

“Indeed,” Ada confirms. “This is a classic constraint satisfaction problem. We have certain variables, the slots on the crossword puzzle where we can place words. Then we have a domain, the possible words we can put in each slot. And finally, the constraints are the rules of the crossword puzzle.”

She further explains, “Imagine you’re coloring a map, and you can’t use the same color for neighboring countries — that’s a constraint satisfaction problem too! The countries are your variables, the colors you have are the domain, and the rule that neighboring countries can’t be the same color is your constraint.”

Grace nods, starting to understand. “So we’re using the rules of a crossword puzzle to limit where we can put words. But how do we implement this?”

“We’ll create a ‘Crossword’ class, which is the structure of the crossword puzzle, and a ‘CrosswordCreator’ class, which will handle placing the words into the crossword,” Ada says, revealing the code she’d been developing in the background. “A lot of this code is thanks to Brian Yu and David J. Malan, who did a great job explaining these concepts in their CS50 course.”

Ada points at the screen. “Our Crossword class reads a simple crossword grid from a structure file, something like this:

#___#
#_##_
#_##_
#_##_
#____

A # character represents a blocked cell in the grid, while an underscore represents an open cell where a word can be placed. The grid is read row by row from top to bottom.

Once read, the class uses this grid to determine where the variables — our word slots — are. It also figures out where variables overlap, as the same letter must be placed in both variables at that point.”

“And what about the CrosswordCreator class?” asks Grace.

Ada smiles, “That’s where the magic happens. It takes a Crossword instance and uses an algorithm to assign words to variables. It ensures that all the constraints are satisfied — that is, the words fit the slots, they’re all unique, and where they overlap, they share the same letter.”

Satisfied, Grace leans back, her understanding of crossword puzzles has deepened significantly. “That’s pretty smart. I can’t wait to see it in action!”

The Crossword and CrosswordGenerator

Grace squints at the screen, clearly intrigued by the complexity of the problem. “So, how does the code look for the Crossword class?"

Ada swiftly navigates to the code, “This is the code for the Crossword class, we receive the initial layout of the crossword in the constructor (__init__ method) via the structure parameter. Then, determine_variables finds all the locations where words can be placed. The check_vertical and check_horizontal methods help identify word slots running vertically and horizontally, respectively."

class Crossword:
    def __init__(self, structure, width, height, words):
        self.structure = structure
        self.width = width
        self.height = height
        self.words = words
        self.variables = set()
        self.overlaps = dict()
        self.variable_to_number = {}
        self.determine_variables()
        self.determine_overlaps()

    def determine_variables(self):
        for i in range(self.height):
            for j in range(self.width):
                self.check_vertical(i, j)
                self.check_horizontal(i, j)
        self.initialize_variable_to_number()

    def determine_overlaps(self):
        for v1 in self.variables:
            for v2 in self.variables:
                if v1 != v2:
                    self.set_overlap(v1, v2)

She scrolls further down, “And here, the determine_overlaps function figures out where our word slots overlap—where two words will share a letter. If one exists, the get_variable_at_cell method returns a variable located at a given grid cell."

Grace looks overwhelmed but fascinated, “Wow, that’s quite some code. But how do we take these variables and fill them with words?”

“That’s where the CrosswordCreator class comes into play," Ada explains, scrolling down to the next chunk of code. "This class takes a Crossword object and tries to fill it in. We initialize it with the crossword puzzle we want to fill and a list of words that can be used as answers."

class CrosswordCreator():

    def __init__(self, crossword, update_callback=None, finish_callback=None):
        self.update_callback = update_callback
        self.finish_callback = finish_callback
        self.crossword = crossword
        self.domains = {
            var: self.crossword.words.copy()
            for var in self.crossword.variables
        }
        self.assignment = None

    def solve(self):
        self.enforce_node_consistency()
        self.ac3()
        return self.backtrack(dict())

Ada points at the solve method, "This is the method that does the heavy lifting—it calls several methods that enforce node consistency and constraint propagation (enforce_node_consistency and ac3 methods), then it uses backtracking search (backtrack method) to find a valid assignment for all variables that satisfies all constraints."

Grace nods, gaining more clarity, “I see, so CrosswordCreator is like a smart robot that knows how to fill in the crossword puzzle properly."

“Exactly,” Ada beams at Grace’s understanding, “The beauty of this approach is that it can adapt to different crossword structures and word lists. So, we can use it to create many different crossword puzzles, not just one!”

Grace, animated with curiosity, poses a thought-provoking question, “Can we see it in action? See the program as it tries to place the words in the grid. Kind of like visualizing the backtracking?”

Ada lights up, “Yes. But we need additional code to initialize Pygame and create an event loop. This is where the CrosswordEventLoop class comes into play."

The CrosswordEventLoop

Grace scoots closer, eager to explore the next phase of code, “Sounds fun! What does it do?”

“Well,” Ada begins, opening the code for the CrosswordEventLoop class, "This class primarily handles the visual aspect of our crossword puzzle.

It allows us to initialize Pygame, draw the cells of the crossword grid, add letters and numbers into those cells, and continuously update the assignment of words to variables as our program runs."

Ada explains how each method in the CrosswordEventLoop class contributes to the visualization process. She points out the initialize_pygame and terminate_pygame methods, used to start and stop the Pygame window.

class CrosswordEventLoop:
    def __init__(self, crossword):
        self.crossword = crossword
        self.assignment = None
        self.running = True

    def initialize_pygame(self):
        pygame.init()
        self.screen = pygame.display.set_mode((self.crossword.width * CELL_SIZE,
                                               self.crossword.height * CELL_SIZE))
        pygame.display.set_caption("Crossword")

    def terminate_pygame(self):
        pygame.quit()

She describes the draw_cell, draw_letter, and draw_number methods, which are responsible for drawing the cells of the crossword grid, placing letters into those cells, and adding clue numbers at the start of each word.

def draw_cell(self, i, j):
    color = (255, 255, 255) if self.crossword.structure[i][j] else (0, 0, 0)
    pygame.draw.rect(self.screen, color,
                     (j * CELL_SIZE,
                      i * CELL_SIZE,
                      CELL_SIZE,
                      CELL_SIZE))

    if self.crossword.structure[i][j]:
        pygame.draw.rect(self.screen, (0, 0, 0),
                         (j * CELL_SIZE,
                          i * CELL_SIZE,
                          CELL_SIZE,
                          CELL_SIZE),
                         LINE_WIDTH)

def draw_letter(self, i, j, letter):
    font = pygame.font.Font(None, 48)
    text = font.render(letter, True, (0, 0, 0))
    self.screen.blit(text,
                     (j * CELL_SIZE + (CELL_SIZE - text.get_width()) / 2,
                      i * CELL_SIZE + (CELL_SIZE - text.get_height()) / 2))

def draw_number(self, i, j, number):
    font = pygame.font.Font(None, 24)
    text = font.render(str(number), True, (0, 0, 0))
    self.screen.blit(text, (j * CELL_SIZE + 5, i * CELL_SIZE + 5))

She further explains, “The draw_assignment method, refreshes the display with each new assignment made by the algorithm; this way, we get to visualize the algorithm's progress in real-time. This method is called every time the algorithm makes a new assignment."

def draw_assignment(self, only_grid=False):
    self.screen.fill((0, 0, 0))

    for i in range(self.crossword.height):
        for j in range(self.crossword.width):
            self.draw_cell(i, j)

            variable = self.crossword.get_variable_at_cell(i, j)
            if variable is not None:
                if self._get_variable_number(variable) is not None:
                    self.draw_number(i, j,
                                     self.crossword.variable_to_number[variable])

    if only_grid:
        return

    if self.assignment is None:
        return

    assignment_copy = self.assignment.copy()
    for variable, word in assignment_copy.items():
        direction = variable.direction
        for k in range(len(word)):
            i = variable.i + (k if direction == Variable.DOWN else 0)
            j = variable.j + (k if direction == Variable.ACROSS else 0)
            if self.crossword.structure[i][j]:
                self.draw_letter(i, j, word[k])

    pygame.display.update()

“And what happens when the algorithm successfully fills in the crossword?” Grace queries, her eyes lit with anticipation.

Ada smiles, “That’s where the finish method comes into play. It stops the event loop and indicates that the crossword puzzle has been completed."

“The run method is our main event loop. It continually checks for Pygame events and updates the screen with the current state of the crossword puzzle," Ada concludes, pointing to the last method in the class.

def run(self):
    self.initialize_pygame()
    self.draw_assignment(only_grid=True)
    pygame.image.save(self.screen, "crosswords/crossword.png")

    while self.running:
        for event in pygame.event.get():
            if event.type == QUIT:
                self.running = False
        if self.assignment:
            self.draw_assignment()
    self.terminate_pygame()

Grace, awestruck, murmurs, “This is brilliant! It’s like breathing life into the crossword puzzle.”

Ada nods in agreement, her eyes filled with a coder’s satisfaction, “Indeed, it’s fascinating to see your code come to life. Let’s run it and watch the magic unfold!” Her fingers dance on the keyboard as she says this, breathing life into the digital crossword.

3. Cueing up the clues

“That was exciting,” Grace affirms, her eyes sparkling with anticipation. “Now that we have our grid filled with the correct answers, the essence of a crossword puzzle lies in its clues, right?” Ada nods in agreement.

“To generate these intriguing clues, we will harness the power of OpenAI,” Grace continues, excitement clear in her voice. “This adds an extra layer of creativity to our crossword puzzle and makes the whole process much more efficient.”

Grace gestures towards the screen, “To accomplish this, we’ll revisit our previously developed base class, OpenAIResponseProcessor. Remember the one we discussed, Ada? It’s designed to simplify the interaction between our application and the OpenAI API.”

Ada nods in understanding as Grace pulls up the code for the new class, CrosswordCueGenerator:

class CrosswordCueGenerator(OpenAIResponseProcessor):
    def _create_prompt(self, answer):
        return f"Generate a clue for the crossword answer '{answer}'."

    def _parse_response(self, response_text):
        return response_text.strip().replace("\"", "")

    def _create_cache_filename(self, answer):
        return f"cache/{answer}.json"

She points at the lines of code on the screen, “In this class, the _create_prompt method generates a prompt for the OpenAI API to create a clue based on a given crossword answer.

The response from the API is then processed by the _parse_response method, which removes any extraneous quotation marks.

Finally, the _create_cache_filename method creates a filename based on the crossword answer for caching the API's response."

“The idea behind this class,” Grace concludes, “is to streamline the clue generation process. This saves us from the time-consuming task of crafting individual clues and allows us to use AI’s potential to produce engaging and challenging clues for our crossword puzzle.”

Returning to her chair, Ada grins in amazement, “This is a game-changer, Grace! I can hardly wait to see the clues the AI generates!”

4. Making it printable: Generating a PDF

Ada leans back in her chair, eyeing the displayed crossword grid on the screen, brimming with satisfaction. “Our crossword puzzle seems almost ready, Grace. Seeing the words fit neatly into the grid is exciting, right?”

“Indeed it is,” Grace responds, sharing Ada’s joy. “But now comes the real deal. Let’s make our creation tangible and printable. After all, there’s something uniquely satisfying about solving a crossword puzzle on paper, don’t you agree?”

Ada grins, nodding. “Absolutely. Nothing beats the feeling of a pen in hand, scribbling down answers and filling in boxes.”

“That’s the spirit,” Grace says, pulling up her code editor. “To achieve this, we must generate a PDF file of our crossword puzzle. And for that, we have our PDFGenerator class.”

She explains the class to Ada: “It uses the library reportlab to generate the PDF. It adds the crossword grid as an image, then includes each clue below it. And, of course, each clue comes with a number and a direction, just as you would expect in a crossword puzzle.”

Grace reveals the PDFGenerator class on the screen:

class PDFGenerator:
    def __init__(self, filename="./crosswords/crossword.pdf", img_filename="./crosswords/crossword.png"):
        self.filename = filename
        self.grid_image = img_filename
        self.styles = getSampleStyleSheet()

    def create_pdf(self, assignment, crossword, crossword_cue_generator):
        doc = SimpleDocTemplate(self.filename, pagesize=landscape(letter))
        elements = []

        crossword_image = Image(self.grid_image, 300, 300)
        elements.append(crossword_image)
        elements.append(Paragraph("", self.styles["Normal"]))

        for variable, word in sorted(assignment.items(), key=lambda item: crossword.get_variable_number(item[0])):
            cue = crossword_cue_generator.generate(word)
            text = f"{crossword.get_variable_number(variable)}: {variable.direction} => {cue}"
            elements.append(Paragraph(text, self.styles["Normal"]))

        doc.build(elements)

With anticipation building in the room, Grace confidently taps a few keys, activating the program. As the system hums in response, the screen flickers momentarily, then a new window appears, showcasing a neatly formatted PDF document.

“Voila,” Grace announces, turning to Ada with a triumphant smile. “Here’s our crossword puzzle, ready for print. Now anyone can take up a pen and start solving it!”

The PDF document displays an image of the crossword grid at the top, each black and white cell delineated sharply. Below it is a list of clues, each numbered and marked with a direction.

Grace and Ada spend a moment appreciating the culmination of their hard work: a machine-generated crossword puzzle ready to challenge its solver.

The generated PDF shows the crossword and the cues, image by the author.

The FedEx Demo

As the FedEx Day at QuantumQuirk dawned, Ada and Grace were buzzing with energy, ready to reveal their innovative creation. The city of Tallinn, with its mix of modern architecture and cozy establishments, hummed along with them.

Amidst the glassy structures, the beacon of QuantumQuirk stood, its heart beating with tech innovation and creativity.

Dressed in their professional best, they entered the company’s main conference room, which fosters collaborative and creative thinking. They had poured their passion and coding skills into their crossword puzzle generator, and it was time to share it with their fellow ‘Quirkers’ team.

The room filled with the murmur of excited chatter, quieting as the generated crossword puzzle appeared on the large screen. Grace started the presentation by describing how their project engaged with OpenAI API to create answers and clues.

Her words flowed smoothly, painting a vivid picture of their journey. In the meantime, Ada brought these words to life, skillfully maneuvering through the code on the screen and visually explaining the algorithm’s process.

The audience watched, their eyes glued to the screen as Ada demonstrated how the crossword grid filled with words. Every backtracking step, every shift, and word placement added to the suspense. The innovative interplay of Python, Pygame, and the OpenAI API became evident.

As they reached the climax, Ada triggered the PDFGenerator class. A ripple of anticipation swept through the room. After what felt like an eternity, the completed crossword puzzle with automatically generated clues materialized in a neat PDF on the screen.

The room held its breath for a moment before erupting in applause.

Grace and Ada shared a satisfied glance. The applause was a testament to their hard work, determination, and passion for innovation. They had turned an idea into reality and added another feather to QuantumQuirk’s creativity cap.

And beyond the applause and accolades, they had shown their fellow Quirkers that with a blend of creativity, skill, and AI power, even a simple concept like a crossword puzzle could transform into an innovative marvel. Indeed, it had been a FedEx Day to remember.

Eager to delve deeper into this world of crossword creation? The complete source code for this innovative project is readily accessible to you. You’ll find it housed in this GitHub repository.

Let it serve as your inspiration, guide, or stepping stone on your journey of coding and creation. Don’t just read about innovation — experience it, interact with it, and make it your own.

Happy coding!

References and Further Reading:

  • CS50’s Introduction to Artificial Intelligence with Python: Gain a more in-depth understanding of artificial intelligence and its application with Python. This course offers a comprehensive introduction to the world of AI. Find it here.
  • Constraint Satisfaction Problems in Artificial Intelligence: This concept is the backbone of our crossword puzzle generator. If you’re curious about understanding the theory behind the algorithm we’ve used, refer to this link.
  • A Beginner’s Guide to the Python time.sleep() Function: This article provides a basic understanding of the sleep() function we used to pause our script. Check it out here.
  • The Pygame Library: If you’re curious about the Pygame library and its various functionalities, explore more about it here.
  • ReportLab User Guide: This guide gives a complete overview of how to use ReportLab to create PDF files. It can be found here.
  • OpenAI API documentation: Gain a comprehensive understanding of the OpenAI API, which we used to generate clues for our crossword puzzle. Find the complete documentation here.

Remember, the more you immerse yourself in these resources, the better equipped you’ll be. Happy learning!

Python
OpenAI
Artificial Intelligence
Pygame
Machine Learning
Recommended from ReadMedium