
鸿蒙开发之ArkTS并发技术中国象棋
一、工具
DevEco Studio
二、开发过程
创建ChessView.ets文件
添加一个布局用来显示棋盘:
[
[5, 4, 3, 2, 1, 2, 3, 4, 5],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 6, 0, 0, 0, 0, 0, 6, 0],
[7, 0, 7, 0, 7, 0, 7, 0, 7],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[14, 0, 14, 0, 14, 0, 14, 0, 14],
[0, 13, 0, 0, 0, 0, 0, 13, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[12, 11, 10, 9, 8, 9, 10, 11, 12],
]
通过二维数组来定义棋盘位置
1、2、3、4、5、6、7、8、9、10、10、11、12、13、14分别代表
蓝:帅、士、象、马、車、兵、炮
红:帅、士、象、马、車、兵、炮
棋盘布局:
用ForEach+Column嵌套ForEach+Row
Row() {
ForEach(pieceX, (icon1:number,index1:number ) => {
Column() {
ForEach(pieceY, (icon2:number,index2:number ) => {
Row() {
Image(this.bg[AiPlayData.chessInfo.piece[icon2][icon1]])
.width(40)
.height(40)
.onClick(()=>{
this.setPlay(index1,index2)
})
}
.justifyContent(FlexAlign.Center)
})
}
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
红蓝方下棋方法:
首先判断下棋方红/蓝,然后根据点击的位置,确定車、马、炮、兵、象、士、帅棋子
机器人下棋算法:
因为机器人算法耗时,所以我们来创建一个线程执行,防止卡UI。
async function AiAsync(piece:number[][],isRedGo:boolean,depth:number,startZobristKey:number,startZobristKeyCheck:number,ZobristInfo:Map<number, number>): Promise {
try {
let task: taskpool.Task = new taskpool.Task(startAI,piece,isRedGo,depth,startZobristKey,startZobristKeyCheck,ZobristInfo);
let move = await taskpool.execute(task)
return move as Move
} catch (e) {
console.error("taskpool execute error is: " + e);
}
return new Move(new Pos(-1, -1), new Pos(-1, -1))
}
这里我们选择开启taskpool线程:
taskpool的线程开启方法简单,适合短时间使用。他和Worker线程不同,Worker偏向线程的维度,支持长时间占据线程执行,需要主动管理线程生命周期。
创建线程函数
@Concurrent
async function startAI(piece:number[][],isRedGo:boolean,depth:number,startZobristKey:number,startZobristKeyCheck:number,ZobristInfo:Map<number, number>): Promise{
// 添加生产相关逻辑
let move:Move=AI.getBestMove(piece,isRedGo,depth,startZobristKey,startZobristKeyCheck,ZobristInfo)
return move;
}
启动线程:
try {
let task: taskpool.Task = new taskpool.Task(startAI,piece,isRedGo,depth,startZobristKey,startZobristKeyCheck,ZobristInfo);
let move = await taskpool.execute(task)
return move as Move
} catch (e) {
console.error("taskpool execute error is: " + e);
}
完成后返回Move对象:
return move as Move
下面是线程执行函数的getBestMove(),主要通过循环遍历AI算法得到最终会结果。
public static getBestMove( piece:number[][], isRedGo:boolean, depth:number, startZobristKey:number, startZobristKeyCheck:number, ZobristInfo:Map<number, number>):Move {
let pieceClone:number[][] = [
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0]
];
for (let i = 0; i <10; i++) {
pieceClone[i] = piece[i];
}
let searchCnt = 0;
let goodMove:Move = new Move(new Pos(-1, -1), new Pos(-1, -1));
let bestMove:Move = new Move(new Pos(-1, -1), new Pos(-1, -1));
AI.clearHistory();
for (let i = 2; i <= depth; i += 2) {
goodMove = AI.getGoodMove(pieceClone, isRedGo, i, startZobristKey, startZobristKeyCheck, ZobristInfo);
if (AI.goodValue >= -AI.Win / 2) {
bestMove = goodMove;
}
}
if (bestMove.fromPos == (new Pos(-1, -1)) == true) {
bestMove = AI.getLegalMove(pieceClone, isRedGo);
}
//Log.i("博弈树搜索算法比对","搜索结点数:"+String.valueOf(searchCnt));
return bestMove;
}
然后就是AI算法了:
首先定义象棋数据Info
setting //棋子行走过中的状态变化
infoSet //通过知识库AI算法遍历完之后的数据修改方法
knowledgeBase //知识库象棋棋子的行走的方法封装。
chessInfo //整局象棋的所有数据,每走一步,这个数据会实时更新,UI显示更具这个数据刷新的。
export class AiPlayData {
static setting:Setting = new Setting(false,true,true,2)
static infoSet:InfoSet =new InfoSet()
static knowledgeBase:KnowledgeBase = new KnowledgeBase()
static chessInfo:ChessInfo = new ChessInfo
}
有了Info之后就可以对Info进行 循环遍历
下面是AICore的封装
AI //人机思考下棋循环遍历算法封装
AiPlay //人机开始下棋的方法
BestMove //人机棋子移动的最佳位置
KnowledgeBase //人机棋子移动的知识库
Move //人机棋子移动的方法
Rule //人机棋子移动的规则
TransformInfo //人机棋子信息转换
TransformTable //人机棋子转换数据
export async function AiPlay(): Promise {
if (AiPlayData.chessInfo.status != 1) return;
let depth = AiPlayData.setting.mLevel * 2;
if (AiPlayData.chessInfo.IsRedGo == true) {
let move = AiPlayData.knowledgeBase.readBestMoves(AiPlayData.chessInfo.ZobristKey, AiPlayData.chessInfo.ZobristKeyCheck, depth);
if (move.fromPos.y == -1) {
//move = AI.getBestMove(AiPlayData.chessInfo.piece, true, depth, AiPlayData.chessInfo.ZobristKey, AiPlayData.chessInfo.ZobristKeyCheck, AiPlayData.infoSet.ZobristInfo);
move =await AiAsync(AiPlayData.chessInfo.piece, true, depth, AiPlayData.chessInfo.ZobristKey, AiPlayData.chessInfo.ZobristKeyCheck, AiPlayData.infoSet.ZobristInfo)
AiPlayData.knowledgeBase.saveBestMove(AiPlayData.chessInfo.ZobristKey, AiPlayData.chessInfo.ZobristKeyCheck, depth, move);
blueAfter(move)
}else {
blueAfter(move)
}
} else {
let move = AiPlayData.knowledgeBase.readBestMoves(AiPlayData.chessInfo.ZobristKey, AiPlayData.chessInfo.ZobristKeyCheck, depth);
if (move.fromPos.y == -1) {
//move = AI.getBestMove(AiPlayData.chessInfo.piece, false, depth, AiPlayData.chessInfo.ZobristKey, AiPlayData.chessInfo.ZobristKeyCheck, AiPlayData.infoSet.ZobristInfo);
move =await AiAsync(AiPlayData.chessInfo.piece, false, depth, AiPlayData.chessInfo.ZobristKey, AiPlayData.chessInfo.ZobristKeyCheck, AiPlayData.infoSet.ZobristInfo)
AiPlayData.knowledgeBase.saveBestMove(AiPlayData.chessInfo.ZobristKey, AiPlayData.chessInfo.ZobristKeyCheck, depth, move);
redAfter(move)
}else {
redAfter(move)
}
}
}
因为下棋的规则确定了,所以我们每次在获取到棋子时就可以对棋子的下一步进循环遍历,获取最佳走法。
效果图:
import { Rule } from '../AICore/Rule';
import { Pos } from '../Info/Pos';
import { promptAction } from '@kit.ArkUI'
import { AI } from '../AICore/AI';
import { Zobrist } from '../Info/Zobrist';
import { TransformTable } from '../AICore/TransformTable';
import { AiPlay, AiPlayData } from '../AICore/AiPlay';
let pieceX:number[] = [0,1,2,3,4,5,6,7,8]
let pieceY:number[] = [0,1,2,3,4,5,6,7,8,9]
@Entry
@Component
struct ChessComponent {
@State bg: Resource[] = [
$r("app.media.null"), $r("app.media.1"), $r("app.media.2"), r("app.media.4"),r("app.media.6"),r("app.media.8"),$r("app.media.9"),
$r("app.media.10"), $r("app.media.11"), r("app.media.13"),r("app.media.board"))
.width(360)
.width(360)
Row() {
ForEach(pieceX, (icon1:number,index1:number ) => {
Column() {
ForEach(pieceY, (icon2:number,index2:number ) => {
Row() {
Image(this.bg[AiPlayData.chessInfo.piece[icon2][icon1]])
.width(40)
.height(40)
.onClick(()=>{
this.setPlay(index1,index2)
})
}
.justifyContent(FlexAlign.Center)
})
}
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
}.width('100%')
.height("60%")
Row(){
Button('悔棋', { type: ButtonType.Normal, stateEffect: true })
.borderRadius(8)
.backgroundColor(0x317aff)
.width(90)
.height(40)
.onClick(()=>{
if (AiPlayData.chessInfo.isMachine == true && AiPlayData.chessInfo.status == 1) {
promptAction.showToast({
message: '请等待电脑思考',
duration: 1000
});
}else {
let cnt = 0;
let total = 2;
if (AiPlayData.chessInfo.status == 2 && AiPlayData.chessInfo.isMachine == true) {
total = 1;
}
if (AiPlayData.infoSet.preInfo.length >= total) {
while (!AiPlayData.infoSet.preInfo.isEmpty()) {
let tmp = AiPlayData.infoSet.preInfo.pop();
cnt++;
try {
AiPlayData.infoSet.recallZobristInfo(AiPlayData.chessInfo.ZobristKeyCheck);
AiPlayData.chessInfo.setInfo(tmp);
AiPlayData.infoSet.curInfo.setInfo(tmp);
} catch (error) {
console.log('error'+error)
}
if (cnt >= total) {
break;
}
}
}
}
this.bg[0] = r("app.media.chuhan4"))
.backgroundImageSize(1)
.justifyContent(FlexAlign.Center)
}
setPlay(x: number,y:number){
if (AiPlayData.chessInfo.status == 1) {
AiPlayData.chessInfo.Select = [x, y];
let realPos:Pos = Rule.reversePos(new Pos(AiPlayData.chessInfo.Select[0], AiPlayData.chessInfo.Select[1]), AiPlayData.chessInfo.IsReverse);
let i = realPos.x, j = realPos.y;
if (i >= 0 && i <= 8 && j >= 0 && j <= 9) {
if (AiPlayData.chessInfo.IsRedGo == true && AiPlayData.setting.isPlayerRed == true) {
if (AiPlayData.chessInfo.IsChecked == false) {
if (AiPlayData.chessInfo.piece[j][i] >= 8 && AiPlayData.chessInfo.piece[j][i] <= 14) {
AiPlayData.chessInfo.prePos = new Pos(i, j);
AiPlayData.chessInfo.IsChecked = true;
AiPlayData.chessInfo.ret = Rule.PossibleMoves(AiPlayData.chessInfo.piece, i, j, AiPlayData.chessInfo.piece[j][i]);
//console.log("x+y:"+AiPlayData.chessInfo.ret[0]+AiPlayData.chessInfo.ret[1]+AiPlayData.chessInfo.ret[2]+AiPlayData.chessInfo.ret[3]+AiPlayData.chessInfo.ret[4]+AiPlayData.chessInfo.ret[5])
//playEffect(selectMusic);
}
} else {
if (AiPlayData.chessInfo.piece[j][i] >= 8 && AiPlayData.chessInfo.piece[j][i] <= 14) {
AiPlayData.chessInfo.prePos = new Pos(i, j);
AiPlayData.chessInfo.ret = Rule.PossibleMoves(AiPlayData.chessInfo.piece, i, j, AiPlayData.chessInfo.piece[j][i]);
//playEffect(selectMusic);
} else if (AiPlayData.chessInfo.ret.some(item => item.x === new Pos(i, j).x && item.y === new Pos(i, j).y)) {
let tmp = AiPlayData.chessInfo.piece[j][i];
AiPlayData.chessInfo.piece[j][i] = AiPlayData.chessInfo.piece[AiPlayData.chessInfo.prePos.y][AiPlayData.chessInfo.prePos.x];
AiPlayData.chessInfo.piece[AiPlayData.chessInfo.prePos.y][AiPlayData.chessInfo.prePos.x] = 0;
if (Rule.isKingDanger(AiPlayData.chessInfo.piece, true)) {
AiPlayData.chessInfo.piece[AiPlayData.chessInfo.prePos.y][AiPlayData.chessInfo.prePos.x] = AiPlayData.chessInfo.piece[j][i];
AiPlayData.chessInfo.piece[j][i] = tmp;
promptAction.showToast({
message: '帅被将军',
duration: 1000
});
} else {
AiPlayData.chessInfo.IsChecked = false;
AiPlayData.chessInfo.IsRedGo = false;
AiPlayData.chessInfo.curPos = new Pos(i, j);
AiPlayData.chessInfo.updateAllInfo(AiPlayData.chessInfo.prePos, AiPlayData.chessInfo.curPos, AiPlayData.chessInfo.piece[j][i], tmp);
try {
AiPlayData.infoSet.pushInfo(AiPlayData.chessInfo);
} catch (error) {
console.error(error+error);
}
//playEffect(clickMusic);
let key = 0;
if (Rule.isKingDanger(AiPlayData.chessInfo.piece, false)) {
key = 1;
}
if (Rule.isDead(AiPlayData.chessInfo.piece, false)) {
key = 2;
}
if (key == 1) {
promptAction.showToast({
message: '将军',
duration: 1000
});
} else if (key == 2) {
//playEffect(winMusic);
AiPlayData.chessInfo.status = 2;
promptAction.showToast({
message: '红方获得胜利',
duration: 1000
});
}
if (AiPlayData.chessInfo.status == 1) {
if (AiPlayData.chessInfo.peaceRound >= 60) {
AiPlayData.chessInfo.status = 2;
promptAction.showToast({
message: '双方60回合内未吃子,此乃和棋',
duration: 1000
});
} else if (AiPlayData.chessInfo.attackNum_B == 0 && AiPlayData.chessInfo.attackNum_R == 0) {
AiPlayData.chessInfo.status = 2;
promptAction.showToast({
message: '双方都无攻击性棋子,此乃和棋',
duration: 1000
});
} else if (AiPlayData.infoSet.ZobristInfo.get(AiPlayData.chessInfo.ZobristKeyCheck) as number >= 4) {
AiPlayData.chessInfo.status = 2;
promptAction.showToast({
message: '重复局面出现4次,此乃和棋',
duration: 1000
});
}
}
AiPlayData.chessInfo.isMachine = true;
this.bg[0] = $r("app.media.null")
AiPlay().then(()=>{
this.bg[0] = $r("app.media.null")
})
}
}
}
}else if (AiPlayData.chessInfo.IsRedGo == false && AiPlayData.setting.isPlayerRed == false) {
if (AiPlayData.chessInfo.IsChecked == false) {
if (AiPlayData.chessInfo.piece[j][i] >= 1 && AiPlayData.chessInfo.piece[j][i] <= 7) {
AiPlayData.chessInfo.prePos = new Pos(i, j);
AiPlayData.chessInfo.IsChecked = true;
AiPlayData.chessInfo.ret = Rule.PossibleMoves(AiPlayData.chessInfo.piece, i, j, AiPlayData.chessInfo.piece[j][i]);
}
} else {
if (AiPlayData.chessInfo.piece[j][i] >= 1 && AiPlayData.chessInfo.piece[j][i] <= 7) {
AiPlayData.chessInfo.prePos = new Pos(i, j);
AiPlayData.chessInfo.ret = Rule.PossibleMoves(AiPlayData.chessInfo.piece, i, j, AiPlayData.chessInfo.piece[j][i]);
//playEffect(selectMusic);
} else if (AiPlayData.chessInfo.ret.some(item => item.x === new Pos(i, j).x && item.y === new Pos(i, j).y)) {
let tmp = AiPlayData.chessInfo.piece[j][i];
AiPlayData.chessInfo.piece[j][i] = AiPlayData.chessInfo.piece[AiPlayData.chessInfo.prePos.y][AiPlayData.chessInfo.prePos.x];
//piece[j][i] = AiPlayData.chessInfo.piece[AiPlayData.chessInfo.prePos.y][AiPlayData.chessInfo.prePos.x];
AiPlayData.chessInfo.piece[AiPlayData.chessInfo.prePos.y][AiPlayData.chessInfo.prePos.x] = 0;
if (Rule.isKingDanger(AiPlayData.chessInfo.piece, false)) {
AiPlayData.chessInfo.piece[AiPlayData.chessInfo.prePos.y][AiPlayData.chessInfo.prePos.x] = AiPlayData.chessInfo.piece[j][i];
AiPlayData.chessInfo.piece[j][i] = tmp;
//piece[j][i] = tmp;
promptAction.showToast({
message: '将被将军',
duration: 1000
});
} else {
AiPlayData.chessInfo.IsChecked = false;
AiPlayData.chessInfo.IsRedGo = true;
AiPlayData.chessInfo.curPos = new Pos(i, j);
AiPlayData.chessInfo.updateAllInfo(AiPlayData.chessInfo.prePos, AiPlayData.chessInfo.curPos, AiPlayData.chessInfo.piece[j][i], tmp);
try {
AiPlayData.infoSet.pushInfo(AiPlayData.chessInfo);
} catch (error) {
console.error(error+error);
}
//playEffect(clickMusic);
let key = 0;
if (Rule.isKingDanger(AiPlayData.chessInfo.piece, true)) {
key = 1;
}
if (Rule.isDead(AiPlayData.chessInfo.piece, true)) {
key = 2;
}
if (key == 1) {
//playEffect(checkMusic);
promptAction.showToast({
message: '将军',
duration: 1000
});
} else if (key == 2) {
//playEffect(winMusic);
AiPlayData.chessInfo.status = 2;
promptAction.showToast({
message: '黑方获得胜利',
duration: 1000
});
}
if (AiPlayData.chessInfo.status == 1) {
if (AiPlayData.chessInfo.peaceRound >= 60) {
AiPlayData.chessInfo.status = 2;
promptAction.showToast({
message: '双方60回合内未吃子,此乃和棋',
duration: 1000
});
} else if (AiPlayData.chessInfo.attackNum_B == 0 && AiPlayData.chessInfo.attackNum_R == 0) {
AiPlayData.chessInfo.status = 2;
promptAction.showToast({
message: '双方都无攻击性棋子,此乃和棋',
duration: 1000
});
} else if (AiPlayData.infoSet.ZobristInfo.get(AiPlayData.chessInfo.ZobristKeyCheck) as number >= 4) {
AiPlayData.chessInfo.status = 2;
promptAction.showToast({
message: '重复局面出现4次,此乃和棋',
duration: 1000
});
}
}
AiPlayData.chessInfo.isMachine = true;
this.bg[0] = $r("app.media.null")
AiPlay().then(()=>{
this.bg[0] = $r("app.media.null")
})
}
}
}
}
}
}
}
}
最后根据棋子行走移动后的信息判断是否将军。
完整项目代码:https://gitee.com/stevenllv/MyApplication
