CSS AI II: Declared Intelligence
After writing about crafting game AI with just CSS, I showed off the game around the office. The responses were enthusiastic, but it was obvious that further work was needed.
In particular, the original CSS took 200 lines just to do the game AI. It was very repetitive view the source of the demo page. This gave Theo, a coworker with a PhD in programming language design goosebumps. So, he asked an interesting question: can the game AI be written in CSS in a way that doesn't make a coder want to tear their eyes out?
Suprisingly, the answer is yes!
100 Bottles, Version II
Play the new demo. It has several improvements from the old one:
- The convuluted
::after
code that was attempting to remove the ability to click on beers not in play was removed. It was replaced withpointer-events: none
on future bottles. - The AI always wins. This game turns out to be a first-player-loses game.
- The Game AI takes only 22 lines. That's an order of magnitude less than the original.
- Numbers were added to the UI, to help avoid confusion when playing the game.
Now with 80% Less CSS
How was the CSS cleaned up so much? The answer is interesting: less CSS is required because the AI now plays perfect games, and perfect play in this game follows a pattern. A large amount of the code overhead was required because the first 120 lines were 'randomly' generated, so that the AI wasn't perfect.
Since there's no random feature in CSS, this means that all the sub-optimal play had to be hard-coded. That required a different declaration for each beer (two declarations per beer, actually).
However, the game has a simple strategy that is easy to declare in CSS:
- For each turn, drink the 11th beer, if possible. (beer #11, #22, #33, ..., #99).
- If you can't drink the 11th beer, drink any beer, and hope your opponent makes a mistake.
This works, because if you drink the 99th beer, you win. That means that if you drink beer 88, your opponent must drink at least 1 beer and at most 10 beers. This means that they may drink beers from #90-#98. When they drink any of these beers, you can drink the 99th beer and win. Abstracting this backwards gives the above rules.
Here's how this code looks like in CSS (To view the code bigger, just check out the demo's source):
/* Make the next 10 beers 'active' */ .b0:checked ~ input, input:nth-of-type(11n + 2):checked + input + input + input + input + input + input + input + input + input + input ~ input, input:nth-of-type(11n + 3):checked + input + input + input + input + input + input + input + input + input ~ input, ... input:nth-of-type(11n + 10):checked + input + input ~ input, input:nth-of-type(11n + 11):checked + input ~ input { opacity:1; background-position:0 0; } /* Make all beers past the next 10 beers 'inactive' */ .b0:checked + input + input + input + input + input + input + input + input + input + input ~ input, input:nth-of-type(11n + 2):checked + input + input + input + input + input + input + input + input + input + input + input + input + input + input + input + input + input + input + input + input ~ input, input:nth-of-type(11n + 3):checked + input + input + input + input + input + input + input + input + input + input + input + input + input + input + input + input + input + input + input ~ input, ... input:nth-of-type(11n + 10):checked + input + input + input + input + input + input + input + input + input + input + input + input ~ input, input:nth-of-type(11n + 11):checked + input + input + input + input + input + input + input + input + input + input + input ~ input { opacity:.3; cursor:default; }
What would this look like in JS?
Let's assume we wanted to do the above in JavaScript. Our code would potentially follow this structure:
- Attach a click handler to some elements (potentially inputs, potentially other elements).
- When the user clicks on a beer, attach a class to it and all beers up to it, to display them as drunk.
- Determine the number of the beer drunk.
- Have the computer drink a few beers (determined by the formula: 11 - beerIndex % 11).
It's obvious that this is cleaner than the CSS. Rather than 20 lines to determine how the computer reacts, we only need 1 or two lines of JS.
However, after playing with developing CSS like this, the one thing that I don't miss doing in JS is event handling. It feels very clean to leave the event handling up to the browser and its native form elements. There's something extremely satisfying about this, since the browser handles all the edge cases and interactions that are too easy to forget in JS.