Flutter象棋:7.象棋着法检测

到现在为止,我们的象棋游戏能支持玩家与云库引擎正常地交替下棋了!但现在的游戏没有细致的规则限制,玩家可以不按照中国象棋的规则行棋。本节我们需要限制用户的行棋动作,遵循象棋游戏的通用规则。

本节概要

  • 中国象棋游戏规范介绍
  • 实现可行着法枚举工具类
  • 实现着法合法性检查工具类
  • 应用着法合法性检查

中国象棋游戏规范

中国象棋游戏规范

知道大家都不爱看文档,所以我简单地把行棋的规则和一般的输赢判断规则列举一下。

着法合法性检查规则:

  • 车走直线、马踩日,炮打翻山、象飞田,兵卒过河左右走,仕象不离老将边;
  • 行棋着法不能吃已方棋子;
  • 车只能在一条线上走子,直到遇到第一个障碍棋子,如果棋子是敌方的,可以吃掉它;
  • 马走一个日字,但有「蹩腿」的行动限制;
  • 象走一个田字,不能过河界,且有被「填心」的行动限制;
  • 炮可以在一条直线上走子,直至遇到第一个障碍棋子,如果越过障碍棋子后在直线上遇到的第一个棋子是敌方棋子,可以吃掉它;
  • 兵卒在冲过对方的河界之前,只能一次向前行走一步,过了敌方河界以后,可以向前方或左右方向行走一步;
  • 一步着法过后,不能造成双方的老将对面,即两方老将都在同一列,且中间无格挡棋子;

胜负检查判定方法:

  • 如果无论已方走什么可行着法都无法解除对方下一步对已方老将的攻击,则表示已方已经输了;
  • 如果对方行棋后,已方无合法的着法可走,则表示已方已经输了;
  • 如果一方在进棋过程中反复捉对方的将或大子,造成了三次以上的连续重复局面,则判长捉和长将方为负;
  • 如果超过 60 个回合无吃子,则判和棋;

准备工作

为了实现中国象棋游戏的规则检查,我们有些准备工作要先做一下:

第一件事,我们要增强一下 dart 的基本算法方法集,建立一个自己的 math-ex 工具集。我们在 lib/common 文件夹下新建一个 math-ext.dart 文件,实现两个方法:

abs(value) => value > 0 ? value : -value;
		
		// 二分查找,没有找到 dart 的标准库的实现,就自己实现一个了
		int binarySearch(List array, int start, int end, int key) {
		  //
		  if (start > end) return -1;
		
		  if (array[start] == key) return start;
		  if (array[end] == key) return end;
		
		  final middle = start + (end - start) ~/ 2;
		  if (array[middle] == key) return middle;
		
		  if (key < array[middle]) {
		    return binarySearch(array, start + 1, middle - 1, key);
		  }
		
		  return binarySearch(array, middle + 1, end - 1, key);
		}
		

第二件事,在进行着法检查的过程中,我们时常要假定这步棋已经走出,再看局面是否是合法的,例如会不会千万老将会面,会不会千万自己的老将可能被对方直接攻击。这些尝试操作需要改动棋子的位置布署,所以我们希望在一个 clone 的临时棋盘上进行这种验证操作。

在 Phase 类中,我们添加一个名叫 clone 的构造方法,它可以基于现有 Phase 对象克隆一个相同的临时 Phase 对象:

// 克隆一个一模一样的 phase 对象,修改其内容时不影响原来的 Phase 对象
		Phase.clone(Phase other) {
		  //
		  _pieces = List();
		
		  other._pieces.forEach((piece) => _pieces.add(piece));
		
		  _side = other._side;
		
		  halfMove = other.halfMove;
		  fullMove = other.fullMove;
		}
		

此外,我们还需要给 Phase 类添加一个 testMove 的方法,规则检查时会做假设性行棋,然后判断局面是否会有老将对面等 Case,直接改动棋子分布数组而不用关心招法的合法性:

