实现效果
使用el ui组件
五子棋
四子重力棋
项目目录
组件
src
- ├── components
- │ ├── chassBody.vue // 包含所有棋子组件
- │ └── chassHeader.vue // 包含大部分的功能组件(模式选择、界面设置等)
- | └── outcomeWindow.vue // 一轮游戏结束的弹窗组件
- ├── stores
- │ ├── chassData.js // 棋子需要使用的数据 (user1的所有棋子、user2的所有棋子、总棋数...)
- │ └── layoutData.js // 界面布局使用的数据 (棋盘的规格、棋子的大小....)
- ├── utils
- │ ├── chassOutcome.js // 计算棋子的胜负
- │ ├── connectFourComputer2.js // 连续四子棋的人机对战2的逻辑
- │ ├── handleGameClick.js // 不同游戏模式下棋子的点击事件
- │ └── userVsComputer.js // 人机对战的逻辑
- ├── App.vue 主组件
- ├── main.js 项目入口文件
- └── README.md 说明文档
响应式布局
棋盘是蓝色的背景 动态确定背景大小 使用grid布局写的棋子参数来源于右下角的规格,原本准备拿动态组件写的,但想到棋盘永远是那样只是下棋逻辑变了数据也是一样,所以随着游戏的增加多写工具函数就行了
<div class="bg" :style="{ width: `calc(${chassSize} * ${chassCol} + ${chassSize})`, height: `calc(${chassSize} * ${chassRow} + ${chassSize})` }">
<div class="chassBg" :style="computeClass()">
<chassBody></chassBody>
</div>
</div>
// 动态确定大小
const computeClass=()=>{
return{
width: `calc(${chassSize.value} * ${chassCol.value})`,
height: `calc(${chassSize.value} * ${chassRow.value})`,
// 用引号包裹包含特殊字符的属性名
'grid-template-columns': `repeat(${chassCol.value}, 1fr)`,
'grid-template-rows': `repeat(${chassRow.value}, 1fr)`
}
}
动态棋子颜色 都是通过绑定函数在函数中返回css样式的
<div @click="chassClick(rowindex,colindex)" :style="calculateColor(rowindex,colindex)" class="chassCircle"></div>
</div>
// 判断点击颜色
const calculateColor=(rowindex,colindex)=>{
for (const item of user1Chass.value) {
// 当前下棋位置在用户一数据中时返回用户一颜色 点击时先添加进对应用户数据再判断
if (item.rowindex === rowindex && item.colindex === colindex) {
return {
backgroundColor: user1ChassColor.value
};
}
}
for (const item of user2Chass.value) {
// 当前下棋位置在用户二数据中时返回用户二颜色
if (item.rowindex === rowindex && item.colindex === colindex) {
return {
backgroundColor: user2ChassColor.value
};
}
}
return ''
}
如这些可以改变棋盘参数和模式的都设置了localStorage.setItem为了不每次刷新都重新设置样式
棋盘实现
主要是棋子的点击 selectGameClick外部工具函数用于判断是哪类游戏 并使用对应下棋逻辑传pinia是因为组件外使用pinia需要获取对应实例
// 输赢状态
const outcomeTitle=computed(()=> ChassData.outcomeTitle)
// 棋子数量
const chassNumber=computed(()=> ChassData.chassNumber)
// 用户一的数据
const user1Chass=computed(()=> ChassData.user1Chass)
// 用户二的数据
const user2Chass=computed(()=> ChassData.user2Chass)
// 当前模式
const pattern=computed(()=> ChassData.pattern)
// 玩家一棋子颜色
const user1ChassColor=computed(()=>layoutData.user1ChassColor)
// 玩家二棋子颜色
const user2ChassColor=computed(()=>layoutData.user2ChassColor)
// 当前棋子
const currentChess=reactive({
rowindex:0,
colindex:0
})
// 棋盘行列
const chassRow=computed(()=>{
return parseInt(ChassData.Specifications.split("*")[0])
})
const chassCol=computed(()=>{
return parseInt(ChassData.Specifications.split("*")[1])
})
const chassClick = async (rowindex, colindex) => {
// 检查该位置是否已经有棋子
const isOccupied = user1Chass.value.some(item => item.rowindex === rowindex && item.colindex === colindex) || user2Chass.value.some(item => item.rowindex === rowindex && item.colindex === colindex);
if (!isOccupied&&!outcomeTitle.value) {
await selectGameClick(pinia, pattern.value, rowindex, colindex, user1Chass, user2Chass, outcomeTitle);
currentChess.rowindex = rowindex;
currentChess.colindex = colindex;
ChassData.setCurrentUser();
ChassData.setChassNumber();
}
}
export async function selectGameClick(pinia, pattern, rowindex, colindex, user1Chass, user2Chass, outcomeTitle) {
const ChassData = userChassData(pinia);
// 选择游戏 不同游戏 规则也不同
if (pattern.includes('五子棋')) {
return await selectGobangClick(pinia, pattern, rowindex, colindex, user1Chass, user2Chass, outcomeTitle);
} else if (pattern.includes('连续四子棋')) {
return await selectConnectFourClick(pinia, pattern, rowindex, colindex, user1Chass, user2Chass, outcomeTitle);
}
}
// 五子棋的规则 也就是棋子点击后的效果
async function selectGobangClick(pinia, pattern, rowindex, colindex, user1Chass, user2Chass, outcomeTitle) {
const ChassData = userChassData(pinia);
const chassNumber = ChassData.chassNumber;
if (pattern.includes('双人对战')) {
chassNumber % 2 === 0 ? ChassData.setUser1Chass(rowindex, colindex) : ChassData.setUser2Chass(rowindex, colindex);
} else if (pattern.includes('人机对战1级')) {
ChassData.setUser1Chass(rowindex, colindex);
if (!outcomeTitle.value) {
const ComputerData = await Computer1(user1Chass.value, user2Chass.value, rowindex, colindex, ChassData.Specifications);
ChassData.setChassNumber();
ChassData.setCurrentUser();
ChassData.setUser2Chass(ComputerData.rowindex, ComputerData.colindex);
}
} else if (pattern.includes('人机对战2级')) {
ChassData.setUser1Chass(rowindex, colindex);
if (!outcomeTitle.value) {
const ComputerData = await Computer2(user1Chass.value, user2Chass.value, rowindex, colindex, ChassData.Specifications);
ChassData.setChassNumber();
ChassData.setCurrentUser();
ChassData.setUser2Chass(ComputerData.rowindex, ComputerData.colindex);
}
}
}
// 连续四子棋的规则 也就是棋子点击后的效果
async function selectConnectFourClick(pinia, pattern, rowindex, colindex, user1Chass, user2Chass, outcomeTitle) {
const ChassData = userChassData(pinia);
const chassNumber = ChassData.chassNumber;
if (pattern.includes('双人对战')) {
chassNumber % 2 === 0 ? ChassData.setUser1ChassConnectFour(rowindex, colindex) : ChassData.setUser2ChassConnectFour(rowindex, colindex);
} else if (pattern.includes('人机对战1级')) {
ChassData.setUser1ChassConnectFour(rowindex, colindex);
if (!outcomeTitle.value) {
const ComputerData = await Computer1(user1Chass.value, user2Chass.value, rowindex, colindex, ChassData.Specifications);
ChassData.setChassNumber();
ChassData.setCurrentUser();
ChassData.setUser2ChassConnectFour(ComputerData.rowindex, ComputerData.colindex);
}
} else if (pattern.includes('人机对战2级')) {
ChassData.setUser1ChassConnectFour(rowindex, colindex);
if (!outcomeTitle.value) {
const ComputerData = await connectFourComputer2(user1Chass.value, user2Chass.value, rowindex, colindex, ChassData.Specifications);
ChassData.setChassNumber();
ChassData.setCurrentUser();
ChassData.setUser2ChassConnectFour(ComputerData.rowindex, ComputerData.colindex);
}
}
}
其实准备要动态分组件写三种模式的 但想到棋盘永远是那样只是下棋逻辑变了数据也是一样,所以随着游戏的增加多写工具函数就行了,if(!outcomeTitle.value)是因为电脑是紧挨用户的棋后的但用户下完这次后可能就赢了也不用电脑再下了 一些值的改变也是这个逻辑
pinia和胜利条件
主要是其中的下棋过程,其他的都是定义加一个修改也就set加其变量名 五子棋及其变种都是多个方向判断后得出结果 只是数量不同
- topBottomOutcome 上下判断
- leftRightOutcome 左右判断
- tiltOutcome 斜判断 有左上到右下 有右上到左下 一共八个方位四种可能 if一次判断三个同时调用三个函数满足一个就获胜
/**
*
* @param {Array} user - 判断用户的棋子数组
* @param {number} row - 当前落子行索引
* @param {number} col - 当前落子列索引
* @param {number} victoryLength - 胜利条件的棋子数量
* @param {number} count - 判断后当前的连续棋子数量 需要大于等于victoryLength胜利
* @param {number} rowLength -棋盘的行数
* @param {number} colLength -棋盘的列数
*/
// row一行 col一列
const setUser1Chass=(rowindex,colindex)=>{
user1Chass.value.push({rowindex,colindex})
// console.log(user1Chass.value);
// 只要有一个返回true 游戏结束
console.log(rowLength,colLength);
if(topBottomOutcome(user1Chass.value, rowindex,colindex)||leftRightOutcome(user1Chass.value, rowindex,colindex)||tiltOutcome(user1Chass.value, rowindex, colindex, rowLength.value, colLength.value)){
outcomeTitle.value='user1胜利'
}
}
// row一行 col一列
const setUser2Chass=(rowindex,colindex)=>{
user2Chass.value.push({rowindex,colindex})
// console.log(user2Chass.value);
// 只要有一个返回true 游戏结束
console.log(rowLength,colLength);
if(topBottomOutcome(user2Chass.value, rowindex,colindex)||leftRightOutcome(user2Chass.value, rowindex,colindex)||tiltOutcome(user2Chass.value, rowindex, colindex, rowLength.value, colLength.value)){
outcomeTitle.value='user2胜利'
}
}
topBottomOutcome
上下判断只需要行数据就行了 也就是row sort 会遍历数组的返回值是true时换位 一些数的排序非常好用 数组有汉字或符号会出问题 排完序后有五个连续的就是上下判断成功胜利了
export function topBottomOutcome(user,row,col,victoryLength){
// 有四次顺序连续就是五个棋子了
let count=1
const rowsArr=[]
user.forEach((item)=>{
if(item.colindex===col){
rowsArr.push(item.rowindex)
}
})
// 排序方便计算是否有连续的五个 a-b大于返回true降序
rowsArr.sort((a,b)=>a-b)
for (let i = 0; i < rowsArr.length-1; i++) {
if (rowsArr[i] === rowsArr[i + 1] - 1) {
count++;
if (count >= victoryLength) {
return true;
}
} else {
// 若不连续,重置计数
count = 1;
}
}
}
leftRightOutcome
同上下判断一样但只需要col 列数据就够了
export function leftRightOutcome(user,row,col,victoryLength){
// 有四次顺序连续就是五个棋子了
let count=1
const colsArr=[]
user.forEach((item)=>{
// 必须是同一列
if(item.rowindex==row){
colsArr.push(item.colindex)
}
})
// 排序方便计算是否有连续的五个 a-b大于返回true降序
colsArr.sort((a,b)=>a-b)
for (let i = 0; i < colsArr.length-1; i++) {
if (colsArr[i] === colsArr[i + 1] - 1) {
count++;
if (count >= victoryLength) {
return true;
}
} else {
count = 1;
}
}
}
tiltOutcome
总共是调用了countTilt(正斜和负斜) 里面分别就是(右下,左上,右上,左下)四次checkDirection 当第一次调用countTilt时也就是正斜右下,左上连续大于等于5时就直接返回也就是胜利 没有就再判断负斜(右上,左下)都不满足就没有斜线满足五子棋
export function tiltOutcome(user,row,col,rowLength,colLength,victoryLength){
// 定义一个函数 用来计算斜方向的所有棋子是否有连续五个
function countTilt(dr,dc){
let count=1
const checkDirection = (step) => {
// 每个方向如大方向是左上到右下 第一次就是左上第二次就是右下
let localCount = 0;
// 当前点击不算
for (let i = 1; i <= victoryLength - 1; i++) {
// row当前点击位置的行数据col列数据 i * dr i * dc方向如左上还是右下 右上还是左下
const newRow = row + step * i * dr;
const newCol = col + step * i * dc;
// 如果该user里的数据有这个坐标 且没超出棋盘范围
if (newRow >= 0 && newRow < rowLength && newCol >= 0 && newCol < colLength &&
user.some(item => item.rowindex === newRow && item.colindex === newCol)) {
localCount++;
} else {
// 一旦不连续,停止检查
break;
}
}
// 正斜时左上连续数量然后右下数量
return localCount;
};
// 分别检查两个相反方向 (第一次正斜时 行和列都加1就是右下 负斜时行+1列加-1 左下)
const forwardCount = checkDirection(1);
// (第一次正斜时 行和列都-1就是左上 负斜时行-1列加1 右上)
const backwardCount = checkDirection(-1);
// 计算一次判断总连续棋子数量(第一次正斜线 第二次负斜线)
count += forwardCount + backwardCount;
return count >= victoryLength;
}
// dc区分先是左上到右下 如果满足直接退出
if (countTilt(1, 1)) {
return true;
}
// 从右上到左下
return countTilt(1, -1);
}
人机模式
- 当模式是"双人对战"时通过棋子总数判断往user1加还是user2
- Computer1也就是第一级人机
- Computer2也就是第二级人机
但人机是在用户下完后就下的,四子棋也是差不多只是电脑二的逻辑变了一点因为有重力的关系ChassData.setUser2Chass(ComputerData.rowindex,ComputerData.colindex)就要随用户的下棋并添加而添加 添加什么坐标 就是Computer1,Computer2
if(pattern.value=="双人对战"){
chassNumber.value%2==0?ChassData.setUser1Chass(rowindex,colindex):ChassData.setUser2Chass(rowindex,colindex);
}else if(pattern.value=="人机对战1级"){
// 主体数据 ,客体数据,主题当前点击坐标,棋盘格局
ChassData.setUser1Chass(rowindex,colindex)
if(!outcomeTitle.value){
let ComputerData = await Computer1(user1Chass.value,user2Chass.value,rowindex,colindex,ChassData.Specifications)
ChassData.setChassNumber()
ChassData.setCurrentUser()
ChassData.setUser2Chass(ComputerData.rowindex,ComputerData.colindex)
}
}else if(pattern.value=='人机对战2级'){
ChassData.setUser1Chass(rowindex,colindex)
// 由于电脑是用户下棋后立马下 而可能用户下的新一步改变胜负所以要暂停变化值
if(!outcomeTitle.value){
let ComputerData = await Computer2(user1Chass.value,user2Chass.value,rowindex,colindex,ChassData.Specifications)
ChassData.setChassNumber()
ChassData.setCurrentUser()
ChassData.setUser2Chass(ComputerData.rowindex,ComputerData.colindex)
}
}
人机1Computer1
人机1的下棋只会在最新坐标的周围下
实现是循环 for中的第二个值也就是条件其实就是true继续,false停止我们不满足条件时就一直继续
定义八个方位,然后使用随机数获取其中之一,并将其与当前新用户坐标数据相加就是电脑的在周围下
当然这八个方位也不一定都行,可能在板边可能已被占用就要根据.some设置满足条件才会添加就能知道是否已被占用再和棋盘范围一起判断就行只有都不冲突时才会退出循环
const Computer1Arr=[
[1, 1],
[1, 0],
[1, -1],
[0, 1],
[0, 0],
[0, -1],
[-1, 1],
[-1, 0],
[-1, -1]
]
// 实现获取最大行 列
let rowLength=parseInt(Specifications.split("*")[0])
let colLength=parseInt(Specifications.split("*")[1])
// true 一直满足条件
let i=true
// 每次重复时改变j值来源于Computer1Ar
for(let j=Math.floor(Math.random() * 9);i;j=Math.floor(Math.random() * 9)){
// 改变后的行 列
rowindex=rowindex+Computer1Arr[j][0]
colindex=colindex+Computer1Arr[j][1]
// 检查该位置是否在棋盘内且未被占用
const isOccupied = [...user1Chass, ...user2Chass].some(item =>
item.rowindex === rowindex && item.colindex === colindex
);
// 加上检测是否在范围外 都不满足退出 满足继续变j值
if((rowindex>=0&&rowindex<rowLength)&&(colindex>=0&&colindex<colLength)&&!isOccupied){
i=false
console.log(rowindex,colindex);
}
}
return {
rowindex,
colindex
}
人机2Computer2
当用户中最长连续的未被堵住的长度大于等于3时就会电脑就会堵他,当不大于时会检测电脑中的所有棋子的所有可能也是最长连续的未被堵住的长度的棋子的基础上下也就是赢 ,当电脑中的最长长度大于等于4时就直接下不再堵用户的棋。 也有问题就是如用户有[1,2] [1,3] [1,5]电脑判断不到这种中间不是连续的但也没被阻挡的
ai使用
是由ai辅导写的就是我上面的话 我是指定传入的数据然后根据这些数据给ai后生成的并加以修改成需要的样子 然后由于是我本身传的数据能看懂大概然后再一点一点把共同部分封装成全局函数
export async function Computer2(user1Chass, user2Chass, rowindex, colindex, Specifications) {
// 最大行 最大列
let rowLength = parseInt(Specifications.split("*")[0]);
let colLength = parseInt(Specifications.split("*")[1]);
// 八个方位
const getDirections = () => [
[-1, -1], [-1, 0], [-1, 1],
[0, -1], [0, 1],
[1, -1], [1, 0], [1, 1]
];
// 将user2Chass转化为Map方便寻找
const createUser2ChassMap = (user2Chass) => {
const user2ChassMap = new Map();
user2Chass.forEach(({ rowindex, colindex }) => {
user2ChassMap.set(`${rowindex}-${colindex}`, true);
});
return user2ChassMap;
};
// 找到 当前新点击的坐标 最长的连续棋子方向
let maxLength = 0;
let bestDirection = null;
const directions = getDirections();
// 循环其八个方位
for (const [dr, dc] of directions) {
let length = countConsecutive(user1Chass, rowindex, colindex, dr, dc) +
countConsecutive(user1Chass, rowindex, colindex, -dr, -dc) + 1;
if (length >= 3 && length > maxLength) {
maxLength = length;
bestDirection = [dr, dc];
}
}
// 计算 user2 连续未堵死的最长长度
const user2ChassMap = createUser2ChassMap(user2Chass);
const { maxUser2Length, bestUser2Direction, startRow, startCol, endRow, endCol } = findMaxUser2ConsecutiveLength(
user2Chass, directions, rowLength, colLength, user1Chass, user2ChassMap
);
// 就是当不堵用户的优先级大于堵用户的游戏及时
if ((maxLength <= 3 && maxUser2Length >= 3)||maxUser2Length>=4) {
// 可能User2没有连续被没被堵死的
if (bestUser2Direction) {
const [dr, dc] = bestUser2Direction;
const result = tryPlaceAtStartAndEnd(startRow, startCol, endRow, endCol, dr, dc, rowLength, colLength, user1Chass, user2Chass);
if (result) {
return result;
}
}
return randomPlacement(rowindex, colindex, rowLength, colLength, user1Chass, user2Chass);
}
// 如果当前点击的位置的方位中有>=3个时就堵
if (bestDirection) {
const [dr, dc] = bestDirection;
// 尝试正向
let newRow = rowindex + dr;
let newCol = colindex + dc;
if (isValidMove(newRow, newCol, rowLength, colLength, user1Chass, user2Chass)) {
return {
rowindex: newRow,
colindex: newCol
};
}
// 尝试反向
newRow = rowindex - dr;
newCol = colindex - dc;
if (isValidMove(newRow, newCol, rowLength, colLength, user1Chass, user2Chass)) {
return {
rowindex: newRow,
colindex: newCol
};
}
//当前点击的位置的方位中有<=3个时 且电脑user2中最长的也小于<=3时 length>0是因为第一次user2Chass里没数据所以使用人机1的逻辑在其周围下
} else if(user2Chass.length > 0){
if (bestUser2Direction) {
const [dr, dc] = bestUser2Direction;
const result = tryPlaceAtStartAndEnd(startRow, startCol, endRow, endCol, dr, dc, rowLength, colLength, user1Chass, user2Chass);
if (result) {
return result;
}
}
return randomPlacement(rowindex, colindex, rowLength, colLength, user1Chass, user2Chass);
}
return randomPlacement(rowindex, colindex, rowLength, colLength, user1Chass, user2Chass);
}
检测传入棋子的所有连续
这个同正斜的胜利条件一样chassOutcome.js里的countTilt一样
function checkConsecutiveLength(rowindex, colindex, dx, dy, rowLength, colLength, user2ChassMap) {
let length = 0;
for (let i = 0; ; i++) {
const newRow = rowindex + i * dx;
const newCol = colindex + i * dy;
if (newRow < 0 || newRow >= rowLength || newCol < 0 || newCol >= colLength) break;
if (user2ChassMap.has(`${newRow}-${newCol}`)) {
length++;
} else {
break;
}
}
return length;
}
user2的放置棋子是头还是尾放置
即使知道了最长的那条也不确定是头放还是尾放置 都试试 其实可以加权重比如说那边空位多优先考虑那边
// user2的在起始端和结束端放置棋子的逻辑
function tryPlaceAtStartAndEnd(startRow, startCol, endRow, endCol, dr, dc, rowLength, colLength, user1Chass, user2Chass) {
// 尝试在起始端放置
let newRow = startRow - dr;
let newCol = startCol - dc;
if (isValidMove(newRow, newCol, rowLength, colLength, user1Chass, user2Chass)) {
return {
rowindex: newRow,
colindex: newCol
};
}
// 尝试在结束端放置
newRow = endRow + dr;
newCol = endCol + dc;
if (isValidMove(newRow, newCol, rowLength, colLength, user1Chass, user2Chass)) {
return {
rowindex: newRow,
colindex: newCol
};
}
return null;
}
检测点击位置的所有连续
也是判断其对应方向的所有连续数量(上,下,左,右,左上,右下,右上,左下)
- dr,dc方向
- userChass传入的用户数据
- row, col当前新点击的坐标
- count计数
function countConsecutive(userChass, row, col, dr, dc) {
let count = 0;
row += dr;
col += dc;
while (userChass.some(item => item.rowindex === row && item.colindex === col)) {
count++;
row += dr;
col += dc;
}
return count;
}
该位置是否有效
和人机1一样的判断逻辑
// 辅助函数:检查位置是否有效
function isValidMove(row, col, rowLength, colLength, user1Chass, user2Chass) {
return (row >= 0 && row < rowLength) &&
(col >= 0 && col < colLength) &&
!([...user1Chass, ...user2Chass].some(item =>
item.rowindex === row && item.colindex === col
));
}
第一次电脑的下棋或所有条件不满足时
同人机1一样的下棋方式 周围下
function randomPlacement(rowindex, colindex, rowLength, colLength, user1Chass, user2Chass) {
let i = true;
let newRowindex = rowindex;
let newColindex = colindex;
for (let j = Math.floor(Math.random() * 9); i; j = Math.floor(Math.random() * 9)) {
newRowindex = newRowindex + Computer1Arr[j][0];
newColindex = newColindex + Computer1Arr[j][1];
const isOccupied = [...user1Chass, ...user2Chass].some(item =>
item.rowindex === newRowindex && item.colindex === newColindex
);
if ((newRowindex >= 0 && newRowindex < rowLength) && (newColindex >= 0 && newColindex < colLength) && !isOccupied) {
i = false;
}
}
return {
rowindex: newRowindex,
colindex: newColindex
};
}
总结
几乎每个人都会玩五子棋以及变种井字游戏 只需要一个笔一张纸就能玩,现在变种更多了 如重力连续四子棋这些需要实物才能玩 网上购物或那种线下益智玩具店就能购买,重力连续四子棋已加入,现在加五子棋类游戏只用写下棋逻辑就行了 还有对应的人机2逻辑 ,如要加围棋(没下过)需要添加新的胜负判断 下棋后还要加上是否能吃子
需要改进的地方
由于只使用了一个组件表现棋盘 所以游戏及其模式选择用了太多if 这样游戏多了以后没注释就有点麻烦,还有人机交战2还是有明显缺陷如用户有[1,2] [1,3] [1,5]电脑判断不到这种中间不是连续的但也没被发现然后阻挡的