写点什么

鸿蒙开发案例:黑白棋

作者:zhongcx
  • 2024-10-14
    北京
  • 本文字数:10412 字

    阅读完需:约 34 分钟

黑白棋,又叫翻转棋(Reversi)、奥赛罗棋(Othello)、苹果棋或正反棋(Anti reversi)。黑白棋在西方和日本很流行。游戏通过相互翻转对方的棋子,最后以棋盘上谁的棋子多来判断胜负。它的游戏规则简单,因此上手很容易,但是它的变化又非常复杂。有一种说法是:只需要几分钟学会它,却需要一生的时间去精通它。

此代码基于 API 12 编写,包含了游戏的基本逻辑,包括初始化棋盘、处理玩家和 AI 的走法、检查游戏状态、以及动画展示等部分。以下是具体的功能和使用的算法分析:

【实现的功能】

1. 棋盘初始化:创建了一个 8×8 的棋盘,并在中心位置放置了四个棋子作为初始布局,遵循了翻转棋的标准规则。

2. 棋子展示与翻转:定义了 ChessCell 类来表示棋盘上的每个单元格,支持显示黑色或白色棋子,并且能够翻转棋子,即从黑色变为白色或反之亦然。此过程伴随有动画效果。

3. 有效走法检测:通过 findReversible 方法来查找某个位置放置新棋子后可翻转的棋子集合。此方法考虑了八个方向上的可能走法。

4. 玩家切换:currentPlayerIsBlackChanged 方法用于切换当前玩家,并检查当前玩家是否有有效的走法。如果没有,就检查对手是否有走法,如果没有则判定游戏结束并显示胜利者。

5. AI 走法:在单人模式下,AI 会选择一个随机的有效走法进行下棋。6. 游戏结束判定:当没有任何一方可以走棋时,游戏结束并显示胜利信息。

【使用的算法】

1. 有效走法检测算法

有效走法检测是通过 findReversible 函数实现的。该函数接收行号、列号和当前玩家的颜色作为参数,并返回一个数组,该数组包含所有可以在指定方向上翻转的棋子对象。

