Skip to content

Going Beyond PuzzleJS

Kenny Young edited this page Jan 24, 2025 · 1 revision

Useful snippets

PuzzleJS has been used extensively in Puzzleday 2023-4's puzzles, so there are many more examples there of how to use it. And Puzzleday has been web-first since 2021, so there are many solutions for how to do something that you could likely copy and paste into your puzzle to get you 90% of the way there. The entirety of the 2021-2024 events are stored in the old-event-libraries section of the pd2025-webification repo.

Drag and Drop

Here are some specific examples of functionality from previous puzzles that will likely be useful. First, drag and drop, as used in this puzzle: wingspan

And here's the relevant code:

<!DOCTYPE html>
<html lang="en-us">
<head>
    ...
    <script>
        var dragging = null;
        window.addEventListener("load", () => {
            const slots = document.querySelectorAll(".slot");
            for (const slot of slots) {
                if (slot.children.length > 0) {
                    slot.children[0].addEventListener("dragstart", (e) => {
                        e.dataTransfer.dropEffect = "move";
                        dragging = e.target;
                    });
                    slot.children[0].addEventListener("dragend", (e) => {
                        dragging = null;
                    });
                }
                slot.addEventListener("dragover", (e) => {
                    e.preventDefault();
                    e.dataTransfer.dropEffect = "move";
                });
                slot.addEventListener("drop", (e) => {
                    e.preventDefault();
                    if (e.target.classList.contains("slot")) {
                        dragging.parentElement.removeChild(dragging);
                        e.target.appendChild(dragging);
                    }
                });
            }
            ...
        });
    </script>
</head>
<body> 
<div class="page-div">
    ...
    <div class="content-div">
        ...
        <div class="wrapper">
            <div class="grid">
                <div class="row1 column1 online-only"><img class="key" src="images/forest.png" alt="forest" /></div>
                <div tabindex="0" class="slot row1 column2 online-only"></div>
                <div tabindex="0" class="slot row1 column3 online-only"></div>
                <div tabindex="0" class="slot row1 column4 online-only"></div>
                <div class="row1 column5 online-only"></div>
                <div tabindex="0" class="slot row1 column6"><img draggable="true" alt="A card with a picture of a bird on it" src="images/acorn-woodpecker.png"/></div>
                <div tabindex="0" class="slot row1 column7"><img draggable="true" alt="A card with a picture of a bird on it" src="images/blue-winged-warbler.png"/></div>
                <div tabindex="0" class="slot row1 column8"><img draggable="true" alt="A card with a picture of a bird on it" src="images/bobolink.png"/></div>
                <div class="row2 column1 online-only"><img class="key" src="images/meadow.png" alt="grassland" /></div>
                <div tabindex="0" class="slot row2 column2 online-only"></div>
                <div tabindex="0" class="slot row2 column3 online-only"></div>
                <div tabindex="0" class="slot row2 column4 online-only"></div>
                <div class="row2 column5 online-only"></div>
                <div tabindex="0" class="slot row2 column6"><img draggable="true" alt="A card with a picture of a bird on it" src="images/canada-goose.png"/></div>
                <div tabindex="0" class="slot row2 column7"><img draggable="true" alt="A card with a picture of a bird on it" src="images/gray-catbird.png"/></div>
                <div tabindex="0" class="slot row2 column8"><img draggable="true" alt="A card with a picture of a bird on it" src="images/king-rail.png"/></div>
                <div class="row3 column1 online-only"><img class="key" src="images/water.png" alt="wetland" /></div>
                <div tabindex="0" class="slot row3 column2 online-only"></div>
                <div tabindex="0" class="slot row3 column3 online-only"></div>
                <div tabindex="0" class="slot row3 column4 online-only"></div>
                <div class="row3 column5 online-only"></div>
                <div tabindex="0" class="slot row3 column6"><img draggable="true" alt="A card with a picture of a bird on it" src="images/missisippi-kite.png"/></div>
                <div tabindex="0" class="slot row3 column7"><img draggable="true" alt="A card with a picture of a bird on it" src="images/red-shouldered-hawk.png"/></div>
                <div tabindex="0" class="slot row3 column8"><img draggable="true" alt="A card with a picture of a bird on it" src="images/turkey-vulture.png"/></div>
            </div>
        </div>
    </div>
</div>
</body> 
</html>

Yup, that's all the code needed for basic drag and drop! The full puzzle also supports using the keyboard for accessibility. Structurally, the HTML is actually set up as a single Grid layout, despite appearing to the user to be two completely separate grids. That's just a CSS trick; the separation between the two visible grids is just a narrower column. Most of the CSS classes are used to describe the grid layout, but the "slot" class is actually used only by the Javascript!

Specifically, when the page loads, the Javascript loops through every slot, and it adds two event listeners: one for when you're dragging a card over it but haven't released the mouse, which just updates the visuals, and another for when you drop the card to remove it from the old slot and put it in the new one. It also adds two event listeners to every card, which you can see is just a direct child of some of the slots according to the HTML. Those listeners are for when you start a drag, which updates the visual and also saves which card you're moving into a temporary variable (that's the only way to safely remove it from the old slot when the drag is finished without accidentally deleting it), and for when you finish a drag to clear that variable.

Line Drawing

