TL;DR
You can skip all that writing below and just head straight over to the finished CSS-Only 1D chess game if you want. The game uses hidden <input type="radio"> elements driven by selectively shown <label for="..."> elements to move between all possible states of a 1D chess game.
Backstory
When browsing Hacker News a bit over a week ago, I saw the 1D-Chess project by Rowan Monk and for whatever reason I thought, "hey, the mechanics of this seem simple enough, I should try and make a CSS-only version of it."
With a bit of a search on the web to see what else there was out there in a similar vein, I also stumbled across a much earlier version of the same game from Christian Selig which preceded Rowan's version by about five years.
After playing more than a handful of games of 1D chess and finally figuring out "the secret" to winning, it was time to start on my own version.
Getting Started
I first wanted to make a mockup board to figure out how I was going to move the pieces around and what inputs I'd be able to use to track the game state and how little in the way of HTML elements I could get away with for everything.
Now that the primer is out of the way, let's start out with the first iteration of the board I went with. It was entirely hand crafted as I like to do in my prototyping phase, and it looked roughly like this:
<div class="board">
<div class="tile">
<div class="king white"></div>
</div>
<div class="tile">
<div class="knight white"></div>
</div>
<div class="tile">
<div class="rook white"></div>
</div>
<div class="tile">
</div>
<div class="tile">
</div>
<div class="tile">
<div class="rook black"></div>
</div>
<div class="tile">
<div class="knight black"></div>
</div>
<div class="tile">
<div class="king black"></div>
</div>
</div>
Then to allow you to "pick up" a piece for movement, I had initially inserted <label> elements into each of the pieces and had corresponding <input type="checkbox"> elements.
<style type="text/css">
body:has([name="king_white"]:checked) .piece.king.white {
/* style to indicate it has been "picked up" */
}
body:has([name="king_white"]:checked) .piece:not(.king.white) > label {
display: none;
}
/* ... */
</style>
<input type="checkbox" name="king_white">
<input type="checkbox" name="knight_white">
<!-- ... -->
<div class="board">
<div class="tile">
<div class="piece king white">
<label for="king_white"></label>
</div>
</div>
<div class="tile">
<div class="piece knight white">
<label for="knight_white"></label>
</div>
</div>
<!-- ... -->
</div>
The first thought behind this draft was so that if you had clicked a piece, it would then use display: none on the labels for selecting the other pieces. One of the biggest downsides to this initial setup was that pieces were sitting within individual tiles, meaning that when it came to moving them from tile to tile, we would then need a corresponding set of <div> and <label> elements for the piece in each tile we wanted it to be in.
Getting Over It
So I then moved the pieces separately to the tiles within the board with the view that I would position the pieces over the appropriate tile rather than within it.
This led the HTML to look roughly as follows:
<style type="text/css">
:root {
--tile-size: 100px;
}
body:has([name="king_white"]:checked) .piece:not(.king.white) > label {
display: none;
}
.king.white {
left: calc(var(--tile-size) / 2);
}
/* ... */
</style>
<input type="checkbox" name="king_white">
<input type="checkbox" name="knight_white">
<!-- ... -->
<div class="board">
<div class="tile"></div>
<div class="tile"></div>
<!-- ... -->
<div class="piece king white">
<label for="king_white"></label>
</div>
<div class="piece knight white">
<label for="knight_white"></label>
</div>
<!-- ... -->
</div>
While the CSS here is obviously just illustrative, the gist of it is that the position of each piece is then calculated based on the size of the tiles.
Checkbox or Radio?
The problem with the approach of storing the state for performing actions in chess as checkboxes is that if you use a checkbox to indicate "picking up" the piece, then for moving the piece you need to toggle multiple inputs which means multiple clicks, e.g.
- A click to move the piece to the tile you need
- A click to "put down" the piece (i.e. so it's no longer selected); and optionally
- A click to "take" another piece if you're capturing it
So this means you've got potentially three clicks just to complete what is realistically just one action in a game of chess.
To solve this, it means we're really looking at using <input type="radio"> for storing our game state instead and there's just three types of state to store if we're going that way:
- A piece being picked up (i.e. ready to move/capture)
- Performing the desired action (i.e. resolving the move/capture)
- Terminal states (i.e. stalemate, checkmate, etc)
That means we would then be encoding every possible board state into individual radio button values. That means a lot of radio buttons, right? Well... at this point I was pretty sure it was going to be a fair few, but nothing too wild given the limited board size and number of pieces. I hadn't whipped out the calculator yet, but I wasn't going to let it scare me off.
At this point the HTML was looking roughly like this now:
<style type="text/css">
:root {
--tile-size: 100px;
}
body:has(#initial_state_king_selected:checked) .piece:not(.king.white) > label {
display: none;
}
.piece {
--tile: 0;
left: calc(var(--tile-size) * (var(--tile) - 0.5));
}
body:has([name="initial_state"]:checked) .king.white {
--tile: 1;
}
body:has([name="initial_state"]:checked) .knight.white {
--tile: 2;
}
/* ... */
</style>
<input type="radio" name="state" id="initial_state">
<input type="radio" name="state" id="initial_state_king_selected">
<input type="radio" name="state" id="initial_state_knight_selected">
<!-- ... -->
<div class="board">
<div class="tile"></div>
<div class="tile"></div>
<!-- ... -->
<div class="piece king white">
<label for="initial_state_king_selected"></label>
</div>
<div class="piece knight white">
<label for="initial_state_knight_selected"></label>
</div>
<!-- ... -->
</div>
Before we're even into states past the initial selection of your piece, you can see we're starting to grow a bit in size and complexity. I knew I was ultimately going to need to write a helper script to generate the inputs, labels, and selectors for many of the states because it'd be unreasonable to write them all by hand, so at this stage I moved my sights to a Ruby script to help out.
Helper Script
I won't really get into the Ruby script side of things as I think it's probably the least interesting part of making this; it exists solely just to reduce the tedium of manually crafting all of the elements. However, the approximate structure I went with for it is as follows:
| File | Purpose |
|---|---|
lib/engine.rb |
Simple 1D chess engine for calculating valid moves, determining winners, etc |
lib/state.rb |
Encodes all of the possible states to include what we need to use in the HTML/CSS |
templates/index.html.erb |
HTML for the page as an ERB template; uses the encoded state information |
templates/styles.css.erb |
CSS for the page as an ERB template; uses the encoded state information |
compile.rb |
Ties all of these together to generate the states, compile the templates, and output the final HTML |
After getting the first version of in place, I had a fully playable 1D chess game. It didn't look great at all, but it was functional. Or mostly functional I guess would better describe it.
There were a few issues that I encountered in terms of the gameplay. The main thing was that a few of the moves the computer was making didn't make any sense, so I had to tweak it a few times because my initial chess engine implementation wasn't great.
The other issue was that I had too many states. I needed to group like states together to reduce things, I also wanted states be considered terminal as soon as there was no possibility of a win down the path.
Repetitive Text
While I was at it, having a text indicator of what is happening was probably a solid idea, so I had the state generator build all of that text depending on the game state too, e.g. "White to move" and "Selected King at 1" etc. Then I had that text selectively shown depending on the state.
How did that look though? Roughly like this:
<div class="status-panel">
<div class="status s0">White to move.</div>
<div class="status s0_sel_0">Selected King at 1..</div>
<!-- ... -->
<div class="status s1_sel_0">Selected King at 1..</div>
<!-- ... -->
<div class="status s6_sel_0">Selected King at 1..</div>
<!-- ... -->
</div>
A lot of repetition that can be reduced to keep the footprint a bit smaller. So I grouped like status text together and ended up with something a bit more like this instead:
<div class="status-panel">
<div class="status status-group-0">White to move.</div>
<div class="status status-group-1">Selected King at 1..</div>
<div class="status status-group-2">Selected King at 2..</div>
<!-- ... -->
</div>
There was also a lot of repetition in my earlier CSS compilation. Lots of individual or duplicated selectors and rules that I spent a bit of time cleaning up to make them a bit nicer to look at. The repetition wasn't a critical issue because the page would be compressed when served up, so the filesize would come way down anyway, but it was irking me so it had to go (or mostly go, at least) as well.
3D 1D Chess
So with all of that in place, it was onto making the thing look pretty. For whatever reason, into my brain popped the title "3D 1D Chess" and I thought it was quite a funny title, so that's when I decided that instead of having the top down view that "everyone else has," I was going to make it "3D."
So off I went. Instead of constructing the pieces out of a multitude of <div> elements to make them 3D (which is entirely possible!), I chose to make only the board itself 3D and instead using billboard sprites for the pieces. I grabbed a few public domain piece SVGs and set about using those.
I used one SVG per piece type (King, Knight, Rook) and used CSS filters to adjust the colour for them. The source pieces were more of a gold colour, but with some nicely tuned filters, getting a decent white and a decent black colour for the pieces wasn't too bad.
With a few more CSS variables in place, I was able to drive a lot of more of the state display with them and was also able to use them for things like hover states, etc. For example, here are some of the CSS variables I had:
.piece {
--direction: 1; /* facing right (1) or left (-1) */
--lifted: 0; /* if the piece was "picked up" */
--brightness: 1; /* adjust piece's brightness */
/* ... */
}
I was able to lean on things like the --lifted variable for the :hover state of pieces by setting it to 0.5 to show the piece slightly off the board but not fully picked up, indicating that it is a piece you can pick up to move.
Adding Some Polish
On top of all of that, I started styling generally with different cursors depending on what actual is being performed. Adding some subtle (or at least somewhat subtle) things like a red glow behind a piece you can capture with your selected piece and all that sort of thing.
Once that was mostly finished, I set my sights on showing off a bit more of that third dimension I had going for it. I first added a simple animation that would trigger when the game was won. The animation would continually spin the board slowly. It looked sort of cool and I liked it.
The problem with the first version? The pieces wouldn't spin with it, so I had to spin each of them the opposite direction so they would always be facing the viewer. The next problem? After half a turn, the knights would then be facing the opposite directions, so I had to add a separate animation for them to flip using transform: scaleX(-1) when they hit half a turn so they'd always be facing where you'd expect them to be.
With all that fun out of the way and ith some other tweaks and cleanup as I went along, I was pretty much at the finish line. But who really wants to finish a project like this? Apparently not me, I guess, because I then decided I wanted to show off that 3D effect a bit more obviously. How did I do that? By adding some "rotation" controls to the page.
I knew I had to use <button> elements and rely on the :active state to determine when they were being pressed, but I had a few false starts for actually controlling the rotation, and ultimately ended up settling on using two separate CSS animations that each drove custom properties; one for rotation to the left, one for rotation to the right, then adding them together to generate the final rotation value.
It looks roughly like this in terms of the CSS:
@property --rotate-base-left {
syntax: '<angle>';
initial-value: 0turn;
inherits: true;
}
@property --rotate-base-right {
syntax: '<angle>';
initial-value: 0turn;
inherits: true;
}
:root {
--rotate-base-left: 0turn;
--rotate-base-right: 0turn;
--rotate-button-speed: 10000ms;
}
body {
animation:
rotate-left var(--rotate-button-speed) linear infinite paused,
rotate-right var(--rotate-button-speed) linear infinite paused;
}
body:has(.rotate-left:active) {
animation-play-state: running, paused;
}
body:has(.rotate-right:active) {
animation-play-state: paused, running;
}
.game-area {
--game-area-rotate-y: calc(var(--rotate-base-left) + var(--rotate-base-right));
}
I later learned of the animation-composition property when trying to solve some other visual quirks I introduced with the rotations and I think it could potentially be a simpler approach to this instead of the custom properties, however I never got around to trying it as it took me long enough to get the rotation controls in a good state without throwing another wrench into the works.
3D/2D 1D Chess?
So now that I'm once again nearing the finish line, what does my brain tell me? "Hey David," (that's me) "why don't you add a toggle so you can switch between the 3D view and the traditional 2D top down view? Then you can call it 3D/2D 1D Chess!" Ahhhhh... it's happening again... scope creep! I've managed to fend it off for now by writing this blog post instead, but we'll see how long that lasts.
Board Themes
Part way through writing the blog post I decided I wanted to add some "themes" for the board with a little swatch toggle in the row of controls so you could cycle through a handful of themes. First I wanted some traditional themes and a couple of fun themes like neon pink and vaporwave style aesthetics, but the non-traditional ones turned out to be not so great (at least how I could style them) so I stuck with just two extra themes; "ebony marble" and "tournament green."
The HTML for this was pretty straightforward:
<input type="radio" name="theme" id="theme-default" checked>
<input type="radio" name="theme" id="theme-ebony-marble">
<input type="radio" name="theme" id="theme-tournament-green">
<div class="theme-buttons" title="Switch board theme">
<label class="theme-button" for="theme-ebony-marble">
<span class="board-theme-swatch"></span>
</label>
<label class="theme-button" for="theme-tournament-green">
<span class="board-theme-swatch"></span>
</label>
<label class="theme-button" for="theme-default">
<span class="board-theme-swatch"></span>
</label>
</div>
And the CSS for both controlling the theming and what button is shown is pretty simple too:
:root {
--board-white-tile-colour: #f9e2ac;
--board-black-tile-colour: #ad7f65;
/* ... */
}
body:has(#theme-ebony-marble:checked) {
--board-white-tile-colour: #d9dce1;
--board-black-tile-colour: #4a4f57;
/* ... */
}
body:has(#theme-tournament-green:checked) {
--board-white-tile-colour: #f1ebd8;
--board-black-tile-colour: #5f7a52;
/* ... */
}
body:has(#theme-default:checked) [for="theme-ebony-marble"],
body:has(#theme-ebony-marble:checked) [for="theme-tournament-green"],
body:has(#theme-tournament-green:checked) [for="theme-default"] {
display: inline-flex;
}
.board-theme-swatch {
display: block;
background: var(--board-black-tile-colour);
width: 15px;
height: 15px;
}
Basically some tile colour variables depending on the selected theme, a small swatch square for the current theme colour, and selectively displaying the label to cycle to the next theme. Pretty simple stuff once broken down a bit.
Another Dimension
So I couldn't help myself. After I'd written the blog post I ended up adding 2D mode... and it's now the default. Just click the 2D text in the heading to toggle 3D mode. The working title of the post was "Making CSS-only 3D 1D Chess," but now I get to call it "Making CSS-only 3D/2D 1D Chess" which is even more exiciting!
Did I need to add another dimension to the game? No, I did not. Do I have any great reason for doing it other than it let me have an even cooler name for the project? I don't know, it just is what it is, I guess.
So how did I get that other dimension in place? You guessed it. A checkbox input. Check and uncheck the checkbox with the appropriate <label> drives the transition between 2D and 3D. A little extra HTML and CSS also let me show what mode it currently is so I could make the mode selector a little more discreet and a little more fun.
This is what the the HTML and CSS looks like for that:
<style type="text/css">
body:has(#view-2d:checked) .dimension [data-view="2d"] {
display: inline;
}
body:has(#view-2d:checked) .dimension [data-view="3d"] {
display: none;
}
body:has(#view-2d:not(:checked)) .dimension [data-view="2d"] {
display: none;
}
body:has(#view-2d:not(:checked)) .dimension [data-view="3d"] {
display: inline;
}
</style>
<!-- ... -->
<input type="checkbox" name="view-2d" id="view-2d" checked>
<!-- ... -->
Current mode:
<span class="dimension">
<label for="view-2d" data-view="3d">3D</label>
<label for="view-2d" data-view="2d">2D</label>
</span>
To actually make the board toggle to 2D mode required having the appropriate images for each piece, so I found some public domain piece images and dropped them in the folder and updated the CSS for the individual pieces to use them. Then I leaned on the CSS variables I'd slowly been stacking up throughout the course of making the game to do most of the heavy lifting with this bit of work:
body:has(#view-2d:checked) {
--board-border-thickness: 2px;
--board-rotate-x: 0deg;
--rotate-base-left: 0turn;
--rotate-base-right: 0turn;
/* brightness(1) to make it a NOOP, otherwise using `none` cancels out the other filters being stacked on elsewhere */
--piece-white-filter: brightness(1);
--piece-black-filter: brightness(1);
}
Past that, it was mainly just some minor clean up and some extra polish needed for the 2D mode until I was happy enough with it. Things like changing the "move" highlights to the circles, disabling the rotation controls and the win state rotation animation, removing the shell/box from the board (doesn't need all that fancy 3D stuff for a 2D view as you can imagine), and generally just tweaks and nudges to the pieces to look nice.
The End
Anyway, that's pretty much the end of my post. Is there a lesson to be had from it, maybe its that you should try some stuff just because you can. Doing stuff can be fun and you can learn things when you do stuff with some absurd constraints like this. Sometimes the stuff ends up boring and you toss it aside, but sometimes it ends up kinda sort of cool and you're happy to show it to other people because even if they don't think it's cool, you know it's cool yourself and that's what matters most.
P.S. This post definitely has some of that "step 1. draw two circles, step 2. draw the rest of the f@#king owl" vibe to it (also, side note, let's bring the grawlix back) and that's because I wrote it well after actually finishing the project instead of taking any form of notes during. I will happily answer questions about it to fill in some of those gaps though, so there's that. Thanks for reading this far.
P.P.S. Here's a final link to the game so you don't have to scroll up to find it again. Thanks again for checking out the post, it probably took more effort to put together than the game itself. I also had to put together a website so I could even have somewhere to post this too, fortunately the rest of the website wasn't too bad to whip up because I could just do a good ol' stream of consciousness for most of the content.