findReversible(row: number, col: number, color: number): ChessCell[] {    let reversibleTiles: ChessCell[] = [];    const directions = [        [-1, -1], // 左上        [-1, 0], // 正上        [-1, 1], // 右上        [0, -1], // 左        [0, 1], // 右        [1, -1], // 左下        [1, 0], // 正下        [1, 1]  // 右下    ];    for (const direction of directions) {        let foundOpposite = false;        let x = row;        let y = col;        do {            x += direction[0];            y += direction[1];            if (x < 0 || y < 0 || x >= this.chessBoardSize || y >= this.chessBoardSize) {                break;            }            const cell = this.chessBoard[x][y];            if (cell.frontVisibility === 0) {                break;            }            if (cell.frontVisibility === color) {                if (foundOpposite) {                    let tempX: number = x - direction[0];                    let tempY: number = y - direction[1];                    while (tempX !== row || tempY !== col) {                        reversibleTiles.push(this.chessBoard[tempX][tempY]);                        tempX -= direction[0];                        tempY -= direction[1];                    }                }                break;            } else {                foundOpposite = true;            }        } while (true);    }    return reversibleTiles;}
复制代码

该算法首先定义了所有可能的方向,然后逐一检查这些方向上是否存在有效的可翻转棋子序列。对于每一个方向,它会尝试移动到下一个位置,并检查那个位置上的棋子状态,如果遇到空位则停止搜索;如果遇到同色棋子,那么在这之前的所有异色棋子都可以被翻转;否则继续搜索直到边界或同色棋子出现。

2. AI 随机下棋策略

AI 的随机下棋策略是在 aiPlaceRandom 函数中实现的。该函数首先收集所有当前玩家可以下棋的位置,然后从中随机选择一个位置进行下棋。

aiPlaceRandom() {    let validMoves: [number, number][] = [];    for (let i = 0; i < this.validMoveIndicators.length; i++) {        for (let j = 0; j < this.validMoveIndicators[i].length; j++) {            if (this.validMoveIndicators[i][j].isValidMove) {                validMoves.push([i, j]);            }        }    }    if (validMoves.length > 0) {        const randomMove = validMoves[Math.floor(Math.random() * validMoves.length)];        let chessCell = this.chessBoard[randomMove[0]][randomMove[1]];        this.placeChessPiece(randomMove[0], randomMove[1], chessCell)    }}
复制代码

这里通过双重循环遍历所有的 validMoveIndicators 数组,找到所有标记为有效走法的位置,将其坐标存储在一个数组中。之后,从中随机选取一个坐标,调用 placeChessPiece 函数进行下棋。

3. 棋子翻转动画

棋子的翻转动画是由 ChessCell 类中的 flip 方法控制的。根据当前棋子的状态,调用相应颜色的展示方法(如 showBlack 或 showWhite),并根据传入的时间参数来决定是否启用动画。

flip(time: number) {    if (this.frontVisibility === 1) { // 当前是黑子,要翻转成白子        this.showWhite(time);    } else if (this.frontVisibility === 2) { // 当前是白子,要翻转成黑子        this.showBlack(time);    }}
复制代码

翻转操作会触发动画效果,通过旋转和透明度的变化来模拟棋子的翻转动作。这些算法共同作用,使得该游戏具有了基础的棋子翻转、有效走法检测和 AI 下棋的能力。

【完整代码】

import { promptAction } from '@kit.ArkUI'; // 导入用于弹出对话框的工具
@ObservedV2class ChessCell { // 定义棋盘上的单元格类 @Trace frontVisibility: number = 0; // 单元格上的棋子状态:0表示无子, 1表示黑子,2表示白子 @Trace rotationAngle: number = 0; // 单元格卡片的旋转角度 @Trace opacity: number = 1; // 透明度 isAnimationRunning: boolean = false; // 标记动画是否正在运行
flip(time: number) { // 翻转棋子的方法 if (this.frontVisibility === 1) { // 当前是黑子,要翻转成白子 this.showWhite(time); } else if (this.frontVisibility === 2) { // 当前是白子,要翻转成黑子 this.showBlack(time); } }
showBlack(animationTime: number, callback?: () => void) { // 展示黑色棋子 if (this.isAnimationRunning) { // 如果已经有动画在运行,则返回 return; } this.isAnimationRunning = true; // 设置动画状态为运行中 if (animationTime == 0) { // 如果不需要动画 this.rotationAngle = 0; // 设置旋转角度为0 this.frontVisibility = 1; // 黑子 this.isAnimationRunning = false; // 设置动画状态为停止 if (callback) { // 如果有回调函数,则执行 callback(); } }
animateToImmediately({ // 开始动画 duration: animationTime, // 动画持续时间 iterations: 1, // 动画迭代次数 curve: Curve.Linear, // 动画曲线类型 onFinish: () => { // 动画完成后的回调 animateToImmediately({ // 再次开始动画 duration: animationTime, // 动画持续时间 iterations: 1, // 动画迭代次数 curve: Curve.Linear, // 动画曲线类型 onFinish: () => { // 动画完成后的回调 this.isAnimationRunning = false; // 设置动画状态为停止 if (callback) { // 如果有回调函数,则执行 callback(); } } }, () => { // 动画开始时的回调 this.frontVisibility = 1; // 看到黑色 this.rotationAngle = 0; // 设置旋转角度为0 }); } }, () => { // 动画开始时的回调 this.rotationAngle = 90; // 设置旋转角度为90度 }); }
showWhite(animationTime: number, callback?: () => void) { // 展示白色棋子 if (this.isAnimationRunning) { // 如果已经有动画在运行,则返回 return; } this.isAnimationRunning = true; // 设置动画状态为运行中 if (animationTime == 0) { // 如果不需要动画 this.rotationAngle = 180; // 设置旋转角度为180度 this.frontVisibility = 2; // 白子 this.isAnimationRunning = false; // 设置动画状态为停止 if (callback) { // 如果有回调函数,则执行 callback(); } }
animateToImmediately({ // 开始动画 duration: animationTime, // 动画持续时间 iterations: 1, // 动画迭代次数 curve: Curve.Linear, // 动画曲线类型 onFinish: () => { // 动画完成后的回调 animateToImmediately({ // 再次开始动画 duration: animationTime, // 动画持续时间 iterations: 1, // 动画迭代次数 curve: Curve.Linear, // 动画曲线类型 onFinish: () => { // 动画完成后的回调
this.isAnimationRunning = false; // 设置动画状态为停止 if (callback) { // 如果有回调函数,则执行 callback(); } } }, () => { // 动画开始时的回调 this.frontVisibility = 2; // 看到白色 this.rotationAngle = 180; // 设置旋转角度为180度 }); } }, () => { // 动画开始时的回调 this.rotationAngle = 90; // 设置旋转角度为90度 }); }
showWhiteAi(animationTime: number, callback?: () => void) { // 展示白色棋子 if (this.isAnimationRunning) { // 如果已经有动画在运行,则返回 return; } this.isAnimationRunning = true; // 设置动画状态为运行中 if (animationTime == 0) { // 如果不需要动画 this.rotationAngle = 180; // 设置旋转角度为180度 this.frontVisibility = 2; // 白子 this.isAnimationRunning = false; // 设置动画状态为停止 if (callback) { // 如果有回调函数,则执行 callback(); } } this.rotationAngle = 180; // 设置旋转角度为180度 this.frontVisibility = 2; // 白子

animateToImmediately({ // 开始动画 duration: animationTime * 3, // 动画持续时间 curve: Curve.EaseOut, iterations: 3, // 动画迭代次数 onFinish: () => { // 动画完成后的回调 animateToImmediately({ // 再次开始动画 duration: animationTime, // 动画持续时间 iterations: 1, // 动画迭代次数 curve: Curve.Linear, // 动画曲线类型 onFinish: () => { // 动画完成后的回调
this.isAnimationRunning = false; // 设置动画状态为停止 if (callback) { // 如果有回调函数,则执行 callback(); } } }, () => { this.opacity = 1; }); } }, () => { this.opacity = 0.2; }); }}
@ObservedV2class TileHighlight { @Trace isValidMove: boolean = false;}
@Entry@Componentstruct OthelloGame { @State chessBoard: ChessCell[][] = []; @State cellSize: number = 70; @State cellSpacing: number = 5; @State transitionDuration: number = 200; @State @Watch('currentPlayerIsBlackChanged') currentPlayerIsBlack: boolean = true; // 先手,true表示黑棋 @State chessBoardSize: number = 8; // 假设棋盘大小为8×8 @State validMoveIndicators: TileHighlight [][] = [] @State isTwoPlayerMode: boolean = false; //true:双击游戏,false:单人游戏 @State isAIPlaying:boolean = false//true:AI正在下棋 currentPlayerIsBlackChanged() { setTimeout(() => { const color = this.currentPlayerIsBlack ? 1 : 2; // 1是黑子,2表示白子 let hasMoves = this.hasValidMoves(color);
if (!hasMoves) { let opponentHasMoves = this.hasValidMoves(!this.currentPlayerIsBlack ? 1 : 2); if (!opponentHasMoves) { const winner = this.determineWinner(); console.log(winner); promptAction.showDialog({ title: '游戏结束', message: `${winner}`, buttons: [{ text: '重新开始', color: '#ffa500' }] }).then(() => { this.initGame(); }); } else { this.currentPlayerIsBlack = !this.currentPlayerIsBlack; // 切换下一玩家 } } else { if (!this.currentPlayerIsBlack) { // 当前是白棋, 模拟AI下棋 if (!this.isTwoPlayerMode) { setTimeout(() => { this.aiPlaceRandom(); }, this.transitionDuration + 20); } } } }, this.transitionDuration + 20); }
aiPlaceRandom() { let validMoves: [number, number][] = []; for (let i = 0; i < this.validMoveIndicators.length; i++) { for (let j = 0; j < this.validMoveIndicators[i].length; j++) { if (this.validMoveIndicators[i][j].isValidMove) { validMoves.push([i, j]); } } }
if (validMoves.length > 0) { const randomMove = validMoves[Math.floor(Math.random() * validMoves.length)]; let chessCell = this.chessBoard[randomMove[0]][randomMove[1]]; this.placeChessPiece(randomMove[0], randomMove[1], chessCell) } }
placeChessPiece(i: number, j: number, chessCell: ChessCell) {

let reversibleTiles = this.findReversible(i, j, this.currentPlayerIsBlack ? 1 : 2); console.info(`reversibleTiles:${JSON.stringify(reversibleTiles)}`);
if (reversibleTiles.length > 0) { if (this.currentPlayerIsBlack) { this.currentPlayerIsBlack = false; chessCell.showBlack(0); for (let i = 0; i < reversibleTiles.length; i++) { reversibleTiles[i].flip(this.transitionDuration); } } else { this.currentPlayerIsBlack = true; if (this.isTwoPlayerMode) { //双人游戏,无动画 chessCell.showWhite(0); for (let i = 0; i < reversibleTiles.length; i++) { reversibleTiles[i].flip(this.transitionDuration); }
} else { //单人游戏,落子需要闪烁动画 this.isAIPlaying = true//AI 正在下棋 chessCell.showWhiteAi(this.transitionDuration, () => { for (let i = 0; i < reversibleTiles.length; i++) { reversibleTiles[i].flip(this.transitionDuration); } this.currentPlayerIsBlackChanged() this.isAIPlaying = false//AI下完了 }); } } } }
hasValidMoves(color: number) { let hasMoves = false; for (let row = 0; row < this.chessBoardSize; row++) { for (let col = 0; col < this.chessBoardSize; col++) { if (this.chessBoard[row][col].frontVisibility === 0 && this.findReversible(row, col, color).length > 0) { this.validMoveIndicators[row][col].isValidMove = true; hasMoves = true; } else { this.validMoveIndicators[row][col].isValidMove = false; } } } return hasMoves; }
aboutToAppear(): void { for (let i = 0; i < this.chessBoardSize; i++) { this.chessBoard.push([]); this.validMoveIndicators.push([]) for (let j = 0; j < this.chessBoardSize; j++) { this.chessBoard[i].push(new ChessCell()); this.validMoveIndicators[i].push(new TileHighlight()) } }
this.initGame() }
initGame() { this.currentPlayerIsBlack = true for (let i = 0; i < this.chessBoardSize; i++) { for (let j = 0; j < this.chessBoardSize; j++) { this.chessBoard[i][j].frontVisibility = 0 } } // 初始棋盘布局 this.chessBoard[3][3].frontVisibility = 2; this.chessBoard[3][4].frontVisibility = 1; this.chessBoard[4][3].frontVisibility = 1; this.chessBoard[4][4].frontVisibility = 2;
this.currentPlayerIsBlackChanged(); }
findReversible(row: number, col: number, color: number): ChessCell[] { let reversibleTiles: ChessCell[] = []; const directions = [ [-1, -1], // 左上 [-1, 0], // 正上 [-1, 1], // 右上 [0, -1], // 左 [0, 1], // 右 [1, -1], // 左下 [1, 0], // 正下 [1, 1]// 右下 ]; for (const direction of directions) { let foundOpposite = false; let x = row; let y = col; do { x += direction[0]; y += direction[1]; if (x < 0 || y < 0 || x >= this.chessBoardSize || y >= this.chessBoardSize) { break; } const cell = this.chessBoard[x][y]; if (cell.frontVisibility === 0) { break; } if (cell.frontVisibility === color) { if (foundOpposite) { let tempX: number = x - direction[0]; let tempY: number = y - direction[1]; while (tempX !== row || tempY !== col) { reversibleTiles.push(this.chessBoard[tempX][tempY]); tempX -= direction[0]; tempY -= direction[1]; } } break; } else { foundOpposite = true; } } while (true); } return reversibleTiles; }
determineWinner(): string { let blackCount = 0; let whiteCount = 0; for (let row of this.chessBoard) { for (let cell of row) { if (cell.frontVisibility === 1) { blackCount++; } if (cell.frontVisibility === 2) { whiteCount++; } } } if (blackCount > whiteCount) { return "黑棋获胜!"; } if (whiteCount > blackCount) { return "白棋获胜!"; } return "平局!"; }
hasValidMove(color: number): boolean { for (let row = 0; row < this.chessBoardSize; row++) { for (let col = 0; col < this.chessBoardSize; col++) { if (this.chessBoard[row][col].frontVisibility === 0 && this.findReversible(row, col, color).length > 0) { return true; } } } return false; }
build() { Column({ space: 20 }) { Row() { Row() { Text(``)// 显示单元格的值或空字符串 .width(`${this.cellSize}lpx`)// 设置宽度 .height(`${this.cellSize}lpx`)// 设置高度 .textAlign(TextAlign.Center)// 文本居中 .backgroundColor(Color.Black)// 设置背景颜色 .borderRadius('50%')// 设置圆角 .padding(10) Text(`黑棋行动`) .fontColor(Color.White) .padding(10) } .visibility(this.currentPlayerIsBlack ? Visibility.Visible : Visibility.Hidden)

Row() { Text(`白棋行动`) .fontColor(Color.White) .padding(10) Text()// 显示单元格的值或空字符串 .width(`${this.cellSize}lpx`)// 设置宽度 .height(`${this.cellSize}lpx`)// 设置高度 .textAlign(TextAlign.Center)// 文本居中 .backgroundColor(Color.White)// 设置背景颜色 .fontColor(Color.White) .borderRadius('50%')// 设置圆角 .padding(10) } .visibility(!this.currentPlayerIsBlack ? Visibility.Visible : Visibility.Hidden) } .width(`${(this.cellSize + this.cellSpacing * 2) * 8}lpx`) // 设置宽度 .justifyContent(FlexAlign.SpaceBetween) .margin({ top: 20 })
Stack() { //棋盘背景 Flex({ wrap: FlexWrap.Wrap }) { ForEach(this.validMoveIndicators, (row: boolean[], _rowIndex: number) => { ForEach(row, (item: TileHighlight, _colIndex: number) => { Text(`${item.isValidMove ? '+' : ''}`)// 显示单元格的值或空字符串 .width(`${this.cellSize}lpx`)// 设置宽度 .height(`${this.cellSize}lpx`)// 设置高度 .margin(`${this.cellSpacing}lpx`) .fontSize(`${this.cellSize / 2}lpx`)// 设置字体大小 .fontColor(Color.White) .textAlign(TextAlign.Center)// 文本居中 .backgroundColor(Color.Gray)// 设置背景颜色 .borderRadius(2) // 设置圆角 }); }); } .width(`${(this.cellSize + this.cellSpacing * 2) * 8}lpx`) // 设置宽度 //棋子 Flex({ wrap: FlexWrap.Wrap }) { ForEach(this.chessBoard, (row: ChessCell[], rowIndex: number) => { ForEach(row, (chessCell: ChessCell, colIndex: number) => { Text(``)// 显示单元格的值或空字符串 .width(`${this.cellSize}lpx`)// 设置宽度 .height(`${this.cellSize}lpx`)// 设置高度 .margin(`${this.cellSpacing}lpx`)// 设置边距 .fontSize(`${this.cellSize / 2}lpx`)// 设置字体大小 .textAlign(TextAlign.Center)// 文本居中 .opacity(chessCell.opacity) .backgroundColor(chessCell.frontVisibility != 0 ? (chessCell.frontVisibility === 1 ? Color.Black : Color.White) : Color.Transparent)// 设置背景颜色 .borderRadius('50%')// 设置圆角 .rotate({ // 设置旋转 x: 0, y: 1, z: 0, angle: chessCell.rotationAngle, // 旋转角度 centerX: `${this.cellSize / 2}lpx`, // 中心点X坐标 centerY: `${this.cellSize / 2}lpx`, // 中心点Y坐标 }) .onClick(() => { // 单击事件处理 if (this.isAIPlaying) { console.info(`ai正在落子,玩家不可继续落子`) return; } if (chessCell.frontVisibility === 0) { // 没有棋子,需要落子 this.placeChessPiece(rowIndex, colIndex, chessCell) } }); }); }); } .width(`${(this.cellSize + this.cellSpacing * 2) * 8}lpx`) // 设置宽度 } .padding(`${this.cellSpacing}lpx`) .backgroundColor(Color.Black)
Row() { Text(`${this.isTwoPlayerMode? '双人游戏' : '单人游戏'}`) .height(50) .padding({ left: 10 }) .fontSize(16) .textAlign(TextAlign.Start) .backgroundColor(0xFFFFFF) Toggle({ type: ToggleType.Switch, isOn: this.isTwoPlayerMode }) .margin({ left: 200, right: 10 }) .onChange((isOn: boolean) => { this.isTwoPlayerMode = isOn }) } .backgroundColor(0xFFFFFF) .borderRadius(5)
Button('重新开始').onClick(() => { // 按钮点击事件 this.initGame() }); } .height('100%').width('100%') // 设置高度和宽度 .backgroundColor(Color.Orange) }}
复制代码


用户头像

zhongcx

关注

还未添加个人签名 2024-09-27 加入

还未添加个人简介

评论

发布
暂无评论
鸿蒙开发案例:黑白棋_zhongcx_InfoQ写作社区