FireMonkey3D之中国象棋程序制定规则
Posted 交流学习编程知识
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FireMonkey3D之中国象棋程序制定规则相关的知识,希望对你有一定的参考价值。
声明:本程序设计参考象棋巫师源码(开发工具dephi 11,建议用delphi 10.3以上版本)。
上一章我们设计了图形界面,可以开始轮流走棋了。但是,由于没有按中国象棋的规则进行限制,所有的棋子都可以在棋盘上随意走动,这章我们开始制定行棋规则。
2.1、记录局面
在制定规则之前,我们要先考虑把当前局面记录下来,这样棋子移动后才能知道移动后的局面。棋盘是10×9的格子组成,我们就用二维数组来记录局面变化情况,同时用一个一维数组记录每个棋子的位置:
var chessbd:array[0..9,0..8] of Byte; //记录当前棋局,添加到csPieceMove单元的TPieceMove里 pcPos:array[0..31] of TPoint; //记录棋子所在位置,声明在csCommon单元
在startUp函数里,我们将chessbd 初始化,同时记录初始棋局,代码较前章稍作修改,已标记:
procedure TPieceMove.Startup; var i:Integer; P:TPoint; const startPos: array[0..31] of Byte =//棋子的初始位置 ($09, $19, $29, $39, $49, $59, $69, $79, $89, $17, $77, $06, $26, $46, $66, $86, $00, $10, $20, $30, $40, $50, $60, $70, $80, $12, $72, $03, $23, $43, $63, $83); begin Player:=0; FillChar(Chessbd,SizeOf(chessbd),32);{<--添加代码-->} for I := 0 to 31 do begin chess[i].Visible:=False; chess[i].ResetRotationAngle; P:=Point(startPos[i] shr 4,startPos[i] and $F); chess[i].Position.Point:=Point3D(P.X-4,P.Y-4.5,-0.16); chess[i].Visible:=true; chessbd[P.Y,P.X]:=i; pcPos[i]:=P;{<--添加代码-->} end; end;
移动棋子之后,chessbd将发生变化,我们定义function TPieceMove.MovePiece(s,d:TPoint):Byte;这个函数记录移动后的变化:
{搬一步棋} function TPieceMove.MovePiece(s,d:TPoint):Byte; var sid,did:Byte; begin did:=chessbd[d.y,d.x]; sid:=chessbd[s.y,s.x]; chessbd[s.Y,s.X]:=32; chessbd[d.Y,d.X]:=sid; if did<32 then begin pcPos[did]:=Point(9,0); end; pcPos[sid]:=d; Result:=did; end;
2.2、制定规则
现在可以制定规则,限制棋子移动的范围。中国象棋走棋规则:车炮走直线,炮打隔山子,马跳日、象飞田,士走斜线,兵有进无退。重要的设计思路:
根据src源点、dest目标点的纵横坐标差的绝对值判断棋子的移动轨迹是否合理。
- 兵(卒):有进无退,过河平移,每次一格。伪代码 :
if 纵坐标绝对差值+横坐标绝对差值<>1 then 返回假;if 未过河 and 横坐标绝对差值=1 then 返回假。
- 马:跳日字,且马眼无子。伪代码 :if 纵坐标绝对差值*横坐标绝对差值<>2 then 返回假; if 别腿 then 返回假。如何判断别腿?下图讲解:
从图解中不难看出,马横跳时,马眼的横坐标是(源点横坐标+目标点横坐标)/2,纵坐标与源点相同;竖跳时,马眼的纵坐标是(源点纵坐标+目标点纵坐标)/2,横坐标与源点相同。伪代码:
if 横跳 and 马眼无棋 then 返回真;if 竖跳 and 马眼无棋 then 返回真。
- 象(相):走田字,象眼的位置简单得多,坐标【(源点横坐标+目标点横坐标)/2,(源点纵坐标+目标点纵坐标)/2)】,与马的判断相同。伪代码:
if 已过河 or 纵坐标绝对差值<>2 or 横坐标绝对差值<>2 then 返回假;if 象眼无棋 then 返回真。
- 士(仕): 走斜线,且不能出宫。伪代码:if 在宫里 and 纵坐标绝对差值*横坐标绝对差值=1 then 返回真。
- 帅(将):不出宫,每次一格。伪代码:if 在宫里 and 纵坐标绝对差值+横坐标绝对差值=1 then 返回真。以上几种棋判断走法很简单,就不发代码了,文章最后有完整源码。
- 车炮:走直线。虽说直线容易判断,但是走棋判断就稍复杂些。我们从源点搜索到目标点,看中间有无棋子挡住,如此判断。代码如下:
var H,V,i,j,csPc:integer; hasPc:boolean; begin V:=Abs(d.Y-s.Y); H:=Abs(d.X-s.X); if csPc in [PIECE_ROOK,PIECE_CANNON] then begin if H*V<>0 then Exit(False);//车炮走直线 hasPc:=chessbd[d.Y,d.X]<32; j:=0; if H=0 then //纵向行棋,Left相同,判断src与dest之间是否有棋 for I :=Min(s.Y,d.Y)+1 to Max(s.Y,d.Y)-1 do if chessbd[i,s.X]<32 then Inc(j); if V=0 then//横向行棋,Top相同,同上 for I :=Min(s.X,d.X)+1 to Max(s.X,d.X)-1 do if chessbd[s.Y,i]<32 then Inc(j); if (j=0)and((csPc=4)or((csPc=5)and(hasPc=False))) then Exit(True); if (j=1)and(csPc=5)and(hasPc) then Exit(True); //炮须隔子吃棋 end; end;
2.3、是否将军
中国象棋里能将军的棋子也就4种:兵(卒)、马、炮、车,所以我们判断是否将军时,只要判断对方的这4种棋子是否将军即可。这里我们要为兵(卒)、马、帅(将)定义步长,以便判断。所谓步长,就是指兵、马、帅走一步能到的位置,以原点坐标(0,0)为起点,确定以上棋子能走到位置,兵、帅的走法一致,步长也一样;马八个方向都可以走,还得定义马眼的位置;这里把士、相的步长也一并定义了,后面有用 。车炮步长不定,所以不能定义。代码如下:
KingMV: array [0..3] of TPoint=((X:1;Y:0),(X:0;Y:-1),(X:-1;Y:0),(X:0;Y:1)); //将(帅)卒(兵)步长 KnightMV: array [0..7] of TPoint=((X:-1;Y:-2),(X:-2;Y:-1),(X:-2;Y:1),(X:-1;Y:2),(X:1;Y:-2),(X:2;Y:-1),(X:2;Y:1),(X:1;Y:2)); //马步长 KnightPin:array [0..7] of TPoint=((X:0;Y:-1),(X:-1;Y:0),(X:-1;Y:0),(X:0;Y:1),(X:0;Y:-1),(X:1;Y:0),(X:1;Y:0),(X:0;Y:1));//马眼 AdvisorMV:array [0..3] of TPoint=((X:-1;Y:-1),(X:-1;Y:1),(X:1;Y:-1),(X:1;Y:1));//士(仕)步长 BishopMV: array [0..3] of TPoint=((X:-2;Y:-2),(X:-2;Y:2),(X:2;Y:-2),(X:2;Y:2));//相(象)步长
定义步长之后,我们就可以根据步长来判断帅(将)周边是否有以上4种有攻击力的棋子(注意:将帅面对面也是被认为是一种被将军!):
function TPieceMove.IsChecked:Boolean; var dest,src,P:TPoint; i,j,D,H,V,K:Integer; begin Result:=False; D:=(1-Player) shl 4;//乘16,0或16,代表对面的棋子 dest:=pcPos[4+Player shl 4];//首先要获取将(帅)的位置,以将(帅)为终点,判断是否被将军 for I := 0 to 3 do //将(帅)四周有没有兵(卒) begin src:=kingMV[i]+dest; if InBoard(src)and(PcCode[chessbd[src.Y,src.X]]=PIECE_PAWN)and((KingMV[i].Y+Player shl 2)=1) then Exit(True); end; for I := 0 to 7 do //将(帅)是否在马口 begin src:=KnightMV[i]+dest; if InBoard(src) then begin P:= dest+AdvisorMV[i shr 1];//马腿的位置是士的步长 if (chessbd[src.Y,src.X] in [D+1,D+7])and(chessbd[P.Y,P.X]=32) then Exit(True); end; end; for I in [D,D+4,D+8,D+9,D+10] do //车炮将(帅) begin if pcPos[i].X=9 then Continue; src:=pcPos[i]; H:=Abs(src.X-dest.X); V:=Abs(src.Y-dest.Y); K:=0; if (H*V<>0) then Continue; if H=0 then for j :=Min(src.Y,dest.Y)+1 to Max(src.Y,dest.Y)-1 do if chessbd[j,src.X]<32 then begin Inc(K); end; if V=0 then for j :=Min(src.X,dest.X)+1 to Max(src.X,dest.X)-1 do if chessbd[src.Y,j]<32 then Inc(K); if (k=0)and(i in [D,D+4,D+8]) then Exit(True);//车将(帅) if (k=1)and(PcCode[i]=PIECE_CANNON) then Exit(True);//炮 end; end;
以上代码也不复杂,不再另外讲解。中国象棋里我们要考虑,如果走棋之后,走棋方处于将军的状态,就不能走这步棋,所以得撤回这步走棋:
{撤销搬一步棋} procedure TPieceMove.UndoMovePiece(s,d:TPoint;id:Byte); begin chessbd[s.Y,s.X]:=chessbd[d.Y,d.X]; chessbd[d.Y,d.X]:=id; if id<32 then begin pcPos[id]:=d; end; pcPos[chessbd[s.Y,s.X]]:=s; end;
2.4、是否赢棋
判断是否赢棋,就是某一方被将军后,无法解将,或是某一方子被剃光头。以此来确定赢棋,设计思路:被将军的一方生成所有的走法,逐一尝试这些走法看是否能解将。红黑双方各有16个棋,除去已经被吃掉的棋,逐一生成走法即可,直接上代码(看注释):
function InBoard(P:TPoint):Boolean;//是否在棋盘上 begin Result:=(P.X in [0..8])and(P.Y in [0..9]); end; function InPalace(id:Integer;P:TPoint):Boolean;//是否在九宫格内 begin Result:=(P.X div 3=1)and(((P.Y in [0,1,2])and(id>15))or((P.Y in [7,8,9])and(id<16))); end; function SameSide(d,s:TPoint):Boolean;//是否处于同一阵营 begin Result:=(pcMove.chessbd[d.Y,d.X] shr 4)=(pcMove.chessbd[s.Y,s.X] shr 4); end; {定义走法,即src和dest} type TMoves=record src,dest:TPoint; end; {生成所有走法} function TPieceMove.GenerateMoves:TArray<TMoves>; var i,j,k,D:Integer; srcPt,destPt,P:TPoint; mvs:TMoves; procedure AddMV; begin //scr与dest属于不同阵营,就记下这个走法 if Sameside(destPt,srcPt)=False then begin mvs.src:=srcPt;mvs.dest:=destPt; Result:=Result+[mvs]; end; end; begin D:=Player shl 4; //找到本方的棋 for i := D to D+15 do begin if pcPos[i].X=9 then Continue; srcPt:=pcPos[i]; case PcCode[i] of PIECE_KING: //将(帅) for j := 0 to 3 do begin destPt:=KingMV[j]+srcPt; if InPalace(D+4,destPt) then AddMV; end; PIECE_ADVISOR: //士仕 for P in AdvisorMV do begin destPt:=P+srcPt; if InPalace(I,destPt) then AddMV; end; PIECE_BISHOP: //象相 for j:=0 to 3 do begin P:=AdvisorMV[j]+srcPt;//象眼是士的步长 destPt:=BishopMV[j]+srcPt; if InBoard(destPt)and(chessbd[P.Y,P.X]=32) then AddMV; end; PIECE_KNIGHT: //马 for j := 0 to 7 do begin destPt:=KnightMV[j]+srcPt; if InBoard(destPt) then begin P:=KnightPin[j]+srcPt; if chessbd[P.Y,P.X]=32 then AddMV; end; end; PIECE_ROOK,PIECE_CANNON: //车炮 for j:= 0 to 3 do begin P:=KingMV[j];//KingMV的步长为1,以KingMV为起点,向四个方向搜索 destPt:=srcPt+P; k:=0; while(InBoard(destPt))do begin if (chessbd[destPt.Y,destPt.X]=32)and(k=0) then AddMV else begin if i in [D,D+8] then//车找到棋子终止搜索 begin AddMV; Break; end; if chessbd[destPt.Y,destPt.x]<32 then //计数,炮搜索到棋子后继续向前 Inc(k); if k=2 then //找到炮的隔山子终止搜索 begin AddMV; Break; end; end; destPt.Offset(P); end; end; PIECE_PAWN://兵卒 for j := 0 to 3 do begin P:=KingMV[j]; destPt:=P+srcPt; if (InBoard(destPt))and((Cross_River(i,destPt.Y)and(P.Y=0))or(P.Y+1=(i shr 4 shl 1))) then AddMV; end; end; end; end;
剩下的工作就是逐一走这些走,判断是否仍处于将军状态,再撤销这些走法(为什么用IsMate?纯粹是与象棋巫师一致,实在不明白为什么这样的函数名,我最初用的GameOver):
{是否赢棋} function TPieceMove.IsMate:Boolean; var MVS:TArray<TMoves>; i,id:Integer; src,dest:TPoint; begin MVS:=GenerateMoves; for I := 0 to High(MVS) do begin id:=MovePiece(src,dest); if not IsChecked then begin UndoMovePiece(src,dest,id); Exit(False); end else UndoMovePiece(src,dest,id); end; Result:=True; end;
2.5、响应规则
在csBoard事件里添加canMove、MovePiece、IsChecked,IsMate等规则函数即可,见源码。
下一章将开始AI算法。
本章节源码百度云盘:
提取码:1234。
以上是关于FireMonkey3D之中国象棋程序制定规则的主要内容,如果未能解决你的问题,请参考以下文章