How to convert a puzzle into an HTML page - PuzzleServer/mainpuzzleserver GitHub Wiki

Do you normally create your puzzles in Publisher and deliver as PDF? Are you scared about the idea of turning your static puzzle into an interactive webpage? Don't worry, it's a lot easier than you think! If you're new to HTML, start with the basic overview below. Yes, there's some external reading, but the better you understand how webpages work on a fundamental level, the easier it is to know how to put what's in your head onto the screen. Otherwise, skip down to some of the examples or resources available to make it easy to get past the basic structure and onto the specifics of your puzzle.

IF YOU ARE BRAND NEW TO CODING AND DON'T EVEN KNOW WHICH TOOLS TO USE, START WITH OUR ENVIRONMENT SETUP GUIDE FIRST!!!

Contents

HTML Basics

Everything on a webpage is a box. Images are boxes. Text is a box. Even audio is a box, assuming it has playback controls visible somewhere on the page. All these boxes are nested inside each other, sometimes quite deeply. How big they are depends on the type of box, its contents, and any extra styling applied via CSS. We'll cover CSS later, since while it affects how the individual elements on a page look, you'll usually start with just HTML to create the structure and layout of your page.

Within your HTML code, the type of every box is called a "tag" or "element". Elements are denoted by , which means you'll need to do something special if you want to actually display an angle bracket on your page (those are called "escape characters", but you likely won't need to worry about them). The contents of a particular element starts with the , everything that it contains, and then a matching </element name> with a forward slash / preceding the tag's name. Remember: every must have a matching closing !

Element list

Because each element displays a specific type of information in a specific way, it helps to know what the different elements do. The main ones you'll use are:

HTML metadata

  • <html>: This single tag contains the entirety of your webpage.
  • <head>: Information about your page that's not contained on the page itself lives here. For example, the text that the browser puts in the tab for this page, links to CSS or Javascript files that your page uses, etc. Each page has only one head.
  • <title>: The text that the browser puts in the tab for this page. This tag must be inside the head tag.
  • <script>: Javascript code can be written directly in here instead of being linked from another file. This tag should be inside the head tag.
  • <style>: CSS rules can be written directly in here instead of being linked from another file. This tag should be inside the head tag.
  • <link>: When using separate CSS files, you link to them using a link tag. You don't do this for Javascript though. This element should be inside a head tag. This element doesn't need a closing .
  • <body>: All the visible contents of your page lives here. Each page has only one body.

Note that once you learn how to use these metadata elements, their use will generally be the same for every page. You'll see how to use them in the examples below, and you can just copy and paste them as a template for all new pages.

Basic layout

  • <div>: A basic box. On its own it's invisible, but style, contents, or both will fill it to make it as big as it needs to be, or as big as you specify. By default, a div will take up an entire row of space on your page, so if an outer box contains two divs, one will appear on the next line below the other. This behavior can be changed with CSS.
  • <span>: Another basic box. The main difference between this and a div is that a span will happily live inline with other inline elements like spans, so two spans will need to have line breaks manually put between them if you want them to live on separate lines.
  • <br>: A line break. Each one of these you put will add another line of vertical space to your page. This element doesn't need a closing .

Tables

  • <table>: A table is an easy way to set up a uniform series of boxes of a specific height and width, optionally with each cell's boundaries marked with a visible border. Tables contain specific elements for each row and column.
  • <tr>: A row within a table. Rows are defined first, so they'll be directly inside a table element in most cases.
  • <td>: A cell within a row. The number of cells in a row defines the number of columns in a table, and it's generally good practice to keep the number of cells per row consistent within a table.
  • <th>: A special "header" cell, usually used within the first row of a table when you want it styled differently than the rest of the rows.

Text

  • <p>: A paragraph of text, which usually has extra space after it. Text can also be put directly inside most other elements.
  • <h1>,<h2>,...<h6>: Headers, which are bigger than the regular page text. By default, there are 6 available levels of headers, with h1 being the biggest.
  • <ul>: A bulleted list. Each bullet point is specified by a <li> element directly inside the ul element, and nested lists can be created by putting another ul element inside a li element.
  • <ol>: A numbered list. The numbers increment automatically, so you don't need to specify them as part of the list items.
  • <a>: A link to another webpage, or another part of the current page. Any text between the tags is what's displayed on the page. The location of the page, or "URL", is specified as an attribute inside the . See more on attributes below.

Multimedia

  • <img>: An image, either supplied by your webpage, or linked from somewhere else on the web. The size can be controlled with CSS. This element doesn't need a closing .
  • <audio>: Embeds audio on a page, typically from a supplied file like an MP3. Audio playback controls are usually shown, but can be customized.
  • <video>: Embeds video on a page, typically from a supplied file like an MP4. Video playback controls are usually shown, but can be customized.
  • <canvas>: A drawing area that's usually populated by running some Javascript code, either once or many times every second to create an animation. This can also be interactive: users can draw in it in different ways, even changing the initial image supplied by the page.
  • <iframe>: A box that embeds an entire separate webpage within yours. This can also be used just on specific pieces of website, for example to embed just a Youtube video or a Google map on your page.

Input

  • <button>: A clickable button that can have text displayed on it. Usually Javascript is used to run some code when a button is clicked, so buttons should only be used if you plan on having some sort of interactivity.
  • <input>: A generic way of getting user input, although the most common way is with a text box. Browsers often provide standardized controls for many specific types of input, like calendars for picking dates, checkboxes, buttons just like the button element above, file pickers for uploading files, and more. Usually, the data provided by the user is then operated on by Javascript code that's triggered by different user interactions like clicking the mouse or typing on the keyboard. The type of control is specified as an attribute inside the . This element doesn't need a closing .
  • <select>: A dropdown list of items, where each item is specified by an <option> element inside the select element.

