• 作者:陈大鱼头
  • 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]都是 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  OOXOOO  XXX  OOO  XOO  OXO  OOX  OXO  OXOOOO  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游戏差不多都是用它来实现的,尽管不是特地实用,然而联合选择器,是能够帮忙咱们在业务中解决很多问题的。

参考资料

  1. 纯 CSS 井字棋:并不神秘的 CSS AI 编程之旅