这件事不能直接调用 Phase 的 move 方法,move 方法中会进行着法合法性检查,如果假设性行棋调用合法性检测,这就陷入了逻辑循环。

  // 在判断行棋合法性等环节,要在克隆的棋盘上进行行棋假设,然后检查效果
		  // 这种情况下不验证、不记录、不翻译
		  void moveTest(Move move, {turnSide = false}) {
		    // 修改棋盘
		    _pieces[move.to] = _pieces[move.from];
		    _pieces[move.from] = Piece.Empty;
		
		    // 交换走棋方
		    if (turnSide) _side = Side.oppo(_side);
		  }
		

准备工作完成了,我们来检查玩家的着法合法性!

着法合法性检查

深呼吸一轮,我们要开始飘过一片逻辑海洋!

我们在 lib/cchess 文件夹下新建 steps-enum.dart,这个类里面放置着法枚举工具类 StepsEnumerator 的实现:

import '../common/math.ext.dart';
		import 'cc-base.dart';
		import 'phase.dart';
		
		// 枚举当前局面下所有合符象棋规则的着法
		class StepsEnumerator {
		  //
		  static List enumSteps(Phase phase) {
		    //
		    final steps = [];
		
		    for (var row = 0; row < 10; row++) {
		      //
		      for (var col = 0; col < 9; col++) {
		        //
		        final from = row * 9 + col;
		        final piece = phase.pieceAt(from);
		
		        if (Side.of(piece) != phase.side) continue;
		
		        var pieceSteps;
		
		        if (piece == Piece.RedKing || piece == Piece.BlackKing) { // 将的着法
		          pieceSteps = enumKingSteps(phase, row, col, from);
		        } else if (piece == Piece.RedAdvisor || piece == Piece.BlackAdvisor) { // 仕的着法
		          pieceSteps = enumAdvisorSteps(phase, row, col, from);
		        } else if (piece == Piece.RedBishop || piece == Piece.BlackBishop) { // 象的着法
		          pieceSteps = enumBishopSteps(phase, row, col, from);
		        } else if (piece == Piece.RedKnight || piece == Piece.BlackKnight) { // 马的着法
		          pieceSteps = enumKnightSteps(phase, row, col, from);
		        } else if (piece == Piece.RedRook || piece == Piece.BlackRook) { // 车的着法
		          pieceSteps = enumRookSteps(phase, row, col, from);
		        } else if (piece == Piece.RedCanon || piece == Piece.BlackCanon) { // 炮的着法
		          pieceSteps = enumCanonSteps(phase, row, col, from);
		        } else if (piece == Piece.RedPawn || piece == Piece.BlackPawn) { // 兵的着法
		          pieceSteps = enumPawnSteps(phase, row, col, from);
		        } else {
		          continue;
		        }
		
		        steps.addAll(pieceSteps);
		      }
		    }
		
		    return steps;
		  }
		
		  // 将的着法枚举
		  static List enumKingSteps(
		    Phase phase, int row, int col, int from) {
		    //
		    final offsetList = [
		      [-1, 0],
		      [0, -1],
		      [1, 0],
		      [0, 1]
		    ];
		
		    final redRange = [66, 67, 68, 75, 76, 77, 84, 85, 86];
		    final blackRange = [3, 4, 5, 12, 13, 14, 21, 22, 23];
		    final range = (phase.side == Side.Red ? redRange : blackRange);
		
		    final steps = [];
		
		    for (var i = 0; i < 4; i++) {
		      //
		      final offset = offsetList[i];
		      final to = (row + offset[0]) * 9 + col + offset[1];
		
		      if (!posOnBoard(to) || Side.of(phase.pieceAt(to)) == phase.side) {
		        continue;
		      }
		
		      if (binarySearch(range, 0, range.length - 1, to) > -1) {
		        steps.add(Move(from, to));
		      }
		    }
		
		    return steps;
		  }
		
		  // 士的着法枚举
		  static List enumAdvisorSteps(
		    Phase phase, int row, int col, int from) {
		    //
		    final offsetList = [
		      [-1, -1],
		      [1, -1],
		      [-1, 1],
		      [1, 1]
		    ];
		
		    final redRange = [66, 68, 76, 84, 86];
		    final blackRange = [3, 5, 13, 21, 23];
		    final range = phase.side == Side.Red ? redRange : blackRange;
		
		    final steps = [];
		
		    for (var i = 0; i < 4; i++) {
		      //
		      final offset = offsetList[i];
		      final to = (row + offset[0]) * 9 + col + offset[1];
		
		      if (!posOnBoard(to) || Side.of(phase.pieceAt(to)) == phase.side) {
		        continue;
		      }
		
		      if (binarySearch(range, 0, range.length - 1, to) > -1) {
		        steps.add(Move(from, to));
		      }
		    }
		
		    return steps;
		  }
		
		  // 象的着法枚举
		  static List enumBishopSteps(
		    Phase phase, int row, int col, int from) {
		    //
		    final heartOffsetList = [
		      [-1, -1],
		      [1, -1],
		      [-1, 1],
		      [1, 1]
		    ];
		
		    final offsetList = [
		      [-2, -2],
		      [2, -2],
		      [-2, 2],
		      [2, 2]
		    ];
		
		    final redRange = [47, 51, 63, 67, 71, 83, 87];
		    final blackRange = [2, 6, 18, 22, 26, 38, 42];
		    final range = phase.side == Side.Red ? redRange : blackRange;
		
		    final steps = [];
		
		    for (var i = 0; i < 4; i++) {
		      //
		      final heartOffset = heartOffsetList[i];
		      final heart = (row + heartOffset[0]) * 9 + (col + heartOffset[1]);
		
		      if (!posOnBoard(heart) || phase.pieceAt(heart) != Piece.Empty) {
		        continue;
		      }
		
		      final offset = offsetList[i];
		      final to = (row + offset[0]) * 9 + (col + offset[1]);
		
		      if (!posOnBoard(to) || Side.of(phase.pieceAt(to)) == phase.side) {
		        continue;
		      }
		
		      if (binarySearch(range, 0, range.length - 1, to) > -1) {
		        steps.add(Move(from, to));
		      }
		    }
		
		    return steps;
		  }
		
		  // 马的着法枚举
		  static List enumKnightSteps(
		    Phase phase, int row, int col, int from) {
		    //
		    final offsetList = [
		      [-2, -1],
		      [-1, -2],
		      [1, -2],
		      [2, -1],
		      [2, 1],
		      [1, 2],
		      [-1, 2],
		      [-2, 1]
		    ];
		    final footOffsetList = [
		      [-1, 0],
		      [0, -1],
		      [0, -1],
		      [1, 0],
		      [1, 0],
		      [0, 1],
		      [0, 1],
		      [-1, 0]
		    ];
		
		    final steps = [];
		
		    for (var i = 0; i < 8; i++) {
		      //
		      final offset = offsetList[i];
		      final nr = row + offset[0], nc = col + offset[1];
		
		      if (nr < 0 || nr > 9 || nc < 0 || nc > 9) continue;
		
		      final to = nr * 9 + nc;
		      if (!posOnBoard(to) || Side.of(phase.pieceAt(to)) == phase.side) {
		        continue;
		      }
		
		      final footOffset = footOffsetList[i];
		      final fr = row + footOffset[0], fc = col + footOffset[1];
		      final foot = fr * 9 + fc;
		
		      if (!posOnBoard(foot) || phase.pieceAt(foot) != Piece.Empty) {
		        continue;
		      }
		
		      steps.add(Move(from, to));
		    }
		
		    return steps;
		  }
		
		  // 车的着法枚举
		  static List enumRookSteps(
		    Phase phase, int row, int col, int from) {
		    //
		    final steps = [];
		
		    // to left
		    for (var c = col - 1; c >= 0; c--) {
		      final to = row * 9 + c;
		      final target = phase.pieceAt(to);
		
		      if (target == Piece.Empty) {
		        steps.add(Move(from, to));
		      } else {
		        if (Side.of(target) != phase.side) {
		          steps.add(Move(from, to));
		        }
		        break;
		      }
		    }
		
		    // to top
		    for (var r = row - 1; r >= 0; r--) {
		      final to = r * 9 + col;
		      final target = phase.pieceAt(to);
		
		      if (target == Piece.Empty) {
		        steps.add(Move(from, to));
		      } else {
		        if (Side.of(target) != phase.side) {
		          steps.add(Move(from, to));
		        }
		        break;
		      }
		    }
		
		    // to right
		    for (var c = col + 1; c < 9; c++) {
		      final to = row * 9 + c;
		      final target = phase.pieceAt(to);
		
		      if (target == Piece.Empty) {
		        steps.add(Move(from, to));
		      } else {
		        if (Side.of(target) != phase.side) {
		          steps.add(Move(from, to));
		        }
		        break;
		      }
		    }
		
		    // to down
		    for (var r = row + 1; r < 10; r++) {
		      final to = r * 9 + col;
		      final target = phase.pieceAt(to);
		
		      if (target == Piece.Empty) {
		        steps.add(Move(from, to));
		      } else {
		        if (Side.of(target) != phase.side) {
		          steps.add(Move(from, to));
		        }
		        break;
		      }
		    }
		
		    return steps;
		  }
		
		  // 炮的着法枚举
		  static List enumCanonSteps(
		    Phase phase, int row, int col, int from) {
		    //
		    final steps = [];
		    // to left
		    var overPiece = false;
		
		    for (var c = col - 1; c >= 0; c--) {
		      final to = row * 9 + c;
		      final target = phase.pieceAt(to);
		
		      if (!overPiece) {
		        if (target == Piece.Empty) {
		          steps.add(Move(from, to));
		        } else {
		          overPiece = true;
		        }
		      } else {
		        if (target != Piece.Empty) {
		          if (Side.of(target) != phase.side) {
		            steps.add(Move(from, to));
		          }
		          break;
		        }
		      }
		    }
		
		    // to top
		    overPiece = false;
		
		    for (var r = row - 1; r >= 0; r--) {
		      final to = r * 9 + col;
		      final target = phase.pieceAt(to);
		
		      if (!overPiece) {
		        if (target == Piece.Empty) {
		          steps.add(Move(from, to));
		        } else {
		          overPiece = true;
		        }
		      } else {
		        if (target != Piece.Empty) {
		          if (Side.of(target) != phase.side) {
		            steps.add(Move(from, to));
		          }
		          break;
		        }
		      }
		    }
		
		    // to right
		    overPiece = false;
		
		    for (var c = col + 1; c < 9; c++) {
		      final to = row * 9 + c;
		      final target = phase.pieceAt(to);
		
		      if (!overPiece) {
		        if (target == Piece.Empty) {
		          steps.add(Move(from, to));
		        } else {
		          overPiece = true;
		        }
		      } else {
		        if (target != Piece.Empty) {
		          if (Side.of(target) != phase.side) {
		            steps.add(Move(from, to));
		          }
		          break;
		        }
		      }
		    }
		
		    // to bottom
		    overPiece = false;
		
		    for (var r = row + 1; r < 10; r++) {
		      final to = r * 9 + col;
		      final target = phase.pieceAt(to);
		
		      if (!overPiece) {
		        if (target == Piece.Empty) {
		          steps.add(Move(from, to));
		        } else {
		          overPiece = true;
		        }
		      } else {
		        if (target != Piece.Empty) {
		          if (Side.of(target) != phase.side) {
		            steps.add(Move(from, to));
		          }
		          break;
		        }
		      }
		    }
		
		    return steps;
		  }
		
		  // 兵的着法枚举
		  static List enumPawnSteps(
		    Phase phase, int row, int col, int from) {
		    //
		    var to = (row + (phase.side == Side.Red ? -1 : 1)) * 9 + col;
		
		    final steps = [];
		
		    if (posOnBoard(to) && Side.of(phase.pieceAt(to)) != phase.side) {
		      steps.add(Move(from, to));
		    }
		
		    if ((phase.side == Side.Red && row < 5) || (phase.side == Side.Black && row > 4)) {
		      //
		      if (col > 0) {
		        to = row * 9 + col - 1;
		        if (posOnBoard(to) && Side.of(phase.pieceAt(to)) != phase.side) {
		          steps.add(Move(from, to));
		        }
		      }
		
		      if (col < 8) {
		        to = row * 9 + col + 1;
		        if (posOnBoard(to) && Side.of(phase.pieceAt(to)) != phase.side) {
		          steps.add(Move(from, to));
		        }
		      }
		    }
		
		    return steps;
		  }
		
		  static posOnBoard(int pos) => pos > -1 && pos < 90;
		}
		