Page example

Let's look at an example webpage to see how the browser lays out all its boxes: image This page has some text at the top, a box with space inside and outside of it, and then the puzzle itself, which is a few lines of text and a grid of pictures.

First, to look at the page's code, hit F12 or go to the ... menu and look for a Developer Tools option (it may be hidden under a sub-menu). In the window or panel that pops up, look at the Elements tab: image

Just by following the text on the page, you can see what elements were used to build it. For example, the text at the top with the puzzle name and author is built out of divs and an h1 tag. The grid of images is a table... but where is the image itself coming from? If you expand the style tag, you'll see that it's set completely with CSS! The Developer Tools should also have an option turned on by default to highlight the boxes on the page when you hover your mouse over an element in the Developer Tools window.

This is the box for all of the text on top of the border: image You'll see different colors have been highlighted on the page. Blue is the size of the content itself. Orange is an extra margin that's been applied outside the top of this box via CSS. You can also specify padding, which the browser shows in green, between the edge of the box and the content inside it.

This is the box for the puzzle content inside the border: image The border itself is just created on that div with CSS; any box can have its border made visible. The padding inside the box that's limiting the width of the text is shown in green, but you'll see that the white space outside of the box isn't affected by this div. Instead, it's specified by one of the outer boxes.

Attributes

Clearly, there's a lot more that goes into defining a page than just what's between the tags. You can already see a lot of the tags have attributes inside the . As mentioned in the list of tags, some elements require certain attributes in order to be useful. For example, all of the link tags that are bringing in CSS files have the file location specified by an "href" attribute. The script tag, on the other hand, uses a "src" attribute for the exact same thing. The list of possible attributes for each tag can be found online in documentation.

Additionally, all elements support a set of "global" attributes. The one seen here is the "class" attribute. On its own, class does nothing. It's primary purpose is to provide a way for CSS to style multiple elements the same way with a single rule. Each class is essentially a group. Other useful global attributes include the "id" attribute, which needs to be unique for every element (unlike class), the "style" attribute, which lets you write CSS rules directly in your HTML tag that apply only to that element and all of its contents, and the "title" attribute, which lets you specify popup text when the mouse is hovered over that element.

CSS Overview

Once you get your basic page layout set, you'll want to spend a lot of time making it look pretty. Because some CSS rules can affect the layout of the elements, you might also need to go back and adjust the HTML too. But what exactly is a CSS rule? Basically, CSS provides many different ways to specify which elements you want to apply which style values to.

At the most basic level are all of the different ways you can affect a style value. These are called CSS "properties". For example, there's a property for color, and for height, and for margins (like mentioned above!). A particular group of properties that are all applied together to an element or set of elements is called a "rule", and the elements that a rule applies to are specified via a "selector".

In CSS, a rule is enclosed in {curly brackets}, and the selector is placed directly before the brackets. For example, let's say we want to make all p elements red and bold. We'd write a rule like this:

p {
    color: red;
    font-weight: bold;
}

Reading that in English, you'd say that this rule applies two properties to all p elements: it sets their color property to the value red, and it sets the font weight to bold. You can see that the value you want to set for a particular property is specified after a colon : and each property:value pair within a rule is separated by a semicolon ; and although it's formatted nicely here with line breaks and white space, the white space actually doesn't matter.

Where things start to get complicated is that you can write as many rules as you want that apply to the SAME element or group of elements! CSS decides which ones to use based on which ones come later in the code, as well as which ones are the most specific. That means you can actually overwrite one rule with another, or have part of one rule applied AND part of another. For example, let's say that in addition to our red rule above, we want any paragraph that has a class attribute of "green" to be green instead of red. To do that, we'd write a second rule that looks like this:

p.green {
    color: green;
}

Because this rule is more specific (it applies to only SOME p elements instead of all of them), it will override the red color for every p that also has a "green" class. However, all p elements will still ALSO have their text be bold, because our new rule says nothing about changing the font weight, and so our previous rule for all p elements still applies that particular property.

Selectors

Most of the time, you'll likely end up using classes to do your styling; rarely are there styles that you want to apply to every single element of a particular type. CSS uses a dot . before a class name to denote that it's a class instead of an element. So in our example ".green" is the selector that tells CSS to apply that rule to a class named "green". But wait, we actually wrote "p.green"! That's totally valid; selectors can be combined in many different ways. The way we wrote our rule means "first select all p elements, then select the ones with a "green" class, and then apply these rules to them". If we had written the selector as just ".green", then that rule would apply to ALL elements with the green class, not just p elements.

Another useful selector is the id selector. CSS uses a # hashtag to denote that it's an id instead of an element. As mentioned before, every element's id must be unique, so an id is a good way to style just a single element. You can also select something that's only specifically nested inside something else by using a descendant selector. This selector is very tricky because it's just a blank space! It's the one place in CSS where white space DOES MATTER, so be careful with your spaces. For example, if we had written "p .green" with a space before the dot, what we would actually be telling CSS is to look for an element with a green class that's INSIDE a p element, instead of looking for a p element that HAS a green class.

One useful property of descendant selection is that it will find something at any level inside another element; it doesn't have to just be an immediate child. For example, looking at our puzzle above, if we were to write a rule for ".content-div .puzzle-entry", we would successfully select our .puzzle-entry div even though there are multiple other divs in between it and its ancestor .content-div. If you only want to look for an immediate child, you can use the > angle bracket. By doing that, a rule for ".content-div>.puzzle-entry" would fail to find anything since .puzzle-entry is not a direct child of the .content-div.

