I made a simple Connect4 game for the web. This was a learning task so and so exploring different ideas and documenting issues along the way was just as important as finishing.
When building anything for the web I prefer not to use frameworks as they make it harder to understand the real effort or complexity caused by decisions made along the way. One decision I made early on was how to divide parts of the game between CSS and Javascript.
As much as possible, CSS will be responsible for displaying game states regardless of whether the game was being played, partially tested, or used only as a visual. Javascript will be used to add interactivity (click handlers, drag-and-drop, game clock, etc.) and turn-by-turn logic (evaluating and updating board state, adding a computer player).
This post is to look at the CSS component of the game. There were some challenges in making it work as initially envisioned but even more in using the same code to demonstrate and explain it here.
Getting started
First, we need elements to represent the different parts of the game. Second, a way of referencing those elements for styling (or logic).
Basic elements of the game
.board {}
.space {}
.p1 {}
.p2 {}
How can we represent a space being occupied by one of the players' pieces? A few possibilities come to mind, shown below. In all cases we'll need some interactivity to add child elements or modify classes so the choice right now mostly affects ease of access in CSS selectors. For that reason I prefer the last option.
Markup for representing pieces
Distinguishing a space matches between players it will be the .p1 and .p2 classes which carry it along.
The styling of .space will create the holes in the board. To distinguish any space as being occupied by either player it will simply have a .p1 or .p2 class added which will then set --player-color. This color can be used to style the space and the board itself when there is a winner.
After writing this up, the use of --player-color and --p1/p2-color looks confusing. The former is for applying the property to things (e.g. a space or the board) while the latter is for configuring the value itself. (TODO: explain better and/or refactor.)
Can we represent win conditions with CSS alone, and will it suck?
Modern CSS gives provides decent mechanisms for selecting elements based on more than just tag, class, or id names. The board state itself is just holding a series of values to express whether a space is empty or occupied by Player 1 or 2.
To detect a win we'll need to count those elements in a sequence to satisfy the rules of the game. To do that it will help to have a visual of each space element's index.
To apply some property to the board based on selectors matching its descendents the :has selector will be most helpful. In this case, it will only be to set the variable --winner-color with a player when their positions match one of the possible win conditions.
Great work, 25% bug-free! Now about that other 75%... the cases which failed show a win whenever part of the sequence wraps around the last column.
The board is laid out as a 1-dimensional list, left-to-right, top-to-bottom. Selectors with the sibling combinator (+) are simply going down the list left-to-right. There are limited positions for where the first (or last) piece in a connected sequence of 4 can be to fit the board. What are they?
Constraints in plain English
Horizontal: the first (left-most) piece must be in the first 4 columns
Vertical: the first (top-most) piece must be in the first 3 rows
Diagonal, right-down: the first (top-left-most) piece must be in the first 4 columns AND first 3 rows
Diagonal, left-down: the first (top-right-most) piece must be in the last 4 columns AND first 3 rows
Visualizing and implementing constraints
Constraints as visual zones—hover cursor over positions to see valid sequences.Horizontal
Not the most elegant but not terrible? It might take a second to see why :nth-child was needed but the selectors and combinators are nothing exotic. It would be nicer if .p1 could be factored out as & as in nested rules or if :nth-child could accept multiple ranges in which case only one line would be necessary.
What if we wrap every 7 spaces in a row element? Deeper markup usually makes for more complicated selectors but it won't hurt to see where it leads.
What if we used 2-dimensional (row container) markup? Err...
.board:has (
/* OK */
.row > .p1 + .p1 + .p1 + .p1,
/* HMMM this isn't going to work is it */
.row > .p1 + * + * + * + * + * + *
) { ... }
I don't think this is it. As far as I can tell CSS does not allow wrapping selectors in parentheses to disambiguate order, so something like .row > (...) + .row > (...) + .row > (...) + .row > (...) will simply break styling.
Suppose it was a requirement to have row containers. Maybe it had to be represented with table elements instead of divs, or some other reason... I couldn't come up with any solution better than brute-forcing all possible win states because of the nesting challenges.
Continuing the implementation with constraints
Solution for diagonal checks. Each row with valid starting range gets a matching rule.
In the actual game each player takes turns filling the board from the bottom up. Turns can be taken by a human user (clicking for example) or computer player. Holding and mutating the board state with only legal moves and stopping or restarting gameplay after a win is detected is a job for Javascript.
That will be the subject of another post.
Miscellaneous observations
It would be more accurate to say that the CSS discussed here is not actually checking a win but rather given a legal board state, it is detecting a valid sequence of 4 player pieces.
Apparently the scoped attribute for style tags was removed from HTML5 a long time ago but in its place CSS got the @scope { ... } rule. While it can be used effectively for the same purpose the attribute makes much more sense to me. I like to do weird things like set style elements to display: block and apply different styles depending on whether they're scoped or not.
If :nth-child() accepted ranges and/or lists of values that would make foolhardy excursions like this one much less verbose. Example: .p1:nth-child(1...4, 11...14, 18...21) { ... }
As mentioned while looking at row containers, having parentheses to disambiguate selector level would open up possibilities. Or hey, lambda-like macros for selectors...
The CSS spec has something called a forgiving selector list which I'd never heard of before.
Dave Jacobs, 1942–2025
2025.01.07
An important mentor to myself and many passed away this morning.
He retired just about two years ago after 60 years on the job. He packed up and left the Bronx for Florida to spend time well deserved with family.
Dave was generous with his time and his knowledge. Stubborn in his beliefs. Simply put, he always cared and if you showed that you cared too, you could earn his respect. It wasn’t always easy but it was always rewarding and remains so years later.
Every morning before anyone from HR had their coffee, Dave would show up at his desk to shoot the shit with a buddy over the phone. Once in a while he’d drop to a whisper; a Dave whisper still carried at least three aisles over.
"See yous later."
The Dream Team... there were many like it, but this one was mine.
CGI Bikeshed
2024.11.21
Since getting started in web development back in the early 2000s many languages and frameworks have left their mark. Contact forms and guest books abounded and were often propped up by a script in someone's cgi-bin directory. Then came PHP which brought niceties such as embedding code and markup in the same file to make templated, dynamic pages. There were frameworks with varying modularity and opinions on how to structure a site before they were called apps.
Eventually Ruby on Rails entered the scene and knocked a lot of people's socks off. That was all swell until Django came along and demonstrated the convenience of an automatically generated admin interface. That felt powerful.
Javascript frameworks were next such as Meteor.js and others. This wave brought isomorphic apps, which operated server and client side, into the picture. Do operations on the client and let the server listen and react, taking care of things like persistent data storage.
I'm not reminiscing. Capabilities and understanding of security threats have improved greatly. It's still a wild west, but it was an even wilder west back then. Sitting at the computer today, wanting to do little more on my own sites than publish a some words now and again without having access to a terminal and without the worry of maintaining yet another thing, I'm thinking about CGI and how straightforward the technology choice was for sites which were mostly static, but a tiny bit dynamic.
For this site I'm not dealing with any of that. There are three files, one HTML, one CSS, and one JS, which I edit and overwrite on the server when I feel like it. It would be nice to do this from my phone however, or even a public computer, without having any special tools. What I'm imagining is just a simple editing page from which I can click "Publish" and a script to do nothing more than append the new content to this HTML file.
Keeping dynamic functionality separate from the website has some advantages. Security-wise, access control can be managed by the server application. The editor can be shut down with no effect on anyone just browsing the static site. Backup and migration of content is as simple as copying a few static files. In the future, exploring different platforms and approaches can be developed without bothering anything.
This online book is a straightforward guide to getting started with code by building on the web. I've used a similar method before to teach others, eventually transitioning to Javascript (before anyone complains that HTML and CSS aren't "real" coding) and it's much more enjoyable than dealing with terminals, binaries, etc.
Good iTunes, Bad iTunes, Good Music, Bad Music
2024.10.13
I was importing a pile of CDs recently now that I have a proper drive to do so. Few of them are available on streaming services and, in any case, I've had enough experience with labels going in and out of licensing agreements to know that an album could disappear at any time if I don't own and manage the files myself. Might as well do that for the albums I own physically already. I haven't imported a CD since Apple Music was called iTunes and icon had an actual CD in it. Not too much should be different though, right?
Importing the first few in the stack was fine. As expected, there were a few discs which it could not find album artwork for which isn't surprising. I'm not actually sure where Music gets its metadata from these days. Does it attempt to pull from Apple Music (the streaming service) first and then fall back to MusicBrainz or similar? Just MusicBrainz? Anyway it's easy to live without artwork for a little while as long as the other information is there, enough to look up the album later even if it's a little obscure.
At some point, I was pleased to be asked to disambiguate based on what Music had looked up. It would be nice to get a little more information when the options are similar, such as when it returned three different options for "David Lang - Pierced, Real Quiet...". None of them were exactly as written on the album packaging but it wasn't clear if there was any better or worse one to choose. For this Henry Kaiser disc it was much easier.
I'm really curious about this "Ihop Archive".
Getting deeper into my stack, there was a problem. Some discs showed up with no metadata at all. No big deal right? I'll import the disc and then manually edit it once I put the next one in. After a few minutes, the import is finished and I open Recently Added but...
View Options clearly missing its chance to show just a middle finger emoji.
Why does each track show up as its own album? Why is there no way to change the view to... anything except this album-shelf view? Opening the Show View Options menu items gave a glimmer of false hope. Multiple selection is also disabled in this view, so I couldn't just select the unknown tracks and give them an album name in one shot. Actually, I couldn't even select an album at all because clicking an album takes you into its own view with the lone track. I'd need to right-click the album, Get Info, and edit there. Is it faster to edit all the info one track at a time, or to do this right-click on album just to set the album name, and then go back and edit the tracks together? I'm not sure but it's annoying either way. Others have complained about this in the wasteland which is the Apple Support Community.
Luckily, there was one trick which still worked. Create a new Smart Playlist, leave the options at default which means Artist is blank. Now I've got a song list which I can select and edit as needed. One possible downside is that for this to work it needs to be done one disc at a time. That was okay for me because I planned on getting it done right then, but I can imagine the nightmare if I thought I could batch them a couple at a time and that had included the experimental album with a hundred something tracks...
An additional note on importing: when Music fails to gather any metadata an alert pops up asking if you still want to import. That's helpful especially if automatic import is enabled and it also led to me trying something I consider a little weird: editing the metadata on the disc itself. Not really, but in the view of the disc in Apple Music.
It isn't indicated in the UI but all fields are editable here.
If I wanted, I could pop a disc in, see that it can't find any metadata, edit it all from there in a normal view with all tracks, and not even notice the annoying individual track albums that occur when importing without metadata. Importing takes a few minutes per disc though which is the perfect amount of time to edit metadata, so despite realizing this it didn't seem very practical for my task. And it feels kind of weird to edit something that can't really be edited. (This would feel less weird if the UI gave an indicator that some metadata was edited and that the track would need to be imported for it to be preserved. A little colored dot before the track name would be dandy.)
Alternative to 80/20
2024.10.06 at 11:31
Misumi runs cheaper than 80/20, calculates pricing on custom dimensions, and ships direct. Wait while I put my wallet in a vault.
Take a breather
2024.10.02 at 10:05
I lay down on the concrete floor and buttoned up my shirt jacket. I lied down on the concrete floor and buttoned up my shirt jacket. I laid myself down on the concrete floor and buttoned up my shirt jacket. Frequently I find myself saying a thing one way, writing it another, and thinking a bunch of stupid ways in between when they're not the same and I notice. Chickens lay eggs, people lie down. Some people lie to get laid.
John Oliver
2024.09.30 at 13:05
Speaking of John Oliver... he was interviewed on the New York Times Podcast and discusses a bit about how his show Last Week Tonight gets made from research, to writing, rage, and... maybe, journalism...
Elsewhere Online: Experimental History by Adam Mastroianni
2024.09.30 at 12:40
Perhaps... no, absolutely my favorite thing to read this year since being introduced via the article How to get 7th graders to smoke. If John Oliver had an academic alter ego who wrote a blog. I have yet to read a post that didn't provide a laugh and new insight into something I can practically comprehend.