这是一个很长的工具,应用它可以列举当前局面下,行棋方可能走的全部着法,但不包含避免老将对面、避免行棋后对方可以直接攻击已方老将等逻辑。

接着,我们在 lib/cchess 文件夹下新建 steps-validate.dart 文件,将在这个工具类中放置某一着法是否合法的检查逻辑:

import '../common/math.ext.dart';
		import 'cc-base.dart';
		import 'cc-rules.dart'
		import 'phase.dart';
		
		// 着法合法性验证
		class StepValidate {
		  //
		  static bool validate(Phase phase, Move move) {
		    //
		    if (Side.of(phase.pieceAt(move.to)) == phase.side) return false;
		
		    final piece = phase.pieceAt(move.from);
		
		    var valid = false;
		
		    if (piece == Piece.RedKing || piece == Piece.BlackKing) { // 将
		      valid = validateKingStep(phase, move);
		    } else if (piece == Piece.RedAdvisor || piece == Piece.BlackAdvisor) { // 仕
		      valid = validateAdvisorStep(phase, move);
		    } else if (piece == Piece.RedBishop || piece == Piece.BlackBishop) { // 象
		      valid = validateBishopStep(phase, move);
		    } else if (piece == Piece.RedKnight || piece == Piece.BlackKnight) { // 马
		      valid = validateKnightStep(phase, move);
		    } else if (piece == Piece.RedRook || piece == Piece.BlackRook) { // 车
		      valid = validateRookStep(phase, move);
		    } else if (piece == Piece.RedCanon || piece == Piece.BlackCanon) { // 炮
		      valid = validateCanonStep(phase, move);
		    } else if (piece == Piece.RedPawn || piece == Piece.BlackPawn) { // 兵
		      valid = validatePawnStep(phase, move);
		    }
		
		    if (!valid) return false;
		
		    if (ChessRules.willBeChecked(phase, move)) return false;
		
		    if (ChessRules.willKingsMeeting(phase, move)) return false;
		
		    return true;
		  }
		
		  static bool validateKingStep(Phase phase, Move move) {
		    //
		    final adx = abs(move.tx - move.fx), ady = abs(move.ty - move.fy);
		
		    final isUpDownMove = (adx == 0 && ady == 1);
		    final isLeftRightMove = (adx == 1 && ady == 0);
		
		    if (!isUpDownMove && !isLeftRightMove) return false;
		
		    final redRange = [66, 67, 68, 75, 76, 77, 84, 85, 86];
		    final blackRange = [3, 4, 5, 12, 13, 14, 21, 22, 23];
		    final range = (phase.side == Side.Red) ? redRange : blackRange;
		
		    return binarySearch(range, 0, range.length - 1, move.to) >= 0;
		  }
		
		  static bool validateAdvisorStep(Phase phase, Move move) {
		    //
		    final adx = abs(move.tx - move.fx), ady = abs(move.ty - move.fy);
		
		    if (adx != 1 || ady != 1) return false;
		
		    final redRange = [66, 68, 76, 84, 86], blackRange = [3, 5, 13, 21, 23];
		    final range = (phase.side == Side.Red) ? redRange : blackRange;
		
		    return binarySearch(range, 0, range.length - 1, move.to) >= 0;
		  }
		
		  static bool validateBishopStep(Phase phase, Move move) {
		    //
		    final adx = abs(move.tx - move.fx), ady = abs(move.ty - move.fy);
		
		    if (adx != 2 || ady != 2) return false;
		
		    final redRange = [47, 51, 63, 67, 71, 83, 87], blackRange = [2, 6, 18, 22, 26, 38, 42];
		    final range = (phase.side == Side.Red) ? redRange : blackRange;
		
		    if (binarySearch(range, 0, range.length - 1, move.to) < 0) return false;
		
		    if (move.tx > move.fx) {
		      if (move.ty > move.fy) {
		        final heart = (move.fy + 1) * 9 + move.fx + 1;
		        if (phase.pieceAt(heart) != Piece.Empty) return false;
		      } else {
		        final heart = (move.fy - 1) * 9 + move.fx + 1;
		        if (phase.pieceAt(heart) != Piece.Empty) return false;
		      }
		    } else {
		      if (move.ty > move.fy) {
		        final heart = (move.fy + 1) * 9 + move.fx - 1;
		        if (phase.pieceAt(heart) != Piece.Empty) return false;
		      } else {
		        final heart = (move.fy - 1) * 9 + move.fx - 1;
		        if (phase.pieceAt(heart) != Piece.Empty) return false;
		      }
		    }
		
		    return true;
		  }
		
		  static bool validateKnightStep(Phase phase, Move move) {
		    //
		    final dx = move.tx - move.fx, dy = move.ty - move.fy;
		    final adx = abs(dx), ady = abs(dy);
		
		    if (!(adx == 1 && ady == 2) && !(adx == 2 && ady == 1)) return false;
		
		    if (adx > ady) {
		      if (dx > 0) {
		        final foot = move.fy * 9 + move.fx + 1;
		        if (phase.pieceAt(foot) != Piece.Empty) return false;
		      } else {
		        final foot = move.fy * 9 + move.fx - 1;
		        if (phase.pieceAt(foot) != Piece.Empty) return false;
		      }
		    } else {
		      if (dy > 0) {
		        final foot = (move.fy + 1) * 9 + move.fx;
		        if (phase.pieceAt(foot) != Piece.Empty) return false;
		      } else {
		        final foot = (move.fy - 1) * 9 + move.fx;
		        if (phase.pieceAt(foot) != Piece.Empty) return false;
		      }
		    }
		
		    return true;
		  }
		
		  static bool validateRookStep(Phase phase, Move move) {
		    //
		    final dx = move.tx - move.fx, dy = move.ty - move.fy;
		
		    if (dx != 0 && dy != 0) return false;
		
		    if (dy == 0) {
		      if (dx < 0) {
		        for (int i = move.fx - 1; i > move.tx; i--) {
		          if (phase.pieceAt(move.fy * 9 + i) != Piece.Empty) return false;
		        }
		      } else {
		        for (int i = move.fx + 1; i < move.tx; i++) {
		          if (phase.pieceAt(move.fy * 9 + i) != Piece.Empty) return false;
		        }
		      }
		    } else {
		      if (dy < 0) {
		        for (int i = move.fy - 1; i > move.ty; i--) {
		          if (phase.pieceAt(i * 9 + move.fx) != Piece.Empty) return false;
		        }
		      } else {
		        for (int i = move.fy + 1; i < move.ty; i++) {
		          if (phase.pieceAt(i * 9 + move.fx) != Piece.Empty) return false;
		        }
		      }
		    }
		
		    return true;
		  }
		
		  static bool validateCanonStep(Phase phase, Move move) {
		    //
		    final dx = move.tx - move.fx, dy = move.ty - move.fy;
		
		    if (dx != 0 && dy != 0) return false;
		
		    if (dy == 0) {
		      //
		      if (dx < 0) {
		        //
		        var overPiece = false;
		
		        for (int i = move.fx - 1; i > move.tx; i--) {
		          //
		          if (phase.pieceAt(move.fy * 9 + i) != Piece.Empty) {
		            //
		            if (overPiece) return false;
		
		            if (phase.pieceAt(move.to) == Piece.Empty) return false;
		            overPiece = true;
		          }
		        }
		
		        if (!overPiece && phase.pieceAt(move.to) != Piece.Empty) return false;
		        //
		      } else {
		        //
		        var overPiece = false;
		
		        for (int i = move.fx + 1; i < move.tx; i++) {
		          //
		          if (phase.pieceAt(move.fy * 9 + i) != Piece.Empty) {
		            //
		            if (overPiece) return false;
		
		            if (phase.pieceAt(move.to) == Piece.Empty) return false;
		            overPiece = true;
		          }
		        }
		
		        if (!overPiece && phase.pieceAt(move.to) != Piece.Empty) return false;
		      }
		    } else {
		      //
		      if (dy < 0) {
		        //
		        var overPiece = false;
		
		        for (int i = move.fy - 1; i > move.ty; i--) {
		          //
		          if (phase.pieceAt(i * 9 + move.fx) != Piece.Empty) {
		            //
		            if (overPiece) return false;
		
		            if (phase.pieceAt(move.to) == Piece.Empty) return false;
		            overPiece = true;
		          }
		        }
		
		        if (!overPiece && phase.pieceAt(move.to) != Piece.Empty) return false;
		        //
		      } else {
		        //
		        var overPiece = false;
		
		        for (int i = move.fy + 1; i < move.ty; i++) {
		          //
		          if (phase.pieceAt(i * 9 + move.fx) != Piece.Empty) {
		            //
		            if (overPiece) return false;
		
		            if (phase.pieceAt(move.to) == Piece.Empty) return false;
		            overPiece = true;
		          }
		        }
		
		        if (!overPiece && phase.pieceAt(move.to) != Piece.Empty) return false;
		      }
		    }
		
		    return true;
		  }
		
		  static bool validatePawnStep(Phase phase, Move move) {
		    //
		    final dy = move.ty - move.fy;
		    final adx = abs(move.tx - move.fx), ady = abs(move.ty - move.fy);
		
		    if (adx > 1 || ady > 1 || (adx + ady) > 1) return false;
		
		    if (phase.side == Side.Red) {
		      //
		      if (move.fy > 4 && adx != 0) return false;
		      if (dy > 0) return false;
		      //
		    } else {
		      //
		      if (move.fy < 5 && adx != 0) return false;
		      if (dy < 0) return false;
		    }
		
		    return true;
		  }
		}
		