CSS also lets you write one rule that applies to multiple selectors by separating the selectors with , commas. For example, if you wanted to write a rule that makes both p and span elements red, you'd write it like:

p,
span {
    color: red;
}

Another important thing to remember is inheritance. When you apply a rule to something, that rule will also apply to everything nested inside that element. That means that if you were to want to write a rule to make everything in the .content-div green in our example puzzle above, all you'd have to do is write a rule that says:

.content-div {
    color: green;
}

You wouldn't have to worry about making any other rules for any other specific elements or classes because the entire puzzle is contained inside that .content-div. This can also lead to frustration when you DON'T want certain properties to be inherited though, so make sure you lay out your elements very carefully so you'll have an easy time selecting them when it comes time to style them.

Because these rules could be coming from different files, style tags, or style attributes, it can sometimes be difficult to know where exactly an element is getting all of its different styles from. Luckily, the Developer Tools have a section to help with that. When you first opened it, you might have noticed another section of the panel with a bunch of CSS rules. If you click on an element in the HTML tree, that panel will tell you all the rules that are being applied to that element, in the order they're applied (which is helpful for seeing which rules are overwriting others): image

Here, you can see that the p elements are getting their font type from a rule applied to the body element, their size and color from a rule on the .content-div class (with that rule overwriting the size property from the body rule), and their spacing from a rule applied specifically on p elements. If you click on the "Computed" section of that panel instead of "Styles", it will show a handy summary of ONLY the properties that are being used on that element, as well as the size (measured in pixels) that it ended up being after all that styling was applied (note the shading colors used in that diagram are the same ones shown above on the page):
image

Properties and Values

Because you're going to be setting values for every property you use, you should familiarize yourself with the different units CSS uses and the range of values they accept. The first one we've already seen is color. There are actually multiple ways to specify color, the first being color names like we used here. A more precise way is to specify their individual components, which in this case are red, green, and blue, in that order. Each component has 255 possible values, and you can use the rgb() function directly with those values.

For example, pure red is rgb(255, 0, 0) because it has no blue or green components. If you know how to count in hexadecimal, you can also use hex notation to specify red as #ff0000. Note that the preceding # must have no space between it and the value components. Here, you can see that hex ff is equal to 255 in decimal. There are plenty of online color pickers that will let you just pick a color visually and give you the numbers you need to recreate it.

Another type of value is a "keyword". Many properties have a specific set of words that are the only values they accept, and these values can be found in online documentation. In our font-weight example, besides bold, you could have also used bolder, normal, and lighter, as well as numeric values. The numbers have no unit here, but other properties do take numbers with units. The most common unit is length, since many properties set the size of something.

The smallest unit of length measurement is the pixel, abbreviated px in CSS. It is also the smallest visible unit of your screen, so it's easy to take screenshots, zoom in, and then count the individual pixels. CSS has many other types of length units, some of which are relative to other measurements, but those can be very tricky to use properly. The most common one you'll use is percentage, which uses the standard % percent sign. It does what you probably think it does: it takes up whatever percent of the available space it has. For example, if you want a div to take up only half the width available to it, you'd set its width to 50%. Remember not to leave a space between the number and the unit!

Many properties actually set multiple things in a single statement because it's less to write. For example, the "border" property takes three values separated by spaces to set the color, width, and line style, in any order. You don't have to use this shorthand though; you can set individual properties as well. For example, "border: 1px dotted #ff0000;" is equivalent to:

{
    border-color: red;
    border-width: 1px;
    border-style: dotted;
}

Here are the properties you're most likely to need for basic puzzles:

Content

  • color: Sets the color of the content in a box, which is usually the color of its text.
  • height: Sets the height of the box to something specific or relative, depending on the unit. Note that the exact height of the box may be different than what you set if the box's layout style causes it to interact with parent boxes in a specific way (for example, its parent may have a max-height set) or its content is bigger than the size you set (for example, your font is 20px tall but you only want the box to be 10px tall). You can also set min-height and max-height to make your boxes less dependent on content size.
  • width: Similar to height, sets the width of a box, but with some caveats. Some layout styles (see more in the next section), for example, will cause boxes to only be as wide as their contents, no matter how big you set their width. This will likely be a big cause of consternation, and one easy way to set consistent widths is to use tables. Conversely, if you want flexible widths, CSS layout styles like Grid or Flex may be what you're looking for.
  • visibility: Makes a box invisible, but leaves it on the page so the layout doesn't change. This can be useful if you want to uncover something hidden while keeping the everything on the page from moving around.
  • opacity: Useful for making something partially see-through. Opacity is the opposite of transparency.
  • background-image: Lets you show an image underneath the content of its box, just like in the example puzzle above, which shows an image underneath the table cells.
  • background-color: Sets the background color of a box to something besides white.
  • cursor: Sets the mouse pointer when it's hovered over an element. This is useful to hint to users whether something is interactable (for example, making it a hand with the "pointer" value when hovering over something that's clickable like a button or a link, or the typing I beam with the "text" value when hovering over something that users can type into).

Boxes

  • border: A shorthand property to set all 4 borders of a box to a specific color, width, and style of line.
    • Individual borders can be set with their own properties: border-top, border-right, border-bottom, and border-left, all of which are also shorthand properties to set that border's color, width, and style.
    • If you want to set a single property for a single border, you can use its property as needed, for example border-top-color or border-left-width.
    • You can also set just a single property for all borders by using the appropriate border-color, border-width, or border-style property.
    • border-radius: Rounds the corners of your boxes. This is a shorthand property for each individual corner.
    • border-collapse: Tells the individual cells in a table whether they should both show their borders side-by-side, or collapse them into a single border. For example, in a table with 2 columns, if you separate the borders and set them to 1px width, you'll actually see a 2px line down the middle, with 1px belonging to the left cell's right border, and the other belonging to the right cell's left border, assuming no separation between the cells.
    • border-spacing: Tells the individual cells in a table to put extra space between them, effectively setting a margin between them.
  • margin: Sets extra space outside of a box between it and the other contents of its parent. This is a shorthand property; the margin on each of the 4 sides can be individually set.
  • padding: Sets extra space inside of a box between its border and its contents. This is a shorthand property; the padding on each of the 4 sides can be individually set.

