序言:你是否想過單純使用 CSS 也能夠製做一款遊戲?甚至能夠雙人對決!這是一篇很是有趣的文章,做者詳細講解了使用純 CSS 製做四子連珠遊戲的思路以及使用奇淫巧技解決困難問題的方法。由於案例自己比較複雜,而本人水平有限,翻譯必有不恰當之處,歡迎留言評論。css
原文:How the Roman Empire Made Pure CSS Connect 4 Possiblehtml
翻譯:nzbinhtml5
實驗是學習新技巧、思考新想法、並突破自身極限的有趣的方式。「純 CSS」演示很早就有了,可是隨着瀏覽器和CSS的發展,新的挑戰又出現了。CSS 和 HTML 預處理器也促進了純 CSS 演示的發展。有時候,預處理程序用於每一個可能的硬編碼場景,好比 :checked
的長字符串和相鄰兄弟選擇器。css3
在本文中,我將介紹使用純 CSS 製做的四子連珠遊戲的關鍵思想。在個人實驗中,我儘可能避免硬編碼,而且不使用預處理器,專一於保持代碼的簡潔。如下是遊戲的全部代碼以及演示:編程
See the Pen Pure CSS Connect 4 by Bence Szabó (@finnhvman) on CodePen.瀏覽器
我認爲在「純 CSS」類型中有一些概念是必不可少的。一般,表單元素用於管理狀態和捕獲用戶操做。當我發現有人使用 <button type="reset">
重置或者從新開始新遊戲時,我很是興奮。只須要將元素包裹在 <form>
標籤中並添加按鈕。在我看來,這是一個比刷新頁面更方便的解決方案。安全
第一步就是建立表單元素,再在表單中建立一些用做圓孔(the slots)的 input,而後添加劇置按鈕。如下是使用 <button type="reset">
的基本演示:編程語言
See the Pen Pure HTML Form Reset by Bence Szabó (@finnhvman) on CodePen.函數
爲了讓演示好看一些,我使用 radial-gradient()
,而不是在遊戲板(the board)或者圓盤(the discs)上貼一張圖片。我常用 Lea Verou 製做的 CSS3 圖案庫。它是使用漸變製做的圖案集,並且很容易編輯。我使用了currentcolor,很是適合圓盤的圖案。我添加了頭部,而且複用了本身製做的純 CSS 波紋按鈕。佈局
如今,佈局和圓盤已經設計好了,只是還不能遊戲
接下來,須要讓用戶輪流將圓盤放到四子連珠的遊戲板上。在四子連珠遊戲中,玩家(一個紅色,一個黃色)輪流將圓盤放置在面板的列中。遊戲板有 7 列 6 行(一共有 42 個圓孔)。每個圓孔能夠爲空或者被一個紅色或黃色的圓盤佔用。因此,一個圓孔能夠有三種狀態(空、紅色或者黃色)。在同一列中掉落的圓盤會堆疊在一塊兒。
首先我爲每一個圓孔放置了兩個 checkbox 。當它們都沒有被選中時,圓孔就被認爲是空的,當其中一個被選中時,相應的玩家就會把他的圓盤放進去。
當其中任何一個被選中以後,應該把它隱藏起來,避免出現二者都被選中的狀態。這些 checkbox 是直接的兄弟類,因此若是選中第一個以後,可使用 :checked
僞類和相鄰兄弟選擇器(+
)來隱藏兩個元素。可是若是選中第二個呢?你能夠隱藏第二個,可是怎麼才能影響第一個呢?惋惜沒有選擇前一個的兄弟選擇器,這不是 CSS 選擇器的工做方式。我不得不拒絕這個想法。
實際上,一個 checkbox 自己能夠有三個狀態,可使用 indeterminate
狀態。問題是,僅僅使用 HTML 不能將其置於不肯定狀態。即便能夠,當再次點擊複選框時,它也會轉換成選中狀態。強迫第二個玩家在移動圓盤時進行雙擊是不現實的。
我仔細閱讀了 MDN 上關於 :indeterminate
的文檔後發現 radio input 通用都有 indeterminate 狀態。名稱相同的 radio 按鈕在未選中時都處於這種狀態。哇,這是一個真正的初始狀態!真正有用的是,選中後一個同胞元素也會對前者產生影響!因而我在遊戲板上放置了 42 對 radio input。
從以往的經從來看,使用 label ,並經過合理的順序搭配 checkbox 或 radio 能夠解決問題,但我認爲 label 不能使代碼更簡潔。
爲了得到更好的用戶體驗,我但願交互區域能夠更大一些,因此合理的作法是讓玩家點擊一個列來移動圓盤。經過在合適的元素上添加絕對和相對位置,我將同一列的控件相互疊加。這樣,在每一列中只能選擇最下面的圓孔。我仔細地設置了每一行的圓盤降低的時間,它們的時間函數近似於一個二次曲線,與現實中的自由落體類似。到目前爲止,遊戲的各部分都作好了,可是下圖清晰地顯示出只有紅色的玩家才能操做。
儘管已經設置了全部的控件,但只有紅色的圓盤能夠落在遊戲板上
我用彩色且半透明的矩形對 Radio input 的可點擊區域用進行了可視化顯示。黃色和紅色的 input 在每列上重疊 6 次(= 6 行),將最下面一行的紅色的 input 放在頂部。紅色和黃色的混合造成了橙黃色,能夠在遊戲板上看到。每一列中可用的圓孔越少,這種橙黃色就越不強烈,由於 radio input 只有在 :indeterminate
狀態時纔會顯示。因爲在每一個圓孔上,紅色 input 老是蓋住黃色 input,因此只有紅色的玩家可以移動。
我只有一個模糊的想法,就是能不能使用普通的兄弟選擇器解決玩家輪流遊戲的問題。這個想法就是統計選中的 input 的數量,爲偶數(0、二、4等)時紅色玩家移動,爲奇數時黃色玩家移動。很快我就意識到通常的兄弟選擇器不能(也不該該!)按照我想要的方式工做。
還有一種方式就是使用 nth 選擇器。儘管我喜歡使用even
和odd
這樣的關鍵詞,但我仍是走進了死衚衕。:nth-child 選擇器 「統計」父類中的子元素,包括全部類型,類、僞類等等。:nth-of-type 選擇器 「統計」在父類中某類型的子類,不包括類或僞類。因此問題就在於沒法經過 :checked 狀態去統計。
CSS counters 也能夠統計,因此爲何不試試呢?計數器的一個常見用法是在文檔中對標題(甚至多個級別)進行編號。它們由 CSS 規則控制,能夠在任什麼時候候被重置,其增長(或遞減!)值能夠是任意整數。計數器「counter()」函數顯示在 content 屬性中。
因此最簡單的方法就是設置計數器,而後統計四子連珠遊戲中 :checked
的 input 的數量。這種方法只有兩個困難。首先,你不能在一個計數器上執行算術運算來檢測它是偶數仍是奇數。其次,你不能基於計數器的值在元素上應用 CSS 規則。
我使用二進制解決了第一個問題。計數器的初始值設爲 0 。當紅色玩家選中 radio 按鈕時,計數器加 1。當黃色玩家選中 radio 按鈕時,計數器就減 1,以此類推。所以,計數器的值始終是 0 或 1,偶數或奇數。
解決第二個問題須要更多的創造力(read: hack)。如上所述,計數器只能顯示在 ::before
和 ::after
僞元素中。這是顯而易見的,但它們如何影響其餘元素呢?至少計數器值能夠改變僞元素的寬度。不一樣的數有不一樣的寬度。字符 1
一般比 0
纖細,但這是很難控制的。若是改變的是字符的數量,而不是字符自己,那麼由此產生的寬度變化就是可控的。在 CSS 計數器中使用羅馬數字並很多見。用羅馬數字表示的 1 和 2 與字符 1 和 2 是相同的,它們的像素寬度也是相同的。
個人想法是將一個玩家(黃色)的單選按鈕貼着左邊放置,並將另外一個玩家(紅色)的單選按鈕貼着共享父容器的右邊放置。最初,紅色的按鈕被覆蓋在黃色的按鈕上,而後容器的寬度變化會致使紅色的按鈕「消失」,顯示黃色的按鈕。能夠將其比做現實中有兩個窗格的滑動窗口,一個窗格是固定的(黃色按鈕),另外一個是可滑動的(紅色按鈕)。區別在於,在遊戲中只有一半的窗口是可見的。
到目前爲止,還不錯,但我並不滿意使用 font-size
(以及其餘 font
屬性)間接控制寬度。更好的方式是使用 letter-spacing
,由於它只在一個維度上改變了大小。出乎意料的是,即便是一個字母也有字母間距(在字母后面呈現),兩個字母就有兩個字母間距。可靠性的關鍵就是保證寬度是可預知的。寬度爲 0 的字符加上單字母和雙字母間距均可以,可是將 font-size
設置爲 0 是存在風險的。爲了兼容全部瀏覽器,能夠將 letter-spacing
(以像素爲單位)設置的大一些而且將 font-size
設置的小一點(1px
),是的,我說的是子像素。
我須要容器的寬度在初始大小(=w
)與至少兩倍以上大小(>=2w
)之間交替變換,以便可以徹底隱藏和顯示黃色按鈕。假設 v
是 'i' 字符的渲染寬度(小寫羅馬字母表示,在不一樣的瀏覽器中不一樣),c
是 letter-spacing
的渲染寬度(常量)。我須要 v + c = w
爲真,但這是不可能的,由於 c
和 w
是整數,而 v
是非整數。最後我使用了 min-width
和 max-width
屬性來約束可能的寬度值,所以我還將可能的計數器值更改成 'i' 和 'iii' ,以確保文本在流下變寬並溢出約束。經過方程 v + c < w
, 3v + 3c > 2w
,,v << c
,能夠獲得2/3w < c < w
。結論就是「字母間距」必須比初始寬度小一些。
我一直覺得僞元素顯示的計數值是 radio 按鈕的父元素,惋惜不是。可是,我注意到僞元素的寬度改變了其父元素的寬度,在本例中父元素是 radio 按鈕的容器。
若是你在想,難道不能用阿拉伯數字來解決嗎?你說得對,計數器的值在 '1' 和 '111' 之間交替變換也是能夠的。儘管如此,羅馬數字最早給了我啓示,它們也是點擊器標題的不錯的方式,因此我保留了它們。
從紅色玩家開始,而後輪流遊戲
應用所討論的技術使 radio input 的父容器在選中紅色 input 時寬度加倍,在選中黃色 input 寬度變爲原來的寬度。在原始寬度的容器中,紅色 input 位於黃色 input 之上,而在雙寬度容器中,紅色 input 被移開。
在現實生活中,四子連珠遊戲並不會告訴你是贏了仍是輸了,可是提供適當的反饋是任何軟件良好用戶體驗的一部分。下一個目標是檢測玩家是否贏得了遊戲。要想贏得比賽,玩家必須在一列、一行或對角線上放四個圓盤。在許多編程語言中,這是一個很是簡單的任務,可是在純 CSS 世界中,這是一個巨大的挑戰。將它分解成子任務是系統地處理這個問題的方法。
我使用一個 flex 容器做爲 radio 按鈕和圓盤的父類。一個黃色的 radio 按鈕、一個紅色的 radio 按鈕和一個表明圓盤並與圓孔重疊的 div 。這樣的圓孔重複了42 次,並排列成多列。所以,列中的圓孔是相鄰的,這使得使用相鄰選擇器識別列中的四個是最容易的:
<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
這是一個簡單但醜陋的解決方案。爲了檢測一列中四子相連的狀況,每一個玩家都有 11 個類型和類選擇符連接在一塊兒。在圓孔元素後面添加一個類名爲 .outcome
的 div
能夠展現輸出的信息。在被列包裹的一列中,檢測四子相連存在問題,可是咱們先把這個問題放到一邊。
若是採用相似的方法判斷一行中是否有四子相連,那將是一個可怕的想法。每一個玩家將會有 56 個選擇器(若是我算對了的話),更不用說他們會有相似的檢測錯誤的狀況。在未來,:nth-child(An+B [of S]) 或者 column combinators 會派得上用場.
爲了更好的語義化,能夠爲每一個列添加一個新的 div
,並在其中排列圓孔元素。這一修改也將消除上述檢測錯誤的狀況。而後,檢測一行中的有四子相連能夠用如下方法:選擇第一個紅色 radio input 被選中的一個列,而後再選擇第一個紅色 radio input 被選中的相鄰同胞列,重複兩次。這聽起來很麻煩,須要"parent"選擇器。
選擇父節點是不可行的,可是選擇子節點是可行的。如何用選擇器及其組合方式檢測一行中的四子相連? 選擇一個列,再選擇它的第一個被選中的紅色 radio input,而後選擇相鄰的列,再選擇它的第一個被選中的紅色 radio input ,以此類推,再重複兩次。這聽起來仍然很麻煩,但倒是可行的。訣竅不只在 CSS 中,並且在 HTML 中,下一列必須是上一列中建立嵌套結構的單選按鈕的同胞元素。
<div class="grid column"> <input type="radio" name="slot11"> <input type="radio" name="slot11"> <div class="disc"></div> ... <input type="radio" name="slot16"> <input type="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
語義混亂了,這些選擇器只適用於紅色的玩家(黃色的玩家有另外一輪),可是它確實有用。有一個好處是不會出現檢測錯誤的列或行。結果的顯示也必須進行修改,任何匹配列使用的 ::after
僞元素都應該是一致的。所以,必須在最後一個位置以後添加一個僞第八列。
如上面的代碼片斷所示,列的特殊的位置關係能夠檢測一行中的四子相連。可使用一樣的技術並經過調整這些位置來檢測對角線上的四子相連。注意對角線能夠在兩個方向上。
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
在最終的代碼中,選擇器的數量很是龐大,若是使用 CSS 預處理器則能夠顯著減小聲明長度。儘管如此,我認爲演示的代碼仍是比較短的。它應該是在中間的某個地方,從硬編碼一個選擇器到使用 4 個神奇的選擇器(列,行,兩個對角線)。
當有玩家得到勝利就會顯示一條信息
任何軟件都有邊緣狀況須要處理。四子相連遊戲的可能結果不只是紅色或黃色的玩家獲勝,並且會出現遊戲板被填滿的平局。從技術上講,這種狀況不會破壞遊戲或產生任何錯誤,所缺乏的是對玩家的反饋。
咱們的目標是檢測出黑板上有 42 個 :checked
的單選按鈕,而且它們都沒有處於 :indeterminate
狀態。這就要求爲每一個單選按鈕作一個選擇。單選按鈕處於 :indeterminate
時是 invalid ,不然是 valid 。所以,我爲每一個 input 添加了 required
屬性,而後在表單上使用 :valid
僞類來檢測平局。
當遊戲板被填滿時會顯示平局的信息。
檢測平局結果出現了一個 bug。在極少數的狀況下會出現黃色玩家最終勝利的狀況,勝利和平局的消息都顯示出來了。這是由於這些結果的檢測和顯示方法是正交的。我解決了這個問題,確保獲勝消息有一個白色的背景,並在平局消息之上。還必須延遲平局消息的過渡,這樣它就不會與獲勝消息混合出現了。
黃方勝利的信息蓋住了平局結果
雖然許多單選按鈕是經過絕對定位隱藏在彼此後面的,可是全部處於不肯定狀態的按鈕仍然能夠經過 tab 鍵來訪問。這使得玩家能夠將他們的圓盤放入任意的圓孔中。處理這個問題的一種方法是簡單地禁止使用 tabindex
屬性進行鍵盤交互:將其設置爲 -1
意味着不該該經過連續的鍵盤導航來訪問它。爲了解決這個問題,必須在每一個單選按鈕上添加這一屬性。
<input type="radio" name="slot11" tabindex="-1" required> <input type="radio" name="slot11" tabindex="-1" required> <div class="disc"></div> ...
最實質性的缺點是,因爲輪流遊戲的解決方案不可靠,遊戲板沒有響應,而且可能在小的視圖窗口上出現故障。我不敢冒險重構響應式的解決方案,因爲實現的本質,硬編碼看起來更安全。
另外一個問題是觸摸設備上的 sticky hover 。在正確的位置添加一些媒體查詢是解決這個問題最簡單的方法,可是這會消除自由落體動畫。
有人可能認爲 :indeterminate
僞類已經獲得了普遍的支持,事實的確如此。問題是它只在一些瀏覽器中獲得部分支持。注意兼容性表中的註釋1:MS IE 和 Edge 在單選按鈕上不支持它。若是您在這些瀏覽器中查看演示程序,您的光標將變成 not-allowed
的光標,這是無心的,但有點優雅的降級。
不是全部瀏覽器都支持 radio 按鈕的 :indeterminate 屬性。
感謝閱讀到最後一部分!讓咱們看看這個遊戲的一些數據:
140 個 HTML 元素
350 行 (合理地) CSS
0 行 JavaScript
0 個外部資源
總的來講,我對結果很滿意,反饋也很好。作這個演示我確實學到了不少,我但願能夠分享更多這樣的文章!