- 作者:陳大魚頭
- github: KRISACHAN
前言
不知道大家小時候有沒有玩過一款遊戲叫『井字棋』的。
它長這樣:
(我贏了,快誇我 ~o(´^`)o)
上面的就是本次文章的最終結果,一個用純CSS實現的AI井字棋遊戲,Mmmm,雖然看起來有點蠢。。。
地址在此:
https://codepen.io/krischan77...
遊戲的規則比較簡單,就是在一個九宮格(據說十六宮格,二十五宮格也行~反正是格子就行),只要你下的棋能連成一條直線,就算贏。
所以這次魚頭就來教大家怎樣才能在這個遊戲中獲勝。
額,不對,大霧呀~
是怎樣通過純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佈局線上生成的網站:http://grid.malven.co/
圖一的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]
都是 O 與 X 兩個的存在,而不是同一個的原因就是為了保證狀態不可逆,當 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遊戲差不多都是用它來完成的,雖然不是特別實用,但是結合選擇器,是可以幫助我們在業務中解決很多問題的。