游戏对战设计

游戏对战概述

游戏对战算法是五子棋游戏的核心,游戏双方进入同一房间后,就可以进行对战,当有一方的 5 个棋子连成一条线时,表示获胜,并弹出相应的信息提示,如图 18.13 所示,这时单击弹出的对话框中的 “确定” 按钮,可以重新开始游戏(即清空棋盘上的所有棋子并重新开始游戏,由当前失败的一方先下棋)。

image 2024 04 18 11 34 43 336
Figure 1. 图18.13 游戏对战页面

游戏对战页面初始化

游戏对战的主要逻辑代码是在 public 文件夹下的 chessBoard.js 文件中实现的。在该文件中,首先获取 index.html 页面中的元素,并定义客户端监听网址及端口;然后为 index.html 页面中的 “进入” 按钮添加 click 事件监听,在该事件监听中,主要处理执行玩家进入房间、玩家列表显示、棋子显示、下棋等操作时的页面显示状态。关键代码如下:

let chess = document.getElementById("chess");
let layer = document.getElementById("layer");
let context = chess.getContext("2d");
let context2 = layer.getContext("2d");
// let first = document.querySelectorAll('.first')
// let currentPlayer = document.querySelector('#currentPlayer')
let winner = document.querySelector('#winner')
let cancelOne = document.querySelector('#cancelOne')
let colorSelect = document.querySelector('#colorSelect')
let user = document.querySelector('#user')
let room = document.querySelector('#room')
let enter = document.querySelector('#enter')
let userInfo = document.querySelector('.userInfo')
let waitingUser = document.querySelector('.waitingUser')

let socket = io.connect('http://127.0.0.1:3000')

let userList = []

enter.addEventListener('click', (event) => {
	if( !user.value || !room.value ){
      alert('请输入用户名或者房间号...')
      return;
  }
  let ajax = new XMLHttpRequest()
  ajax.open('get', 'http://127.0.0.1:3000?room=' + room.value + '&user=' + user.value)
  ajax.send()
  ajax.onreadystatechange = function () {
  	if (ajax.readyState === 4 && ajax.status === 200) {
  		//用户请求进入房间
  		socket.emit('enter', {
				userName: user.value,
				roomNo: room.value
			})
			//canDown:true用户执黑棋, canDown:false用户执白棋
			socket.on('userInfo', (data) => {
				obj.me = data.canDown
			})
			//显示房间用户信息
			socket.on('roomInfo', (data) => {
				if (data.roomNo === room.value) {
					userInfo.innerHTML = ''
					for (let user in data.roomInfo) {
						if (user !== 'full') {
							userList.push(user)
							let div = document.createElement('div')
							let userName = document.createTextNode(user)
							div.appendChild(userName)
							div.setAttribute('class', 'userItem')
							userInfo.appendChild(div)
							if (data.roomInfo[user].canDown) {
								div.style.backgroundColor = 'black'
								div.style.color = 'white'

								waitingUser.innerHTML = `等待${user}落子...`
							} else {
								div.style.backgroundColor = 'white'
								waitingUser.innerHTML = `等待其他用户加入...`
							}
						}
					}
				}
			})
			//下棋
			layer.onclick = function (e) {
		    let x = e.offsetX ;
		    let y = e.offsetY ;
		    let j = Math.floor(x/30) ;
		    let i = Math.floor(y/30) ;
		    if (chessBoard[i][j] === 0) {
		      socket.emit('move', {
		      	i:i,
		      	j:j,
		      	isBlack: obj.me,
		      	userName: user.value,
		      	roomNo: room.value
		      })
		    }
			}
			//显示棋子
			socket.on('moveInfo', (data) => {
				if (data.roomNo === room.value) {
					let {i, j, isBlack} = data
					oneStep(i, j, isBlack,false)
					if (data.isBlack) {
					  chessBoard[i][j] = 1;
					} else {
					  chessBoard[i][j] = 2;
					}
					if (checkWin(i,j,obj.me)) {
		      	socket.emit('userWin', {
		      		userName: data.userName,
		      		roomNo: data.roomNo
		      	})
		      }
		      userList.forEach((item, index, array) => {
		      	if (item !== data.userName) {
		      		waitingUser.innerHTML = `等待${item}落子...`
		      	}
		      })
				}
			})
			//提示胜利者,禁止再下棋
			socket.on('userWinInfo', (data) => {
				if (data.roomNo === room.value) {
					alert(`${data.userName}胜利!`)
					waitingUser.innerHTML = `${data.userName}胜利!`
					waitingUser.style.color = 'red'
					for (let i = 0; i < 15; i++) {
						for (let j = 0; j < 15; j++) {
							chessBoard[i][j] = 3
						}
					}
					reStart()
					waitingUser.style.color = 'black'
				}
			})
			//提示用户房间已满
			socket.on('roomFull', (data) => {
				alert(`房间${data}已满,请更换房间!`)
				room.value = ''
			})
			//提示用户重名
			socket.on('userExisted', (data) => {
				alert(`用户${data}已存在,请更换用户名!`)
			})

			socket.on('userEscape', ({userName, roomNo}) => {
				if (roomNo === room.value) {
					alert(`${userName}逃跑了,请等待其他用户加入!`)
					reStart()
				}
			})
  	}
  }
})

function checkLeave(){
	socket.emit('userDisconnect', {
		userName: user.value,
		roomNo: room.value
	})
}

绘制棋盘