Fonts

  • font-size: Sets the font size, usually to something specific in pixels.
  • font-style: This is how you can make text italic.
  • font-weight: This is how you can make text bold.
  • text-decoration: This is how you can make text underlined or strikethrough. Like borders, this is a shorthand property that lets you set the line position, thickness, color, and style.
  • font-family: Sets the font for text, either to something specific by settings its name, like Arial or Times New Roman, or to a more general category like "sans-serif" (fonts without tails) or "monospace" (every character takes up the same number of pixels in width). Note that when setting a font to something specific, there are only a few "web-safe" fonts that are guaranteed to be available to users and not need to be supplied with your website. If you don't use the safe fonts, the browser may fall back to something else.
  • text-align: This is how you can left or right-align text.
  • vertical-align: This is how you can place text at the top, bottom, or middle of its containing box, although not all elements respond to this property. If you find your element doesn't respond to this property, you may need to change which layout style its parent is using. For example, using Flex or Grid layout lets you instead use the align-items property for vertical alignment.
  • text-indent: An alternative way to indent text if you don't want to mess with padding or margins.
  • text-orientation: This is how you can make vertical text.
  • text-transform: This is useful for displaying user input a certain way, for example if you want all text that users type to be uppercase, regardless of whether or not they actually typed an uppercase letter.
  • text-wrap: This is how you can turn off text wrapping, for example if you want text to intentionally overflow its containing box or to be clipped.
    • To get those effects, you will likely need to set other properties as well. For example, you'll likely need to set the white-space property to "nowrap", the text-overflow property to the desired value, and possibly also the word-break property to "keep-all" if you have incredibly long strings of text without any spaces that you want to overflow.

Advanced Layout

There are also some powerful properties that affect the HTML layout. The first is position, which has a few specific uses. For example, if you want something on your page to be in an exact position you can set it to "absolute" or "fixed", and everything else on the page won't interact with it at all.

Likely more useful is the display property. This is how you enable more advanced layouts. The first tricky thing though is that this property affects all of its contents slightly different than how it affects how its parent displays it, because it actually does both! You'll likely find yourself using this property mostly with divs, possibly with two nested divs so that the outer one controls just how it interacts with its parents, and the inner one controls just the layout of its children.

Let's go back to our example. If you look at the table, you'll see a couple of divs that have no other content than just the table: image

Why did we do that? Well, that's how we got the table centered. You see that the outer div actually has a class called "center"; the only thing it does is set the text-align to "center", which means all of its children should be centered. However, on its own, a table just doesn't want to center itself like that. If we set the inner div with the class "wrapper" to its default display type of "block", you'll see that it takes up all the width available on the line, which is what we expect of a default div based on what we said above: image

So instead, our wrapper actually has an "inline" value (in this case, "inline-block" specifically). This makes the content take up only its actual width, which you can see by the blue highlighting in the first image being only the width of the table, whereas in the second "block" image, the highlighting also includes all the space to the right of the table. Thus, we come to our first rule of thumb: use the display value "block" when you want something to be on its own line, and "inline" if not.

That's how you control how a box interacts with its parent. What about if you want a box to control its children? That's where we get to the other display values. There are two VERY powerful display layouts called Grid and Flex. Grid is more intuitive, so let's look at it first. Basically, Grid lets you set up a series of columns and/or rows that can be spaced independently of their content:
image

In this puzzle, there are 4 equally sized columns, and instead of setting them to a concrete value like you would with a table, they are instead just set to take up 25% of the container's available space. That means they'll automatically change size as the window size changes! Grid and Flex really shine when your content isn't all of equal size (which if it were, it would be easy to just use a table). For example, if the window is narrower, the text will automatically expand onto further lines, whereas with a table, it would likely overflow.

Flex, short for "flexbox", is useful when your item size is flexible, meaning it could add extra padding or lines of text and still look fine. Flex layouts can grow or shrink all their children when the container size changes, and they can also wrap the children onto a new line if you enable wrapping and set a minimum shrink size. This means that flex layouts are most useful in one-dimensional situations, like across a single line.

Looking at another puzzle, you can see that this row of text input is set up as a flexbox: image

If we mess with this page a little and force this container to be smaller, we can see that the boxes will automatically wrap onto subsequent lines since wrapping has been enabled for this flexbox: image

Finally, one last trick to remember is that setting display to "none" will completely remove an item from a page, including the space it took up! This is different than the "visibility" property mentioned above, which blanks out an item, but maintains its space in the layout.

Javascript

At this point, you likely know enough for many basic puzzle layouts, but if you're still curious about Javascript, here's enough to get you started. The crucial thing to know about Javascript is that any code you see in a page is generally only run once, as soon as it's encountered when the page loads. That can be useful for setting up the page if you want to do something fancy like build your UI programmatically instead of manually writing HTML, but it's not that helpful for interactive pages that respond when a user does something.

To do that, you're going to want to rely on "events". Javascript has many built-in events, most of which fire in response to a specific user interaction. For example, there are events that fire when the page finishes loading, when the user clicks or scrolls with the mouse, when the user types, and even when the user prints! To make use of these events, your code will register a "listener", which is a block of code that automatically runs in response to an event firing.

