Overview
After writing a Tic-Tac-Toe game without any JavaScript, I challenged myself to do something even more involved. I decided that a nice animated "Connect Four" would be a fun experiment.
You can see the final version here: https://codepen.io/AJSpetner/pen/NWveNMw
Obviously, this is possibly the worst way to code a "Connect Four" game, but doing challenges like this forces me to think out of the box and sharpens my mind for developing (or managing developers, which is most of what I do these days).
Challenges
Working within the confines of CSS means that there is no real ability to apply logic or maintain memory states other than the state of toggled elements, such as checkboxes and radio buttons. You can create selectors based on combinations of selections, but you are also limited by the narrow scopes of CSS selectors. For example, you can only style based on parent elements or preceding siblings and there is no easy way to style based on the count of toggled elements.
How it works
As such, I designed this game with 84 (42 for red, 42 for yellow) hidden checkbox <input>
elements, each linked to a <label>
, both with classes to indicate color, column, and row.
<form class="board-container">
<input type="checkbox" id="r-r1c1" class="move red r1 c1">
<input type="checkbox" id="r-r1c2" class="move red r1 c2">
<input type="checkbox" id="r-r1c3" class="move red r1 c3">
<input type="checkbox" id="r-r1c4" class="move red r1 c4">
<input type="checkbox" id="r-r1c5" class="move red r1 c5">
<input type="checkbox" id="r-r1c6" class="move red r1 c6">
<input type="checkbox" id="r-r1c7" class="move red r1 c7">
...
The checkboxes are at a level above and/or preceding all other elements within the game. The labels for each column are stacked on top of each other such that the label for the lower cells are on top of the higher cells. When a label's checkbox is checked, the label is hidden, thus allowing the label below it to be clicked.
The default is to show all red labels and hide all yellow labels, because Red's turn is first. A series of ridiculously long rules checks for every possible odd-number count of checked inputs. If the number of checked inputs is odd, that means we need to hide all red labels and show the yellow ones, because it is Yellow's turn.
Determining a winner
The rules to check for a winner were actually easier to write, since we just need to check for three match patterns for both red and yellow. A horizontal win is the easiest, as we are just looking for a checked box immediately followed by three checked boxes of the same color which are not in the first column (because if any the subsequent checked boxes were in the first column, it would mean the row wraps, which is not a winning situation).
input.move.red:checked
+ input.move.red:not(.c1):checked
+ input.move.red:not(.c1):checked
+ input.move.red:not(.c1):checked
A vertical win is a bit more complicated since we need to find a checked box immediately followed by six checkboxes (state irrelevant), immediately followed by another checked box, immediately followed by six more checkboxes, immediately followed by yet another checked box, immediately followed by six checkboxes, immediately followed by one last checked box.
input.move.red:checked
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red:checked
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red:checked
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red:checked
Then there are the diagonal wins. There are two types of diagonal wins, forward and reverse. Both are detected similarly to a vertical win, with the following exceptions:
For a forward diagonal win, we need seven check boxes between the checked boxes, and we also need to ensure that the first checked box is only in the first four columns (because if it were in columns 5, 6, or 7 that would mean that we don't have a straight diagonal line.
input.move.red:is(.c1, .c2, .c3, .c4):checked
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red:checked
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red:checked
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red:checked
And for the reverse diagonal win, we want only five checkboxes between the checked boxes, and we want to make sure that the first checked box is only one of the last four columns (same reason as above).
input.move.red:is(.c4, .c5, .c6, .c7):checked
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red:checked
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red:checked
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.move.red
+ input.red:checked
Tie game
Perhaps the most annoying rule to write was the tie game rule. Here we just need to check if there are 42 checked boxes, regardless of their color. If there have been 42 moves played and we didn't trigger a winner rule, that means the game is over with no winner. Since, as we mentioned above, there is no way to select based on the number of elements that match a particular selection, we just had to check for a checked box followed (not necessarily immediately) by 41 additional checked boxes (it's long - check the CodePen for the rule, I'm not posting it here).
In conclusion
Once I worked out the logic of the game, I added some crude color and animation styles to make it all work smoothly and look (somewhat) nice. The game works - once there's a winner, the "New Game" <button>
just has type="reset"
on it, so clicking it clears all of the checkboxes, which causes the game to reset to its initial state.
It's actually pretty simple once you think about it (hey, it's CSS - of course it's simple once you've thought it through). While not really worth anything, coding things like this is good for mental stimulation and helps you code better in the long term.
I hope you enjoyed it! I encourage you to dig through the code yourself and let me know what you think. 🙂
Top comments (0)