如何使用純 CSS 製作四子連珠遊戲

來源:互聯網
上載者:User

序言:你是否想過單純使用 CSS 也可以製作一款遊戲?甚至可以雙人對決!這是一篇非常有趣的文章,作者詳細講解了使用純 CSS 製作四子連珠遊戲的思路以及使用奇淫巧技解決困難問題的方法。因為案例本身比較複雜,而本人水平有限,翻譯必有不恰當之處,歡迎留言評論。

原文:How the Roman Empire Made Pure CSS Connect 4 Possible

翻譯:nzbin

實驗是學習新技巧、思考新想法、並突破自身極限的有趣的方式。“純 CSS”示範很早就有了,但是隨著瀏覽器和CSS的發展,新的挑戰又出現了。CSS 和 HTML 前置處理器也促進了純 CSS 示範的發展。有時候,預先處理程式用於每個可能的寫入程式碼情境,比如 :checked 的長字串和相鄰兄弟選取器。

在本文中,我將介紹使用純 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、2、4等)時紅色玩家移動,為奇數時黃色玩家移動。很快我就意識到一般的兄弟選取器不能(也不應該!)按照我想要的方式工作。

還有一種方式就是使用 nth 選取器。儘管我喜歡使用evenodd這樣的關鍵詞,但我還是走進了死胡同。: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' 字元的渲染寬度(小寫羅馬字母表示,在不同的瀏覽器中不同),cletter-spacing 的渲染寬度(常量)。我需要 v + c = w 為真,但這是不可能的,因為 cw 是整數,而 v 是非整數。最後我使用了 min-widthmax-width 屬性來約束可能的寬度值,因此我還將可能的計數器值更改為 'i' 和 'iii' ,以確保文本在流下變寬並溢出約束。通過方程 v + c < w3v + 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 個類型和類選擇符連結在一起。在圓孔元素後面添加一個類名為 .outcomediv 可以展示輸出的資訊。在被列包裹的一列中,檢測四子相連存在問題,但是我們先把這個問題放到一邊。

如果採用類似的方法判斷一行中是否有四子相連,那將是一個可怕的想法。每個玩家將會有 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 個外部資源

總的來說,我對結果很滿意,反饋也很好。做這個示範我確實學到了很多,我希望可以分享更多這樣的文章!

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.