小游戏之斗兽棋(uniapp)
学习的同时别忘了娱乐,注意劳逸结合。
前言
最近学习数据结构与算法,但是平常自己要么刷题,要么看书,很是无聊。就想着游戏中一般都会运用大量的算法,所以就开始写游戏来锻炼自己算法这方面的小能力。后面还会持续做其他小游戏,并发布说说自己做的过程中的心得体会。
一、游戏介绍
游戏采用熟悉的回合制斗兽棋,由玩家和电脑进行对战,双方各执16颗动物棋子。其中包括1个龙、2个虎、2个象、2个熊、2个狐、2个鹰、5个猫。
基本游戏规则如下:
我们小时候玩的斗兽棋,就是一般的象、狮、虎、豹、狼、狗、猫、鼠这种,我当时感觉儿时的比较简单一点,就没用此套卡牌和规则。而布局跟游戏规则是参考他人的,逻辑方面处理都是自己跟着规则慢慢啃出来的,其他优化的地方在下面实现里我也会标出来,整体游戏是可以完美运行的。
游戏的基本玩法就是,玩家先手,棋牌中分为红蓝两方,玩家第一手翻开什么牌,就会固定双方阵容。后面就是把对方的子吃完,即可获取胜利。
二、游戏实现
1. 基本游戏布局(都注释了,不细说)
<template>
<view class="beast">
<!-- 电脑 -->
<view class="player player-1" :class="[player1.camp]">
<!-- 电脑头像 -->
<view class="photo">
<image src="../../static/player-ai.png"></image>
</view>
<!-- 电脑血量 -->
<view class="HP">
<progress :percent="player1.score" stroke-width="44" :activeColor="player1.camp == 'red' ? '#f56c6c': player1.camp == 'blue' ? '#94bcff' : ''"/>
<text class="hp-num">{{ player1.score }}/100</text>
</view>
</view>
<!-- 主游戏区 -->
<view class="map">
<view class="map-box">
<!-- 电脑的棋子 -->
<view class="chess-player chess-player1">
<template v-for="item in player1.list">
<uni-badge class="uni-badge-left-margin" :text="isPlay != 0 ? item.count: ''" absolute="leftTop" size="small">
<view class="chess-item" :class="[item.count == 0 ? 'die': '', player1.camp]">
<image :src="`../../static/${item.name}.png`" mode=""></image>
</view>
</uni-badge>
</template>
</view>
<!-- 电脑 VS 玩家 -->
<view class="chess-play">
<view class="chess-play-row" v-for="item1,index1 in mapList" :key="index1">
<view class="chess-block" v-for="item2,index2 in item1" :key="index2" @click="onPlayer(item2, index1, index2)">
<view
class="chess-item"
:class="[!item2.isFilp ? 'no' : item2.camp, item2.isSelected ? 'selected' : '']"
v-if="item2 && item2.isShow"
>
<image :src="`../../static/${item2.name}.png`" mode="" v-show="item2.isFilp"></image>
</view>
</view>
</view>
</view>
<!-- 玩家棋子 -->
<view class="chess-player chess-player2">
<template v-for="item in player2.list">
<uni-badge class="uni-badge-left-margin" :text="isPlay != 0 ? item.count: ''" absolute="rightTop" size="small">
<view class="chess-item" :class="[item.count == 0 ? 'die': '', player2.camp]">
<image :src="`../../static/${item.name}.png`" mode=""></image>
</view>
</uni-badge>
</template>
</view>
</view>
</view>
<!-- 玩家 -->
<view class="player player-2" :class="[player2.camp]">
<!-- 玩家血量 -->
<view class="HP">
<progress :percent="player2.score" stroke-width="44" :activeColor="player2.camp == 'red' ? '#f56c6c': player2.camp == 'blue' ? '#94bcff' : ''"/>
<text class="hp-num">{{ player2.score }}/100</text>
</view>
<!-- 玩家头像 -->
<view class="photo">
<image src="../../static/player-1.png"></image>
</view>
</view>
<!-- 规则提示框 -->
<uni-popup ref="popupRule" :isMaskClick="false" :maskBackgroundColor="'rgba(0,0,0,.6)'">
<view class="win">
<view class="win-box">
<view class="win-title">
<text>游戏规则</text>
</view>
<view class="win-content">
<view class="rule">
<view class="rule-item">1、龙 > 虎 > 象 > 熊 > 狐 > 鹰 > 猫</view>
<view class="rule-item">2、猫可以吃龙</view>
<view class="rule-item">3、鹰可以吃全部,前提是需要隔一颗棋子才能吃,不分距离。</view>
<view class="rule-item">4、所有动物只能上下左右移动,每次最多可移动一格(鹰狩猎的时候除外)。</view>
<view class="rule-item">5、任意一方血量为0时,游戏结束。</view>
</view>
</view>
</view>
</view>
<view class="win-btns">
<button class="win-btn" @click="onKnow()">我知道了</button>
</view>
</uni-popup>
<!-- 输赢提示框 -->
<uni-popup ref="popupWin" :isMaskClick="false" :maskBackgroundColor="'rgba(0,0,0,.6)'">
<view class="win">
<view class="win-box">
<view class="win-title">
<text v-if="isWin == 1">恭 喜</text>
<text v-if="isWin == 2">很 遗 憾</text>
</view>
<view class="win-content">
<text v-if="isWin == 1">{{ aiLose ? '电脑认输了' : '你 赢 了' }}</text>
<text v-if="isWin == 2">你 输 了</text>
</view>
</view>
</view>
<view class="win-btns">
<button class="win-btn" @click="onExitGame()">退出游戏</button>
<button class="win-btn" @click="onAgainGame()">再来一局</button>
</view>
</uni-popup>
</view>
</template>
<style lang="scss" scoped>
image { width: 100%; height: 100%; }
/deep/.uni-badge { z-index: 9; }
.beast {
width: 100%; height: 100vh; display: flex; flex-direction: column;justify-content: space-between;
}
.player {
padding: 40rpx 20rpx; display: flex;align-items: center;
.photo {
width: 88rpx; height: 88rpx; border-radius: 10rpx; overflow: hidden;display: flex;align-items: center;justify-content: center;
background-color: #ebebeb;
image { width: 80%; height: 80%; }
}
.HP {
flex: 1; position: relative;
.uni-progress { border-radius: 10rpx; overflow: hidden; }
.hp-num { position: absolute; left: 50%; top: 44rpx; transform: translate(-50%, -50%); font-weight: bold; }
}
&.player-1 .photo { margin-right: 20rpx; }
&.player-2 .photo { margin-left: 20rpx; }
&.player-1
&.player-2 .HP .uni-progress { transform: rotateY(180deg); }
&.red .photo { background-color: #f56c6c; }
&.blue .photo { background-color: #94bcff; }
/deep/.uni-progress-inner-bar { transition: all .2s; }
}
.chess-item {
width: 88rpx; height: 88rpx;border-radius: 10rpx;overflow: hidden;display: flex;align-items: center;justify-content: center;
background-color: #ebebeb;
image { width: 80%; height: 80%; }
}
.map {
padding: 0 20rpx;
.map-box { display: flex; justify-content: center; }
.chess-player {
width: 108rpx;
&.chess-player1 {
padding-right: 20rpx;
}
&.chess-player2 {
padding-left: 20rpx; display: flex; flex-direction: column; justify-content: flex-end;
}
.red { background-color: #f56c6c; }
.blue { background-color: #94bcff; }
.die { filter: grayscale(1); }
.uni-badge--x { display: block; }
.uni-badge--x + .uni-badge--x { margin-top: 20rpx; }
}
.chess-play {
flex: 1; height: 1020rpx; overflow: hidden;background-color: #ab865a;border-radius: 10rpx;
display: flex;flex-direction: column;justify-content: space-evenly;padding: 6rpx;
.chess-play-row {
display: flex;
align-items: center;
justify-content: space-evenly;
}
.chess-block {
padding: 10rpx 10rpx 18rpx;border-radius: 10rpx;overflow: hidden;display: flex;align-items: center;justify-content: center;
background-color: #f0cc92; width: 108rpx; height: 116rpx;
}
.chess-item {
user-select: none; transition: all .1s;
&.no { background-color: #1aaf5d; box-shadow: 0 4px 0 #0a7237; }
&.red { background-color: #f56c6c; box-shadow: 0 4px 0 #b34e4e; }
&.blue { background-color: #94bcff; box-shadow: 0 4px 0 #6c8aba; }
&.selected { transform: translateY(-8rpx); }
}
}
}
.win {
width: 540rpx;border-radius: 20rpx;background-color: rgb(119,212,168);margin: 0 auto;padding: 20rpx;
.win-box {
width: 100%; background-color: rgb(254,255,241);border-radius: 10px;padding: 70rpx 30rpx 30rpx;
}
.win-title {
width: 60%;padding: 20rpx;text-align: center;background-color: rgb(255,107,103);clip-path: polygon(0 0, 100% 0, 95% 50%, 100% 100%, 0 100%, 5% 50%);
position: absolute;color: #fff;font-weight: bold;font-size: 40rpx;left: 50%;transform: translateX(-50%);top: -30rpx;
}
.win-content {
width: 100%;min-height: 200rpx; border-radius: 20rpx;border: 1px solid rgb(251,206,87); background-color: rgb(255,243,205); box-shadow: inset 0 0 20rpx 0rpx rgba(251,206,87, .8);
display: flex;align-items: center;justify-content: center;color: rgb(183,92,77); font-weight: bold; font-size: 40rpx;
}
.rule {
width: 100%;padding: 30rpx;text-align: justify;
.rule-item { font-size: 28rpx; }
.rule-item + .rule-item { margin-top: 20rpx; }
}
}
.win-btns {
display: flex;align-items: center;justify-content: space-between;padding: 40rpx 20rpx;
.win-btn {
flex: 1;font-size: 34rpx;color: white;background: linear-gradient(to bottom, rgb(244,200,77), rgb(248,234,111));font-weight: bold;
box-shadow: 0 6rpx 0 0 rgb(168,134,45); -webkit-text-stroke: 1rpx rgb(168,134,45);
}
.win-btn + .win-btn { margin-left: 40rpx; }
}
</style>
2. 定义需要的基本参数
// 电脑
player1: { score: 100, camp: '', list: [] }
// 玩家
player2: { score: 100, camp: '', list: [] }
// 棋子信息
// weight: 权重
// count: 数量
// score: 分数
// camp: 所属阵营(同上) 'red' or 'blue'
// isFilp: 是否翻开
// isShow: 是否显示
// isSelected: 是否选中
// location: 所在二维数组的位置
// 空地的话,我直接用null表示
chessList: [
{name: '龙', weight: 7, count: 1, score: 19, camp: null, isFilp: false, isShow: true, isSelected: false, location: []},
{name: '虎', weight: 6, count: 2, score: 12, camp: null, isFilp: false, isShow: true, isSelected: false, location: []},
{name: '象', weight: 5, count: 2, score: 10, camp: null, isFilp: false, isShow: true, isSelected: false, location: []},
{name: '熊', weight: 4, count: 2, score: 8, camp: null, isFilp: false, isShow: true, isSelected: false, location: []},
{name: '狐', weight: 3, count: 2, score: 5, camp: null, isFilp: false, isShow: true, isSelected: false, location: []},
{name: '鹰', weight: 2, count: 2, score: 3, camp: null, isFilp: false, isShow: true, isSelected: false, location: []},
{name: '猫', weight: 1, count: 5, score: 1, camp: null, isFilp: false, isShow: true, isSelected: false, location: []},
],
// 地图二维数组
mapList: [],
// 选中的棋子
selectChess: null,
// 是否开始了,分配阵营
isStart: false,
// 当前轮到谁了 0=无 1=玩家 2=电脑
isWho: 0,
// 游戏是否开始 0=未开始 1=进行中 2=已结束
isPlay: 0,
// 当前游戏谁赢了 0=无 1=玩家 2=电脑
isWin: 0,
// 副本数组(相当于复制一份当时棋盘的数据),用来进行广度优先算法
bfsList: [],
// 算法每次计算前,都把当前地图重新绘制一遍,0不能走 1可以走
bfsLoadList: [],
// AI是否投降
aiLose: false,
3. 定义需要用到的公共方法
我们一般游戏中都要用到深克隆,防止操作数据时,导致视图也会做变化。
还有数组转换,比如一维数组转为多维数组,再多维转一维数组等。
其他还有打乱数组,比如数字华容道、斗兽棋等游戏
/**
* @description 打乱数组
* @param arr
*/
export const arrayRandom = function(arr = []) {
return arr.sort(() => Math.random() - 0.5);
}
/**
* @description 深度克隆多维数组
* @param {Array} arr 需要深度克隆的数组
* @returns {*} 克隆后的数组
*/
export const deepcopyArray = function(arr) {
var out = [], i = 0, len = arr.length;
for (; i < len; i++) {
if (arr[i] instanceof Array){
out[i] = deepcopyArray(arr[i]);
}
else out[i] = arr[i];
}
return out;
}
/**
* @description 一维数组转二维数组
* @param arr
* @param num
*/
export const arrayChange = function(arr, num) {
if(!Array.isArray(arr)) {
return [];
}
if(Array.isArray(arr) && arr.length <= 1) {
return arr;
}
let a = [];
while(arr.length > 0) {
a.push(arr.splice(0, num))
}
return a;
}
/**
* @description 二维数组转一维数组
* @param
* @param
*/
export const arrTwoToOne = function(arr) {
if(!Array.isArray(arr)) {
return [];
}
return [].concat.apply([], arr)
}
4. 动态生成双方所拥有棋子16颗、棋盘总棋子32颗
这里我用了4个list,因为初始化数据包含双方所拥有的棋子,还有棋盘的32颗(16红+16蓝)
createPlayChess() {
let arr1 = [];
let arr2 = [];
let list1 = JSON.parse(JSON.stringify(this.chessList));
let list2 = JSON.parse(JSON.stringify(this.chessList));
let list3 = JSON.parse(JSON.stringify(this.chessList));
let list4 = JSON.parse(JSON.stringify(this.chessList));
// 电脑的棋子
for (let i = 0; i < list1.length; i++) {
this.player1.list.push(list1[i]);
}
// 玩家的棋子
for (let i = 0; i < list2.length; i++) {
this.player2.list.push(list2[i]);
}
// 棋盘中红色方子
for (let i = 0; i < list3.length; i++) {
for(let j = 0; j < list3[i].count; j++) {
list3[i].camp = 'red'
arr1.push(list3[i]);
}
}
// 棋盘中蓝色方子
for (let i = 0; i < list4.length; i++) {
for(let j = 0; j < list4[i].count; j++) {
list4[i].camp = 'blue'
arr2.push(list4[i]);
}
}
// 因为玩家在界面下面,他的棋子在右边,所以要把数组倒过来,让龙在最下面
this.player2.list.reverse();
// 处理总数据,把一维数组打乱,再转成二维数组
let maplist = JSON.parse(JSON.stringify(this.arrayChange(this.arrayRandom([...arr1, ...arr2]), 4)))
// 给每个棋子加上自己所在位置
maplist.forEach((item1, index1) => {
item1.forEach((item2, index2) => {
item2.location = [index1, index2];
})
})
this.mapList = JSON.parse(JSON.stringify(maplist))
},
5. 定义其他操作(规则、再来一局、游戏输赢等)
// 按钮 - 我知道了
// 隐藏弹框,开始游戏
onKnow() {
this.isPlay = 1;
this.isWho = 1;
this.$refs.popupRule.close()
uni.showToast({title: '玩家先手', icon: 'none'})
},
// 再来一局
// 其实就是把所有初始数据初始化,然后重新生成地图数据
onAgainGame() {
this.player1.camp = '';
this.player1.score = 100;
this.player1.list = [];
this.player2.camp = '';
this.player2.score = 100;
this.player2.list = [];
this.mapList = [];
this.selectChess = null;
this.isStart = false;
this.isWho = 1;
this.isPlay = 1;
this.isWin = 0;
this.aiLose = false;
this.createPlayChess();
this.$refs.popupWin.close();
},
我们还可以根据每次吃完子,无论是电脑还是玩家,来判断双方剩余棋子和当前血量,再判断输赢。
当一方的卡牌都没有了,也就是血量为0时,输赢判定。
// 处理hp和剩余棋子 chess被吃的那个子
handlePlayer(chess) {
if(!chess) return;
// 如果chess是电脑的子
if(chess.camp == this.player1.camp) {
this.player1.list.forEach(item => {
if(item.name == chess.name) {
item.count -= 1;
this.player1.score -= chess.score;
}
})
}
// 如果chess是玩家的子
if(chess.camp == this.player2.camp) {
this.player2.list.forEach(item => {
if(item.name == chess.name) {
item.count -= 1;
this.player2.score -= chess.score;
}
})
}
// 判断输赢
if(this.player1.score <= 0) {
this.isPlay = 2;
this.isWin = 1;
this.$refs.popupWin.open()
}
if(this.player2.score <= 0) {
this.isPlay = 2;
this.isWin = 2;
this.$refs.popupWin.open()
}
}
我们在游戏中可以进行吃子,移动到空地的操作,其实就是把指定子的信息赋值给被吃子或者空地的信息,此时需要注意,信息换过去,当前子的location也需要更换,不然表面虽然移动了,实际后续操作,那个子还在原来的地方。
// 交换位置 - 移动或者吃子
// position1:需要移动的棋子的位置 position2:目标位置
// 如果是吃子操作,则需要传入第三个参数用来判断是否输赢
handlePosition(position1, position2, whoChess) {
let [x1, y1] = position1;
let [x2, y2] = position2;
let chess = JSON.parse(JSON.stringify(this.mapList[x1][y1]));
this.mapList[x2][y2] = chess;
this.mapList[x2][y2].location = [x2, y2]
this.mapList[x1][y1] = null;
this.$forceUpdate()
this.handlePlayer(whoChess); // 传入被吃的那个子 判断输赢
}
6. 玩家操作
对于玩家,我们只需要翻牌,然后吃子,移动等操作,比较简单。
我们每次点击牌都会把当前牌的信息以及坐标传递过来
onPlayer( item, x, y )
(1)如果游戏没开始或者还没轮到玩家,则不能进行操作
if(this.isWho != 1 || this.isPlay != 1) return;
(2)我们开始前,需要先初始化我们需要的操作数据,我们不能直接在主数据上直接判断修改某些子,不然容易导致视图发生改变
let list = [].concat.apply([], this.mapList); // 二维数组转一维数组
let list_some = list.some(i => i && i.isSelected); // 有没有选择棋子
let selectChess = list.filter(i => i && i.isSelected)[0]; // 获取选中的棋子信息
(3)没选棋子之前点击空地,是没用的
if(!list_some && item == null) return;
(4)如果我选了棋子,再去点空地
if(list_some && item == null) {
let bool = false; // 每次只能移动一格
let x1 = selectChess.location[0];
let y1 = selectChess.location[1];
if(
(y1 == y && x1 - 1 == x) ||
(y1 == y && x1 + 1 == x) ||
(x1 == x && y1 - 1 == y) ||
(x1 == x && y1 + 1 == y)
) {
bool = true;
}
if(bool) {
// 查看二 -- 5中的方法,交换位置
this.handlePosition(selectChess.location, [x, y])
} else {
uni.showToast({title: '不能走!', icon: 'none', duration: 500})
// 如果不能走,则把选择的棋子取消掉
this.mapList.forEach(item1 => {
item1.forEach(item2 => item2 ? item2.isSelected = false : '')
})
this.$forceUpdate()
return;
}
// 移动完之后,要把选中的棋子初始化
this.mapList.forEach(item1 => {
item1.forEach(item2 => item2 ? item2.isSelected = false : '')
})
// 移动完轮到电脑
this.onAi()
return;
}
(5)如果我们还没选择自己棋子,是不能点击电脑的棋子的
// 如果点的棋子翻开了,并且点击的是对方的牌,并且还没有选择自己的牌时
if(item.isFilp && item.camp != this.player2.camp && !list_some) {
uni.showToast({title: '这不是你的棋子', icon: 'none', duration: 500})
return;
}
(6)这里得记住,我们无论做了什么操作,都要在操作之后清除一下选中状态
// 每次点击牌之前 清除一下选中状态
this.mapList.forEach(item1 => {
item1.forEach(item2 => item2 ? item2.isSelected = false : '')
})
(7)如果点的棋子是没翻开的状态,则需要把这个棋子翻开。还要注意游戏刚开始需要区分阵营,也是这时候做的操作
// 如果棋子处于没翻转的状态
if(!item.isFilp) {
item.isFilp = true
// 这里玩家第一次翻开棋子,是为了区分阵营
if(!this.isStart) {
this.isStart = true;
this.player2.camp = item.camp
this.player1.camp = item.camp == 'red' ? 'blue' : 'red'
}
this.onAi()
return;
}
(8)如果我们点击的牌是自己的话,则选中这个牌
// 如果点击的是自己的牌 则选中当前牌
if(item.camp == this.player2.camp) {
item.isSelected = !item.isSelected;
this.$forceUpdate()
return;
}
(9)最后一步,吃子,这时候做吃子操作。如果我选中棋子,并且我又点了电脑的棋子,这时候就需要判断能不能吃了。
// 判断选中自己的牌之后,再去选择别人的牌, 说明有吃的意向了
if(selectChess) {
// 公共方法,判断两个子是否存在吃与被吃的关系
if(!this.onIsEat(selectChess, item)) {
uni.showToast({title: '吃不了!', icon: 'none', duration: 500})
this.selectChess = null;
return;
} else {
// 吃
this.handlePosition(selectChess.location, item.location, item);
}
this.onAi()
return;
}
(10) 如何去判断一颗子是否可以吃另一颗子
// 判断animal_1能不能吃animal_2 true=能吃 false=不能吃
onIsEat(animal_1, animal_2) {
// 如果传入的两个牌一样或者没翻开,则不能吃
if(!animal_1 || !animal_2 || animal_1.camp == animal_2.camp || !animal_1.isFilp || !animal_2.isFilp) {
return false;
}
// 获取它们的权重和坐标
let aniWeight_1 = animal_1.weight
let aniWeight_2 = animal_2.weight
let x1 = animal_1.location[0];
let y1 = animal_1.location[1];
let x2 = animal_2.location[0];
let y2 = animal_2.location[1];
// 如果捕食者是 鹰, 则需要判断上下左右的子中,鹰与被吃者中间是否有一颗子
if(aniWeight_1 == 2) {
let bool = false;
if((y1 == y2 && x1 - 2 >= x2)) {
let num = 0;
for (let i = x1 - 1; i > x2; i--) {
if(this.mapList[i][y1] != null) num++;
}
bool = num == 1 ? true : false;
}
if((y1 == y2 && x1 + 2 <= x2)) {
let num = 0;
for (let i = x1 + 1; i < x2; i++) {
if(this.mapList[i][y1] != null) num++;
}
bool = num == 1 ? true : false;
}
if((x1 == x2 && y1 - 2 >= y2)) {
let num = 0;
for (let i = y1 - 1; i > y2; i--) {
if(this.mapList[x1][i] != null) num++;
}
bool = num == 1 ? true : false;
}
if((x1 == x2 && y1 + 2 <= y2)) {
let num = 0;
for (let i = y1 + 1; i < y2; i++) {
if(this.mapList[x1][i] != null) num++;
}
bool = num == 1 ? true : false;
}
return bool;
}
// 其他动物,不但要判断权重能否吃,还要判断是否在上下左右方向上
if(
((aniWeight_1 == 7 && [7,6,5,4,3,2].includes(aniWeight_2)) ||
(aniWeight_1 == 6 && [6,5,4,3,2,1].includes(aniWeight_2)) ||
(aniWeight_1 == 5 && [5,4,3,2,1].includes(aniWeight_2)) ||
(aniWeight_1 == 4 && [4,3,2,1].includes(aniWeight_2)) ||
(aniWeight_1 == 3 && [3,2,1].includes(aniWeight_2)) ||
(aniWeight_1 == 1 && [1,7].includes(aniWeight_2))) &&
((y1 == y2 && x1 - 1 == x2) || (y1 == y2 && x1 + 1 == x2) ||(x1 == x2 && y1 - 1 == y2) || (x1 == x2 && y1 + 1 == y2))
) {
return true;
}
return false;
},
此时玩家的操作基本完成。可以正常翻牌、走动、吃子。
7. 电脑操作
基本上就是根据优先级来处理操作:
首先作为斗兽棋,最重要的就是吃对方子来达到胜利,
其次是别人吃我时,我要逃跑,
然后追击对方,
最后是翻牌操作,
最后的最后就是投降。
(1)不该电脑的时候,电脑不能动,并且刚开始要定义我们需要用到的基本数据
if(this.isPlay != 1) return;
// 把当前地图数据变成一维数组
let list = [].concat.apply([], this.mapList);
// 玩家翻开的棋子
let playerFilpList = list.filter(item => item && item.isFilp && item.camp != this.player1.camp);
// 电脑翻开的棋子
let aiFilpList = list.filter(item => item && item.isFilp && item.camp == this.player1.camp);
(2)电脑吃子,判断电脑翻开的每个子的周围有没有能吃的棋子
let aiEatOwn = []; // 我自己有哪些能吃别人的子
let aiEatList = []; // 我能吃哪些玩家的子
// 判断ai的每个子的周围有没有能吃的棋子
for(let i = 0; i < aiFilpList.length; i++) {
let [x, y] = aiFilpList[i].location;
// 上
if(x - 1 >= 0 && this.onIsEat(aiFilpList[i], this.mapList[x - 1][y])) {
aiEatOwn.push(aiFilpList[i])
aiEatList.push(this.mapList[x - 1][y])
}
// 下
if(x + 1 < 8 && this.onIsEat(aiFilpList[i], this.mapList[x + 1][y])) {
aiEatOwn.push(aiFilpList[i])
aiEatList.push(this.mapList[x + 1][y])
}
// 左
if(y - 1 >= 0 && this.onIsEat(aiFilpList[i], this.mapList[x][y - 1])) {
aiEatOwn.push(aiFilpList[i])
aiEatList.push(this.mapList[x][y - 1])
}
// 右
if(y + 1 < 4 && this.onIsEat(aiFilpList[i], this.mapList[x][y + 1])) {
aiEatOwn.push(aiFilpList[i])
aiEatList.push(this.mapList[x][y + 1])
}
// 如果是鹰的情况
for(let a = x - 2; a >= 0; a--) {
if(this.onIsEat(aiFilpList[i], this.mapList[a][y])) {
aiEatOwn.push(aiFilpList[i])
aiEatList.push(this.mapList[a][y])
}
}
for(let a = x + 2; a < 8; a++) {
if(this.onIsEat(aiFilpList[i], this.mapList[a][y])) {
aiEatOwn.push(aiFilpList[i])
aiEatList.push(this.mapList[a][y])
}
}
for(let a = y - 2; a >= 0; a--) {
if(this.onIsEat(aiFilpList[i], this.mapList[x][a])) {
aiEatOwn.push(aiFilpList[i])
aiEatList.push(this.mapList[x][a])
}
}
for(let a = y + 2; a < 4; a++) {
if(this.onIsEat(aiFilpList[i], this.mapList[x][a])) {
aiEatOwn.push(aiFilpList[i])
aiEatList.push(this.mapList[x][a])
}
}
}
(3)这里我们已经获取到了我们的棋子(捕食者)和玩家的棋子(被捕食者)
我们每次获取到一个可以吃的子,就会把相应的子加入到数组队列中,就会形成两个数组,他们的值是一 一对应的,比如【被吃1,被吃2,被吃3,】 =》 【吃1,吃1,吃2】,这时候我们可以在第二个数组中看到,吃1出现两次,说明它可以吃两个,这样我们后面会根据被捕食者的分数排列,这个过程中,他们所对应的数据都需要跟着变,以便后面我们根据分数找到对应的捕食者坐标。
电脑吃子的时候还不能乱吃,因为每个子都有分数,我们不能亏。
电脑首先要吃分数最高的子,以此类推
电脑吃之前还要判断一下,吃完后自己会不会被吃,不然胡乱吃,自己就亏了,比如我的虎吃了玩家的熊,可是玩家的龙给吃了,这就亏了。
如果是相同的棋子,则直接吃,不用考虑
如果是猫吃龙的情况下,则直接吃,不用考虑
if(aiEatList.length > 0) {
let scoreList = aiEatList.map(item => item.score); // 提取分数数组
// 冒泡排序,根据分数从大到小排列,再把分数对应的敌方子和对应的我方子进行排列
for (let i = 0; i < scoreList.length; i++) {
for (let j = 0; j < scoreList.length - i - 1; j++) {
if (scoreList[j] < scoreList[j + 1]) {
// 交换分数
let temp1 = scoreList[j];
scoreList[j] = scoreList[j + 1];
scoreList[j + 1] = temp1;
// 交换被捕食者
let temp2 = aiEatList[j];
aiEatList[j] = aiEatList[j + 1];
aiEatList[j + 1] = temp2;
// 交换捕食者
let temp3 = aiEatOwn[j];
aiEatOwn[j] = aiEatOwn[j + 1];
aiEatOwn[j + 1] = temp3;
}
}
}
// 这里我们去循环捕食者数组,从大到小吃查看,是否可以吃对方子,
let bool = false;
for (let i = 0; i < aiEatOwn.length; i++) {
// 这个是我们如果吃过之后的那个棋子的数据,因为我们需要判断吃完以后自己会不会被吃的情况
const afterChess = JSON.parse(JSON.stringify(aiEatOwn[i]));
afterChess.location = aiEatList[i].location; // 坐标不需要变化,用吃完之后的棋子坐标
// 这里我们需要判断吃完以后不会被吃,或者自己跟对方的子相等 或者猫吃龙的情况,
// 则说明可以直接吃
// isDotEat() 方法在下面
if((!this.isDotEat(afterChess)) || (aiEatOwn[i].weight <= aiEatList[i].weight)) {
bool = true;
this.handlePosition(aiEatOwn[i].location, aiEatList[i].location, aiEatList[i]);
break;
}
}
if(bool) {
return;
}
}
这里我们用了一个isDotEat方法,主要是电脑用来判断某个点是否有被吃的可能
true=会被吃 false=不会被吃
isDotEat(animal) {
// 克隆一个副本备用
let list = JSON.parse(JSON.stringify(this.mapList));
// 如果传过来的不是一个动物,则直接会被吃,多一事不如少一事
if(!animal) { return true; }
// 拿到当前棋子的坐标
let [x, y] = animal.location;
// 判断当前棋子四周相邻有没有能吃自己的
// onIsEat方法判断两个动物是否有吃与被吃的关系,具体请查看【玩家操作】的第10步
if(
(x - 1 >= 0 && this.onIsEat(list[x - 1][y], animal)) ||
(x + 1 <= 7 && this.onIsEat(list[x + 1][y], animal)) ||
(y - 1 >= 0 && this.onIsEat(list[x][y - 1], animal)) ||
(y + 1 <= 3 && this.onIsEat(list[x][y + 1], animal))
) {
return true;
}
// 判断每个子上下左右有没有鹰要吃自己
// 有鹰并且之间有一颗子
// num = 两子之间存在的棋子数
if(x - 2 >= 0) {
let num = 0, isHawk = false;
for(let a = x - 1; a >= 0; a--) {
if(list[a][y] && list[a][y].isFilp && list[a][y].name == '鹰' && list[a][y].camp == this.player2.camp) { isHawk = true; break; }
if(list[a][y] != null) { num++; }
}
if(num == 1 && isHawk) {
return true;
}
}
if(x + 2 <= 7) {
let num = 0, isHawk = false;
for(let a = x + 1; a <= 7; a++) {
if(list[a][y] && list[a][y].isFilp && list[a][y].name == '鹰' && list[a][y].camp == this.player2.camp) { isHawk = true; break; }
if(list[a][y] != null) { num++; }
}
if(num == 1 && isHawk) {
return true;
}
}
if(y - 2 >= 0) {
let num = 0, isHawk = false;
for(let a = y - 1; a >= 0; a--) {
if(list[x][a] && list[x][a].isFilp && list[x][a].name == '鹰' && list[x][a].camp == this.player2.camp) { isHawk = true; break; }
if(list[x][a] != null) { num++; }
}
if(num == 1 && isHawk) {
return true;
}
}
if(y + 2 <= 3) {
let num = 0, isHawk = false;
for(let a = y + 1; a <= 3; a++) {
if(list[x][a] && list[x][a].isFilp && list[x][a].name == '鹰' && list[x][a].camp == this.player2.camp) { isHawk = true; break; }
if(list[x][a] != null) { num++; }
}
if(num == 1 && isHawk) {
return true;
}
}
return false;
},
(4)逃跑
判断每个子被追杀的情况下,周围有没有空地,有则逃跑
逃跑的时候,判断空地的周围有没有能吃我的,如果有,则不用跑,如果没有则跑
如果自身旁边有比狩猎者等级的我方子,则不用跑,达到一换一的效果
首先我们需要找出我们自己的棋子有哪些能被玩家吃
for(let i = 0; i < aiFilpList.length; i++) {
let a = JSON.parse(JSON.stringify(aiFilpList[i]))
if(this.isDotEat(a)) {
ownPassList.push(aiFilpList[i])
}
}
其次我们需要知道被吃的子周围的空地
for(let i = 0; i < ownPassList.length; i++) {
let [x, y] = ownPassList[i].location;
let arr = []
if(x - 1 >= 0 && this.mapList[x - 1][y] == null) {
arr.push([x - 1, y]);
}
if(x + 1 < 8 && this.mapList[x + 1][y] == null) {
arr.push([x + 1, y])
}
if(y - 1 >= 0 && this.mapList[x][y - 1] == null){
arr.push([x, y - 1])
}
if(y + 1 < 4 && this.mapList[x][y + 1] == null) {
arr.push([x, y + 1])
}
nullList.push({ parent: [x, y], child: [...arr] })
}
然后让最大权重的棋子先跑,我们需要先获取最大权重的棋子
let maxWeightChess = []; // 最大权重棋子的坐标
let maxWeight = 0; // 权重值
let maxChild = []; // 空地
for (let i = 0; i < nullList.length; i++) {
let [x, y] = nullList[i].parent
if(this.mapList[x][y].weight > maxWeight && nullList[i].child.length > 0) {
maxWeight = this.mapList[x][y].weight
maxWeightChess = [x, y]
maxChild = [...nullList[i].child]
}
}
最后我们需要判断,如果我们到达周围的空地上,此时有没有能吃我的。如果有,则不用跑,因为跑了也死,不如去翻牌或者追杀玩家的子,如果没有则跑。
if(maxWeightChess.length > 0 && maxChild.length > 0) {
let [x, y] = maxWeightChess;
let goodspace = []; // 确定下来的正确的坐标
for(let i = 0; i < maxChild.length; i++) {
let [x1, y1] = maxChild[i];
let a = JSON.parse(JSON.stringify(this.mapList[x][y]));
a.location = [x1, y1];
// 在某个空地上能否被吃
if(!this.isDotEat(a)) {
goodspace = [...maxChild[i]]
break;
}
}
if(goodspace.length > 0) {
this.handlePosition(maxWeightChess, goodspace);
return;
}
}
(5)乘胜追击
查找场上自己的子和对方子,每两个子之间是否能连成一条路线,按照权重从大到小排列。
首先用一颗棋子不远万里去吃玩家的子,肯定要找到一条最短并且分值最高的路。
而有些情况下每个子肯定有吃多个,这时我们就要记录所有路线。
我们在开始之前,先看拿一下BFS(广度优先搜索)算法,它的特点就是可以找出两点之间最短的路径。
- 我们要想从起点到终点,肯定要先从起点开始搜索周围相邻的其他坐标,并且每次搜索一个点之后,都要记录当前点是否被搜索过,每个点只搜索一次,以此类推,找到终点。
- 我们刚开始需要把每个点的信息都初始化,比如第一次搜索(0,0)点,它附近节点nearNodes有两个,而自身是起点,所有自身是没有父节点的。
- 再往下搜索(0,1)点,它的附近节点有两个,一个是起点,一个是(1,1)点,记住它的父节点是起点(0,0),又因为起点是第一个点,是搜索过的,这时候我们只需要去搜索(1,1)点。
- 继续搜索(1,1)点,它有三个邻节点,(0,1)(1,0)因为被搜索过,所以直接搜索(2,1)点,记住此时父节点是(0,1)。
- 继续搜索(2,1),它的父节点(1,1),邻节点(3,1)
- 以此类推,最后除了起点和终点,它们的父节点连起来就是一条完整的路径。
- 注:这个算法的特点就是地毯式搜索,不放过任何一个点
以上就是我对这个算法的理解,可能不太准确,如更深了解,请自行百度。
// 基本实现方法
function breadthFirstSearch(root) {
// 队列第一个元素就是起点
let queue = [root];
let visited = new Set();
// 从起点开始搜索
while (queue.length > 0) {
let node = queue.shift();
visited.add(node);
for (let neighbor of node.nearNodes) {
if (!visited.has(neighbor)) {
queue.push(neighbor);
}
}
}
}
接下来我们再看游戏中如何运用:
首先我们需要先选出拿出当前场上自己的子和玩家的子(均翻开),再两两搜索路线,取得一个总的路线
let allChessLoad = [];
if(aiFilpList.length > 0 && playerFilpList.length > 0) {
// 我们先用两个for循环来取出每个自己的子和每个玩家的子
for(let i = 0; i < aiFilpList.length; i++) {
let aiChess = aiFilpList[i]; // 电脑的子
for(let j = 0; j < playerFilpList.length; j++) {
let playerChess = playerFilpList[j]; // 玩家的子
// 注意这里主要判断,电脑的子能不能去吃玩家的子,只有能吃才去搜索路线
if(this.getAiIsEatPlayer(aiChess, playerChess)) {
// 此时要注意,如果ai的子能吃玩家的子,此时的以0、1为代表二维数组是需要重新绘制的,
// 因为每走一步,棋盘都会变化,就要重新绘制
// 因为此时除了捕食子、被捕食子、空地以外所以节点都为0
// 1=空地/自身子和玩家子 0=未翻开的卡/其他无关的子
this.bfsLoadList = []; // 刚开始定义的副本数组,形式 [ [0,1,0,0], [1,1,0,0]...[] ]
for(let a = 0; a < this.mapList.length; a++) {
this.bfsLoadList[a] = []
for(let b = 0; b < this.mapList[0].length; b++) {
let [x1, y1] = aiChess.location;
let [x2, y2] = playerChess.location;
if(this.mapList[a][b] === null || (x1 == a && y1 == b) || (x2 == a && y2 == b)) {
this.bfsLoadList[a][b] = 1;
} else {
this.bfsLoadList[a][b] = 0;
}
}
}
// 处理一下克隆数组,给每个节点加上我们需要的属性值nearNodes、parent、value等
// nearNodes=邻节点
// parent=父节点
// value = 0 / 1
this.bfsList = [];
for(let c = 0; c < this.bfsLoadList.length; c++) {
this.bfsList[c] = []
for(let d = 0; d < this.bfsLoadList[0].length; d++) {
this.bfsList[c][d] = null;
}
}
// 这一步是初始化每个节点的信息,包括父节点以及邻节点信息
for(let e = 0; e < this.bfsLoadList.length; e++) {
for(let f = 0; f < this.bfsLoadList[0].length; f++) {
let node = this.getPointNode(e, f);
let up = this.getPointNode(e - 1, f)
let down = this.getPointNode(e + 1, f)
let left = this.getPointNode(e, f - 1)
let right = this.getPointNode(e, f + 1)
node.nearNodes = [up, down, left, right].filter(item => item && item.value == 1)
this.bfsList[e][f] = node
}
}
// 再去计算捕食的最短路径
let arr = this.searchLoad(aiChess.location, playerChess.location);
if(arr.length > 1) {
// 如果有路,则存下,继续查找下一条路
allChessLoad.push(arr);
}
}
}
}
}
以上用到的方法 包括 getAiIsEatPlayer()、getPointNode()、searchLoad(),下面我们一 一讲解
- getAiIsEatPlayer —— 判断电脑的棋子能不能吃玩家的
getAiIsEatPlayer(animal_ai, animal_player) {
if(!animal_ai || !animal_player || animal_ai.camp == animal_player.camp || !animal_ai.isFilp || !animal_player.isFilp) {
return false;
}
let aniWeight_1 = animal_ai.weight
let aniWeight_2 = animal_player.weight
if(
(aniWeight_1 == 7 && [6,5,4,3,2].includes(aniWeight_2)) ||
(aniWeight_1 == 6 && [5,4,3,2,1].includes(aniWeight_2)) ||
(aniWeight_1 == 5 && [4,3,2,1].includes(aniWeight_2)) ||
(aniWeight_1 == 4 && [3,2,1].includes(aniWeight_2)) ||
(aniWeight_1 == 3 && [2,1].includes(aniWeight_2)) ||
// (aniWeight_1 == 2 && [1].includes(aniWeight_2)) ||
(aniWeight_1 == 1 && [7].includes(aniWeight_2))
) {
return true;
}
return false;
}
// 注意上面我忽略了鹰,因为鹰的路径搜索算法过于复杂,它捕食是需要跳子的,所以暂时去除它
- getPointNode —— 给每个点设置我们需要的数据结构
getPointNode(x, y) {
if (x >= 0 && y >= 0 && x < this.bfsList.length && y < this.bfsList[0].length) {
let node = this.bfsList[x] && this.bfsList[x][y];
if(!node) {
let val = this.bfsLoadList[x][y];
if(val !== undefined) {
node = {
x, // 坐标
y, // 坐标
value: val, // 值 1 / 0
checked: false, // 是否搜索过了
parent: null, // 父节点
nearNodes: [] // 邻节点
}
this.bfsList[x][y] = node;
}
}
return node;
}
return null;
}
- searchLoad —— 主要搜索算法
// mapList地图二维数组 aiChess起点 playerChess终点
searchLoad(aiChess, playerChess) {
let begin = this.getPointNode(aiChess[0], aiChess[1]); // 起点信息
let end = this.getPointNode(playerChess[0], playerChess[1]); // 目标点信息
let path = []; // 最终路线
let quere = [begin]; // 搜索队列
while(quere.length > 0) {
let current = quere.shift()
current.checked = true
for(let item of current.nearNodes) {
if(!item.checked) {
item.parent = current;
quere.push(item)
}
}
// 把节点以及它的父节点都存在路径里,说明这些节点可用
let patharr = [];
let pathNode = current;
while(pathNode) {
patharr.push(pathNode)
pathNode = pathNode.parent
}
path = patharr;
// 如果当前节点就是终点,就跳出
if(current.x == end.x && current.y == end.y) {
break;
}
}
// 如果路径数组的第一个元素不是终点坐标,则返回空
// 为什么是第一个元素呢,因为我们取是从队列头部取,然后查找,存是从尾部存,最后终点就会出现在第一个
if(path.length && path[0].x == playerChess[0] && path[0].y == playerChess[1]) {
return path;
}
return [];
}
最后一步,就是拿到所有路线后,我们需要从路线中搜索最完美的一条,进行追击。
首先我们需要保证路线的路途中是不会被玩家其他子吃掉的
然后再把剩余的路线按照分值从大到小排列,我们就取第一条路线去走
// 如果有能行动的路线
if(allChessLoad.length > 0) {
let lastPoint = []; // 最终的路线数组
let roadFirst = null; //
let handleAllRoad = []; // 根据线路对比出,哪些路能要,哪些不能要
let arrCopy = deepcopyArray(allChessLoad); // 克隆路线数组用来处理路线
// 首先需要判断能吃玩家子的多条路线中,哪些路线半路上就会被玩家吃掉,这样的路线剔除
for(let a = 0; a < arrCopy.length; a++) {
// 因为所有路线中是包含起点和终点的,我们得判断这条路是大于2
if(arrCopy[a].length > 2) {
let item = arrCopy[a];
let {x, y} = arrCopy[a][item.length - 1];
roadFirst = JSON.parse(JSON.stringify(this.mapList[x][y]))
// 这里需要跳过每条路线的头尾,也就是捕食者与被捕食者,只计算路程中
for (let b = item.length - 2; b >= 0; b--) {
roadFirst.location = [arrCopy[a][b].x, arrCopy[a][b].y];
// 如果这条路途中会被吃,则设为null
if(this.isDotEat(roadFirst)) {
arrCopy[a] = null;
roadFirst.location = [];
break;
}
}
} else {
// 如果这条路只包含起、终点,则设为null
arrCopy[a] = null;
}
}
// 过滤空数据
handleAllRoad = arrCopy.filter(item => item != null);
if(handleAllRoad.length > 0) {
// 按照最短路排序 短 > 长
handleAllRoad.sort((a, b) => a.length - b.length)
// 拿到第一条最短的路径
lastPoint = handleAllRoad[0];
// 让最后第一个坐标走到倒数第二个坐标,则完成追击
this.handlePosition(
[lastPoint[lastPoint.length - 1].x, lastPoint[lastPoint.length - 1].y],
[lastPoint[lastPoint.length - 2].x, lastPoint[lastPoint.length - 2].y],
)
return;
}
}
(6)翻牌
到这里说明电脑不能吃,不能跑,不能追,只能翻牌操作
注:翻牌这里有点小技巧,就是如果当前场上有自己的鹰的话,就不要随机翻牌,而是翻鹰上下左右隔一颗子的牌,很大几率前中期就能靠鹰吃不少子
这个我没有做,以后再说
// 过滤还没翻的牌,从没翻开的随机翻一张
let noFlipList = list.filter(item => item && !item.isFilp);
if(noFlipList.length > 0) {
let randomChess = noFlipList[Math.floor(Math.random() * noFlipList.length)];
let [x, y] = randomChess.location;
this.mapList[x][y].isFilp = true;
return;
}
(7)投降 —— 不用说了,自己什么都没有啥可干的了,只能投降了,这一步一般是到最后的最后了
this.aiLose = true;
this.isPlay = 2;
this.isWin = 1;
this.$refs.popupWin.open()
附上完整代码地址(https://gitee.com/zhang_meng_lei/uniapp-game-beast)
三、总结
整个游戏做下来,还是学到很多的,尤其是广度优先搜索。本想着把这个游戏中的电脑做的更聪明,最起码能跟人不相上下,但是牌类游戏中最难的就是,考虑不到很全面,永远不知道每盘游戏排列组合,以及牌与牌之间的联系,就像中国象棋一样,每一盘都是新的,每一盘都是不固定的走法。所以这方面还是比较费事,其他的慢慢的还是可以啃下来的。
最后游戏中更多优化还会留着以后返回来慢慢琢磨,以便把这个电脑算法打造的更厉害一点。
最后的最后 —— 谢谢观看!
更多推荐
所有评论(0)