To make this more useful, you will usually have specific items on your page listen for specific events. For example, if you want to do something when a user clicks a button, you would want that button specifically to listen for when it's clicked, as opposed to having the page just listen for all clicks. The following code is how you would do that:

document.addEventListener("DOMContentLoaded", populatePage);
function populatePage () {
    var myButton = document.getElementById("myButtonHasThisUniqueId");
    myButton.addEventListener("click", doButtonStuff);
    function doButtonStuff (event) {
        // Do whatever you want here when the button is clicked
    }
}

Reading through this line by line, here's what this does:

  1. The first line is run as soon as this code is encountered as part of loading the page. When this code runs, the rest of the page may not have loaded yet if you put your code at the top of the file, so what this line is doing is registering a listener for one of the events that fires when the page is finished loading. We have to do it this way because if the page isn't done loading, then the button we want to add a listener to may not even exist yet!
  2. The rest of the code is the function that runs in response to the page load event firing, so it's NOT run at the same time as the first line. It is, however, also available to run any other time this function is called. A "function" is Javascript's equivalent to a CSS rule: it's only useful if something else makes use of it.
  3. The third line is finding our button on the page. This is Javascript's standard way of reaching into HTML: it doesn't have any automatic mapping, so any time we want to use a piece of HTML inside of Javascript, we have to search for it using a function like getElementById. We're storing the reference to our button inside a "variable" called "myButton", so if we want to do multiple things with our button, we can just use that variable next time instead of using a search function every time.
  4. This is the line that's doing the important thing: it's adding an event listener for the "click" event to our button! You'll notice that it's using the exact same function that we used to listen for our page to finish loading. In fact, this is how you can add listeners for any event to any object.
  5. This is the function that runs when our button is clicked, and you may notice one crucial difference between it and the function that runs when the page loads: there's now a variable called "event" in between the parentheses after our button function's name. That event variable contains all the information about the event that fired, for example which mouse button was used. You may not need that info depending on what you want to do, but most events will supply it if you give them a variable to put it in.

If you're still confused, don't worry; you will likely be able to do a lot without touching Javascript (or at least writing new Javascript) thanks to the tools we have available to you. To learn more about them, keep reading the rest of this page. Additionally, if you want to read official documentation on Javascript or anything else covered so far, Mozilla has great tutorials with a lot of live examples:

PuzzleJS

The first tool you should be aware of is Kenny Young's PuzzleJS Library. This powerful library programmatically generates many types of grid-based puzzles. The repo has many examples of how to use it, and including it in your event is incredibly simple. Just put all the resource files in the Shared Resources for your event, and then any puzzle can use them by including the puzzle.js and puzzle.css files! Documentation on every option available in PuzzleJS is also hosted here. Scroll down to the bottom of the page and click the link to the options reference to see every possible option and how to use it, or the styling reference to see how to override the default styles.

The library uses a simple text description of the layout and rules for your puzzle and then generates the grid when the page loads. It currently supports many types of puzzles and mechanics, with more being added all the time. It can automatically extract letters that a user types and put them into another box for automatic answer generation, fill in squares for paint-based puzzles, and draw lines either on the grid squares or on their borders. It also includes undo/redo functionality and automatically saves progress when the page is closed and re-opened.

In fact, we've already seen multiple puzzles here that use it! For the Picture This puzzle, clicking on squares will highlight them as a way of marking off which squares you've used:
image

Besides preparing the background image, the entire puzzle grid was generated by including a single div with the ".puzzle-entry" class. The library then puts everything into that div. For the background image, a separate div was used and a background-image CSS property was applied to it. The rules for this puzzle are all applied as attributes on the puzzle-entry div. In this case, the show-commands enables the undo-redo buttons, the "notext" mode turns off the ability to type into the grid, and the fill classes are what toggles the highlighting when the user clicks a square. Here's what the relevant parts of the puzzle's code look like:

<html lang="en-us">
<head>
    ...
    <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 { 
            margin: 0px; 
            width: 780px; 
            height: 800px;
            background-image: url(picture-this.png); 
            background-size: contain; 
            background-repeat: no-repeat;
        }
    </style>
</head>
<body>
<div class="page-div">
    ...
    <div class="content-div">
        ...
        <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>
    </div>
</div>
</body>
</html>

First, you can see that the PuzzleJS library is linked using relative links. That means the file locations when uploaded to the server are the same as when being stored locally. This is generally a good practice because it means you never have to change those links, and our admin wiki page on event customization goes over how to set up this structure in more detail. You will also see that the highlight color is actually defined by the user! The CSS class that defines the color is what's given to the fill-classes attribute.

Looking at the background image, you see a couple of properties that operate on the background besides just the image. Those properties tell the page how much to stretch and repeat (or not to!) the image in order to fill its containing box if the image ends up being a different size than the box. You also see that it's just a single image, despite being used in a 6x6 grid. This is mainly done for convenience. This puzzle could have been made using individual images that were added to the background of each cell in the puzzle grid, but doing it that way would have been more work.

Another puzzle that we've already seen is Scene It! This puzzle uses the text handling capabilities of PuzzleJS to automatically extract letters from certain boxes and put them down in a dedicated answer space:
image

Here's what the code to do that looks like:

<html lang="en-us">
...
<body>
<div class="page-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="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>

Structurally, you can see that each video-text pair is actually an HTML list, although there's no bullet point because it's been taken off via CSS. The video files are delivered alongside the puzzle; if you wanted to use Youtube videos instead, you'd need an iframe instead of a video tag. You can also see that each text input is its own puzzle-entry div, as is the answer extraction! Finally, you can see that there are actually two content-div sections, one marked print-only, and one marked online-only. CSS supports styling printed pages, so all those classes are doing is setting the display property to "none" when this puzzle isn't being viewed using the appropriate medium. That way, a user doesn't try to print videos!