我们主要使用 HTML 中的 Canvas 技术来绘制棋盘,在绘制棋盘时,同时监听鼠标的 onmousemove 事件和 onmouseleave 事件,实现下棋的功能,如图 18.14 所示。

image 2024 04 18 11 38 27 146
Figure 2. 图18.14 绘制棋盘

关键代码如下:

//鼠标移动时棋子提示
let old_i = 0;
let old_j = 0;

layer.onmousemove = function (e) {
    if (chessBoard[old_i][old_j] === 0) {
        context2.clearRect(15 + old_j * 30 - 13, 15 + old_i * 30 - 13, 26, 26)
    }
    var x = e.offsetX;
    var y = e.offsetY;
    var j = Math.floor(x / 30);
    var i = Math.floor(y / 30);
    if (chessBoard[i][j] === 0) {
        oneStep(i, j, obj.me, true)
        old_i = i;
        old_j = j;
    }
}

layer.onmouseleave = function (e) {
    if (chessBoard[old_i][old_j] === 0) {
        context2.clearRect(15 + old_j * 30 - 13, 15 + old_i * 30 - 13, 26, 26)
    }
}

//绘制棋子
function oneStep(j, i, me, isHover) {//i,j分别是在棋盘中的定位,me代表白棋还是黑棋
    context2.beginPath();
    context2.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);//圆心会变的,半径改为13
    context2.closePath();
    var gradient = context2.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 15, 15 + i * 30, 15 + j * 30, 0);
    if (!isHover) {
        if (me) {
            gradient.addColorStop(0, "#0a0a0a");
            gradient.addColorStop(1, "#636766");
        } else {
            gradient.addColorStop(0, "#D1D1D1");
            gradient.addColorStop(1, "#F9F9F9");
        }
    } else {
        if (me) {
            gradient.addColorStop(0, "rgba(10, 10, 10, 0.8)");
            gradient.addColorStop(1, "rgba(99, 103, 102, 0.8)");
        } else {
            gradient.addColorStop(0, "rgba(209, 209, 209, 0.8)");
            gradient.addColorStop(1, "rgba(249, 249, 249, 0.8)");
        }
    }
    context2.fillStyle = gradient;
    context2.fill();
}

游戏算法及胜负判定

五子棋的游戏规则是,以落棋点为中心,向 8 个方向查找同一类型的棋子,如果相同棋子数大于等于 5,则表示此类型棋子所有者为赢家,因此以此规则为基础,编写相应的实现算法即可。五子棋棋子查找方向如图 18.15 所示。关键代码如下:

image 2024 04 18 11 42 47 457
Figure 3. 图18.15 判断一枚棋子在8个方向上摆出的棋型
//检查各个方向是否符合获胜条件
function checkDirection(i, j, p, q) {
    //p=0,q=1 水平方向;p=1,q=0 竖直方向
    //p=1,q=-1 左下到右上
    //p=-1,q=1 左到右上
    let m = 1
    let n = 1
    let isBlack = obj.me ? 1 : 2

    for (; m < 5; m++) {
        // console.log(`m:${m}`)
        if (!(i + m * p >= 0 && i + m * p <= 14 && j + m * q >= 0 && j + m * q <= 14)) {
            break;
        } else {
            if (chessBoard[i + m * (p)][j + m * (q)] !== isBlack) {
                break;
            }
        }
    }
    for (; n < 5; n++) {
        // console.log(`n:${n}`)
        if (!(i - n * p >= 0 && i - n * p <= 14 && j - n * q >= 0 && j - n * q <= 14)) {
            break;
        } else {
            if (chessBoard[i - n * (p)][j - n * (q)] !== isBlack) {
                break;
            }
        }
    }
    if (n + m + 1 >= 7) {
        return true
    }
    return false
}

//检查是否获胜
function checkWin(i, j) {
    // console.table(chessBoard)
    if (checkDirection(i, j, 1, 0) || checkDirection(i, j, 0, 1) ||
        checkDirection(i, j, 1, -1) || checkDirection(i, j, 1, 1)) {
        return true
    }
    return false
}

重新开始游戏

当对战双方有一方胜利时,弹出对话框进行提示,单击对话框中的 “确定” 按钮,可以重新开始游戏,该功能是通过 reStart() 方法实现的,代码如下:

//重新开始
function reStart() {
    context2.clearRect(0, 0, 450, 450)
    waitingUser.innerHTML = '请上局失利者先落子!'
    for (let i = 0; i < 15; i++) {
        chessBoard[i] = [];
        for (let j = 0; j < 15; j++) {
            chessBoard[i][j] = 0;
        }
    }
    if (userList.length === 2) {
        if (userList[1] === user.value) {
            obj.me = true
        } else {
            obj.me = false
        }
    }

    old_i = 0;
    old_j = 0;
}

更改棋盘颜色

在五子棋对战页面的棋盘下方有一个颜色块,单击该颜色块,可以弹出颜色选择器,选择指定颜色后可以更改棋盘的颜色,如图 18.16 所示。

image 2024 04 18 11 44 58 693
Figure 4. 图18.16 更改棋盘颜色

改变棋盘颜色功能是通过监听 colorSelect 对象的 change 事件并为指定的区域填充颜色来实现的。代码如下:

//改变棋盘颜色
colorSelect.addEventListener('change', (event) => {
    // console.log(event.target.value)
    context.fillStyle = event.target.value;
    context.fillRect(0, 0, 450, 450,);
    drawLine();
})