这个工具类按不同棋子地行棋规则检查用户的着法是否合规,而且还包含避免老将对面、避免行棋后对方可以直接攻击已方老将等逻辑。

接下来,我们在 lib/cchess 文件夹下新建 cc-rules.dart 文件,在其中提供当前是否有将军,是否已经被杀死等逻辑检查:

import 'cc-base.dart';
		import 'phase.dart';
		import 'step-enum.dart';
		import 'steps-validate.dart';
		
		class ChessRules {
		  // 是否被对方将军
		  static checked(Phase phase) {
		    //
		    final myKingPos = findKingPos(phase);
		
		    final oppoPhase = Phase.clone(phase);
		    oppoPhase.trunSide();
		
		    final oppoSteps = StepsEnumerator.enumSteps(oppoPhase);
		
		    for (var step in oppoSteps) {
		      if (step.to == myKingPos) return true;
		    }
		
		    return false;
		  }
		
		  // 应用指定着法后,是否被将军
		  static willBeChecked(Phase phase, Move move) {
		    //
		    final tempPhase = Phase.clone(phase);
		    tempPhase.moveTest(move);
		
		    return checked(tempPhase);
		  }
		
		  // 应用指定着法后,是否会造成老将对面
		  static willKingsMeeting(Phase phase, Move move) {
		    //
		    final tempPhase = Phase.clone(phase);
		    tempPhase.moveTest(move);
		
		    for (var col = 3; col < 6; col++) {
		      //
		      var foundKingAlready = false;
		
		      for (var row = 0; row < 10; row++) {
		        //
		        final piece = tempPhase.pieceAt(row * 9 + col);
		
		        if (!foundKingAlready) {
		          if (piece == Piece.RedKing || piece == Piece.BlackKing) foundKingAlready = true;
		          if (row > 2) break;
		        } else {
		          if (piece == Piece.RedKing || piece == Piece.BlackKing) return true;
		          if (piece != Piece.Empty) break;
		        }
		      }
		    }
		
		    return false;
		  }
		
		  // 是否已经被对方杀死
		  static bool beKilled(Phase phase) {
		    //
		    List steps = StepsEnumerator.enumSteps(phase);
		
		    for (var step in steps) {
		      if (StepValidate.validate(phase, step)) return false;
		    }
		
		    return true;
		  }
		
		  // 寻找已方的老将位置
		  static int findKingPos(Phase phase) {
		    //
		    for (var i = 0; i < 90; i++) {
		      //
		      final piece = phase.pieceAt(i);
		
		      if (piece == Piece.RedKing || piece == Piece.BlackKing) {
		        if (phase.side == Side.of(piece)) return i;
		      }
		    }
		
		    return -1;
		  }
		}
		