To define the puzzles, you see that the text attribute uses a combination of #,., and spaces, where a . is a blank box and a # is a yellow box whose letter will automatically be copied over to a corresponding answer box. The specific answer box it's tied to is defined by a numbered index, as seen in the extracts attribute. In fact, there's nothing special about the answer extraction lines; since they don't have user input disabled on them, a user could actually type into them and have it appear under the corresponding puzzle!

Useful snippets

PuzzleJS has been used extensively in Puzzleday 2023'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 2022-2024 events are stored in private repos, and Josh Bodner can get you access if you need to use something out of there. Send mail to [email protected] to request access.

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.

Templates

Finally, to help with creating new events, here are some full-featured templates designed specifically to work with our website. All of the places that need to be changed for each specific puzzle are marked with CHANGE_ME:

<!DOCTYPE html>
<html lang="en-us">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- CHANGE_ME: title of your puzzle, followed by - Microsoft Intern Puzzleday 2024 -->
    <title>Example Puzzle - Microsoft Intern Puzzleday 2024</title>
    <link rel="stylesheet" href="../../resources/puzzle-base-styles.css">
    <link rel="stylesheet" href="../../resources/puzzle-print-styles.css" media="print">
    <script type="text/javascript" src="../../resources/resize.js"></script>
    <!-- CHANGE_ME: To use the puzzlejs library, include the following three lines -->
    <!-- <link rel="stylesheet" href="../../resources/puzzle.css"/> -->
    <!-- <link rel="stylesheet" href="../../resources/puzzle-basic-colors.css"/> -->
    <!-- <script type="text/javascript" src="../../resources/puzzle.js"></script> -->
    <!-- Class names used by puzzle.js that should not be manually used or overridden:
        .across                     .black                      .black-cell                     .blue
        .blue-laser                 .border-bottom              .border-top                     .border-left
        .border-right               .bottom-clue                .cell                           .class-fill
        .clipboard-button           .clue                       .commands                       .copy-only
        .corner-focus               .crossword-clues            .darkgray                       .down
        .edge-base                  .extract                    .extract-code                   .given
        .given-fill                 .given-text                 .gray                           .green
        .green-laser                .hovered                    .indigo                         .indigo-laser
        .inner-cell                 .interesting                .left-clue                      .lightgray
        .marked                     .no-copy                    .no-input                       .nopointer
        .orange                     .orange-laser               .outer-cell                     .path
        .puzzle-about               .puzzle-about-back          .puzzle-about-button            .puzzle-about-close
        .puzzle-about-credits       .puzzle-about-savedstate    .puzzle-about-scroller          .puzzle-commands
        .puzzle-entry               .puzzle-redo-button         .puzzle-reset-button            .puzzle-undo-button
        .red                        .red-laser                  .reticle-back                   .reticle-front
        .right-clue                 .small-text                 .strikethrough                  .text
        .top-clue                   .unselectable               .violet                         .violet-laser
        .white                      .white-laser                .x-mode                         .yellow
        .yellow-laser -->
    <!-- CHANGE_ME: add necessary page-specific styles for this puzzle only -->
    <style>
        .typable {
            display: inline-block;
            box-sizing: border-box;
            width: 12px;
            border-bottom: 2px solid black;
            color: red;
            text-transform: uppercase;
        }

        .spoiler {
            background-color: black;
            color: black;
            font-weight: bold;
        }

        .example-help {
            display: block;
            margin: 0 auto;
        }
    </style>
    <!-- CHANGE_ME: add necessary page-specific scripts for this puzzle only -->
    <script>
        window.onload = function example_events() {
            for (ced of document.getElementsByClassName("typable")) {
                ced.addEventListener("keypress", example_onKeyPress);
            }
        }

        function example_onKeyPress() {
            window.getSelection().selectAllChildren(this);
        }

        function example_unSpoiler() {
            for (elem of document.getElementsByClassName("spoiler")) {
                elem.classList.remove("spoiler");
                elem.classList.add("answer-blue");
            }
        }
    </script>
