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

m53469發表於2021-09-09

前言

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

它長這樣:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/3.gif

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

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

地址在此:

https://codepen.io/krischan77...

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

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

額,不對,大霧呀~

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

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/4.jpeg

正文

先手選擇

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

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

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

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

有以下的效果:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/5.gif

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

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

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

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/7.gif

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

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

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/8.gif

原始碼如下:

<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也可以實現!

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/9.jpg

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

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

例如有以下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>

所以我們有:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/10.gif

程式碼如下:

<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>

所以我們有以下效果:

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/11.gif

程式碼如下:

<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/

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/12.png

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

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/13.png

棋盤畫好了,棋子呢?

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

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

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/14.jpeg

大霧啊~

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

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

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

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

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

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

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

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

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/15.gif

那麼我們回到井字棋來。

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

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

狀態碼 含義
00 無子
01 我方落子
10 電腦落子

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

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/16.gif

所以思路就是每個格子放兩個<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 之後就不讓它復原。

對,就是這樣。

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/17.jpg

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

我們知道,一個格子裡可以渲染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

我們看實操栗子。

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/18.gif

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

怎麼確定呢?

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

比如玩家在“ 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: '可惜你輸了~';
}

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/19.jpg

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

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

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/20.jpg

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

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

input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~#c-result,
...... {
    display: block;
}

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

初始化

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

重新整理頁面啊!!!

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

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/21.jpg

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

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

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/22.gif

一鍵初始化,非常方便~

結語

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

https://fish-pond-1253945200.cos.ap-guangzhou.myqcloud.com/img/css/tic-tac-toe-game/23.jpg

參考資料

  1. 純 CSS 井字棋:並不神祕的 CSS AI 程式設計之旅

相關文章