Hans Sprecher

CSS AI II: Declared Intelligence

Hans Sprecher, @honzie

11.5.2013

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:

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:

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 and view 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:

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.

Want to help me abuse CSS more? Drop a line to @honzie.