Burp Suite Enterprise Edition is now available in our secure Cloud  –  Learn more

Implementing Tic Tac Toe with 170mb of HTML - no JS or CSS

Gareth Heyes | 21 July 2023 at 14:00 UTC
CSS

Some HTML showing popovers to construct a Tic Tac Toe board

I love it when Chrome releases a new feature, I especially like it when it is experimental. In this post I'm going to show you how I created Tic Tac Toe (Noughts and crosses) with HTML, using one of those experimental features. Creating the game in CSS is trivial - as I've proven when I created a 3D first person shooter - so I wanted to challenge myself to create a game with just HTML.

How it started

It all started when Chrome implemented popovers in HTML . My first action was to test it for XSS vectors and, with the help of the XSS community, we found some pretty cool vectors that allow you to exploit hidden inputs and meta tags .

You would think this is where the story ends, but that's not the case. An idea popped into my head and once it's there, my brain goes into overdrive and I'm compelled to do it. The idea went like this: if you could create a popup using pure HTML, then you have some form of state e.g. the popup is showing or not. I thought you could then use this state information to create a pure HTML game and for whatever reason, Tic Tac Toe stuck in my head. Idea now fully committed, I spent my lunch hours creating the game.

Popovers are quite simple; you use a popover attribute to define the element you want to use as a popover element, which is then hidden. Then you use a popovertarget attribute in another element, usually either an input or a button, to link to the element to show it when you click the button:

<button popovertarget=myPopover>Show popover</button>
<div id=myPopover popover>Hello world</div>

The basic idea

Can you see where this is going? You could use the popups to show a Tic Tac Toe board, and then use the ids for each move. Putting this theory to the test, I did a little experiment to see if you could use multiple popups one after another and it worked! The basic idea now confirmed, I then started to write some code that would generate the permutations. At first I used tables to construct the board, and buttons for the choices:

for(i=1;i<10;i++) {
   html +=`<table>
   <tr>
   <td><button popovertarget="x${i}">...</button></td>
   <td><button popovertarget="x${i}">...</button></td>
   <td><button popovertarget="x${i}">...</button></td>
   </tr>
    //…
   `;
}
for(i=1;i<10;i++){
   html +=`
   <div id="x${i}" popover>
   <table>
   <tr>
   <td>${i==1?'<button>X</button>':'<button popovertarget="x${i}-o1">...</button>'}</td>
   <td>${i==2?'<button>X</button>':'<button popovertarget="x${i}-o2">...</button>'}</td>
   <td>${i==3?'<button>X</button>':'<button popovertarget="x${i}-o3">...</button>'}</td>
   </tr>
   //...
   `;
}

The first loop generates all of the buttons, without any choices being made. When you click on a button, it targets the popup generated by the second loop. You have to brute force all the players choices, and the second loop then generates a board that targets the next choice and so on. Traditionally "X" goes first, which is convenient because it reduces the amount of boards we need to create. Popovertarget passes every choice made by the player, for example: x1-o2-x3 means X chooses the first position, O chooses the second and X chooses the third, and so on. Nesting loops creates a lot of iterations. You can calculate this by multiplying the number of loops by nine each time, so nine to the power of the number of nested loops. For example:

9=9
9*9=81
9*9*9=729
9*9*9*9=6561
9*9*9*9*9=59049
//...

As you can see, the amount of iterations quickly starts to get out of hand.

Performance problems

I continued building the game in this way however, by the time I got to the sixth or seventh permutation things started to slow down. I discovered that Chrome apparently has a limit of approximately 1023mb that can be stored in a string. However, I got round that limit by splitting it into different strings.

When I got to the later permutations, performance still tanked when rendering the HTML in the browser. I thought about this for a while and decided to abandon tables completely. Chrome was rendering over half a million nodes, so clearly using tables for this was a bad idea. I used br tags and removed the tables to resolve this issue, which got me up to the second to last loop.

Here was the biggest challenge though, the last two loops were huge. It had to calculate every possible choice. However, still determined to make my idea work, I felt sure I could make some optimizations. Firstly, I removed all impossible moves. This was quite simple as you could just continue when the current loop was the same choice as the previous ones, e.g. you can't make a choice of x1-o1:

if(i === q || j === q || k === q || l === q /*...*/) {
   continue;
}

This would skip all invalid choices, but Chrome was struggling even when I tried generating this code without tables. Thinking about this for a while, I was sure that Chrome could handle text nodes. I removed buttons when a choice was made and used non-breaking space to pad the text and this worked! The last loop was huge but thankfully, because it didn't need to make any choices, I could just use text nodes.

I wanted to use a marquee when the game was solved but this was too much work for Chrome, so instead I just displayed a message. Because I brute forced every possible choice in the game, it could detect which side won and even know if the game was a draw.

There was one more optimization I could make: when the player had solved the game, there was no need to generate further choices. But how do you know when to stop? I had to check the previous loop and if the previous loop was solved, there was no need to generate the HTML of the current loop:

for (q = 1; q < 10; q++) {
//...
   if(isSolved({[i]:"X",[j]:"O",[k]:"X",[l]:"O",[m]:"X",[n]:"O",[o]:"X",[p]:"O"})) {
   break;
}
//...
}

The above code checks the position of the board for the previous loops but does not check the current loop, as it does not know if a choice has been made. This could be applied to each nested loop. The final game is around 170mb, and not a particularly fancy 170mb at that, but what do you expect from a game coded in pure HTML with over half a million nodes 😀. You can check out the final game on my website:

Tic Tac Toe in pure HTML