二营长,快掏个CSS出来给我画个井字棋游戏

2023-11-06 18:40

本文主要是介绍二营长,快掏个CSS出来给我画个井字棋游戏,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

不知道大家小时候有没有玩过一款游戏叫『井字棋』的。

它长这样:

(我赢了,快夸我 ~o(´^`)o)

上面的就是本次文章的最终结果,一个用纯CSS实现的AI井字棋游戏,Mmmm,虽然看起来有点蠢。。。

地址在此:

https://codepen.io/krischan77/pen/qBdYZLy

游戏的规则比较简单,就是在一个九宫格(据说十六宫格,二十五宫格也行~反正是格子就行),只要你下的棋能连成一条直线,就算赢。

所以这次鱼头就来教大家怎样才能在这个游戏中获胜。

额,不对,大雾呀~

是怎样通过纯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>

来画棋盘啦

接下来我们就是画棋盘,其实棋盘是个比较常规的九宫格,可以实现的方式有很多,不过这次鱼头要安利个grid布局在线生成的网站: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游戏差不多都是用它来完成的,虽然不是特别实用,但是结合选择器,是可以帮助我们在业务中解决很多问题的。

参考资料

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

关于奇舞周刊

《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。

这篇关于二营长,快掏个CSS出来给我画个井字棋游戏的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/358359

相关文章

SpringBoot返回文件让前端下载的几种方式

《SpringBoot返回文件让前端下载的几种方式》文章介绍了开发中文件下载的两种常见解决方案,并详细描述了通过后端进行下载的原理和步骤,包括一次性读取到内存和分块写入响应输出流两种方法,此外,还提供... 目录01 背景02 一次性读取到内存,通过响应输出流输出到前端02 将文件流通过循环写入到响应输出流

SpringBoot+Vue3整合SSE实现实时消息推送功能

《SpringBoot+Vue3整合SSE实现实时消息推送功能》在日常开发中,我们经常需要实现实时消息推送的功能,这篇文章将基于SpringBoot和Vue3来简单实现一个入门级的例子,下面小编就和大... 目录前言先大概介绍下SSE后端实现(SpringBoot)前端实现(vue3)1. 数据类型定义2.

前端Visual Studio Code安装配置教程之下载、汉化、常用组件及基本操作

《前端VisualStudioCode安装配置教程之下载、汉化、常用组件及基本操作》VisualStudioCode是微软推出的一个强大的代码编辑器,功能强大,操作简单便捷,还有着良好的用户界面,... 目录一、Visual Studio Code下载二、汉化三、常用组件1、Auto Rename Tag2

vite搭建vue3项目的搭建步骤

《vite搭建vue3项目的搭建步骤》本文主要介绍了vite搭建vue3项目的搭建步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录1.确保Nodejs环境2.使用vite-cli工具3.进入项目安装依赖1.确保Nodejs环境

Nginx搭建前端本地预览环境的完整步骤教学

《Nginx搭建前端本地预览环境的完整步骤教学》这篇文章主要为大家详细介绍了Nginx搭建前端本地预览环境的完整步骤教学,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录项目目录结构核心配置文件:nginx.conf脚本化操作:nginx.shnpm 脚本集成总结:对前端的意义很多

一文详解Python如何开发游戏

《一文详解Python如何开发游戏》Python是一种非常流行的编程语言,也可以用来开发游戏模组,:本文主要介绍Python如何开发游戏的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录一、python简介二、Python 开发 2D 游戏的优劣势优势缺点三、Python 开发 3D

前端缓存策略的自解方案全解析

《前端缓存策略的自解方案全解析》缓存从来都是前端的一个痛点,很多前端搞不清楚缓存到底是何物,:本文主要介绍前端缓存的自解方案,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录一、为什么“清缓存”成了技术圈的梗二、先给缓存“把个脉”:浏览器到底缓存了谁?三、设计思路:把“发版”做成“自愈”四、代码

通过React实现页面的无限滚动效果

《通过React实现页面的无限滚动效果》今天我们来聊聊无限滚动这个现代Web开发中不可或缺的技术,无论你是刷微博、逛知乎还是看脚本,无限滚动都已经渗透到我们日常的浏览体验中,那么,如何优雅地实现它呢?... 目录1. 早期的解决方案2. 交叉观察者:IntersectionObserver2.1 Inter

Vue3视频播放组件 vue3-video-play使用方式

《Vue3视频播放组件vue3-video-play使用方式》vue3-video-play是Vue3的视频播放组件,基于原生video标签开发,支持MP4和HLS流,提供全局/局部引入方式,可监听... 目录一、安装二、全局引入三、局部引入四、基本使用五、事件监听六、播放 HLS 流七、更多功能总结在 v

JS纯前端实现浏览器语音播报、朗读功能的完整代码

《JS纯前端实现浏览器语音播报、朗读功能的完整代码》在现代互联网的发展中,语音技术正逐渐成为改变用户体验的重要一环,下面:本文主要介绍JS纯前端实现浏览器语音播报、朗读功能的相关资料,文中通过代码... 目录一、朗读单条文本:① 语音自选参数,按钮控制语音:② 效果图:二、朗读多条文本:① 语音有默认值:②