中国象棋游戏的胜负判断非常复杂,以至于象棋游戏的裁判都需要进行等级考试!
通常来说,人机对战的胜负判断要比真实比较比赛的裁判简单很多。在人机对战游戏中,我们要实现的逻辑是有限的:
- 如果 A 方行棋后,无论对方走什么棋,下一步都能吃掉对方的老将,则可判断 A 方获胜
- 如果 A 方行棋后,对方无棋可走,则可判断 A 方获胜
- 如果 A 方连续将军,造成三个连续的重复局面,则判断 A 方败阵
- 无吃子超过 60 个回合,判和棋
其它的关于「捉、闲」、「一将一闲」这些情况,我们不在 App 中实现。
本节概要
- 静态检查游戏结果
- 检查长将情况
- 检查和棋情况
开启新对局
在进行对战结果的检查前, 我们先看看如何开启一个新的对局。
棋盘走几步之后,想「重新开始一个新的对局」的需求比较强烈,我们优先解决这个强需求。
我们在 Phase 类中添加一个新的方法 initDefaultPhase,将原来 defaultPhase 构造方法中所有代码移动到 initDefaultPhase 方法中,在空的 defaultPhase 方法中添加对 initDefaultPhase 方法的调用,调整后的 Phase 类像下边这样:
...
class Phase {
...
Phase.defaultPhase() {
initDefaultPhase();
}
void initDefaultPhase() {
//
_side = Side.Red;
_pieces = List(90);
// 从上到下第一行
_pieces[0 * 9 + 0] = Piece.BlackRook;
_pieces[0 * 9 + 1] = Piece.BlackKnight;
_pieces[0 * 9 + 2] = Piece.BlackBishop;
_pieces[0 * 9 + 3] = Piece.BlackAdvisor;
_pieces[0 * 9 + 4] = Piece.BlackKing;
_pieces[0 * 9 + 5] = Piece.BlackAdvisor;
_pieces[0 * 9 + 6] = Piece.BlackBishop;
_pieces[0 * 9 + 7] = Piece.BlackKnight;
_pieces[0 * 9 + 8] = Piece.BlackRook;
// 从上到下第三行
_pieces[2 * 9 + 1] = Piece.BlackCanon;
_pieces[2 * 9 + 7] = Piece.BlackCanon;
// 从上到下第四行
_pieces[3 * 9 + 0] = Piece.BlackPawn;
_pieces[3 * 9 + 2] = Piece.BlackPawn;
_pieces[3 * 9 + 4] = Piece.BlackPawn;
_pieces[3 * 9 + 6] = Piece.BlackPawn;
_pieces[3 * 9 + 8] = Piece.BlackPawn;
// 从上到下第十行
_pieces[9 * 9 + 0] = Piece.RedRook;
_pieces[9 * 9 + 1] = Piece.RedKnight;
_pieces[9 * 9 + 2] = Piece.RedBishop;
_pieces[9 * 9 + 3] = Piece.RedAdvisor;
_pieces[9 * 9 + 4] = Piece.RedKing;
_pieces[9 * 9 + 5] = Piece.RedAdvisor;
_pieces[9 * 9 + 6] = Piece.RedBishop;
_pieces[9 * 9 + 7] = Piece.RedKnight;
_pieces[9 * 9 + 8] = Piece.RedRook;
// 从上到下第八行
_pieces[7 * 9 + 1] = Piece.RedCanon;
_pieces[7 * 9 + 7] = Piece.RedCanon;
// 从上到下第七行
_pieces[6 * 9 + 0] = Piece.RedPawn;
_pieces[6 * 9 + 2] = Piece.RedPawn;
_pieces[6 * 9 + 4] = Piece.RedPawn;
_pieces[6 * 9 + 6] = Piece.RedPawn;
_pieces[6 * 9 + 8] = Piece.RedPawn;
for (var i = 0; i < 90; i++) {
_pieces[i] ??= Piece.Empty;
}
}
...
}
在 Battle 类中,我们添加一个 newGame 方法,处理重新开始一个新对局的逻辑:
newGame() {
Battle.shared.phase.initDefaultPhase();
_focusIndex = _blurIndex = Move.InvalidIndex;
}
回到 _BattlePageState 类中,我们再添加一个也叫 newGame 的方法,由它来调用 Battle 的 newGame 方法:
newGame() {
//
confirm() {
Navigator.of(context).pop();
Battle.shared.newGame();
setState(() {});
}
cancel() => Navigator.of(context).pop();
// 开始新方法之前需要用户确认
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('放弃对局?', style: TextStyle(color: ColorConsts.Primary)),
content: SingleChildScrollView(child: Text('你确定要放弃当前的对局吗?')),
actions: [
FlatButton(child: Text('确定'), onPressed: confirm),
FlatButton(child: Text('取消'), onPressed: cancel),
],
);
},
);
}
这个方法弹出一个确认对话框,玩家确认的话就将局面回到初始状态。
我们用在 _BattlePageState 的 build 方法中找到这一段代码:
FlatButton(child: Text('新对局', style: buttonStyle), onPressed: () {})
将 newGame 方法指给「新对局」按钮的 onPressed 属性:
FlatButton(child: Text('新对局', style: buttonStyle), onPressed: newGame),
现在可以重新开始新对局了!接着我们来处理胜、负、和的检测问题。
对战结果检查
我们先在 Phase 类中添加添加一个 BattleResult 类型的字段:
class Phase {
...
BattleResult result = BattleResult.Pending; // 结果未决
...
}
接着,我们在 _BattlePageState 中添加三个方法,分别用于展示胜得、失败、和棋:
...
class _BattlePageState extends State {
...
// 显示胜利框
void gotWin() {
//
Battle.shared.phase.result = BattleResult.Win;
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('赢了', style: TextStyle(color: ColorConsts.Primary)),
content: Text('恭喜您取得了伟大的胜利!'),
actions: [
FlatButton(child: Text('再来一盘'), onPressed: newGame),
FlatButton(child: Text('关闭'), onPressed: () => Navigator.of(context).pop()),
],
);
},
);
}
// 显示失败框
void gotLose() {
//
Battle.shared.phase.result = BattleResult.Lose;
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('输了', style: TextStyle(color: ColorConsts.Primary)),
content: Text('勇士!坚定战斗,虽败犹荣!'),
actions: [
FlatButton(child: Text('再来一盘'), onPressed: newGame),
FlatButton(child: Text('关闭'), onPressed: () => Navigator.of(context).pop()),
],
);
},
);
}
// 显示和棋框
void gotDraw() {
//
Battle.shared.phase.result = BattleResult.Draw;
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text('和了', style: TextStyle(color: ColorConsts.Primary)),
content: Text('您用自己的力量捍卫了和平!'),
actions: [
FlatButton(child: Text('再来一盘'), onPressed: newGame),
FlatButton(child: Text('关闭'), onPressed: () => Navigator.of(context).pop()),
],
);
},
);
}
...
}
其实就是弹出三个不一样的对话框,告诉用户胜利或失败或和棋了。
之后,我们来扫描对战结果。在 Battle 类中,我们来修改 scanBattleResult 方法,修改后应该是下边这样的:
...
class Battle {
...
// 检查对局是否已经有胜负结果了
BattleResult scanBattleResult() {
//
final forPerson = (_phase.side == Side.Red);
if (scanLongCatch()) {
// born 'repeat' phase by oppo
return forPerson ? BattleResult.Win : BattleResult.Lose;
}
if (ChessRules.beKilled(_phase)) {
return forPerson ? BattleResult.Lose : BattleResult.Win;
}
return (_phase.halfMove > 120) ? BattleResult.Draw : BattleResult.Pending;
}
// 是否存在长将长捉
scanLongCatch() {
// todo:
return false;
}
...
}
关于重复局面和长捉 scanLongCatch 的检查比较复杂,我们将这个问往后放。
最后,我们修改 _BattlePageState 的 onBoardTap 和 engineToGo 方法,在两个方法中,都有对扫描结果处理的 switch 语句,修改它们当判断出胜负后,调用上边的三个方法显示胜负结果。
修改 _BattlePageState 的 onBoardTap 方法,改成下边的样子:
onBoardTap(BuildContext context, int index) {
...
// 之前已经有棋子被选中了
if (Battle.shared.focusIndex != Move.InvalidIndex &&
Side.of(phase.pieceAt(Battle.shared.focusIndex)) == Side.Red) {
//
...
// 现在点击的棋子和上一次选择棋子不同边,要得是吃子,要么是移动棋子到空白处
if (Battle.shared.move(Battle.shared.focusIndex, index)) {
//
final result = Battle.shared.scanBattleResult();
switch (result) {
case BattleResult.Pending:
engineToGo();
break;
case BattleResult.Win:
gotWin();
break;
case BattleResult.Lose:
gotLose();
break;
case BattleResult.Draw:
gotDraw();
break;
}
}
//
} else {
...
}
...
}
将 _BattlePageState 的 engineToGo 方法改成下边的样子:
engineToGo() async {
...
if (response.type == 'move') {
//
...
final result = Battle.shared.scanBattleResult();
switch (result) {
case BattleResult.Pending:
changeStatus('请走棋...');
break;
case BattleResult.Win:
gotWin();
break;
case BattleResult.Lose:
gotLose();
break;
case BattleResult.Draw:
gotDraw();
break;
}
//
} else {
...
}
}
一切都很好!经过这么长时间的努力,现在我们的象棋游戏有严格的行棋规则,人和云端主机能决出一个胜负了。
运行产品试试看,多与云端服务器切磋两把吧!实际上提升棋力最好的方式就是与比自己水平高的对手下棋。
这一节的内容很多了,刻提交代码到 git 仓库,让自己放松一下喽!
elapse@elapse-PC:~/Language/Flutter/chinese_chess$ git add .
elapse@elapse-PC:~/Language/Flutter/chinese_chess$ git commit -m '胜负逻辑'
[master 8f50019] 胜负逻辑
4 files changed, 138 insertions(+), 9 deletions(-)
elapse@elapse-PC:~/Language/Flutter/chinese_chess$ sudo git push
[sudo] elapse 的密码:
Username for 'https://rocketgit.com': elapse
Password for 'https://[email protected]':
枚举对象: 19, 完成.
对象计数中: 100% (19/19), 完成.
使用 4 个线程进行压缩
压缩对象中: 100% (9/9), 完成.
写入对象中: 100% (10/10), 2.04 KiB | 2.04 MiB/s, 完成.
总共 10 (差异 6),复用 0 (差异 0)
remote: RocketGit: Info: == Welcome to RocketGit! ==
remote: RocketGit: Info: you are connecting from IP 2408:8256:686:857c:65a5:d57e:83b8:2920 by http(s).
remote: RocketGit: Info: date/time: 2020-08-30 01:59:33 (UTC), debug id 5f9e32.
To https://rocketgit.com/user/elapse/chinese_chess
9bbb010..8f50019 master -> master
elapse@elapse-PC:~/Language/Flutter/chinese_chess$
小节回顾
本节在玩家或引擎走棋后,对局面做游戏结果检查。在已经有游戏结果的时候弹出游戏结果界面。
此外,我们实现了在游戏结束以及游戏过程中重新开启一个新对局的功能。
「长将」的逻辑,我们交在后续课程中实现,而像「长捉」、「限着」等更完善的中国象棋游戏规则,大家可以尝试自己去挑战实现!