二營長,快掏個CSS出來給我畫個井字棋遊戲

m53469發表於2021-09-09
  • 作者:陳大魚頭
  • github:

前言

不知道大家小時候有沒有玩過一款遊戲叫『井字棋』的。

它長這樣:

圖片描述

(我贏了,快誇我 ~o(´^`)o)

上面的就是本次文章的最終結果,一個用純CSS實現的AI井字棋遊戲,Mmmm,雖然看起來有點蠢。。。

地址在此:

遊戲的規則比較簡單,就是在一個九宮格(據說十六宮格,二十五宮格也行~反正是格子就行),只要你下的棋能連成一條直線,就算贏。

所以這次魚頭就來教大家怎樣才能在這個遊戲中獲勝。

額,不對,大霧呀~

是怎樣透過純CSS來實現上面這個遊戲~

圖片描述

正文

先手選擇

透過開頭的GIF圖,我們可以看到其實這個遊戲是有先手選擇的。

我們可以選擇是玩家先下,還是電腦先下。

那麼如果透過單純的HTML標籤 + CSS屬性,該如何完成呢?

首先我們轉換下思路,先手選擇不是“我方”跟“電腦方”的選擇,而是“選擇我”以及“不選擇我”之間兩種狀態的切換,那麼基於這個原理,我們就很快可以聯想到<input type="checkbox"/>

有以下的效果:

圖片描述

但這裡還有一個問題,就是雖然我們實現了雙向選擇的效果,但是開頭的GIF圖裡先手選擇是一個好看的switch,明顯<input type="checkbox"/>無法實現這個功能,那怎麼呢?

嗯,所以我們還是用JS模擬吧!

(吃瓜群眾:說好的CSS呢?給我打)

圖片描述

對不起,我們可以用<label>標籤來模擬。

<label>標籤可以透過for="#hash"來跟<input id="#hash">來進行關聯,所以我們有以下效果:

圖片描述

原始碼如下:


<style>

.switch {

display: inline-block;

width: 48px;

height: 24px;

background: #c4d7d6;

vertical-align: bottom;

margin: 0  10px;

border-radius: 16px;

position: relative;

cursor: pointer;

}

.switch::before {

content: '';

position: absolute;

display: block;

width: 16px;

height: 16px;

top: 4px;

left: 4px;

background: #2e317c;

border-radius: 100%;

transition: all  0.25s;

}

#switch:checked ~ label[for='switch']::before {

left: 28px;

background: #863020;

}

</style>

checkbox: <input  type="checkbox"  id="switch"  />

<label  for="switch"  class="switch"></label>

然後我們再觀察圖1,可以發現,當我們選擇時,是可以控制“電腦走”的按鈕的。

那麼這個又該怎麼實現呢?

CSS實現不了,我們用JS吧。

(吃瓜群眾:??????)

秋,秋,秋得嘛跌。CSS也可以實現!

圖片描述

我們看到上面的原始碼中有**~**這個選擇器。

這玩意叫做“兄弟選擇器”,可以選擇同層級順序排後的兄弟節點,而且不管距離由多遠,總是心連心~。

例如有以下HTML結構:


<span>This is not red.</span>

<p>Here is a paragraph.</p>

<code>Here is some code.</code>

<span>And here is a span.</span>

以下CSS:


p ~ span {

color: red;

}

這樣一樣可以選中<code>後面的<span>

所以我們有:

圖片描述

程式碼如下:


<style>

#computer {

width: 100px;

display: inline-block;

background: #131824;

color: #eef7f2;

border-radius: 5px;

margin-top: 10px;

padding: 5px;

box-sizing: border-box;

cursor: pointer;

transition: all  0.25s;

}

#switch ~ #computer {

display: none;

}

#switch:checked ~ #computer {

display: block;

}

</style>

checkbox: <input  type="checkbox"  id="switch"  />

<label  for="switch"  class="switch"></label>

<div  id="computer"  class="computer">電腦走!</div>

選擇完之後呢?

我們再回過頭來看圖1,選擇先手的功能是以彈窗的形式出現的,就是為了確保選擇先手之前不汙染棋盤。所以這該怎麼做呢?

透過上面的DEMO,我們發現有個:checked選擇器,這個選擇器任何可選元素的選中狀態,例如<input type="radio"><input type="checkbox">以及<option>

所以我們有以下效果:

圖片描述

程式碼如下:


<style>

.switch {

display: inline-block;

width: 48px;

height: 24px;

background: #c4d7d6;

vertical-align: bottom;

margin: 0  10px;

border-radius: 16px;

position: relative;

cursor: pointer;

}

.switch::before {

content: '';

position: absolute;

display: block;

width: 16px;

height: 16px;

top: 4px;

left: 4px;

background: #2e317c;

border-radius: 100%;

transition: all  0.25s;

}

#switch:checked ~ label[for='switch']::before {

left: 28px;

background: #863020;

}

.btn {

width: auto;

display: inline-block;

background: #131824;

color: #eef7f2;

border-radius: 5px;

margin-top: 10px;

padding: 5px;

box-sizing: border-box;

cursor: pointer;

transition: all  0.25s;

}

#switch ~ #computer {

display: none;

}

#switch:checked ~ #computer {

display: inline-block;

}

#start:checked ~ .container {

display: none;

}

</style>

<input  type="radio"  id="start"  />

checkbox: <input  type="checkbox"  id="switch"  />

<div  class="container">

<br  />

<label  for="switch"  class="switch"></label>

<br  />

<br  />

<label  for="start"  class="btn">皮皮蝦,我們走</label>

</div>

<div  id="computer"  class="btn">電腦走!</div>

來畫棋盤啦

接下來我們就是畫棋盤,其實棋盤是個比較常規的九宮格,可以實現的方式有很多,不過這次魚頭要安利個gird佈局線上生成的網站:

圖片描述

圖一的DEMO佈局就是用這個工具生成的,非常方便~

圖片描述

棋盤畫好了,棋子呢?

好了,我們棋盤已經畫好,那麼棋子呢?

嗯,可以去文具店花15塊錢買一盒黑白棋,然後就可以下了,好了,本文完結。

圖片描述

大霧啊~

有了棋盤我們就應該畫棋子了,棋子該怎麼畫呢?

其實怎麼畫都不要緊,重要的是得保證每個格子都能下兩方的棋子。

在我們畫棋子之前我們先談談<input />的狀態管理。

作為可替換元素的<input />,可真是個神器,因為有它以及後續瀏覽器對它功能的不斷完善,所以也是變得越來越強大。

根據我們以往的開發經驗以及上文的描述,我們很容易就能聯絡到兩個儲存正負狀態的屬性<input type="radio"><input type="checkbox">

以上兩個不同屬性的<input />都能儲存選擇狀態。

唯一不同的是<input type="radio">選擇狀態本身是單向不可逆的,只有透過所關聯的<input type="radio">才可以進行切換。

<input type="checkbox">則是雙向可逆的,狀態改變只在當前標籤就可以完成。效果如下:

圖片描述

那麼我們回到井字棋來。

我們棋盤的每個格子會有三種狀態,一個是初始時,一個是我方落子,另一個是電腦落子。

如果以數字來表示,則有:

| 狀態碼 | 含義 |

| :--------- | :------- |

| 00 | 無子 |

| 01 | 我方落子 |

| 10 | 電腦落子 |

結合上面的資訊,我們不難選出<input type="radio">來畫棋子,所以我們有:

圖片描述

所以思路就是每個格子放兩個<input type="radio">,透過選擇的一個標籤來確定棋子內渲染的樣式。棋子樣式可以隨自己美化,根據需求我們來畫<label>就行。

所以我們棋盤的HTML就如下:


<form  id="container"  class="container">

<input  type="radio"  name="c-radio-0"  id="c-radio-0-X"  />

<input  type="radio"  name="c-radio-0"  id="c-radio-0-O"  />

......

  

<div  id="c-board"  class="c-center">

<div  class="c-grid"  id="c-grid-0">

<label  for="c-radio-0-X"></label>

<div></div>

</div>

......

</div>

<div  id="c-computer"  class="c-btn">

電腦走!

<label  for="c-radio-0-O"></label>

......

</div>

基本的棋盤佈局就這麼完成了,接下來就是下手規則的處理了。

來啦,互相傷害啊

那麼下面我們就一步一步的解析落子程式。

首先我們來康康工具人標籤:


<div  class="c-grid"  id="c-grid-0">

<label  for="c-radio-0-X"></label>

<div></div>

</div>

透過上面我們不難知道<label for="c-radio-0-X"></label>就是落子標籤,那麼這個<div></div>是幹啥的呢?

你可別看這個標籤都沒有,像個一無所有的舔狗一樣,但是需要用到它的時候,它可以馬上變成一個非常有用的工具人。

這個標籤的作用就是用來承載落子的標記。

比如我們定義己方標籤的id規則是input[id*='-編號-X'],電腦方是input[id*='-編號-0'],那麼我們就可以透過**~**選擇器來確定這個工具人渲染的樣式,例如:


input[id*='-0-X']:checked~#c-board  #c-grid-0  div::before {

content: 'X';

background: var(--color1);

color: var(--color3);

}

  

input[id*='-0-O']:checked~#c-board  #c-grid-0  div::before {

content: 'O';

background: var(--color2);

color: var(--color3);

}

來到這裡要格外提一點,每一個格子的input[id]都是OX兩個的存在,而不是同一個的原因就是為了保證狀態不可逆,當checked之後就不讓它復原。

對,就是這樣。

圖片描述

我們確定了落子的渲染方式,接下來就是確定如何落子了。

我們知道,一個格子裡可以渲染input[id*='-0-X']以及input[id*='-0-O'],我們也可以透過點選來確定渲染哪一個,可是我們如何確定點選的是哪個呢?

我們先來捋捋思路。

首先我方下棋,這沒什麼問題,就跟小X王學習機一樣,哪裡不懂點哪裡就可以,so easy~

但是電腦方是由電腦控制,在本DEMO裡,需要透過點選下方的“電腦走”按鈕,來讓它自動落子,所以最開始需要讓它隱藏起來。


#c-computer { display: none; }

還有就是我方落完子之後,這個按鈕需要出現,按了之後需要隱藏,所以我們只需要交替讓它顯示就可以,也就是這樣:


#c-computer,

input:checked~input:checked~#c-computer,

input:checked~input:checked~input:checked~input:checked~#c-computer,

input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~#c-computer,

input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~#c-computer {

display: none;

}

  

input:checked~#c-computer,

input:checked~input:checked~input:checked~#c-computer,

input:checked~input:checked~input:checked~input:checked~input:checked~#c-computer,

input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~#c-computer {

display: block;

}

這裡的意思就是我第一個:checked<input />後面的按鈕要display: block,再來一個則要display: none起來,如此一個接著一個,一個接著一個,一個接著一個。。

電腦方落子位置

我方落子位置可以透過我們主動點選確定,那麼電腦方呢?

畢竟是電腦,要是落子位置還要我們確定,那就尷大尬了。

首先我們來看下電腦方相關的HTML結構。


<div  id="c-computer"  class="c-btn">

電腦走!

<label  for="c-radio-0-O"></label>

<label  for="c-radio-1-O"></label>

<label  for="c-radio-2-O"></label>

<label  for="c-radio-3-O"></label>

<label  for="c-radio-4-O"></label>

<label  for="c-radio-5-O"></label>

<label  for="c-radio-6-O"></label>

<label  for="c-radio-7-O"></label>

<label  for="c-radio-8-O"></label>

</div>

透過上面,我們可以發現,當我們點**“電腦走”**按鈕時,實際上是點label[for$='-O']

但是label的層級結構也是確定的,那麼不就很容易跟label[for$='-X']的位置衝突了嗎?

既然我們這裡提到了**“層級”**,那麼我們不難想到,可以透過z-index來確定點選是的是哪個label

我們看實操栗子。

圖片描述

所以我們就可以控制每次電腦落子的位置。

怎麼確定呢?

我們可以根據**“玩家”**的落子位置來確定。

比如玩家在**“0號位置”已經有個:checked,那麼我們就可以按照我們的想法來確定“電腦”**的落子位置,以此類推。

例如這樣:


#c-radio-0-X:checked~#c-radio-4-X:checked~#c-radio-8-O:checked~#c-computer  label[for='c-radio-2-O'],

...... {

z-index: 2;

}

  

#c-radio-0-O:not(:checked)~#c-radio-2-O:not(:checked)~#c-radio-4-X:checked~#c-radio-6-O:not(:checked)~#c-radio-8-O:not(:checked)~#c-computer  label[for='c-radio-0-O'],

...... {

z-index: 2;

}

輸贏判斷

好了,終於到了我們最後一個環節了,就是如何判斷輸贏。

這部分就是透過雙方落子位置來確定。

眾所周知,我們有以下幾種贏法:

以字母**“X”**代表贏的規則:


<!--

XXX OOO OOO XOO OXO OOX XOO OOX

OOO XXX OOO XOO OXO OOX OXO OXO

OOO OOO xxx XOO OXO OOX OOX XOO

-->

應該沒有漏吧,就是以上幾種,所以我們只需要判斷雙方的落子是否滿足以上的規則即可,所以我們有:


#c-radio-0-X:checked~#c-radio-1-X:checked~#c-radio-2-X:checked~#c-result  #c-info::before,

#c-radio-3-X:checked~#c-radio-4-X:checked~#c-radio-5-X:checked~#c-result  #c-info::before,

#c-radio-6-X:checked~#c-radio-7-X:checked~#c-radio-8-X:checked~#c-result  #c-info::before,

#c-radio-0-X:checked~#c-radio-3-X:checked~#c-radio-6-X:checked~#c-result  #c-info::before,

#c-radio-1-X:checked~#c-radio-4-X:checked~#c-radio-7-X:checked~#c-result  #c-info::before,

#c-radio-2-X:checked~#c-radio-5-X:checked~#c-radio-8-X:checked~#c-result  #c-info::before,

#c-radio-0-X:checked~#c-radio-4-X:checked~#c-radio-8-X:checked~#c-result  #c-info::before,

#c-radio-2-X:checked~#c-radio-4-X:checked~#c-radio-6-X:checked~#c-result  #c-info::before {

content: '恭喜你贏了~';

}

  

#c-radio-0-O:checked~#c-radio-1-O:checked~#c-radio-2-O:checked~#c-result  #c-info::before,

#c-radio-3-O:checked~#c-radio-4-O:checked~#c-radio-5-O:checked~#c-result  #c-info::before,

#c-radio-6-O:checked~#c-radio-7-O:checked~#c-radio-8-O:checked~#c-result  #c-info::before,

#c-radio-0-O:checked~#c-radio-3-O:checked~#c-radio-6-O:checked~#c-result  #c-info::before,

#c-radio-1-O:checked~#c-radio-4-O:checked~#c-radio-7-O:checked~#c-result  #c-info::before,

#c-radio-2-O:checked~#c-radio-5-O:checked~#c-radio-8-O:checked~#c-result  #c-info::before,

#c-radio-0-O:checked~#c-radio-4-O:checked~#c-radio-8-O:checked~#c-result  #c-info::before,

#c-radio-2-O:checked~#c-radio-4-O:checked~#c-radio-6-O:checked~#c-result  #c-info::before {

content: '可惜你輸了~';

}

圖片描述

(吃瓜群眾:“完美個頭,要是沒輸沒贏呢?”)

要是沒輸沒贏,沒輸沒贏,沒輸沒贏,該怎麼辦呢?沒辦法了,用JS吧。。。

圖片描述

對不起,我錯了,這個功能只需要給這個提示標籤一個預設文字即可。

當然我們得寫個讓提示彈窗出現的邏輯。


input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~#c-result,

...... {

display: block;

}

就是全部空格都:checked以及幾個關鍵空格佔滿的時候,就讓它展示。

初始化

如果我們想玩下一盤該怎麼辦?

重新整理頁面啊!!!

(吃瓜群眾:“就這?”)

圖片描述

當然不是就這啊,接下來要給大家介紹最後一個姿勢:<input type="reset">

<input type="reset">呈按鈕狀,可以一鍵初始化表單內所有的<input />,就像這樣

圖片描述

一鍵初始化,非常方便~

結語

<input />是一個非常有用且有趣的可替換標籤,業界中大部分的純CSS遊戲差不多都是用它來完成的,雖然不是特別實用,但是結合選擇器,是可以幫助我們在業務中解決很多問題的。

圖片描述

參考資料

後記

如果你喜歡探討技術,或者對本文有任何的意見或建議,非常歡迎加魚頭好友一起探討,當然,魚頭也非常希望能跟你一起聊生活,聊愛好,談天說地。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2819/viewspace-2825141/,如需轉載,請註明出處,否則將追究法律責任。

相關文章