功能完备的井字棋——基于css3和vue
Posted hans774882968
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了功能完备的井字棋——基于css3和vue相关的知识,希望对你有一定的参考价值。
我在csdn上浏览过几个井字棋的代码,有的照抄 借鉴太多已有的react版本的井字棋项目,有的代码冗余多、耦合度高,总体质量也不算高。相信我这个版本的代码质量能胜过他们的QAQ
效果demo
被csdn搞🤮了,图片只能传5mb,先拿这个阉割的凑合看看吧……目前我没发现有bug,如果有神犇发现了可以评论区告诉我qwq
这个井字棋项目的功能基本上都在“游戏说明”里了。
- 可以输入你喜欢的双方标识,左侧为先手~
- 人机模式下不允许查看轮到AI落子的那些棋局~
由于作者水平有限,目前仅支持电脑先手~- 提供了“历史棋局”功能~
对象名为什么叫g1?可以看到我有两个对象g1和g2,g2全程没有使用。把它放着,意在展示可以通过复制html代码+修改变量名为g2,从而复制一个子游戏。
一些细节问题
- 游戏的各个阶段的表示
用的是ES6的Symbol,在这里它的作用类似于c语言的enum,好处主要是方便,且能提高可读性。
不过下面的gameEnd()就没有用Symbol,直接用数字标识,因为我懒得起名字……
const NOTSTARTED = Symbol(),SELFGAME = Symbol(),AIGAME = Symbol(),ENDING_ANIME = Symbol();
求当下是否处于游戏阶段:
inGameProcess(){return this.state === SELFGAME || this.state === AIGAME;}
- 电脑的决策
井字棋游戏在双方都是最优决策的情况下,是必然平局的。但是电脑的最优决策并不容易写出,这也是为什么我只做了电脑先手。电脑的决策不是我自己想的,参考的代码:决策
大致过程就是特判电脑的第1步和第2步,后续过程另外考虑。可以直接看代码,注释很详细了。我的主要工作就是把上面链接的电脑决策代码给简化了一些。
- 判定游戏结束
之前的方案
gameEnd(){
//行
for(let i = 0;i < 3;++i){
let c = [0,0];
for(let j = 0;j < 3;++j){
if(this.currentBoard[ID(i,j)] > 0) c[this.currentBoard[ID(i,j)]-1]++;
}
if(c[0] === 3) return 1;
if(c[1] === 3) return 2;
}
//列
for(let j = 0;j < 3;++j){
let c = [0,0];
for(let i = 0;i < 3;++i){
if(this.currentBoard[ID(i,j)] > 0) c[this.currentBoard[ID(i,j)]-1]++;
}
if(c[0] === 3) return 1;
if(c[1] === 3) return 2;
}
let c;
//对角线
c = [0,0];
for(let i = 0;i < 3;++i) if(this.currentBoard[ID(i,i)] > 0) ++c[this.currentBoard[ID(i,i)]-1];
if(c[0] === 3) return 1;
if(c[1] === 3) return 2;
//反对角线
c = [0,0];
for(let i = 0;i < 3;++i) if(this.currentBoard[ID(i,2-i)] > 0) ++c[this.currentBoard[ID(i,2-i)]-1];
if(c[0] === 3) return 1;
if(c[1] === 3) return 2;
//无赢家的前提下再看棋盘是否已满
if(this.currentBoard.indexOf(0) === -1) return 3;
return 0;
}
现在新增一个需求,就是对形成3连子的格子染色突出。我们发现如果加上满足新需求的代码,耦合度将会太高。
因此我们改成用二维数组allPossible来存每种可能的胜利局面。这样修改颜色的耦合代码就只需要加到一处了。
gameEnd(){
const allPossible = [
[0,1,2],[3,4,5],[6,7,8],
[0,3,6],[1,4,7],[2,5,8],
[0,4,8],[2,4,6]
];
for(let cur of allPossible){
let c = [0,0];
for(let idx of cur){
if(this.currentBoard[idx] > 0) ++c[this.currentBoard[idx]-1];
}
if(c[0] === 3 || c[1] === 3){
for(let idx of cur) this.boardColor.splice(idx,1,1);
}
if(c[0] === 3) return 1;
if(c[1] === 3) return 2;
}
//无赢家的前提下再看棋盘是否已满
if(this.currentBoard.indexOf(0) === -1) return 3;
return 0;
}
代码
html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>井字棋</title>
<link rel="stylesheet" type="text/css" href = "./tic.css" />
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script src="https://cdn.staticfile.org/vue/2.5.2/vue.min.js"></script>
</head>
<body>
<h1 class="title">TicTacToe</h1>
<div id="game">
<div class="subgame">
<h1>
<input v-model="g1.fn" class="firstName" />
<span>VS</span>
<input v-model="g1.sn" class="secondName" />
</h1>
<div class="body">
<div class="board">
<div v-for="(val,idx) in g1.currentBoard" @click="g1.putPiece(idx)" v-bind:style="{
cursor: g1.inGameProcess() && val === 0 ? 'pointer' : 'auto',
'background-color': g1.boardColor[idx] ? '#877f6c' : 'white',
}" class="cell">
{{ g1.boardText(val) }}
</div>
</div>
<div class="menu">
<button @click="help_show = true;">游戏说明</button>
<template v-if="g1.state === NOTSTARTED">
<button @click="g1.startGame(SELFGAME)">自己玩</button>
<button @click="g1.startGame(AIGAME)">人机</button>
</template>
<template v-else>
<button @click="g1.returnToMenu()"
v-bind:style="{cursor: g1.inGameProcess() ? 'pointer' : 'not-allowed'}"
:disabled="!g1.inGameProcess()">
返回
</button>
<p>
请棋手
<span class="turn-info">{{ g1.boardText(g1.getCurTurn()) }}</span>
落子
</p>
<p>第 <span class="turn-info">{{ g1.step }}</span> 步</p>
</template>
</div>
</div>
<!-- 展示历史棋盘局面 -->
<div class="history">
<!-- 人机模式下不允许查看轮到AI落子的那些棋局 -->
<div v-for="(board,idx1) in g1.boards"
@click="g1.showHistoryBoard(board,idx1)"
v-bind:style="{
display: g1.state !== NOTSTARTED ? 'grid' : 'none',
cursor: g1.canClickHistoryBoard(idx1) ? 'pointer' : 'not-allowed'
}"
class="history-board">
<!-- 这里复用了.cell样式,并重新设置字体大小 -->
<div v-for="(val,idx2) in board" class="cell history-cell">
{{ g1.boardText(val) }}
</div>
</div>
</div>
<!-- 展示赢家 -->
<transition name="winner">
<div class="winner" v-if="g1.winnerShow">{{ g1.winnerText }}</div>
</transition>
<!-- 用scale(1)控制弹出框出现 -->
<div class="help-container" v-bind:style="{transform: help_show ? 'scale(1)' : 'scale(0)'}">
<div class="help-head">
<span class="help-title">游戏说明</span>
<span class="close" @click="help_show = false;">X</span>
</div>
<p>作者:hans774882968</p>
<p>可以输入你喜欢的双方标识,左侧为先手~</p>
<p>人机模式下不允许查看轮到AI落子的那些棋局~</p>
<p>由于作者水平有限,目前仅支持电脑先手~</p>
<p>提供了“历史棋局”功能~</p>
<p>我怎么这么菜,哭哭~</p>
</div>
</div>
</div>
<script src="./tic.js"></script>
</body>
</html>
css
body{
margin: 0;
}
a{
text-decoration: none;
}
input{
outline: none;
}
:root{
--gap: 2px;
--cell-size: 120px;
--board-size: calc(3 * var(--cell-size) + (3 - 1) * var(--gap));
--history-gap: 1px;
--history-csz: 40px;
--history-bsz: calc(3 * var(--history-csz) + (3 - 1) * var(--history-gap));
}
.title{
color: #cb4042;
text-align: center;
}
#game{
display: flex;
justify-content: space-evenly;
}
.subgame{
position: relative;
}
.subgame>h1{
margin-bottom: 80px;
display: flex;
justify-content: center;
color: #777;
}
.subgame>h1 input{
width: 85px;
height: 42.5px;
padding-left: 15px;
font-size: 20px;
color: #777;
border-radius: 10px;
border: 1px solid #bbb;
}
.subgame>h1 span{
margin: 0 10px;
}
.body{
display: flex;
}
.board{
display: grid;
grid-template-rows: repeat(3,var(--cell-size));
grid-template-columns: repeat(3,var(--cell-size));
grid-gap: var(--gap);
padding: var(--gap);
width: var(--board-size);
height: var(--board-size);
background-color: #cb4042;
}
.cell{
background-color: white;
color: #cb4042;
font-size: 35px;
display: grid;
place-items:center;/* 居于中心 */
user-select: none;
overflow: hidden;
}
.menu{
margin-left: 70px;
width: 200px;
display: flex;
flex-direction用React写井字棋游戏