</head>
<body>
<div class="page-div">
    <div class="header-div">
        <h1>
            <!-- CHANGE_ME: title, same as page title above but without - Microsoft Intern Puzzleday 2024 -->
            Example Puzzle
        </h1>
        <div class="byline-div">
            <!-- CHANGE_ME: author, just your name, no By: or anything like that -->
            Philip Z Loh
        </div>
        <!-- CHANGE_ME: if your puzzle has Data Confirmation, include this line -->
        <!-- <div class="has-dc" title="Data Confirmation checks are available for this puzzle."></div> -->
    </div>
    <div class="content-div">
        <!-- CHANGE_ME: puzzle content goes here -->
        <h4>Interactivity</h4>
        <p>
            Puzzles will look like this! <b>Many of them are interactive</b> - if it looks like you can click around /
            tab around / start typing, you're probably right! You won't need to View Source or go hacking around the
            code though; that's not fun.
        </p>
        <h4>Incorrect Answers</h4>
        <p><b>Go ahead and submit your most creative incorrect answer</b> above to
            get a feel for what an incorrect answer's response looks like.</p>
        <h4>Partial Answers</h4>
        <p>
            If you make progress on the puzzle and get something that looks like a phrase, type it into the answer
            submission page! <b>Partial answers will be confirmed</b> to tell you you're on the right track.</p>
        <ul>
            <li>
                z<div contenteditable="true" class="typable"></div>ro
            </li>
            <li>o<div contenteditable="true" class="typable"></div>e
            </li>
            <li>
                <div contenteditable="true" class="typable"></div>wo
            </li>
            <li>thr<div contenteditable="true" class="typable"></div>e</li>
            <li>fou<div contenteditable="true" class="typable"></div>
            </li>
            <li>fiv<div contenteditable="true" class="typable"></div>
            </li>
            <li>
                <div contenteditable="true" class="typable"></div>ix
            </li>
            <li>
                <div contenteditable="true" class="typable"></div>even
            </li>
            <li>
                <div contenteditable="true" class="typable"></div>ight
            </li>
            <li>nin<div contenteditable="true" class="typable"></div>
            </li>
            <li>t<div contenteditable="true" class="typable"></div>n</li>
            <li>ele<div contenteditable="true" class="typable"></div>en</li>
            <li>twelv<div contenteditable="true" class="typable"></div>
            </li>
            <li>thirte<div contenteditable="true" class="typable"></div>n</li>
            <li>fourt<div contenteditable="true" class="typable"></div>en</li>
            <li>fifte<div contenteditable="true" class="typable"></div>n</li>
            <li>sixt<div contenteditable="true" class="typable"></div>en</li>
            <li>sev<div contenteditable="true" class="typable"></div>nteen</li>
            <li>eightee<div contenteditable="true" class="typable"></div>
            </li>
            <li>ni<div contenteditable="true" class="typable"></div>eteen</li>
        </ul>
        <p>
            Click on this chunk to see what partial answer you can submit for Example Puzzle:
            <span class="spoiler" onclick="example_unSpoiler()">ENTERESSEEEVEEEEEENN</span>.
        </p>
        <h4>Correct Answers</h4>
        <p>
            <b>Typing the correct answer in</b> solves the puzzle! Example Puzzle is worth 0
            points.
        </p>
        <h4>Asking for Help</h4>
        <p>If you're stuck and not having fun, <b>consider asking for help</b>. Use the link on that specific puzzle's
            submission page. Go ahead and try clicking on the link for Example Puzzle, just to see what happens and to
            make sure you have your email setup working.</p>
        <img class="example-help" src="ask-for-help.png" title="Screenshot showing where to click for help" style="max-width: 100%;">
        <!-- END OF PUZZLE CONTENT -->
    </div>
</div>
</body>
</html>

By default, PuzzleJS is NOT included in a puzzle unless it's actually needed since anything added to a puzzle makes it take longer to load. There's also a corresponding answer template, and it also has the option to include PuzzleJS since PuzzleJS can be used in a non-editable state to easily show the solution to a puzzle:

<!DOCTYPE html>
<html lang="en-us">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <!-- CHANGE_ME: check mark, followed by the title of your puzzle, followed by - Microsoft Intern Puzzleday 2024 -->
    <title>βœ” Example Puzzle - Microsoft Intern Puzzleday 2024</title>
    <link rel="stylesheet" href="../../resources/puzzle-base-styles.css">
    <link rel="stylesheet" href="../../resources/puzzle-print-styles.css" media="print">
    <!-- CHANGE_ME: To use the puzzlejs library, include the following three lines -->
    <!-- <link rel="stylesheet" href="../../resources/puzzle.css"/> -->
    <!-- <link rel="stylesheet" href="../../resources/puzzle-basic-colors.css"/> -->
    <!-- <script type="text/javascript" src="../../resources/puzzle.js"></script> -->
    <!-- Class names used by puzzle.js that should not be manually used or overridden:
        .across                     .black                      .black-cell                     .blue
        .blue-laser                 .border-bottom              .border-top                     .border-left
        .border-right               .bottom-clue                .cell                           .class-fill
        .clipboard-button           .clue                       .commands                       .copy-only
        .corner-focus               .crossword-clues            .darkgray                       .down
        .edge-base                  .extract                    .extract-code                   .given
        .given-fill                 .given-text                 .gray                           .green
        .green-laser                .hovered                    .indigo                         .indigo-laser
        .inner-cell                 .interesting                .left-clue                      .lightgray
        .marked                     .no-copy                    .no-input                       .nopointer
        .orange                     .orange-laser               .outer-cell                     .path
        .puzzle-about               .puzzle-about-back          .puzzle-about-button            .puzzle-about-close
        .puzzle-about-credits       .puzzle-about-savedstate    .puzzle-about-scroller          .puzzle-commands
        .puzzle-entry               .puzzle-redo-button         .puzzle-reset-button            .puzzle-undo-button
        .red                        .red-laser                  .reticle-back                   .reticle-front
        .right-clue                 .small-text                 .strikethrough                  .text
        .top-clue                   .unselectable               .violet                         .violet-laser
        .white                      .white-laser                .x-mode                         .yellow
        .yellow-laser -->
    <!-- CHANGE_ME: add necessary page-specific styles for this puzzle only -->
    <style>
        
    </style>
    <!-- CHANGE_ME: add necessary page-specific scripts for this puzzle only -->
    <script>
        
    </script>
