Preface: Have you ever thought that you can make a game by simply using CSS? You can even duel with two people! This is a very interesting article, the author explained in detail the use of pure CSS to make four sub-line game ideas and the use of strange trick techniques to solve difficult problems. Because the case itself is more complex, and my level is limited, translation must be inappropriate, welcome comments.
Original: How the Roman Empire made Pure CSS Connect 4 Possible
Translation: Nzbin
Experimentation is an interesting way to learn new tricks, think new ideas, and break through their limits. The "Pure CSS" demo was long overdue, but as browsers and CSS developed, new challenges emerged. CSS and HTML preprocessor also promote the development of pure CSS presentation. Sometimes, a preprocessor is used for every possible hard-coded scene, such as:checkeda long string and an adjacent sibling selector.
In this article, I'll cover the key ideas of a four-sub-alignment game made with pure CSS. In my experiment, I tried to avoid hard coding, and I didn't use a preprocessor to focus on keeping the code simple. Here are all the code and demos for the game:
See the Pen Pure CSS Connect 4 by Bence Szabó (@finnhvman) on Codepen.
Basic concepts
I think there are some concepts that are essential in the "pure CSS" type. Typically, form elements are used to manage state and capture user actions. I was very excited when I found someone using<button type="reset">reset or restarting a new game. Just wrap the element in the<form>label and add a button. In my opinion, this is a more convenient solution than refreshing the page.
The first step is to create the form element, and then create some input in the form that is used as a round hole (the slots), and then add the Reset button. Here are<button type="reset">the basic demos to use:
See the Pen Pure HTML Form, Reset by Bence Szabó (@finnhvman) on Codepen.
To make the demo look good, I use it instead ofradial-gradient()sticking a picture on the game board (the Board) or the disc (the discs). I often use the CSS3 pattern Library made by Lea Verou. It is a set of patterns made with gradients and is easy to edit. I used the CurrentColor, which is perfect for a disc pattern. I added a header and reused my own pure CSS ripple button.
Now, the layout and disc have been designed, but not yet played.
Put the disc on the game board.
Next, you need to let the user take turns to put the disc on the four-sub-alignment of the game board. In the four-sub-alignment game, the player (a red, a yellow) rotates the disc in the column of the panel. The game board has 7 columns of 6 rows (a total of 42 round holes). Each round hole can be empty or be occupied by a red or yellow disc. Therefore, a round hole can have three states (empty, red, or yellow). The disks that fall in the same column are stacked together.
First I placed two checkboxes for each round hole. When none of them is selected, the round hole is considered empty, and when one is selected, the corresponding player will put his disc in.
When any one of them is selected, it should be hidden to avoid a state where both are selected. These checkboxes are direct sibling classes, so if you select the first one, you can:checkedhide two elements using pseudo-class and adjacent sibling selectors (+). But what if the second one is selected? You can hide the second one, but how do you affect the first one? Unfortunately did not choose the previous sibling selector, this is not the way CSS selectors work. I have to reject the idea.
In fact, a checkbox itself can have three states and can useindeterminatestate. The problem is that using HTML alone cannot put it in an indeterminate state. Even if you can, when you tap the check box again, it will also be converted to the selected state. It is unrealistic to force a second player to double-click when moving the disc.
After I read the documentation on MDN, I:indeterminatefound that radio input generic has a indeterminate status. The radio button with the same name is in this state when unchecked. Wow, this is a real initial state! What really works is that the next sibling element will have an effect on the former! So I placed 42 pairs of radio input on the game board.
In the past, using a label and matching a checkbox or radio in a reasonable order can solve the problem, but I don't think the label will make the code more concise.
To get a better user experience, I want the interactive area to be larger, so it's reasonable to have the player click on a column to move the disc. By adding the absolute and relative positions on the appropriate elements, I overlay the controls on the same column with each other. This allows you to select only the bottom round hole in each column. I carefully set the time for each row of the disk to fall, and their time function approximates a two-time curve, similar to the real freefall. So far, the parts of the game have been done, but it's clear that only red players can operate.
Although all the controls have been set, only the red disc can fall on the gamepad.
I used a color and translucent rectangle to visualize the clickable area of Radio input. Yellow and red input overlap 6 times (= 6 rows) on each column, placing the red input at the top of the bottom row. The red and yellow mixes form an orange-yellow color that can be seen on the game board. The fewer round holes available in each column, the less intense the orange is, because radio input is only:indeterminatedisplayed when the state is present. Because the red input always covers the yellow input on each round hole, only the red player can move.
Take turns game
I only have a vague idea that I can use a normal sibling selector to solve the problem of player rotation. The idea is to count the number of input selected, the red player moves for even (0, 2, 4, etc.), and the yellow player moves when it is odd. Soon I realized that the General Brotherhood selector could not (and should not!) ) Work the way I want.
Another way is to use the nth selector. Although I likeevento use andoddsuch keywords, but I still walked into a dead end. : Nth-child the child elements in the "statistics" parent class, including all types, classes, pseudo-classes, and so on. : Nth-of-type the selector "statistics" a subclass of a type in the parent class, excluding classes or pseudo-classes. So the problem is the inability to pass: checked state to count.
CSS counters can also be counted, so why not try it? A common use of counters is to number headings (or even multiple levels) in a document. They are controlled by CSS rules and can be reset at any time, increasing (or decreasing!). ) value can be any integer. The Counter "counter ()" function is displayed in the Content property.
So the simplest way is to set the counter and then count the number of input in the four-sub-alignment game:checked. This approach is only two difficult. First, you cannot perform an arithmetic operation on a counter to detect whether it is an even or an odd number. Second, you cannot apply CSS rules on the element based on the value of the counter.
I used binary to solve the first problem. The initial value of the counter is set to 0. When the red Player selects the radio button, the counter adds 1. When the yellow player selects the radio button, the counter is reduced by 1, and so on. Therefore, the value of the counter is always 0 or 1, even or odd.
Solving a second problem requires more creativity (read:hack). As mentioned above, counters can only be displayed in::beforeand::afterpseudo-elements. This is obvious, but how do they affect other elements? At least the counter value can change the width of the pseudo-element. The different numbers have different widths. Characters1are usually0thinner than slender, but this is difficult to control. If the number of characters is changed, not the character itself, then the resulting width change is controllable. It is not uncommon to use Roman numerals in CSS counters. The 1 and 2 represented by Roman numerals are the same as the characters 1 and 2, and their pixel widths are the same.
My idea is to place a player (yellow) radio button on the left side and place the other player's (red) radio button on the right side of the shared parent container. Initially, the red button is overwritten with a yellow button, and the width of the container changes to cause the red button to "disappear" and the yellow button is displayed. It can be likened to a sliding window with two panes in the real world, one pane fixed (yellow button) and the other sliding (red button). The difference is that only half of the windows in the game are visible.
So far, not bad, but I'm not happy with the usefont-size(and otherfontattributes) to control the width indirectly. The better way isletter-spacingto use it because it changes size only in one dimension. Unexpectedly, even if a letter has a letter spacing (rendered behind the letter), two letters have a two-letter spacing. The key to reliability is to ensure that the width is predictable. A character with a width of 0 plus a single-letter and two-letter spacing is possible, butfont-sizesetting it to 0 is risky. In order to be compatible with all browsers, you canletter-spacingset the larger (in pixels) andfont-sizeset it a little bit (1px), yes, I'm talking about sub-pixels.
I need the width of the container to alternate between the initial size (=w) and at least twice times the size (>=2w) so that the yellow button can be completely hidden and displayed. The rendering widthvc(constant) is assumed to be the ' I ' character's render width (lowercase roman letters are different in different browsers)letter-spacing. I needv + c = wto be true, but this is not possible, becausecandwis an integer, not anvinteger. Finally I used themin-widthandmax-widthattributes to constrain the possible width values, so I also changed the possible counter values to ' I ' and ' III ' to make sure that the text was widening and overflow the constraint. Through equations,,,,v + c < w3v + 3c > 2wv << ccan be obtained2/3w < c < w. The conclusion is that the "letter spacing" must be smaller than the initial width.
I've always thought that the value of pseudo-element display is the parent element of the radio button, but it's not. However, I notice that the width of the pseudo-element changes the width of its parent element, in this case the parent element is the container for the radio button.
If you're thinking, can't you solve it with Arabic numerals? You're right, the value of the counter alternating between ' 1 ' and ' 111 ' is also possible. Nonetheless, the Roman numerals were the first to give me a hint, they were also a good way to click on the titles of the taps, so I kept them.
Start with a red player and then take turns playing.
Applying the techniques discussed enables the parent container of radio input to double the width of the selected red input and the width of the selected yellow input to the original width. In the original width container, the red input is above the yellow input, and in the double-width container, the red input is moved away.
Recognition mode
In real life, a four-sub-alignment game doesn't tell you whether you won or lost, but providing the right feedback is part of a good user experience for any software. The next goal is to detect if the player has won the game. To win the game, the player must place four discs on a column, line, or diagonal. This is a very simple task in many programming languages, but in the pure CSS world, this is a huge challenge. Breaking it down into subtasks is a way to deal with this problem systematically.
I use a flex container as the parent class for radio buttons and discs. A yellow radio button, a red radio button, and a div that represents the disc and overlaps the round hole. Such a circular hole is repeated 42 times and arranged into multiple columns. Therefore, the round holes in the columns are contiguous, which makes it easiest to identify four of the columns using the adjacent selector:
<div class="grid"> <input type="radio" name="slot11"> <input type="radio" name="slot11"> <div class="disc"></div> <input type="radio" name="slot12"> <input type="radio" name="slot12"> <div class="disc"></div> ... <input type="radio" name="slot16"> <input type="radio" name="slot16"> <div class="disc"></div> <input type="radio" name="slot21"> <input type="radio" name="slot21"> <div class="disc"></div> ...</div>
/* Red four in a column selector */input:checked + .disc + input + input:checked + .disc + input + input:checked + .disc + input + input:checked ~ .outcome/* Yellow four in a column selector */input:checked + input + .disc + input:checked + input + .disc + input:checked + input + .disc + input:checked ~ .outcome
This is a simple but ugly solution. In order to detect a column of four sub-connected cases, each player has 11 types and class selection linked connected together. After the round hole element, add a class name to.outcomedivdisplay the output information. In one column of the listed package, there is a problem with the detection of the quad, but let's put the problem aside.
If a similar method is used to determine if there are four children in a row, it would be a horrible idea. Each player will have 56 selectors (if I am right), not to mention that they will have similar detection errors. In the Future,: Nth-child (an+b [of S]) or column combinators will come in handy.
For better semantics, you can add a new one to each columndivand arrange the round hole elements in it. This modification will also eliminate the above-mentioned detection errors. Then, the detection of four children in a row can be connected in the following way: Select the first red radio input selected column, and then select the first red radio input is selected adjacent sibling column, repeat two times. This sounds troublesome and requires a "parent" selector.
It is not feasible to select a parent node, but it is possible to select a child node. How to detect the four sub-connections in a row with a selector and its combined method? Select a column, select its first selected red radio input, select the adjacent column, select its first selected red radio input, and so on, and then repeat two times. It still sounds a lot of trouble, but it's doable. The trick is not only in CSS, but also in HTML, the next column must be the sibling of the radio button that creates the nested structure in the previous column.
<div class= "grid column" > <input type= "Radio" name= "slot11" > <input type= " Radio "name=" slot11 "> <div class=" disc "></div> ... <input type=" Radio "name=" slot16 "> <input t Ype= "Radio" name= "slot16" > <div class= "disc" ></div> <div class= "column" > <input type= "Radio" Name= "slot21" > <input type= "Radio" name= "slot21" > <div class= "disc" ></div> ... <input Type= "Radio" name= "Slot26" > <input type= "Radio" name= "Slot26" > <div class= "disc" ></div> < div class= "column" > ... </div> </div></div>
/* Red four in a row selectors */input:nth-of-type(2):checked ~ .column > input:nth-of-type(2):checked ~ .column > input:nth-of-type(2):checked ~ .column > input:nth-of-type(2):checked ~ .column::after,input:nth-of-type(4):checked ~ .column > input:nth-of-type(4):checked ~ .column > input:nth-of-type(4):checked ~ .column > input:nth-of-type(4):checked ~ .column::after,...input:nth-of-type(12):checked ~ .column > input:nth-of-type(12):checked ~ .column > input:nth-of-type(12):checked ~ .column > input:nth-of-type(12):checked ~ .column::after
Semantically confusing, these selectors apply only to red players (the yellow player has another round), but it does help. One advantage is that there are no columns or rows to detect errors. The display of the results must also be modified, and the pseudo-elements used by any matching column::aftershould be consistent. Therefore, you must add a pseudo eighth column after the last position.
As shown in the code snippet above, the special positional relationship of a column detects the four children in a row are connected. You can use the same technique and adjust these locations to detect the four sub-connections on the diagonal. Note that the diagonal can be in two directions.
input:nth-of-type(2):checked ~ .column > input:nth-of-type(4):checked ~ .column > input:nth-of-type(6):checked ~ .column > input:nth-of-type(8):checked ~ .column::after,input:nth-of-type(4):checked ~ .column > input:nth-of-type(6):checked ~ .column > input:nth-of-type(8):checked ~ .column > input:nth-of-type(10):checked ~ .column::after,...input:nth-of-type(12):checked ~ .column > input:nth-of-type(10):checked ~ .column > input:nth-of-type(8):checked ~ .column > input:nth-of-type(6):checked ~ .column::after
In the final code, the number of selectors is very large, and if you use a CSS preprocessor you can significantly reduce the length of the declaration. Nonetheless, I think the demo code is still relatively short. It should be somewhere in the middle, from hard-coded a selector to using 4 magical selectors (columns, rows, two diagonal lines).
When a player wins, a message is displayed.
Fix the vulnerability
Any software has edge situations that need to be addressed. The possible result of a four-child game is not only a red or yellow player winning, but a tie that fills the game board. Technically, this situation does not disrupt the game or produce any errors, and what is missing is feedback to the player.
Our goal is to detect 42 radio buttons on the blackboard:checkedand none of them are in:indeterminatestate. This requires that you make a selection for each radio button. The radio button is:indeterminateinvalid, otherwise it is valid. Therefore, I added a property for each inputrequiredand then used pseudo-classes on the form:validto detect a draw.
When the board is filled, a draw message is displayed.
A bug occurred while detecting the draw result. In rare cases there will be a case of the final victory of the yellow player, and the news of the win and the draw are displayed. This is because the detection and display methods of these results are orthogonal. I solved this problem by making sure that the winning message has a white background and is above the draw message. You must also delay the transition of the draw message so that it does not mix with the winning message.
Huangfang Victory's information covered the draw result
Although many radio buttons are hidden behind each other by absolute positioning, all buttons that are in an indeterminate state can still be accessed by the TAB key. This allows the player to place their discs in any round hole. One way to deal with this problem is to simply disallowtabindexkeyboard interaction using attributes: Set it to-1mean that it should not be accessed through continuous keyboard navigation. To solve this problem, you must add this property on each radio button.
<input type="radio" name="slot11" tabindex="-1" required><input type="radio" name="slot11" tabindex="-1" required><div class="disc"></div>...
Limit
The most substantial drawback is that the game board is not responsive and may fail on a small view window due to the unreliable solution of the rotation game. I dare not risk refactoring a responsive solution, hard coding looks more secure due to the nature of the implementation.
Another problem is touching the sticky hover on the device. Adding some media queries in the right place is the easiest way to solve this problem, but this will eliminate free-falling animations.
Some may think that the:indeterminatepseudo-class has been widely supported, and it is true. The problem is that it only gets partial support in some browsers. Note the comments in the compatibility table 1:ms IE and Edge do not support it on radio buttons. If you view the demo program in these browsers, your cursor will turn intonot-alloweda cursor, which is unintentional but somewhat graceful to downgrade.
Not all browsers support the radio button: Indeterminate property.
Summarize
Thanks for reading to the last part! Let's look at some of the data for this game:
Overall, I am satisfied with the results and the feedback is very good. I did learn a lot from doing this demo, and I hope to share more of these articles!