This puzzle uses SVG to draw lines between points that a user clicks on (including a preview of where the line will go before it's complete):
monopoly

And here's how it was done:

<html lang="en-us">
    ...
    <script>
        var lines = [];
        var saved = null;

        function getElementCenterX(element) {
            const rect = element.getBoundingClientRect();
            return rect.left + rect.width / 2 + window.pageXOffset;
        }

        function getElementCenterY(element) {
            const rect = element.getBoundingClientRect();
            return rect.top + rect.height / 2 + window.pageYOffset;
        }

        function createLine(fromId, toId) {
            const fromElement = document.getElementById(fromId);
            const toElement = document.getElementById(toId);
            const line = document.createElementNS("http://www.w3.org/2000/svg", "line");

            line.setAttribute("x1", getElementCenterX(fromElement));
            line.setAttribute("y1", getElementCenterY(fromElement));
            line.setAttribute("x2", getElementCenterX(toElement));
            line.setAttribute("y2", getElementCenterY(toElement));
            line.setAttribute("stroke", "black");

            return line;
        }

        function selectItem(li) {
            var id = li.querySelectorAll("a")[0].id;
            if (saved === null) {
                saved = id; // start the draw
            }
            else if (saved == id) {
                saved = null; // cancel the draw
            }
            else {
                // finish the draw and reset saved
                lines.push([saved, id]);
                saved = null;
            }

            redrawLines();
        }

        function redrawLines(tempId = null) {
            // Clear the svg layer 
            const svgLayer = document.getElementById("svg-overlay");
            svgLayer.innerHTML = "";

            // Draw all the permanent lines
            for (let i = 0; i < lines.length; i++) {
                line = createLine(lines[i][0], lines[i][1]);
                svgLayer.appendChild(line);
            }

            // Draw the temporary line if necessary
            if (tempId != null && tempId != saved) {
                line = createLine(saved, tempId);
                line.setAttribute("stroke", "red");
                svgLayer.appendChild(line);
            }

            // Highlight the currently selected entry
            lis = document.querySelectorAll("li.item");
            for (let i = 0; i < lis.length; i++) {
                if (saved !== null && lis[i].querySelector("#" + saved)) lis[i].style.color = "red";
                else lis[i].style.color = "black";
            }
        }

        window.onload = function() {
            var itemlis = document.querySelectorAll("li.item");
            for (let i = 0; i < itemlis.length; i++) {
                // For the lis in the two columns
                itemlis[i].addEventListener("click", (e) => {
                    selectItem(itemlis[i]);
                });
                itemlis[i].addEventListener("mouseenter", (e) => {
                    if (saved !== null) redrawLines(itemlis[i].querySelectorAll("a")[0].id);
                });
                itemlis[i].addEventListener("mouseleave", (e) => {
                    if (saved !== null) redrawLines(); // No temp connection
                });
            }
        }
    </script>
</head>
<body>
...
    <div class="content-div">
        ...
        <div class="table-container">
            <table>
              <tr>
                <td class="left-column">
                    <ul title="Left Column">
                        <li class="item" tabindex="0">Ailuro- <a id="a1">&#x2022;</a></li>
                        <li class="item">Astro- <a id="a2">&#x2022;</a></li>
                        <li class="item">Biblio- <a id="a3">&#x2022;</a></li>
                        <li class="item">Helio- <a id="a4">&#x2022;</a></li>
                        <li class="item">Hippo- <a id="a5">&#x2022;</a></li>
                        <li class="item">Ornitho- <a id="a6">&#x2022;</a></li>
                        <li class="item">Pyro- <a id="a7">&#x2022;</a></li>
                        <li class="item">Thalasso- <a id="a8">&#x2022;</a></li>
                        <li class="item">Xeno- <a id="a9">&#x2022;</a></li>
                    </ul>
                </td>
                <td class="middle-column"><img src="Letters.png" alt="This is a grid of scattered Greek letters. This portion of the puzzle will require a sighted teammate."/></td>
                <td class="right-column">
                    <ul title="Right Column">
                        <li class="item" tabindex="0"><a id="b1">&#x2022;</a> -drome</li>
                        <li class="item"><a id="b2">&#x2022;</a> -latry</li>
                        <li class="item"><a id="b3">&#x2022;</a> -machy</li>
                        <li class="item"><a id="b4">&#x2022;</a> -nomics</li>
                        <li class="item"><a id="b5">&#x2022;</a> -phagy</li>
                        <li class="item"><a id="b6">&#x2022;</a> -polis</li>
                        <li class="item"><a id="b7">&#x2022;</a> -pter</li>
                        <li class="item"><a id="b8">&#x2022;</a> -saur</li>
                        <li class="item"><a id="b9">&#x2022;</a> -thermic</li>
                    </ul>
                </td>
              </tr>
            </table>
        </div> <!-- table-container -->
        ...
<svg class="online-only" id="svg-overlay" width="10000" height="10000"></svg>
</body>
</html>

SVG is a markup language structured the same as HTML, except SVG is used to create images instead of webpages. These sorts of images can be zoomed in and scaled up infinitely without ever losing detail or resolution because they aren't saving individual pixel data; instead, they're essentially a bunch of instructions for how to draw that image. SVG is therefore very useful for constructing images programmatically by having Javascript create all the necessary SVG elements and put them together.

The text on the sides are made out of lists, which is how the browser can easily tell when something is hovering over it. In fact, Javascript isn't even necessary for that part! CSS includes "pseudo-classes", which are pre-defined classes based on an element's current state. There are classes for when an element is in focus (often useful for accessibility reasons), if it's the first or last child of its parent, and many more. In this case, it's using the hover class to make the text bold when it's being hovered over. It also changes the mouse cursor to the hand.

The image in the middle is just a regular image, but superimposed on top of all of this is an initially blank svg element. The full puzzle contains extra functionality than is shown here, including support for keyboarding, and automatic resizing of the lines the user draws if the window resizes, which is important since the lines are using coordinates based on specific positions on the page, and a change in window size may change the coordinates where elements of the page are.

The actual drawing is also done a little differently than how it appears based on user interactions. Starting at the top of the code, the first 3 functions create a new SVG line element. The line's exact position is based on the location of the items that were clicked on, since HTML and Javascript have the ability to know exactly where on a page something is. That line is just code at this point though; it doesn't actually show up on the screen yet.

The next function gets us a little closer though. This function uses the "saved" variable to keep track of the current line drawing state. If nothing is clicked on, then nothing is saved. If something has already been clicked on and we click it again, we cancel the draw. And if something has already been clicked on and we click something else, we save those two points as a pair in the "lines" variable. That variable is actually a list of all the lines that have been drawn (how do we undraw a line? It doesn't look like the code lets you as shown here since nothing ever removes pairs from that list!)

Finally, at the end of that function, we call the next function to redraw all lines. This function actually clears out everything that was previously drawn on the SVG, loops through the list of lines, and then for every pair of points in that list, uses the previously unused function to create a new SVG line and then adds that line to the main SVG element. It does this every time a new line is drawn. You'll notice that it also does something fancy and highlights the preview of the in-progress line in red (it determines this by seeing which point is currently using the "saved" variable for the in-progress line) as well as the list item it started from.

Finally, when the page first loads, we have to add all these click and hover functions to the list items (although in this case, because we need to also do something when the mouse stops hovering, we use the mouseenter and mouseleave functions). A keen eye will notice that there's actually a tiny bullet point that's a child of every list item and that's the actual element used to determine the line drawing coordinates. It's a very clever way to structure the HTML to make the line drawing look much cleaner, since no matter where you click on the text, the line will always instead be drawn from that tiny bullet point. You may also notice that there's absolutely no differentiation between points on different sides of the image; that means that you're perfectly capable of drawing a line between two points on the same side.

Cursor Control

Another thing you may want to do is limit players to only a single character per text box, but then have the cursor automatically move to the next box so that it acts just like they're typing into a limitless field:
hearts

<head>
...
    <script>
        // Move cursor automatically between fields
        document.addEventListener("DOMContentLoaded", () => {
            let blanks = document.getElementsByTagName("input");
            for (let blank of blanks) {
                blank.addEventListener("keydown", (e) => {
                    if ((e.key == "Backspace") || (e.key == "Delete") || (e.key == "ArrowLeft") || (e.key == "ArrowRight")) {
                        e.preventDefault();
                    }
                    else if (blank.value.length == 1) {
                        findNextBlank(blank, 1);
                    }
                });
                blank.addEventListener("keyup", (e) => { 
                    if ((e.key == "Backspace") && (blank.value.length == 0)) {
                        findPreviousBlank(blank, 1);
                    }
                    else if ((e.key == "Backspace") && (blank.value.length == 1) && (blank.selectionStart == 1)) {
                        blank.value = "";
                    }
                    else if ((e.key == "Backspace") && (blank.value.length == 1) && (blank.selectionStart == 0)) {
                        findPreviousBlank(blank, 1);
                    }
                    else if ((e.key == "Delete") && (blank.value.length == 0)) {
                        findNextBlank(blank, 0);
                    }
                    else if ((e.key == "Delete") && (blank.value.length == 1) && (blank.selectionStart == 0)) {
                        blank.value = "";
                    }
                    else if ((e.key == "Delete") && (blank.value.length == 1) && (blank.selectionStart == 1)) {
                        findNextBlank(blank, 0);
                    }
                    else if ((e.key == "ArrowLeft") && (blank.value.length == 0)) { 
                        findPreviousBlank(blank, 1);
                    }
                    else if ((e.key == "ArrowLeft") && (blank.value.length == 1) && (blank.selectionStart == 1)) {
                        blank.selectionStart = 0;
                        blank.selectionEnd = 0;
                    }
                    else if ((e.key == "ArrowLeft") && (blank.value.length == 1) && (blank.selectionStart == 0)) {
                        findPreviousBlank(blank, 1);
                    }
                    else if ((e.key == "ArrowRight") && (blank.value.length == 0)) {
                        findNextBlank(blank, 1);
                    }
                    else if ((e.key == "ArrowRight") && (blank.value.length == 1) && (blank.selectionStart == 0)) {
                        blank.selectionStart = 1;
                        blank.selectionEnd = 1;
                    }
                    else if ((e.key == "ArrowRight") && (blank.value.length == 1) && (blank.selectionStart == 1)) {
                        findNextBlank(blank, 1);
                    }
                    else if (blank.value.length == 1) {
                        findNextBlank(blank, 1);
                    }
                });
            }
            function findPreviousBlank (currentBlank, position) {
                if (currentBlank.previousElementSibling !== null) {
                    currentBlank.previousElementSibling.focus()
                    currentBlank.previousElementSibling.selectionStart = position; 
                }
                else { // If we're in the last word of the set
                    if (currentBlank.parentElement.classList == currentBlank.parentElement.parentElement.lastElementChild.classList) {
                        const previousBlank = currentBlank.parentElement.parentElement.firstElementChild.lastElementChild;
                        previousBlank.focus();
                        previousBlank.selectionStart = position; 
                    }
                }
            }
            function findNextBlank (currentBlank, position) {
                if (currentBlank.nextElementSibling !== null) {
                    currentBlank.nextElementSibling.focus();
                    currentBlank.nextElementSibling.selectionStart = position; 
                }
                else { // If we're in the first word of the set
                    if (currentBlank.parentElement.classList == currentBlank.parentElement.parentElement.firstElementChild.classList) {
                        const nextBlank = currentBlank.parentElement.parentElement.lastElementChild.firstElementChild;
                        nextBlank.focus();
                        nextBlank.selectionStart = position; 
                    }
                }
            }
        });
    </script>
</head>
<body>
<div class="page-div">
    ...
        <ol>
            <li>When Snow White's friends get into a fight, it's a 
                <span class="word input1-1">
                    <input class="blank" autocomplete="off" maxlength="1"></input>&nbsp;
                    <input class="blank" autocomplete="off" maxlength="1"></input>&nbsp;
                    <input class="blank" autocomplete="off" maxlength="1"></input>&nbsp;
                    <input class="blank" autocomplete="off" maxlength="1"></input>&nbsp;
                    <input class="blank" autocomplete="off" maxlength="1"></input>
                </span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
                <span class="word input1-2">
                    <input class="heart" autocomplete="off" maxlength="1"></input>&nbsp;
                    <input class="heart" autocomplete="off" maxlength="1"></input>&nbsp;
                    <input class="heart" autocomplete="off" maxlength="1"></input>
                </span>.
            </li>
    ...

This might actually be more complicated than you were expecting! Part of that is because we're actually overwriting the default behavior of some of the keyboard keys, so we need to account for every possibility that would normally be handled automatically, and part of it is because we've broken up this series of inputs into two separate spans, since they're two different words. On the plus side, that means they can wrap onto the next line easily, but it does unfortunately mean we need to have extra logic for the cursor to jump between the two spans.

Conceptually this is pretty simple though: we're only using two events (keyup and keydown), and the only special keys we're handling ourselves are backspace, delete, and the arrow keys to move back and forth. The maximum input length is actually enforced in the HTML, so the Javascript can safely assume that it will only ever have 0 or 1 character in the box at any time. For users, this means that if you're at the end of a sentence and you keep typing, then that typing will just be ignored because there's nowhere else to put it.

The spacing between the inputs is our first example of an escape character, in this case for a blank space. Normally, HTML collapses all white space in source files down to a single visible space, even if you had tons of blank lines in your file. This specific escape character is for a "non-breaking space", which won't be collapsed if multiple are used in a row, which you can see is exactly how we put the space between the two input sections. It's not the most elegant solution, but it works!

In the keydown function, we prevent the default behavior for the special keys using a function aptly called "preventDefault". Otherwise, we check to see if there's already a character in the box we're trying to type in and will move the cursor to the next box if there is one. The custom functions to find the next available box make a lot of assumptions, mainly that that box will be empty! If it's not, the cursor will move to the next box after that one thanks to another call to find the next box in the keyup function, but that's it, so if more than 1 box is filled after the one that's typed in, the user will have to do more work to move the cursor to the next available empty box.

And in the meantime, the existing characters in the box will be preserved if the user tries to type in a filled box, which could be desirable, but also could NOT be, depending on the user. Another behavior that isn't implemented is what happens if the backspace key is held down; to delete multiple characters the user has to hit the key multiple times, even though holding a letter key will fill it in ALL the boxes since the letter keys still have their default behavior. There's also no special consideration for modifier keys, like holding shift, when handling the special keys, since that will sometimes reverse the direction of a user's actions.

One thing that might be surprising when looking at the keydown and keyup functions is that, because the default behavior is only overwritten for a few keys, that those few keys are the ONLY things mentioned in those functions! That means that when typing normal letters, you might think those functions do nothing, but that's not the case. Since every key typed fires BOTH a keydown and keyup event, the keydown is what puts the letter in the box, but the keyup function is actually responsible for moving the cursor since the box will now be filled. Cursor placement within the box is also handled in a slightly less straightforward-than-expected way by using a function responsible for selections, since there's no function available just for cursor placement.

Printing

Even though the whole point of making puzzles into webpages is to reduce the need for users to print them, it's not a bad idea to make sure that if somebody WANTS to print, that it will still look nice: nothing runs off the side of the page, it takes up a reasonable number of pages, etc. Setting some reasonable CSS rules makes a good start:

@page {
  margin: 0in;
}

.header-div {
  position: absolute;
  display: grid;
  grid-template-columns: 540px 200px;
}

h1 {
  font-size: 28px;
  margin-left: 0.5in;
  margin-top: 10px;
  text-align: left;
  text-transform: uppercase;
  grid-column: 1;
}

.byline-div {
  font-size: 14px;
  line-height: 48px;
  margin: 0px;
  text-align: right;
  grid-column: 2;
}

.content-div {
  padding: 1in 0.5in 0in 0.5in;
  border: none;
}

For the puzzle template used on the puzzles above, you can see that we're setting our own page margin for the printer, but then making sure the puzzle content is pushed away from the sides enough that it won't get cut off. These rules are used INSTEAD of ones for the screen if they're either linked with a media attribute like <link rel="stylesheet" href="../../resources/puzzle-print-styles.css" media="print"> or are put inside an @media print {} rule in your CSS code, as seen below. The title is also placed slightly different on printed puzzles than on the screen: it's put at the top of the page on the left, with the author's name to the right. It shows the power of those layout CSS properties!

This puzzle has a two column layout on the web, with some interactivity as evidenced by the buttons: image

In addition to the CSS rules mentioned above that all of these puzzles use to style the template (as well as others omitted because they aren't relevant to this example), this puzzle has rules for styling its specific content as well:

<html lang="en-us">
<meta charset="utf-8" />
<head>
    ...
    <style>
        ...
        .island {
            background-color: yellow;
        }
        @media print {
            ...
            h4 {
                page-break-before: always;
            }
            .island {
                background-color: #FFFFFF;
            }
            @page {
                margin-top: 1in;
            }
            @page:first {
                margin-top: 0in;
            }
            .page-num {
                position: absolute;
                font-size: 12px;
                text-align: right;
                bottom: 50;
                right: 50;
            }
            .page2 {
                bottom: -910;
            }
        }
    </style>
</head>
<body>
<div class="page-div">
    <div class="header-div">
        ...
    </div>
    <div class="content-div">
        <div class="direction-area">
            <div class="direction-text">
                <p>Help Pikachu...
            </div>
            <table>
                ...
            </table>
            <br/ class="online-only">
            <div class="center online-only">
                <input id="gridbutton" class="online-only" type="button" value="Turn on region lines" data-status="off"></input>
                <br/ class="online-only">
                <br/ class="online-only">
                <br/ class="online-only">
                <input id="undobutton" class="online-only" type="button" value="Undo"></input>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
                <input id="redobutton" class="online-only" type="button" value="Redo"></input>
            </div>
        </div>
        <br/ class="online-only">
        <div class="print-only page-num">Page 1 of 2</div>
        <h4></h4>
        
        <table>
            ...
        </table>
        <div class="print-only page-num page2">Page 2 of 2</div>
    </div>
</div>
</body>
</html>

And when printed, it ends up like this: image

The first thing you'll notice is that, just like there are things that don't appear when printed, there is also a message that doesn't appear online, thanks to a mirrored CSS rule in an @media screen {} block. There's also been some tweaking done to the size of the text and the play grid on the second page, as well as the removal of the colored background in the play grid. Background removal isn't just good for saving ink, it also increases readability because of the increased contrast.

The next interesting thing is that the first page has a different margin than subsequent ones in order to put the title in the first page's margin. This is trivial to do thanks to a pseudo-class. There's also an easy way to specify exactly where the next page should start thanks to the page-break-before property. In this case, it's set on an element that isn't used for anything else in the puzzle. The page numbers are manually placed just like the title, and unfortunately don't enumerate automatically, although with only a couple of pages, it's luckily not that hard to keep track of.

Example puzzles

Now that we've seen quite a few puzzles, let's think through how we'd go about constructing them from scratch.

Picture This

<!DOCTYPE html>
<html lang="en-us">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Picture This - Microsoft Intern Puzzleday 2023</title>
    <link rel="stylesheet" href="https://puzzlehunt.azurewebsites.net/css/pd2023/base-styles.css">
    <link rel="stylesheet" href="https://puzzlehunt.azurewebsites.net/css/pd2023/print-styles.css" media="print">
    <script type="text/javascript" src="https://puzzlehunt.azurewebsites.net/js/pd2023/resize.js"></script>
    <link rel="stylesheet" href="../../resources/puzzle.css"/>
    <script type="text/javascript" src="../../resources/puzzle.js"></script>
    <style>
        .puzzle-entry .cell { height: 133px; width: 130px; }
        .puzzle-entry .cell.used { background-color: #ff00ff40; }
        .puzzle-entry .cell use { display: none; }
        .center { text-align: center; }
        .wrapper { display: inline-block; }
        .puzzle { 
            margin: 0px; 
            width: 780px; 
            height: 800px;
            background-image: url(picture-this.png); 
            background-size: contain; 
            background-repeat: no-repeat
        }
        @media print { .header-div div.has-dc { margin-top: 13px; margin-right: 6px; } p { font-size: 13px; }}
    </style>
</head>
<body>
<div class="page-div">
    <div class="header-div">
        <h1>
            Picture This
            <div class="byline-div">
                Dana Young
                
                <div class="has-dc" title="Data Confirmation checks are available for this puzzle."></div>
            </div>
        </h1>

        <div class="divider-div">
        </div>
    </div>
    <div class="content-div">
        <p>12 tabletop games are phonetically hidden in the grid below.</p>
        <p>Each picture represents one syllable; some pictures are used more than once.</p>
        <p>Each game is 2 or more pictures in length, and in a straight line horizontal, vertical, diagonal, forwards, or backwards.</p>
        <p>The answer is a 13th game, made from the leftover pictures in reading order.</p>
        <div class="center">
            <div class="wrapper">
                <div class="puzzle">
                    <div class="puzzle-entry" data-show-commands="true" data-mode="notext" data-text="6x6" data-fill-classes="unused used"></div>
                </div>
            </div>
        </div>
        <br />
        <br />
    </div>
</div>
</body>
</html>

First of all, you can see that we've no longer omitted any of the template items, so there's a lot more stuff in the head element. The only ones you really need to worry about are the title, style, and script if there is any, which for this puzzle there's not, besides the linked ones. The CSS files for the screen and print templates, the resize script that's used when embedding the puzzle on our site, and the PuzzleJS files will be the same for every puzzle, other than their paths, which for future events will all be relative links like the PuzzleJS files because they'll all live in the Shared Resources.

The style for this puzzle is pretty simple besides what we've already gone over. The grid cells are sized to line up with the image, and special care was taken to line up the Data Confirmation icon when printing, likely because of a bug or deficiency with the print template. If we had instead just used individual img elements inside each of the table cells (or background-image properties on each cell), it not only would have been more code, it also would have been more work to prepare the bunch of individual images.

Tic-Tac-Toe

How about the Tic-Tac-Toe puzzle? This is what it looks like in its almost-entirety: image

<!DOCTYPE html>
<html lang="en-us">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!--change title-->
    <title>Tic-Tac-Toe - Microsoft Intern Puzzleday 2023</title>
    <link rel="stylesheet" href="https://puzzlehunt.azurewebsites.net/css/pd2023/base-styles.css">
    <link rel="stylesheet" href="https://puzzlehunt.azurewebsites.net/css/pd2023/print-styles.css" media="print">
    <script type="text/javascript" src="https://puzzlehunt.azurewebsites.net/js/pd2023/resize.js"></script>
    <!--add necessary page-specific styles-->
    <style>
        .columns {
            display: grid;
            grid-template-columns: 0.25fr 0.25fr 0.25fr 0.25fr;
            gap: 5px;
        }
        .column {
            text-align: center;
            min-width: 280px;
        }
        table {
            border-collapse: collapse;
        }
        td {
            width: 30px;
            height: 30px;
            border: 0px solid black;
            font-size: 20px;
            text-align: center;
            vertical-align: middle;
            user-select: none;
        }
        td.b-top {
            border-top: 1px solid black;
        }
        td.b-right {
            border-right: 1px solid black;
        }
        td.b-bottom {
            border-bottom: 1px solid black;
        }
        td.b-left {
            border-left: 1px solid black;
        }
        td.highlighted {
            background-color: yellow;
        }
        .instruction {
            display: grid;
            grid-template-rows: 75px 75px;
            gap: 10px;
        }
        .text {
            text-align: left;
        }
        .dot {
            background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2230%22%20height%3D%2230%22%20version%3D%221.1%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20cx%3D%2215%22%20cy%3D%2215%22%20r%3D%223%22%20stroke%3D%22black%22%20fill%3D%22black%22%20stroke-width%3D%221%22%2F%3E%0A%3C%2Fsvg%3E");
            background-repeat: no-repeat;
            background-size: cover;
        }
        input {
            border: 0px solid black;
            width: 28px;
            height: 28px;
            text-align: center;
            font-size: 20px;
            text-transform: uppercase;
            cursor: pointer;
            color: magenta;
        }
        input:focus {
            outline: none;
        }
        @media print {
            .instruction {
                font-size: 12px;
            }
            .column {
                min-width: 180px;
            }
        }
    </style>
    <script>
        // Prompt before leaving the page to prevent data loss
        window.addEventListener("beforeunload", (e) => {
            e.preventDefault();
            e.returnValue = "";
        });
    </script>
</head>
<body>
<div class="page-div">
    <div class="header-div">
        <!--change h1-->
        <h1>
            Tic-Tac-Toe
            <!--change div content-->
            <div class="byline-div">
                Jacob Harmon
            </div>
        </h1>

        <div class="divider-div">
        </div>
    </div>
    <!--change div content-->
    <div class="content-div">
        <div>Help X make the best next move. Their goal is to win, and not to lose! <span class="online-only">This puzzle will not save your state if you leave it, but it will warn you before you leave.</span></div>
        <h3>Rules of Tic-Tac-Toe:</h3>
        <div class="columns">
            <div class="column">
                <div class="instruction">
                    <div class="text"></div>
                    <div class="table">
                        <table>
                            ...
                        </table>
                    </div>
                </div>
            </div>
            <div class="column">
                <div class="instruction">
                    <div class="text">Players X and O take turns adding their symbols to empty spaces in the grid.</div>
                    <div class="table">
                        <table>
                            ...
                        </table>
                    </div>
                </div>
            </div>
            <div class="column">
                <div class="instruction">
                    <div class="text">When one player gets 3 of their symbols in a row (horizontally, vertically, or diagonally), they win.</div>
                    <div class="table">
                        <table>
                            ...
                        </table>
                    </div>
                </div>
            </div>
            <div class="column">
                <div class="instruction">
                    <div class="text">If the grid is filled without either player getting 3 in a row, the game is a draw.</div>
                    <div class="table">
                        <table>
                            ...
                        </table>
                    </div>
                </div>
            </div>
        </div>
        <br />
        <h3>Puzzle:</h3>
        <div class="columns">
            <div class="column">
                <table>
                    <tr>
                        <td class="b-right b-bottom"><input autocomplete="off" maxlength="1"></input></td>
                        <td class="b-right b-bottom b-left"><input class="dot" autocomplete="off" maxlength="1"></input></td>
                        <td class="b-bottom b-left">O</td>
                    </tr>
                    <tr>
                        <td class="b-top b-right b-bottom"><input autocomplete="off" maxlength="1"></input></td>
                        <td class="b-top b-right b-bottom b-left">O</td>
                        <td class="b-top b-bottom b-left"><input autocomplete="off" maxlength="1"></input></td>
                    </tr>
                    <tr>
                        <td class="b-top b-right"><input class="dot" autocomplete="off" maxlength="1"></input></td>
                        <td class="b-top b-right b-left">X</td>
                        <td class="b-top b-left">X</td>
                    </tr>
                </table>
                <br />
                <br />
                <table>
                    ...
                </table>
                <br />
                <br />
                <table>
                    ...
                </table>
                <br />
                <br />
                <table>
                    ...
                </table>
            </div>
            <div class="column">
                <table>
                    <tr>
                        <td class="b-right b-bottom">X</td>
                        <td class="b-right b-bottom b-left"><input autocomplete="off" maxlength="1"></input></td>
                        <td class="b-bottom b-left"><input class="dot" autocomplete="off" maxlength="1"></input></td>
                    </tr>
                    <tr>
                        <td class="b-top b-right b-bottom">O</td>
                        <td class="b-top b-right b-bottom b-left">X</td>
                        <td class="b-top b-bottom b-left"><input autocomplete="off" maxlength="1"></input></td>
                    </tr>
                    <tr>
                        <td class="b-top b-right">O</td>
                        <td class="b-top b-right b-left"><input class="dot" autocomplete="off" maxlength="1"></input></td>
                        <td class="b-top b-left"><input autocomplete="off" maxlength="1"></input></td>
                    </tr>
                </table>
                <br />
                <br />
                <table>
                    ...
                </table>
                <br />
                <br />
                <table>
                    ...
                </table>
                <br />
                <br />
                <table>
                    ...
                </table>
            </div>
            <div class="column">
                <table>
                    <tr>
                        <td class="b-right b-bottom">X</td>
                        <td class="b-right b-bottom b-left"><input autocomplete="off" maxlength="1"></input></td>
                        <td class="b-bottom b-left">O</td>
                    </tr>
                    <tr>
                        <td class="b-top b-right b-bottom">X</td>
                        <td class="b-top b-right b-bottom b-left"><input class="dot" autocomplete="off" maxlength="1"></input></td>
                        <td class="b-top b-bottom b-left">O</td>
                    </tr>
                    <tr>
                        <td class="b-top b-right"><input autocomplete="off" maxlength="1"></input></td>
                        <td class="b-top b-right b-left"><input autocomplete="off" maxlength="1"></input></td>
                        <td class="b-top b-left"><input class="dot" autocomplete="off" maxlength="1"></input></td>
                    </tr>
                </table>
                <br />
                <br />
                <table>
                    ...
                </table>
                <br />
                <br />
                <table>
                    ...
                </table>
                <br />
                <br />
                <table>
                    ...
                </table>
            </div>
            <div class="column">
                <table>
                    <tr>
                        <td class="b-right b-bottom"><input autocomplete="off" maxlength="1"></input></td>
                        <td class="b-right b-bottom b-left">X</td>
                        <td class="b-bottom b-left">O</td>
                    </tr>
                    <tr>
                        <td class="b-top b-right b-bottom"><input autocomplete="off" maxlength="1"></input></td>
                        <td class="b-top b-right b-bottom b-left">O</td>
                        <td class="b-top b-bottom b-left">X</td>
                    </tr>
                    <tr>
                        <td class="b-top b-right"><input class="dot" autocomplete="off" maxlength="1"></input></td>
                        <td class="b-top b-right b-left"><input autocomplete="off" maxlength="1"></input></td>
                        <td class="b-top b-left"><input class="dot" autocomplete="off" maxlength="1"></input></td>
                    </tr>
                </table>
                <br />
                <br />
                <table>
                    ...
                </table>
                <br />
                <br />
                <table>
                    ...
                </table>
                <br />
                <br />
                <table>
                    ...
                </table>
            </div>
        </div>
    </div>
</div>
</body>
</html>

The table contents aren't important since they're just part of the puzzle, but a few of them are left just to see how the styling was done. First, you can see that this uses a pretty hard-coded column layout, so the columns won't shrink too much beyond what's shown here. The only reason for this was so that the text at the top wouldn't wrap onto too many lines, which then wasted space below it for the shorter instruction columns.

Each grid also could have been done with divs, but instead it was done using tables. Since tables have collapsible borders, that ended up looking better than thicker borders that divs would have. PuzzleJS also wasn't used here since at the time, it only supported borders on all 4 sides or a cell or none at all. One interesting detail is that the dots on the table aren't text bullet points. In order to allow typing on the same space as a dot, they were instead done using a background image. In this case, it's a simple filled circle using SVG, and the entirety of the SVG file is embedded right there in the CSS property!

This puzzle also has a bit of Javascript to prevent accidental page closes since it doesn't save the user's progress. Layout-wise, there's no reason for setting up the grids in a 4x4 pattern; as long as the order is maintained since reading the puzzles in reading order is crucial for the solving mechanic, they could have been done in a 2x8 or in 3 columns or whatever. This just happens to be a layout that also is very easy to fit on the printed page, and consequently was also basically identical to the layout the author had when the first draft of the puzzle was created in Word.

Scene It!

image

Scene It has a lot of content, but thanks to PuzzleJS, has relatively compact underlying code:

<!DOCTYPE html>
<html lang="en-us">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Scene It! - Microsoft Intern Puzzleday 2023</title>
    <link rel="stylesheet" href="https://puzzlehunt.azurewebsites.net/css/pd2023/base-styles.css">
    <link rel="stylesheet" href="https://puzzlehunt.azurewebsites.net/css/pd2023/print-styles.css" media="print">
    <script type="text/javascript" src="https://puzzlehunt.azurewebsites.net/js/pd2023/resize.js"></script>
    <link rel="stylesheet" href="../../resources/puzzle.css"/>
    <script type="text/javascript" src="../../resources/puzzle.js"></script>

    <!-- Page-specific styles and scripts -->
    <style>
        @media screen {.content-div { min-width: 1040px; }}
        ol { list-style-type: none; }
        li { text-align: center; margin-bottom: 50px; }
        video { margin-bottom: 10px; }
    </style>
    <script>
        var currentVideo = null;
        window.onload = function() {
            var videoElements = document.getElementsByTagName("video");
            for (let i = 0; i < videoElements.length; i++) {
                videoElements[i].addEventListener("play", function() {
                    for (let j = 0; j < videoElements.length; j++) {
                        if (i !== j && videoElements[j].paused === false) videoElements[j].pause();
                    }
                });
            }
        }
    </script>
</head>
<body>
<div class="page-div">
    <div class="header-div">
        <h1>
            Scene It!
            <div class="byline-div">
                Samantha Piekos
            </div>
        </h1>

        <div class="divider-div">
        </div>
    </div>
    <div class="content-div online-only">
        <ol>
            <li>
                <video controls height="360">
                    <source src="resources/01.mp4" type="video/mp4">
                    Your browser does not support the HTML video tag.
                </video>
                <div class="puzzle-entry" data-mode="linear" data-text=".#..... #. ... ......#.#" data-extracts="9 28 4 1"></div>
            </li>
            <li>
                <video controls height="266">
                    <source src="resources/02.mp4" type="video/mp4">
                    Your browser does not support the HTML video tag.
                </video>
                <div class="puzzle-entry" data-mode="linear" data-text="..#.. .#.. .#...#" data-extracts="30 26 19 13"></div>
            </li>
            <li>
                <video controls height="360">
                    <source src="resources/03.mp4" type="video/mp4">
                    Your browser does not support the HTML video tag.
                </video>
                <div class="puzzle-entry" data-mode="linear" data-text=".#.#. .#.." data-extracts="34 8 31"></div>
            </li>
            <li>
                <video controls height="360">
                    <source src="resources/04.mp4" type="video/mp4">
                    Your browser does not support the HTML video tag.
                </video>
                <div class="puzzle-entry" data-mode="linear" data-text=".#....# ..#." data-extracts="20 10 3"></div>
            </li>
            <li>
                <video controls height="360">
                    <source src="resources/05.mp4" type="video/mp4">
                    Your browser does not support the HTML video tag.
                </video>
                <div class="puzzle-entry" data-mode="linear" data-text="..#.. ...#..#" data-extracts="2 5 16"></div>
            </li>
            <li>
                <video controls height="272">
                    <source src="resources/06.mp4" type="video/mp4">
                    Your browser does not support the HTML video tag.
                </video>
                <div class="puzzle-entry" data-mode="linear" data-text=".#..... #.#." data-extracts="17 22 35"></div>
            </li>
            <li>
                <video controls height="360">
                    <source src="resources/07.mp4" type="video/mp4">
                    Your browser does not support the HTML video tag.
                </video>
                <div class="puzzle-entry" data-mode="linear" data-text="..# ........ #...: .#. #..." data-extracts="7 18 36 23"></div>
            </li>
            <li>
                <video controls height="360">
                    <source src="resources/08.mp4" type="video/mp4">
                    Your browser does not support the HTML video tag.
                </video>
                <div class="puzzle-entry" data-mode="linear" data-text=".#. #.....#.." data-extracts="11 15 6"></div>
            </li>
            <li>
                <video controls height="360">
                    <source src="resources/09.mp4" type="video/mp4">
                    Your browser does not support the HTML video tag.
                </video>
                <div class="puzzle-entry" data-mode="linear" data-text="#...#.#" data-extracts="27 21 24"></div>
            </li>
            <li>
                <video controls height="310">
                    <source src="resources/10.mp4" type="video/mp4">
                    Your browser does not support the HTML video tag.
                </video>
                <div class="puzzle-entry" data-mode="linear" data-text="..#. #..#..." data-extracts="32 29 14"></div>
            </li>
            <li>
                <video controls height="360">
                    <source src="resources/11.mp4" type="video/mp4">
                    Your browser does not support the HTML video tag.
                </video>
                <div class="puzzle-entry" data-mode="linear" data-text="# ... .#....#." data-extracts="25 12 33"></div>
            </li>
        </ol>

        <div class="puzzle-entry" data-mode="linear" data-text="##################" data-extracts="1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18"></div>
        <div class="puzzle-entry" data-mode="linear" data-text="##################" data-extracts="19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36"></div>
    </div> <!-- content-div -->
    <div class="content-div print-only">
        This puzzle contains videos that are only available online.
    </div>
</div>
</body>
</html>

One of the first things to notice is that there's some Javascript! Strictly speaking, it's not needed, but it's a really nice quality of life feature that pauses any other playing video when the user clicks one to start it playing. You'll also see that when printed, this puzzle doesn't waste space printing anything. It would have been perfectly reasonable to just print the text boxes, but since the user has to view this puzzle online anyway because of the videos, it's also perfectly reasonable to require them to just completely solve it online.

You can also see a fallback inside each video tag; that text will appear if the video can't load for some reason. These days, the only major reason would be if they were using a VERY old browser from the 90's or something, but fallbacks like that are still good practice. The fact that each part of the puzzle is put inside a list instead of just a div is completely up to the choice of the author; neither is better. At the bottom, the fact that the final extraction breaks onto two lines is very nice though; while we don't expect most puzzles to look as good on a narrow mobile phone as on a regular screen, it's still good practice not to make things too wide since people have a hard time reading very long lines in general.

Yahtzee

Finally, here's a puzzle that had quite a few options for how it could have been webified: image

<!DOCTYPE html>
<html lang="en-us">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Yahtzee - Microsoft Intern Puzzleday 2023</title>
    <link rel="stylesheet" href="https://puzzlehunt.azurewebsites.net/css/pd2023/base-styles.css">
    <link rel="stylesheet" href="https://puzzlehunt.azurewebsites.net/css/pd2023/print-styles.css" media="print">
    <script type="text/javascript" src="https://puzzlehunt.azurewebsites.net/js/pd2023/resize.js"></script>
    <!--add necessary page-specific styles-->
    <style>
        .content-div {
            min-width: 750px;
        }
        table {
            border: none;
        }
        td {
            border: none;
            padding: 20px 5px 20px 5px;
        }
        .operator {
            font-size: 20px;
            font-weight: bold;
        }
    </style>
</head>
<body>
<div class="page-div">
    <div class="header-div">
        <!--change h1-->
        <h1>
            Yahtzee
            <!--change div content-->
            <div class="byline-div">
                Jason Rajtar
            </div>
        </h1>

        <div class="divider-div">
        </div>
    </div>
    <!--change div content-->
    <div class="content-div">
        <table>
            <tr>
                <td>
                    <img src="images/1DC.png" alt="Dice in a diamond shape with 1 pip in the center" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">+</div>
                </td>
                <td>
                    <img src="images/2DV.png" alt="Dice in a diamond shape with 2 pips on the top and bottom" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">&ndash;</div>
                </td>
                <td>
                    <img src="images/4DC.png" alt="Dice in a diamond shape with 4 pips in each corner" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">+</div>
                </td>
                <td>
                    <img src="images/6DR.png" alt="Dice in a diamond shape with 6 pips in two lines going up and to the right" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">&ndash;</div>
                </td>
                <td>
                    <img src="images/2DH.png" alt="Dice in a diamond shape with 2 pips on the left and right" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">=</div>
                </td>
                <td>
                    <img src="images/blank.png" alt="A blank space" width="100" height="100" />
                </td>
            </tr>
            <tr>
                <td>
                    <img src="images/5SX.png" alt="Dice in a square shape with 5 pips in an X pattern" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">+</div>
                </td>
                <td>
                    <img src="images/3SR.png" alt="Dice in a square shape with 3 pips in a line going up and to the right" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">+</div>
                </td>
                <td>
                    <img src="images/6SV.png" alt="Dice in a square shape with 6 pips in two vertical columns" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">&ndash;</div>
                </td>
                <td>
                    <img src="images/2SL.png" alt="Dice in a square shape with 2 pips in the top left and bottom right corners" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">+</div>
                </td>
                <td>
                    <img src="images/6SV.png" alt="Dice in a square shape with 6 pips in two vertical columns" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">=</div>
                </td>
                <td>
                    <img src="images/blank.png" alt="A blank space" width="100" height="100" />
                </td>
            </tr>
            <tr>
                <td>
                    <img src="images/6DR.png" alt="Dice in a diamond shape with 6 pips in two lines going up and to the right" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">&ndash;</div>
                </td>
                <td>
                    <img src="images/5DP.png" alt="Dice in a diamond shape with 5 pips in a plus pattern" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">&ndash;</div>
                </td>
                <td>
                    <img src="images/5DP.png" alt="Dice in a diamond shape with 5 pips in a plus pattern" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">+</div>
                </td>
                <td>
                    <img src="images/6DL.png" alt="Dice in a diamond shape with 6 pips in two lines going up and to the left" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">&ndash;</div>
                </td>
                <td>
                    <img src="images/1DC.png" alt="Dice in a diamond shape with 1 pip in the center" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">=</div>
                </td>
                <td>
                    <img src="images/blank.png" alt="A blank space" width="100" height="100" />
                </td>
            </tr>
            <tr>
                <td>
                    <img src="images/3SR.png" alt="Dice in a square shape with 3 pips in a line going up and to the right" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">+</div>
                </td>
                <td>
                    <img src="images/6SV.png" alt="Dice in a square shape with 6 pips in two vertical columns" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">&ndash;</div>
                </td>
                <td>
                    <img src="images/3SL.png" alt="Dice in a square shape with 3 pips in a line going up and to the left" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">+</div>
                </td>
                <td>
                    <img src="images/4SC.png" alt="Dice in a square shape with 4 pips in each corner" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">+</div>
                </td>
                <td>
                    <img src="images/6SH.png" alt="Dice in a square shape with 6 pips in two horizontal rows" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">=</div>
                </td>
                <td>
                    <img src="images/blank.png" alt="A blank space" width="100" height="100" />
                </td>
            </tr>
            <tr>
                <td>
                    <img src="images/5DP.png" alt="Dice in a diamond shape with 5 pips in a plus pattern" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">+</div>
                </td>
                <td>
                    <img src="images/5DP.png" alt="Dice in a diamond shape with 5 pips in a plus pattern" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">+</div>
                </td>
                <td>
                    <img src="images/5DP.png" alt="Dice in a diamond shape with 5 pips in a plus pattern" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">+</div>
                </td>
                <td>
                    <img src="images/5DP.png" alt="Dice in a diamond shape with 5 pips in a plus pattern" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">&ndash;</div>
                </td>
                <td>
                    <img src="images/1DT.png" alt="Dice in a diamond shape with 1 pip on the top corner" width="100" height="100" />
                </td>
                <td>
                    <div class="operator">=</div>
                </td>
                <td>
                    <img src="images/blank.png" alt="A blank space" width="100" height="100" />
                </td>
            </tr>
        </table>
    </div>
</div>
</body>
</html>

At first glance, that's kind of a lot of code! Obviously, this isn't just one big image. Serving each dice as an individual image and the operators as actual text allows this page to be used with accessibility tech. Unfortunately, this is often the case with visual puzzles: making them available to more users often does require extra effort. In this case, it's the incredibly descriptive alt text that's the extra work.

Otherwise though, a screen reader will naturally read the contents of a page in the order it's encountered in the code, so using a table layout for this puzzle was actually the correct choice for accessibility reasons! If instead a Grid layout with columns had been chosen, then a screen reader would read in column order, which makes no sense as far as the puzzle mechanic is concerned.

Also of interest is the blank column after the equal sign. Making it an actual (blank) image serves two purposes: it takes up space on the page so that the table is centered on the page when that column is included, and it allows the screen reader to read the alt text to know that something is supposed to be there. Including it was also quite trivial since the table layout automatically takes up only as much space as the contents of each cell need.

Clone this wiki locally