</head>
<body>
<div class="page-div">
    <div class="header-div">
        <h1 class="solution-h1">
            <!-- CHANGE_ME: title, same as page title above but without checkmark or - Microsoft Intern Puzzleday 2024 -->
            Example Puzzle
        </h1>
        <div class="byline-div">
            <!-- CHANGE_ME: author, just your name, no By: or anything like that -->
            Philip Z Loh
        </div>
        <div class="answer-div">
            <!-- CHANGE_ME: primary answer, no need to put it in uppercase -->
            The One Canonical Answer
        </div>
    </div>
    <div class="content-div">
        <!-- CHANGE_ME: solution content goes here -->
        <p>
            This is a puzzle. <span class="answer-red">Puzzle text puzzle text</span>. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
            eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud
            exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit
            in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <span class="answer-blue">Excepteur sint occaecat cupidatat non
            proident</span>, sunt in culpa qui officia deserunt mollit anim id est laborum.
        </p>

        <ol>
            <li>🍎</li>
            <li>Bravo</li>
            <li>Gamma</li>
            <li>〇</li>
            <li>5️⃣</li>
            <li>6</li>
            <li>δΈƒ</li>
            <li>μ—¬λŸ</li>
            <li>???</li>
        </ol>

        <table>
            <tr>
                <th>🍎</th>
                <th>Bravo</th>
                <th>Gamma</th>
            </tr>
            <tr>
                <td>〇</td>
                <td>5️⃣</td>
                <td>6</td>
            </tr>
            <tr>
                <td>δΈƒ</td>
                <td>μ—¬λŸ</td>
                <td>???</td>
            </tr>
        </table>
        <!-- END OF SOLUTION CONTENT -->
    </div>
</div>
</body>
</html>

The referenced CSS files are here. Note that the one thing not here is the Data Confirmation icon the CSS files use, since that may need to change year-to-year for style reasons:

/* Globals and Constants */
:root {
  --ink-black: rgb(51, 51, 51);
  --ruby-red: rgb(206, 10, 10);
  --evening-blue: rgb(10, 10, 206);
  --content-margin: max(calc(100vw - 600px) / 2 - 400px, 40px);
  --dc-width: 50px;
}

html {
  margin: 0px;
  padding: 0px;
}

body {
  min-width: 600px;
  width: 100%;
  margin: 0px;
  padding: 0px;
  color: var(--ink-black);
  font-size: 1.2rem;
  font-family: Tahoma, Verdana, sans-serif;
}

.page-div {
  padding: 0px;
  margin: 20px;
}

.header-div {
  margin: 30px 0px;
  position: relative;
}

.has-dc {
  background-image: url("../../images/comic-dc.png");
  background-repeat: no-repeat;
  background-size: 100%;
  position: absolute;
  top: 60px;
  right: calc(var(--content-margin) + 20px);
  width: var(--dc-width);
  height: var(--dc-width);
}


h1 {
  padding: 0px;
  margin: 0px;
  font-size: 48px;
  font-weight: bold;
  letter-spacing: 1px;
  text-align: center;
}

h1.solution-h1::before {
  content: "βœ”";
  color: var(--ruby-red);
}

.byline-div {
  font-size: 18px;
  font-weight: bold;
  letter-spacing: 1px;
  text-align: center;
  margin: 10px 0px;
}

.byline-div::before {
  content: "by ";
}

.answer-div {
  color: var(--ruby-red);
  font-size: 18px;
  font-weight: bold;
  text-transform: uppercase;
  text-align: center;
}

.answer-div::before {
  color: var(--ink-black);
  font-size: 18px;
  font-weight: bold;
  text-transform: none;
  content: "Answer: ";
}

.content-div {
  padding: 20px var(--content-margin);
  margin: 0px;
  border: 6px solid #000000;
}

.answer-red {
  color: var(--ruby-red);
  text-transform: uppercase;
  font-weight: bold;
}

.answer-blue {
  color: var(--evening-blue);
  text-transform: uppercase;
  font-weight: bold;
}

@media screen {
  .print-only {
    display: none;
  }
}

And here is the base print style file:

@page {
  margin: 0in;
}

body {
  font-size: 0.8rem;
}

.page-div {
  margin: 0px;
}

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

.has-dc {
  -webkit-print-color-adjust: exact;
  background-size: cover;
  position: absolute;
  top: 10px;
  left: 750px;
  width: 25px;
  height: 25px;
}

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;
}

.answer-div {
  grid-column: 1;
  margin-left: -20px;
}

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

.online-only {
  display: none;
}

Finally, here's the resizing script:

window.addEventListener("message", (e) => {
    if ((e.origin.includes("puzzlehunt.azurewebsites.net")) || (e.origin.includes("localhost"))) {
        e.source.postMessage({ width: document.body.scrollWidth, height: document.body.scrollHeight }, e.origin);
    }
});

document.addEventListener("DOMContentLoaded", () => {
    const params = new URLSearchParams(window.location.search);
    if (params.has("embed") && (params.get("embed").length > 0) && (params.get("embed") === "true")) {
        const contents = document.querySelector(".content-div");
        if ((contents !== null) && (contents !== undefined)) {
            contents.style.padding = "0px";
            contents.style.borderWidth = "0px";
        }
        const page = document.querySelector(".page-div");
        if ((page !== null) && (page !== undefined)) {
            page.style.margin = "0px";
        }
        const header = document.querySelector(".header-div");
        if ((header !== null) && (header !== undefined)) {
            header.parentElement.removeChild(header);
        }
        document.body.style.overflowY = "hidden";
    }
    else {
        document.body.style.overflowY = "scroll";
    }
    if (params.has("print") && (params.get("print").length > 0) && (params.get("print") === "true")) {
        window.onafterprint = window.close;
        window.print();
    }
});

A less-featured version of this file is also part of the website in case you don't want to provide your own in the Shared Resources. This one does have a few extra features though. First, when the puzzle loads, if it sees that it's being embedded, it removes the title, author, outline, and extra space around the puzzle since the embedding website takes care of all of that. It also makes sure no extra scrollbars appear on the puzzle's iframe. Finally, it also listens for when the website tells the puzzle to print (as opposed to the user telling it to) and automatically pops the print dialog then closes the new tab when the print is done. This will happen when the user clicks the link on the puzzle to print it or hits ctrl+P but not if the browser's ... menu is used.

For help with how to set up the file structure for webifying multiple puzzles and upload them, see the admin wiki for event customization.

⚠️ **GitHub.com Fallback** ⚠️