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((Player shl 1)=(KingMV[i].Y+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(Cross_River(i,destPt.Y)=False)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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM