Flutter象棋:8.实现胜负逻辑

中国象棋游戏的胜负判断非常复杂,以至于象棋游戏的裁判都需要进行等级考试!

通常来说,人机对战的胜负判断要比真实比较比赛的裁判简单很多。在人机对战游戏中,我们要实现的逻辑是有限的:

  • 如果 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$ 
		
		

小节回顾

本节在玩家或引擎走棋后,对局面做游戏结果检查。在已经有游戏结果的时候弹出游戏结果界面。

此外,我们实现了在游戏结束以及游戏过程中重新开启一个新对局的功能。

「长将」的逻辑,我们交在后续课程中实现,而像「长捉」、「限着」等更完善的中国象棋游戏规则,大家可以尝试自己去挑战实现!