导语
在上一篇系列文章中国象棋 - Chapter 1中阐述了象棋映射到数据模型的抽象分析,在本篇章中,我们将继续聊聊马棋子的实现,并一起分析 ChessHelper类(象棋全局操作类) 的功能实现,整个游戏规则在代码层面上实现后就可以去搭建传说中的引擎桥梁,我们加快脚步,走起。
中国象棋-数据建模
棋子数据模型
棋子-马
我认为象棋中最具艺术的棋子就是马了,马走日,是坐标中稍微有迷惑性的一个变换,同时马还有蹩脚限制,可以在象棋下棋过程中造成很多局势,分析一下,马走日的走法映射数据模型条件即是:马棋子横向移动距离2,纵向为1 或者 横向移动距离1,纵向为2,除了落子点不能为同色棋子外,我们还需要去判断马是否蹩脚,那么如何判断是都蹩脚呢,我们能根据落点坐标和棋子本身坐标来计算出似的该次棋子移动的坐标值,如果这个坐标值上存在棋子,则蹩脚,我们先判断马是横向走日还是纵向走日,如果是横向走日,则憋足位置X轴坐标相同,Y轴坐标取马棋子和目标位置的Y轴中点值,如果是纵向走日,则憋足位置Y轴坐标相同,X轴坐标取马棋子和目标位置的X轴中点值,得出憋足坐标后,根据坐标查询该棋盘是否存在棋子,若有棋子则蹩脚,不满足下棋条件,马棋子的实现类如下:
class HorseChessman(chessType: ChessType, position: Position) : Chessman(chessType, position) {
override fun chessmanRule(nextPosition: Position): Boolean {
//马走日,横向移动距离2,纵向为1 或者 横向移动距离1,纵向为2
return ((Math.abs(nextPosition.column -position.column) == 1 && Math.abs(nextPosition.row -position.row) == 2)
|| (Math.abs(nextPosition.column -position.column) == 2 && Math.abs(nextPosition.row -position.row) == 1))
}
override fun chessboardRule(chessboardInfo: Array<Array<Chessman?>>, nextPosition: Position): Boolean {
ChessLogic.isExistChessman(chessboardInfo,nextPosition)?.let { chessman->
if (chessman.chessType == this@HorseChessman.chessType) return false//同色棋子不能被吃
}
//先判断是行向蹩脚还是列向蹩脚
if (Math.abs(nextPosition.row - position.row) == 2) {//行蹩脚
val queryRow = (nextPosition.row + position.row)/2
ChessLogic.isExistChessman(chessboardInfo, Position(queryRow, position.column))?.let {
return false
}
} else {//列蹩脚
val queryColumn = (nextPosition.column + position.column)/2
ChessLogic.isExistChessman(chessboardInfo, Position(position.row, queryColumn))?.let {
return false
}
}
return true
}
override fun chessmanName(): String {
return "马"
}
}
其他棋子的实现方式也都大同小异,都是在下棋操作前判断2个约束条件,在自我的走法和棋盘对该次移动的约束下仍然不能阻止本次棋子移动,则产生单次下棋行为,至此,一个象棋游戏的实体元素就已经完成,接下来,我们需要把下棋操作,输赢判断的方法在ChessHelper 中完善。
棋盘数据操作
除了上一篇文章中在ChessHelper中已存在棋盘坐标信息,我们还需要新增:当前回合标识,当前提起的棋子,当前的操作状态(自由态,提子态),一个关于棋盘操作的观察者(主要用来通知引擎做对应操作的渲染),因此,我们在ChessHelper中增加对应的成员变量:
object ChessHelper {
//十行九列一共90个坐标点 ,这里为 10 * 11 表示 左下角的第一个点并非从 (0,0) 开始,而是从(1,1) 开始
const val ColumnCapacity : Int = 10
const val RowCapacity : Int = 11
//棋盘上的二维坐标集信息,对应的索引取值是空值或者是棋子对象
private var chessboardInfo : Array<Array<Chessman?>>
= Array(RowCapacity ){Array<Chessman?>(ColumnCapacity) { null } }
//己方视角下操作者 Type
var myRoleType = ChessType.Red
//当前哪方回合
private var turnFlag = ChessType.Red
//当前选择棋子
private var pickedChessman : Chessman? = null
//当前操作状态
var operationStatus : OperationStatus = OperationStatus.ChessFreedom
//棋盘操观察者
var observer : OperateObserver? = null
...
}
除了增加上述所需的信息,之后我们需要增加一些数据处理的方法,例如查询棋盘上棋子信息,提起棋子,放下棋子,棋盘载入棋子信息等等,这些方法无非是一些对标识值的改变及一些信息的传递,我们主要来分析一下下棋落子方法 moveChessman(nextPosition : Position) : MoveResult ,参数为将要落子点的坐标,返回一个执行下棋的结果,先上代码:
fun moveChessman(nextPosition : Position) : MoveResult {
pickedChessman?.let { pickedChessman ->
if (pickedChessman.position.column == nextPosition.column && pickedChessman.position.row == nextPosition.row) {
observer?.onDropBack(pickedChessman)
return@moveChessman MoveResult.DesIsSelf
}
//检测是否符合下棋规则,包括棋子约束和棋盘约束
if (pickedChessman.chessmanRule(nextPosition)
&& pickedChessman.chessboardRule(queryChessboardInfo(),nextPosition)) {
//删掉落点处棋子
ChessLogic.isExistChessman(queryChessboardInfo(),nextPosition)?.let { removeChessman->
queryChessboardInfo()[removeChessman.position.row][removeChessman.position.column] = null
observer?.onRemoveChessman(removeChessman)
if (removeChessman is GeneralChessman) return@moveChessman MoveResult.GameOver
}
/**
* 主要逻辑步骤:
*/
//置空旧坐标对 picked chess的引用
queryChessboardInfo()[pickedChessman.position.row][pickedChessman.position.column] = null
observer?.onMoveChessman(pickedChessman.position.row,pickedChessman.position.column)
//更新被选择棋子的坐标信息
pickedChessman.updateChessmanPosition(nextPosition.row,nextPosition.column)
//新的落点处坐标数组中对应索引指向选择棋子对象
queryChessboardInfo()[nextPosition.row][nextPosition.column] = pickedChessman
//切换回合
turnFlag = if (turnFlag == ChessType.Red) ChessType.Black
else ChessType.Red
return@moveChessman MoveResult.Success
}
return@moveChessman MoveResult.UnSupportChessRule
}
return MoveResult.NoPickedChessMan
}
下棋方法主要有这么几个步骤:
1.首先判断是否有存在被提起的棋子(提子态),否则中断逻辑。
2.存在被提起的棋子,看下棋的目的坐标跟当前提起棋子坐标是否一致,如果一直则表示放下棋子,取消提起状态。
3.判断棋子的自我约束 chessmanRule 和棋盘约束 chessboardRule,若都通过,则移除目的坐标可能存在的棋子,更新提起棋子的坐标信息,如果移除的棋子是 将/帅 棋子,则游戏结束。
4.若游戏没有结束,则置空当前被提起棋子的引用,棋盘恢复到自由状态,最后切换回合。
(上述下棋步骤还缺少一个游戏规则的判定,你找出来了吗,我们下一篇再聊)
如此一来,整个象棋的数据模型已经初步代码实现,你可以以使用源码中 model 包下的类,并写一个终端输入类,通过传入每次想要下棋的目标的坐标,然后调用ChessHelper中的方法,可以初步在控制台上打印每次操作以后的棋盘信息或返回操作失败的原因。
对具象的游戏规则抽象后,并通过代码转换成数据模型,可以说象棋游戏已经完成一半,当然,该例只是一个过程化的游戏数据模型的设计,方便理解加深一个游戏规则抽象为数据模型的印象,在代码层面上还可以进行大量的优化,比如把棋盘信息的二维数组转换成一位数组提高查询效率,比如可以把ChessHelper继续拆分成棋盘类和操作类,降低元素类和操作类的耦合,比如可以设计最优走法功能等等,记住:代码永远没有自己认为的最优状态,它只有不断优化状态。在下一篇中,我们将聊聊游戏引擎并如何利用游戏引擎帮我们创造一个完整的象棋游戏。
源码链接
中国象棋 数据抽象模块在该项目 : Source Code
core/src/com/pimuseum/game/chinesechess/model 路径下