好大的几片代码,让人有点窒息!好在逻辑还算清晰、且与 Flutter 本身没啥关系。

艰巨的规则逻辑实现的差不多了,我们看看如果在玩家走棋的过程中应用这些着法检查。

我们回到 Phase 类,之前已经预留了着法合法性检查的 validateMove 方法定义,现在来实现它:

// 验证移动棋子的着法是否合法
		bool validateMove(int from, int to) {
		  // 移动的棋子的选手,应该是当前方
		  if (Side.of(_pieces[from]) != _side) return false;
		  return StepValidate.validate(this, Move(from, to));
		}
		

看看,一两句代码的事!当复杂的问题被透彻解决了之后,剩下的事件总是这样!

运行产品试试,现在不合棋规的走法,都是不被接受的了!

上传代码到Git:


		elapse@elapse-PC:~/Language/Flutter/chinese_chess$ git add .
		elapse@elapse-PC:~/Language/Flutter/chinese_chess$ git commit -m '象棋着法检测'
		[master 9bbb010] 象棋着法检测
		 10 files changed, 829 insertions(+), 9 deletions(-)
		 create mode 100644 lib/cchess/cc-rules.dart
		 create mode 100644 lib/cchess/steps-enum.dart
		 create mode 100644 lib/cchess/steps-validate.dart
		 create mode 100644 lib/common/math-ext.dart
		elapse@elapse-PC:~/Language/Flutter/chinese_chess$ sudo git push
		[sudo] elapse 的密码: 
		Username for 'https://rocketgit.com': elapse
		Password for 'https://[email protected]': 
		枚举对象: 41, 完成.
		对象计数中: 100% (41/41), 完成.
		使用 4 个线程进行压缩
		压缩对象中: 100% (23/23), 完成.
		写入对象中: 100% (23/23), 6.73 KiB | 3.36 MiB/s, 完成.
		总共 23 (差异 10),复用 0 (差异 0)
		remote: RocketGit: Info: == Welcome to RocketGit! ==
		remote: RocketGit: Info: you are connecting from IP 27.47.4.14 by http(s).
		remote: RocketGit: Info: date/time: 2020-08-28 12:13:17 (UTC), debug id 1410f2.
		To https://rocketgit.com/user/elapse/chinese_chess
		   18c7e5e..9bbb010  master -> master
		elapse@elapse-PC:~/Language/Flutter/chinese_chess$
		
		

本节回顾

本小节中,我们先是像大家介绍了中国象棋游戏的规范知识,如有需要详细地学习中国象棋游戏规范,可以参考我们提供的详细文档-中国象棋游戏规范

接着,我们依据中国象棋游戏规范实现了着法枚举工具类,列表玩家当着可以走的所有着法。

然后,我们根据游戏规范文档来验证某种着法是不是在合法的范围内。例如「马蹩腿」、「象填心」规则的检查。此外,行棋后必须避免老将对面、必须避免自己的老将被对方的棋子直接威胁。

最后,我们在 Phase 类中使用着法合法性检查工具类对玩家的棋子移动进行了限制。

象棋软件中的开发中,「着法枚举」和「着法合法性检查」都是重要的基础工作,这两部分的可靠实现,是象棋游戏能正常运行的基石。这部分代码其实可以很容易地从 Dart 语言转换成其它语言,真心希望棋软开发者能把它有效利用起来!