面試官:你用純CSS來寫個遊戲嗎?

前言

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

它長這樣:html

https://user-gold-cdn.xitu.io/2020/3/29/17125f33ee20ab02?w=932&h=851&f=gif&s=115659

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

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

地址在此:web

codepen.io/krischan77/…編程

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

因此此次魚頭就來教你們怎樣才能在這個遊戲中獲勝。微信

額,不對,大霧呀~工具

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

https://user-gold-cdn.xitu.io/2020/3/29/17125f33ee71775e?w=240&h=185&f=jpeg&s=18949

正文

先手選擇

經過開頭的GIF圖,咱們能夠看到其實這個遊戲是有先手選擇的。

咱們能夠選擇是玩家先下,仍是電腦先下。

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

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

有如下的效果:

https://user-gold-cdn.xitu.io/2020/3/29/17125f33ee85ed13?w=468&h=227&f=gif&s=9270

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

嗯,因此咱們仍是用JS模擬吧!

(吃瓜羣衆:說好的CSS呢?給我打)

https://user-gold-cdn.xitu.io/2020/3/29/17125f33f3e3d0a9?w=499&h=290&f=gif&s=177765

對不起,咱們能夠用<label>標籤來模擬。

<label>標籤能夠經過for="#hash"來跟<input id="#hash">來進行關聯,因此咱們有如下效果:

https://user-gold-cdn.xitu.io/2020/3/29/17125f33f6034b00?w=468&h=227&f=gif&s=7554

源碼以下:

<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://user-gold-cdn.xitu.io/2020/3/29/17125f33eee29f24?w=296&h=296&f=jpeg&s=11844

咱們看到上面的源碼中有**~**這個選擇器。

這玩意叫作「兄弟選擇器」,能夠選擇同層級順序排後的兄弟節點,並且無論距離由多遠,老是心連心~。

例若有如下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://user-gold-cdn.xitu.io/2020/3/29/17125f34312d1010?w=468&h=227&f=gif&s=14849

代碼以下:

<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://user-gold-cdn.xitu.io/2020/3/29/17125f34313dd42b?w=426&h=248&f=gif&s=16404

代碼以下:

<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佈局在線生成的網站:grid.malven.co/

https://user-gold-cdn.xitu.io/2020/3/29/17125f3436c8085e?w=1062&h=619&f=png&s=38673

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

https://user-gold-cdn.xitu.io/2020/3/29/17125f343a82bb5f?w=1615&h=785&f=png&s=100317

棋盤畫好了,棋子呢?

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

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

https://user-gold-cdn.xitu.io/2020/3/29/17125f3440c07914?w=440&h=437&f=jpeg&s=44476

大霧啊~

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

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

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

做爲可替換元素的<input />,可真是個神器,由於有它以及後續瀏覽器對它功能的不斷完善,因此也是變得愈來愈強大。

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

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

惟一不一樣的是<input type="radio">選擇狀態自己是單向不可逆的,只有經過所關聯的<input type="radio">才能夠進行切換。

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

https://user-gold-cdn.xitu.io/2020/3/29/17125f346dc7f296?w=426&h=248&f=gif&s=16006

那麼咱們回到井字棋來。

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

若是以數字來表示,則有:

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

結合上面的信息,咱們不難選出<input type="radio">來畫棋子,因此咱們有:

https://user-gold-cdn.xitu.io/2020/3/29/17125f347ce02557?w=796&h=745&f=gif&s=101876

因此思路就是每一個格子放兩個<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://user-gold-cdn.xitu.io/2020/3/29/17125f347fee9fad?w=578&h=600&f=jpeg&s=41497

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

咱們知道,一個格子裏能夠渲染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://user-gold-cdn.xitu.io/2020/3/29/17125f3481ddc460?w=1615&h=806&f=gif&s=565099

因此咱們就能夠控制每次電腦落子的位置。

怎麼肯定呢?

咱們能夠根據**「玩家」**的落子位置來肯定。

好比玩家在**「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://user-gold-cdn.xitu.io/2020/3/29/17125f3484e16a70?w=198&h=148&f=jpeg&s=3178

(吃瓜羣衆:「完美個頭,要是沒輸沒贏呢?」)

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

https://user-gold-cdn.xitu.io/2020/3/29/17125f34846133d4?w=640&h=322&f=jpeg&s=35341

對不起,我錯了,這個功能只須要給這個提示標籤一個默認文本便可。

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

input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~input:checked~#c-result,
...... {
    display: block;
}
複製代碼

就是所有空格都:checked以及幾個關鍵空格佔滿的時候,就讓它展現。

初始化

若是咱們想玩下一盤該怎麼辦?

刷新頁面啊!!!

(吃瓜羣衆:「就這?」)

https://user-gold-cdn.xitu.io/2020/3/29/17125f34b29bd339?w=1235&h=1223&f=jpeg&s=406639

固然不是就這啊,接下來要給你們介紹最後一個姿式:<input type="reset">

<input type="reset">呈按鈕狀,能夠一鍵初始化表單內全部的<input />,就像這樣

https://user-gold-cdn.xitu.io/2020/3/29/17125f34bc87ca39?w=669&h=540&f=gif&s=81804

一鍵初始化,很是方便~

結語

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

https://user-gold-cdn.xitu.io/2020/3/29/17125f34bd89cb15?w=1024&h=768&f=jpeg&s=63546

參考資料

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

後記

若是你喜歡探討技術,或者對本文有任何的意見或建議,很是歡迎加魚頭微信好友一塊兒探討,固然,魚頭也很是但願能跟你一塊兒聊生活,聊愛好,談天說地。 魚頭的微信號是:krisChans95 也能夠掃碼關注公衆號,訂閱更多精彩內容。

https://user-gold-cdn.xitu.io/2020/3/29/17125f34bff387d7?w=1000&h=480&f=png&s=311000
相關文章
相關標籤/搜索