admin 發表於 2024-4-22 16:37:52

中國象棋遊戲開發實战(C#)

今天起頭,咱们来盗窟一把“中國象棋”這款經典遊戲,基於.NET的C#開辟。提及中國象棋,不少朋侪又该說了,“這有甚麼難的?一张棋盘、几個棋子罢了!”。但是,步伐猿之間有句颇有名的谚语,Talk is cheap工廠搬遷,,Show me the code!。當你真正本身脱手来實現這款小步伐的時辰,你會發明其實不像你當初想象的那末简略,中國象棋的步伐逻辑至關繁杂,每種棋子都有他本身的挪動法則和吃子法則,各類棋局又是千變万化,若是没有一個清楚的思绪和公道的架構,你的開辟将會堕入一場逻辑紊乱的恶梦。

闲言少叙、空话少說、言反正傳(空话仍是很多,呵呵!)

起首讓大师看一眼咱们的步伐運行结果(图1),怎样样?固然谈不上標致,但最少還顺眼吧?呵呵!開個打趣。實在,本項目標界面十分简略,底子用不上甚麼高档的@常%645CH%識或技%s1g67%能@,項目標重心在於步伐的逻辑部門。

图1

除中國象棋最根基的一些法則逻辑以外,本項目還扩大了一些适用的辅助功效,如:對方每走一步棋,體系城市给出你下一步可以走的有哪些棋子;當你用鼠標按住某個棋子的時辰,會呈現一系列绿色的圆圈,以提醒你该棋子可以走到哪些位置(图2)。

图2

必要阐明的是,咱们的這款中國象棋没有斟酌人機對战功效及算法的實現,這触及到人機博弈理論,属於一個比力自力且體系的范畴,為了凸起重點,不讓人機博弈的那些艰涩的算法理論分离大师的注重力,咱们只實征象棋的法則逻辑,而没有设计電脑的AI。若是大师對人機博弈的理論及算法感樂趣,可以自行baidu一下,網上有不少相干資料,也接待大师把本身所了解的人機博弈算法利用到咱们的這個項目中来。

本項目今朝只實現了单機功效,至於局域網联機對战功效,實在其實不繁杂,不過就是經由過程相干的收集协定(TCP、UDP等),發送相干的数据包,并举行解析罢了。

源代码

[画外音]身為步伐猿,没有一個趁心随手的機器键盘怎样行?

要讓你從零實現中國象棋遊戲,你會有哪些實現思绪呢?

有人會說,網上中國象棋步伐一抓一大把,干嗎還要本身從零實現呢?好吧,你要如许說,我暗示無语;另有人會說,網上有不少現成的遊戲引擎,拿過来直接用就好了,只能說你很晓得拿来主义。咱们的這個“麻雀系列”項目標目標,就是但愿朋侪们可以或许從零實現一些小而精的項目,熬炼本身的脱手能力和设计思惟,經由過程提高本身的抽象能力,来磨炼本身的整體架構能力。

好了,回到本項目中来。用甚麼法子来把棋盘、棋子绘制到電脑屏幕上呢?有人會提到DirectX、WPF、Unity3D等等高峻上的技能,仍是那句话,咱们會疏忽掉任何會是咱们分心走神的枝節,在本項目中,咱们仍是采纳最最简略、最古老的GDI+画图技能。有人必定會暗示不屑了,不成否定,DirectX、WPF、Unity3D等确切可以做到更炫的结果、更高的開辟及運行效力,但别忘了咱们的項目是“麻雀系列”,咱们只存眷於咱们關切的部門。

既然咱们采纳的面向工具的步伐设计思惟,咱们就用工具的抽象思惟来構建咱们的步伐世界。大师想一想,實現中國象棋,大致必要哪些工具来互相协作呢?中國象棋由一张棋盘、32個棋子構成,很简略吧,咱们只必要一個棋盘類,一個棋子類。怎样?難以置信?一個步伐只有两個類?對的,你没有看错,咱们這個項目在营業逻辑上只有這两個類。或许有人在說,你在忽悠咱们吧?一個采纳面向工具思惟開辟的項目,只有两個類,開打趣呢吧!大师别急,待我慢漫道来。

先讓大师看一下本項目標根基類图(图3),大师可以看到,步伐总體上只有棋盘類(ChessBoard)和棋子類(ChessPiece)两個類,只不外棋子類又有7個子類,從图3中的英文名称可以看出,這7個子類實在就是中國象棋中的7類棋子,即:車、马、象、士、帅、炮、卒。图3的下部另有几個零星的小類,咱们在這里先行略過。

图3

本項目根基構架的大致思绪是如许的:

棋盘類賣力绘制棋盘、初始化棋子等根基操作,并供给互换红黑两邊位置、悔棋、保护玩家走棋次序、索引坐標和像素坐標的轉换、按照坐標盘問棋子、检测将军及明将、阐發下步可走招法、保留汗青走棋记實等功效。

棋子類賣力绘制本身到棋盘、挪動到方针位置、吃子、删除本身、悔棋等职责,同時供给绘制本身下一步可走位置、果断鼠標是不是在本身范畴内、保护本身的汗青挪動轨迹、保留本身吃掉的對方棋子的调集等功效。

步伐界面采纳傳统的WinForm,他的职责很简略,就是賣力GDI+画图,和鼠標、键盘事務的處置。在這里又會有人暗示對WinForm不平,說WinForm十分古老、傳统、枯燥,不如WPF等其他更加先辈的界面框架。實在,只要咱们想,咱们可以等闲的把界面框架改成WPF或其他。仍是那句话,咱们不會被這些枝節所困扰,咱们必要的是專注。

颠末上面的阐發梳理,全部步伐的布局是否是很清楚了?好的架構是好的步伐的一大部門,若是你的步伐架構没有设计好,在今後的详细编码進程中便會碰到各類逻辑圈套和代码Bug的胶葛,软件将變得難以保护、難以扩大,一旦需求有變更,那你更是會遭到非人的熬煎。好了,步伐的大要骨架已搭建终了,接下来咱们便可以着手详细實現了。

千里之行始於足下,玩象棋起首要有棋盘,怎样绘制棋盘呢?咱们在主窗體上添加一個PictureBox控件(定名為chessboard),就是图4中左邊的阿谁木纹控件,在他的上面用GDI+来画棋盘便可。提到GDI+画图,就离不開OnPaint事務,這個事務對付C#步伐员来讲不算目生吧?每當界面必要刷新時,就會挪用OnPaint事務,咱们必要做的就是在這個事務的處置法子中添加本身的画图代码便可以了。

图4

大师想一下,绘制棋盘是谁的职责呢?咱们晓得,在面向工具步伐设计思惟中,有一条很首要的法則,就是SRP(单一职责原則),一個類尽可能只賣力本身必要賣力的职责,该谁的责任就是谁的责任,一個法子應當归属於他應當归属的類。很明显,棋盘類賣力绘制棋盘,這一點咱们在前面也說過。

OK,咱们創建一個棋盘類,定名為ChessBoard,在该類中添加一個Draw法子,来賣力绘制棋盘根基布局。Draw法子的声明以下:

/// <su妹妹ary>
/// 绘制棋盘
</su妹妹ary>
/// <param name="g">Graphics工具</param>
public void Draw(Graphics g)
{
}
要绘制棋盘,起首要先把網格線画出来,就是中國象棋棋盘的10行9列的網格(图5)。道理很简略,就是挪用Graphics工具的DrawRectangle、DrawLine等法子。

图5

在這里必要先声明几個属性或變量:

BoradWidth:棋盘的宽度;

BoradHeight:高度;

LINE_WEIGHT:線条宽度;

sW:棋盘中每一個小格子的宽度;

sH:棋盘中每一個小格子的高度;

這里必要偏重夸大一下LINE_WEIGHT變量,為甚麼要声明線条宽度呢?由於咱们日常平凡用GDI+绘制矩形或直線時,很少會注重到線条的宽度,但在绘制棋盘的進程中,線条是會占用宽度的,只不外默许是1像素。图6暗示了GDI+绘制直線時線条是占用宽度的,此中,AB之間的部門是代表線条宽度,红線為AB的中線,GDI+绘制直線時,以中線C為直線的X坐標,按照指定的宽度,在中線雙侧衬着直線。

图6

以是,绘制棋盘最外侧矩形邊框的代码以下所示,必要斟酌到線条宽度和外邊框矩形和最外侧的留白部門(图7中的A、B两部門)。

//绘制外邊框
g.DrawRectangle(pen,
LINE_WEIGHT / 2 + sW / 2,
LINE_WEIGHT / 2 + sH / 2,
BoradWidth - LINE_WEIGHT - sW,
BoradHeight - LINE_WEIGHT - sH);
同理,绘制平行横線和平行竖線經由過程两個轮回便可實現,以下。在绘制進程中,一样要斟酌到線条宽度的影响。

//绘制纵向線段
for (float i = sW / 2 + LINE_WEIGHT / 2; i < BoradWidth; i += (sW + LINE_WEIGHT))
{
g.DrawLine(pen,
i,
sH / 2 + LINE_WEIGHT / 2,
i,
(LINE_WEIGHT + sH) * (BOARD_META_HEIGHT / 2 - 1) + sH / 2 + LINE_WEIGHT / 2);
g.DrawLine(pen,
i,
(LINE_WEIGHT + sH) * (BOARD_META_HEIGHT / 2) + sH / 2 + LINE_WEIGHT / 2,
i,
BoradHeight - sH / 2 - LINE_WEIGHT / 2);
}
//绘制横向線段
for (float j = sH / 2 + LINE_WEIGHT / 2; j < BoradHeight; j += (sH + LINE_WEIGHT))
{
g.DrawLine(pen, sW / 2 + LINE_WEIGHT / 2, j, BoradWidth - sW - 2 - LINE_WEIGHT / 2, j);
}

图7

如今咱们已绘制好了棋盘的格子,至於九宫格、“楚天河界”字样、炮位、兵位,事理都是同样的,這里就再也不赘述了。OK,到如今為止,咱们已把棋盘的总體绘制终了了。

在先容若何绘制棋子以前,還要在棋盘類ChessBoard中添加一個byte類型的静态二维数组Matrix,界說以下,该数组用於记實棋盘矩阵各個位置處有無棋子,有棋子的處所為1,没有則為0。

/// <su妹妹ary>
/// 棋盘矩阵
/// </su妹妹ary>
public static byte[,] Matrix = new byte;
棋盘有了,接下来就该画棋子了。咱们晓得,中國象棋统共有32個棋子、分红黑两邊,每一個棋子都有分歧的名称、分歧的位置、分歧的色彩、分歧的走法、分歧的吃子法則。是以,界說棋子類ChessPiece,该類有若干属性及法子。

此中,界說一個Draw法子,原型以下。

/// <su妹妹ary>
/// 绘制本身
/// </su妹妹ary>
/// <param name="g">Graphics工具</param>
public void Draw(Graphics g)
{
}
這個Draw法子若何實現呢?起首画一個圆形,贴上一张布景图,代码以下。至於布景图大师可以随意找一個,本身感觉標致就行。

g.DrawImage(new Bitmap(global::wChess.Properties.Resources.piece), Left,
Top,
ChessBoard.sW,
ChessBoard.sH);
然後在圆形布景图上面寫字,代码以下:

Font font = new Font("楷體", 25);
var fontSize = g.MeasureString(Name, font);
g.DrawString(Name, font, FontBrush, CenterX - fontSize.Width / 2, CenterY - fontSize.Height / 2 + 5);
如许就把棋子画在画布上了,简略吧?仔细的朋侪估量必定會發明上面两段代码中有Left、Top、CenterX、CenterY等没見過的东东,這些恰是咱们接下来要阐明的内容。

棋子實在有两種坐標,一個是像素坐標,即该棋子在棋盘画布上的像素位置;另外一個是索引坐標,代表该棋子在棋盘里每一個小格子構成的二维矩阵中的索引位置。比方图8中,假如棋盘每個小格子的宽和高都為10像素,同時疏忽線条的宽度,則A點的像素坐標為(20,10),它的索引坐標為(2,1)。

图8

棋子類必要有三個代表坐標的属性FixedMetaPosition、OldMetaPosition、FloatMetaPosition,此中FixedMetaPosition暗示棋子落地後的位置,OldMetaPosition代表该棋子在走棋以前的上一個位置,FloatMetaPosition代表该棋子挪動進程中未放下状况的位置。咱们在把棋子绘制到棋盘上的時辰,位置信息用的就是FloatMetaPosition。

這些坐標信息都有甚麼感化呢?棋子不是只必要一個坐標就好了麼?從概况上看,棋子确切只必要一個位置信息便可以了,但详细到中國象棋步伐中,因為必要给玩家供给悔棋功效,以是每個棋子都必要记實本身在走棋以前的上一步位置,即OldMetaPosition;為了给玩家供给友爱的用户體驗,當用鼠標拖動棋子時,该棋子要跟從鼠標挪動,在挪動状况下的姑且位置,就是FloatMetaPosition;而FixedMetaPosition則代表棋子落地後简直定位置。

适才呈現的Left、Top、CenterX、CenterY這几個属性,界說以下,别离代表该棋子地點圆形的外切矩形的左上角坐標和中間點坐標,用於绘制该棋子本身。

private float Left
{
get
{
return (ChessBoard.LINE_WEIGHT + ChessBoard.sW) * FloatMetaPosition.X + ChessBoard.LINE_WEIGHT / 2;
}
}
private float Top
{
get
{
return (ChessBoard.LINE_WEIGHT + ChessBoard.sH) * FloatMetaPosition.Y + ChessBoard.LINE_WEIGHT / 2;
}
}
private float CenterX
{
get
{
return Left + ChessBoard.sW / 2;
}
}
private float CenterY
{
get
{
return Top + ChessBoard.sH / 2;
}
}
大师想一想,棋子除這些坐標信息,還必要有哪些属性?對了,還要着名称(Name)、是红棋仍是黑棋(Team)、该棋子是棋盘上邊一方仍是下邊一方(Side)等属性。此中,Team和Side為罗列類型,界說以下:

/// <su妹妹ary>
/// 红黑方罗列
/// </su妹妹ary>
public enum TEAM
{
RED,
BLACK
}
/// <su妹妹ary>
/// 该棋子是棋盘上邊的一方,仍是下邊的一方
/// </su妹妹ary>
public enum SIDE
{
UP,
DOWN
}
有了以上的這些根本,如今咱们已可以在棋盘上绘制出某個棋子了,有成绩感吧?

到今朝為止,咱们已有棋盘、也有棋子了,但這些棋子還不克不及挪動呢,當玩家在某個棋子上按下鼠標并拖動時,该棋子應當跟從鼠標挪動;玩家在某一名置松開鼠標,该棋子便落到该位置處,這是中國象棋最根基的走棋法子。

好,咱们给棋子類ChessPiece添加一個挪動法子,声明以下:

​      ///
​      /// 挪動到指定位置(返回false阐明當前尚未轮到己方走棋)
​      ///
​      /// 方针位置
​      ///
​      public MOVE_RESULT MoveTo(Point pixelPos)
​      {}
美白淡斑精華液,這個MoveTo法子的感化就是把當前棋子挪動到指定的位置,大师斟酌一下该若何實現?基来源根基理很简略,就是依照中國象棋的走棋法則,先果断當前棋子能不克不及挪動到指定的位置,能的话就把當前棋子的FixedMetaPosition属性设置成鼠標地點的位置。固然了,详细實現進程中,必要斟酌的身分另有不少,這里先無論他。

那末怎样果断當前棋子能不克不及挪動到指定位置呢?咱们晓得,分歧的棋子有分歧的走棋法則,马走日、象飞田、炮隔山、車打一溜烟等等,這麼多分歧的法則,该怎样辦呢?总不克不及用雷同以下代码来處置吧?這就用到面向工具思惟中的封装、担當和多态了,此時恰是重構的好機會。

switch(piece.Name)
{
case “車”:
break;
case “马”:
break;
case “炮”:
break;
……
}
因為分歧的棋子有分歧的走棋法則,咱们必要對棋子類举行重構。在上文中咱们界說了棋子類ChessPiece,用他来代表所有32個棋子中的任一個棋子,该類的大要布局以下:

public class ChessPiece
​    {
​      public string Name { set; get; }
​      public Point FloatMetaPosition { set; get; }
​      public Point FixedMetaPosition { set; get; }
​      public Point OldMetaPosition { set; get; }
​      public TEAM Team { set; get; }
​      public SIDE Side { set; get; }
​      private float Left { set; get; }
​      private float Top { set; get; }
​      private float CenterX { set; get; }
​      private float CenterY { set; get; }
​      private Brush FontBrush { set; get; }

上面代码是棋子類的共有属性,分歧的是各類棋子果断可否挪動到指定位置的法則纷歧样,應當遵照诸如“马走日、象飞田”之類的法則,另有“蹩破绽”、“蹩象腰”等限定。分歧的棋子,其下一步可以挪動到的位置分歧,是以咱们為每種棋子添加一個類,并讓其担當於ChessPiece類。比方,添加代表“马”的Knights類、代表“車”的Rooks類、代表“炮”的Cannons類等。

在父類ChessPiece中添加一個抽象属性NextSteps,代表當前棋子下一步可以挪動的位置调集,界說以下列代码:

​      ///
​      /// 當前棋子下一步可以挪動的位置调集
​      ///
​      public abstract List NextSteps { get; }
然後界說各個子類,按照本身的走棋法則来實現NextSteps這個抽象属性,這些子類提及来比力简略,只有NextSteps這一個属性,没有其他成员,但這個NextSteps属性在實現上其實不简略,下面别离举行阐明。

“車”的走棋法則是只能沿着横向或纵向走直線、且不克不及超出其他棋子。

為了直觀,咱们看图9,红“車”在當前状况下,可以挪動到的位置為绿色圆圈處,可以别离依照“車”的上方、下方、左邊、右邊四個標的目的举行實現。

图9

咱们以该棋子上方的可挪動位置為例举行阐明,基来源根基理是:

假如當前“車”的纵坐標為Y0,寻觅该棋子上方第一個Matrix對應元素為1的位置,假如该位置為Y1。

若是Y1處是己方棋子,因為不克不及吃己方的棋子,以是從该棋子上方第一個位置(Y0-1)、一向到位置(Y1+1),這些位置都是该棋子下一步可以挪動到的位置;

若是Y1處是敌方棋子,因為可以吃敌方的棋子,以是從该棋子上方第一個位置(Y0-1)、一向到位置(Y1),這些位置都是该棋子下一步可以挪動到的位置;

那末怎样寻觅“車”上方第一個Matrix對應元素為1的位置呢?详细實現法子是:在该棋子正上方,從该棋子起頭,向上逐一位置果断棋盘矩阵Matrix對應位置的元素是不是為1,若為0則阐明该位置處没有棋子,继续向上逐一果断,直到达到棋盘最上方;如果1則阐明该位置處有棋子,该位置即是咱们要寻觅的“車”上方第一個Matrix對應元素為1的位置。

详细代码以下:

​      //上方的可以挪動的位置预览
​      private List GetAboveSteps()
​      {
​            List r = new List();
​            var yAbove = FixedMetaPosition.Y;
​            do
​            {
​                if (yAbove - 1 >= 0)
​                {
​                  if (ChessBoard.Matrix == 1)
​                  {
​                        var p = ChessBoard.GetPieceFromMetaPos(FixedMetaPosition.X, yAbove - 1);
​                        //方针位置只要没有己方棋子
​                        if (p.Count > 0 && p.Team != Team)
​                        {
​                            yAbove--;
​                        }
​                        break;
​                  }
​                  yAbove--;
​                }
​                else
​                {
​                  break;
​                }
​            } while (true);
​            for (int i = yAbove; i < FixedMetaPosition.Y; i++)
​            {
​                r.Add(new Point(FixedMetaPosition.X, i));
​            }
​            return r;
​      }
其他三個標的目的的實現道理是同样的,再也不赘述。总之,“車”類Rooks的NextSteps属性的代码以下:

​      public override List NextSteps
​      {
​            get
​            {
​                List r = new List();
​                r.AddRange(GetAboveSteps());
​                r.AddRange(GetBelowSteps());
​                r.AddRange(GetLeftSteps());
​                r.AddRange(GetRightSteps());
​                return r;
​            }
​      }
“马”的走棋法則俗话称為“马走日”,意思是只能從原位置走到“日”字的對角位置;此外,還要斟酌“蹩马腿”的环境。

图10

图10中展現了“马”的所有走棋法則,理論上“马”至多應當有8個位置可以挪動,即ABCDEFGH八個位置。此中,A、B、C三個位置因為没有任何棋子,以是“马”可以挪動到這些位置;D處固然有棋子,但因為是敌方棋子,所有可以吃掉它;因為被右邊的“炮”(I點)“蹩马腿”,所有“马”不克不及挪動到E、F两個位置;G、H两點因為是己方棋子,不克不及吃,以是“马”不克不及挪動到這两個位置。

下面咱们以F點為例举行阐明,其他點同理。

已知F點的索引坐標為(FixedMetaPosition.X + 2, FixedMetaPosition.Y + 1),马腿位置為(FixedMetaPosition.X + 1, FixedMetaPosition.Y)。

起首果断一下F點是不是超越棋盘矩形的范畴,若超越范畴則“马”不克不及挪動到F點;

若没有超越范畴,再果断有無“蹩马腿”,即果断I點在棋盘矩阵對應位置處的元素是不是為1,若為1則阐明被“蹩马腿”,不克不及挪動到该位置;

若為0,再果断方针位置F處有無棋子,若没有棋子則可以挪動到该位置;

如有棋子,再果断F處是己方棋子仍是敌方棋子,如果敌方棋子,因為可以吃掉他,以是可以挪動到该位置;

如果己方棋子,則不成挪動到该位置。

是否是有點启蒙?呵呵,好好捋捋,代码以下:

if (FixedMetaPosition.X + 2 < ChessBoard.BOARD_META_WIDTH && FixedMetaPosition.Y + 1 < ChessBoard.BOARD_META_HEIGHT)
​                {
​                  //蹩马腿
​                  if (ChessBoard.Matrix == 0)
​                  {
​                        //若是方针處没有棋子,則可以挪動
​                        if (ChessBoard.Matrix == 0)
​                        {
​                            r.Add(new Point(FixedMetaPosition.X + 2, FixedMetaPosition.Y + 1));
​                        }
​                        else
​                        {
​                            //固然方针處有棋子,但该棋子是對方的,也能够挪動(此時現實為吃子)
​                            var piece = ChessBoard.GetPieceFromMetaPos(FixedMetaPosition.X + 2, FixedMetaPosition.Y + 1);
​                            if (piece.Count > 0 && piece.Team != this.Team)
​                              r.Add(new Point(FixedMetaPosition.X + 2, FixedMetaPosition.Y + 1));
​                        }
​                  }
​                }
“象”的走棋法則俗称“象飞田”,即只能從原位置走到“田”字格的對角,且不克不及過河。

图11

图11中展現了“象”的所有走棋法則,理論上“象”至多應當有4個位置可以挪動,即ABCD四個位置。A點因為被E點的黑“車”“蹩象腰”,以是不克不及挪動到A位置;B點是敌方的棋子,可以挪動到此并吃掉他;C點因為没有任何棋子,以是可以挪動到此;D點因為有己方的棋子,以是不克不及挪動到此。

下面以A點為例举行阐明。

已知A點的索引坐標為(FixedMetaPosition.X - 2, Fi治療牛皮癬,xedMetaPosition.Y - 2),象腰位置為(FixedMetaPosition.X - 1, FixedMetaPosition.Y - 1)。代码以下:

​                //左上角预览
​                if (FixedMetaPosition.X - 2 >= 0 && FixedMetaPosition.Y - 2 >= 0)
​                {
​                  //没有蹩相腰
​                  if (ChessBoard.Matrix == 0)
​                  {
​                        var ps = ChessBoard.GetPieceFromMetaPos(FixedMetaPosition.X - 2, FixedMetaPosition.Y - 2);
​                        //方针位置没有棋子;或虽有棋子,但该旗子不是己方的
​                        if (ps.Count == 0 || ps.Team != Team)
​                        {
​                            //相不克不及過河
​                            if ((Side == SIDE.UP && FixedMetaPosition.Y - 2 < 5) || (Side == SIDE.DOWN && FixedMetaPosition.Y - 2 >= 5))
​                            {
​                              r.Add(new Point(FixedMetaPosition.X - 2, FixedMetaPosition.Y - 2));
​                            }
​                        }
​                  }
​                }
“士”位於九宫格内,只能沿着九宫格内的斜線挪動,且不克不及挪動到九宫非分特别面。

图12

图12中展現了“士”的所有走棋法則,理論上“士”至多應當有4個位置可以挪動,即ABCD四個位置。AB两點因為没有任何棋子,以是可以挪動到此;C點是敌方的棋子,可以挪動到此并吃掉他;D點因為有己方的棋子,以是不克不及挪動到此。

下面以A點為例举行阐明。已知A點的索引坐標為(FixedMetaPosition.X - 1, FixedMetaPosition.Y - 1),代码以下:

​                //左上角可挪動位置预览
​                if (FixedMetaPosition.X - 1 >= 3)
​                {
​                  if ((Side == SIDE.UP && FixedMetaPosition.Y - 1 >= 0) || (Side == SIDE.DOWN && FixedMetaPosition.Y - 1 >= 7))
​                  {
​                        var p = ChessBoard.GetPieceFromMetaPos(FixedMetaPosition.X - 1, FixedMetaPosition.Y - 1);
​                        //方针位置若没有棋子,或有對方棋子
​                        if (p.Count == 0 || p.Team != Team)
​                        {
​                            r.Add(new Point(FixedMetaPosition.X - 1, FixedMetaPosition.Y - 1));
​                        }
​                  }
​                }
“帅”也位於九宫格内,可以沿着横向或纵向挪動一格,且不克不及挪動到九宫非分特别面。

图13

图13中展現了“帅”的所有走棋法則,理論上“帅”至多應當有4個位置可以挪動,即ABCD四個位置。AB两點因為没有任何棋子,以是可以挪動到此;C點是敌方的棋子,可以挪動到此并吃掉他;D點因為有己方的棋子,以是不克不及挪動到此。

“炮”的走棋法則是只能沿着横向或纵向走直線,只有在吃子的环境下才能超出其他一個棋子。

图14

图14中,“炮”在當前状况下,可以挪動到的位置為绿色圆圈處,可以别离依照“炮”的上方、下方、左邊、右邊四個標的目的举行實現。

咱们以该棋子上方的可挪動位置為例举行阐明,基来源根基理是:

假如當前“炮”的纵坐標為Y0,寻觅该棋子上方所有Matrix對應元素為1的位置,假如有n個,并记入调集piecesAbove。

若是n=0,阐明“炮”的上方没有任何棋子,這些位置都是可以挪動到的位置;

若是n=1,阐明上方只有1個棋子,不管這個棋子是己方仍是敌方,piecesAbove處如下、“炮”以上的位置都是可以挪動到的位置;

若干n>1,阐明上方最少有2個棋子,此時,piecesAbove一下、“炮”以上的位置都是可以挪動到的位置;然後再看piecesAbove處的棋子是己方仍是敌方,如是敌方的棋子則可以挪動到此并吃掉。

代码以下:

​      //上方可以挪動的位置预览
​      private List GetAboveSteps()
​      {
​            List r = new List();
​            //當前棋子(“炮”)上方所有位置上存在棋子的位置调集
​            List piecesAbove = new List();
​            for (int i = FixedMetaPosition.Y; i > 0; i--)
​            {
​                if (ChessBoard.Matrix == 1)
​                  piecesAbove.Add(new Point(FixedMetaPosition.X, i - 1));
​            }
​            switch (piecesAbove.Count)
​            {
​                case 0:
​                  for (int i = 0; i < FixedMetaPosition.Y; i++)
​                  {
​                        r.Add(new Point(FixedMetaPosition.X, i));
​                  }
​                  break;
​                case 1:
​                  for (int i = piecesAbove.Y + 1; i < FixedMetaPosition.Y; i++)
​                  {
​                        r.Add(new Point(FixedMetaPosition.X, i));
​                  }
​                  break;
​                default:
​                  for (int i = piecesAbove.Y + 1; i < FixedMetaPosition.Y; i++)
​                  {
​                        r.Add(new Point(FixedMetaPosition.X, i));
​                  }
​                  var p = ChessBoard.GetPieceFromMetaPos(piecesAbove.X, piecesAbove.Y);
​                  if (p.Count > 0 && p.Team != Team)
​                  {
​                        r.Add(piecesAbove);
​                  }
​                  break;
​            }
​            return r;
​      }
“炮”類Cannon的NextSteps的总體代码以下:

​      public override List NextSteps
​      {
​            get
​            {
​                List r = new List();
​                r.AddRange(GetAboveSteps());
​                r.AddRange(GetBelowSteps());
​                r.AddRange(GetLeftSteps());
​                r.AddRange(GetRightSteps());
​      邱大睿,      return r;
​            }
​      }
“卒”在過河前只能沿着進步標的目的一步一格,不克不及向左、向右、向後挪動(图15);過河後則可以向左、向右挪動(图16),但仍是不克不及撤退退却。

起首果断當前“卒”有無過河,“卒”過河後可以向摆布挪動。怎样果断有無過河呢?若是當前“卒”是棋盘上面一方的,則若其Y坐標大於即是5,就阐明已颠末河;若是是棋盘下面一方的,則若其Y坐標小於5,就阐明已颠末河。

图15

图16

详细代码以下:

public override List NextSteps
​      {
​            get
​            {
​                List r = new List();
​                //只有在兵卒過河今後,才可以摆布挪動
​                if ((Side == SIDE.UP && FixedMetaPosition.Y >= 5) || (Side == SIDE.DOWN && FixedMetaPosition.Y < 5))
​                {
​                  //左邊可挪動位置预览
​                  if (this.FixedMetaPosition.X - 1 >= 0)
​                  {
​                        var p = ChessBoard.GetPieceFromMetaPos(FixedMetaPosition.X - 1, FixedMetaPosition.Y);
​                        //方针位置若没有棋子,或有對方棋子
​                        if (p.Count == 0 || p.Team != Team)
​                        {
​                            r.Add(new Point(FixedMetaPosition.X - 1, FixedMetaPosition.Y));
​                        }
​                  }
​                  //右邊可挪動位置预览
​                  if (this.FixedMetaPosition.X + 1 < ChessBoard.BOARD_META_WIDTH)
​                  {
​                        var p = ChessBoard.GetPieceFromMetaPos(FixedMetaPosition.X + 1, FixedMetaPosition.Y);
​                        //方针位置若没有棋子,或有對方棋子
​                        if (p.Count == 0 || p.Team != Team)
​                        {
​                            r.Add(new Point(FixedMetaPosition.X + 1, FixedMetaPosition.Y));
​                        }
​                  }
​                }
​                //進步標的目的可挪動位置预览
​                switch (Side)
​                {
​                  case SIDE.UP:
​                        if (this.FixedMetaPosition.Y + 1 < ChessBoard.BOARD_META_HEIGHT)
​                        {
​                            var p = ChessBoard.GetPieceFromMetaPos(FixedMetaPosition.X, FixedMetaPosition.Y + 1); ;
​                            //方针位置若没有棋子,或有對方棋子
​                            if (p.Count == 0 || p.Team != Team)
​                            {
​                              r.Add(new Point(FixedMetaPosition.X, FixedMetaPosition.Y + 1));
​                            }
​                        }
​                        break;
​                  case SIDE.DOWN:
​                        if (this.FixedMetaPosition.Y - 生髮,1 >= 0)
​                        {
​                            var p = ChessBoard.GetPieceFromMetaPos(FixedMetaPosition.X, FixedMetaPosition.Y - 1);
​                            //方针位置若没有棋子,或有對方棋子
​                            if (p.Count == 0 || p.Team != Team)
​                            {
​                              r.Add(new Point(FixedMetaPosition.X, FixedMetaPosition.Y - 1));
​                            }
​                        }
​                        break;
​                }
​                return r;
​            }
​      }
經由過程上面一節的論述,咱们把棋子類ChessPiece的7個子類有關NextSteps属性的详细實現完成為了。那末咱们费這麼大劲實現NextSteps抽象属性,目標是甚麼呢?前邊咱们在讲ChessPiece類的MoveTo法子的時辰說過,要想把棋子挪動到某個指定位置,先必要果断當前棋子能不克不及挪動到该指定的位置,能的话就把當前棋子的FixedMetaPosition属性设置成鼠標地點的位置。這就必要给ChessPiece類添加一個法子CanMoveTo,實現以下:

​      ///
​      /// 果断當前棋子是不是可以或许挪動到方针位置
​      ///
​      /// 方针位置
​      ///
​      public bool CanMoveTo(Point metaPt)
​      {
​            return NextSteps.Contains(metaPt);
​      }
看到了吧?NextSteps属性的用處本来在這里,只有當前棋子的NextSteps调集中包括方针位置,則阐明可以挪動到该位置。

好了,下面咱们就继续完成ChessPiece的MoveTo法子。大师想一想,把當前棋子挪動到指定位置,會有几種成果?也就是說,MoveTo法子的返回值有哪些可能?咱们用一個罗列類型来阐明,以下列代码:

​    ///
​    /// 走棋的几種成果
​    ///
​    public enum MOVE_RESULT
​    {
​      NULL,//默许状况
​      ENEMY_TURN,//應當敌方走棋
​      CHECK_ENEMY,//對敌方将军
​      CHECK_SELF,//己方被将军
​      CAN_NOT_MOVE,//不克不及挪動
​      MING_JIANG//明将
​    }
以是咱们把MoveTo法子的返回值類型界說為MOVE_RESULT罗列類型,以下:

public MOVE_RESULT MoveTo(Point pixelPos)
{
}
细說MoveTo法子以前,咱们必要弥補一點,给ChessBoard類添加一個bool類型的静态字段PlayToken,用作玩家的走棋次序標識表记標帜,也就是說,用它来標識表记標帜當前该红黑哪一方走棋,并设定:當PlayToken為true時红方走,false時黑方走。每當一方走一步棋,都要對PlayToken取反,以更新该標識表记標帜,以下代码:

​      ///
​      /// 更新走棋属性標識表记標帜
​      ///
​      public static void UpdatePlayToken()
​      {
​            PlayToken = !PlayToken;
​      }
在MoveTo法子中,先挪用CanMoveTo法子,從而得出可否挪動到该指定位置。若是不克不及挪動,則理當把當前棋子恢回复复兴位,并返回MOVE_RESULT.CAN_NOT_MOVE;若是可以或许挪動,咱们必要做的有如下几點:

1) 更新玩家走棋標識表记標帜

這個很简略,就是把PlayToken取反便可,以下:

​      ///
​      /// 更新走棋属性標識表记標帜
​      ///
​      public static void UpdatePlayToken()
​      {
​            PlayToken = !PlayToken;
​      }
2) 吃對方棋子

分以下几個步调:

a) 果断方针位置處有無棋子;

b) 若是方针位置没有棋子,則设置eatenFlag為false;這個eatenFlag是甚麼呢?他是一個bool類型的變量,代表當前棋子近来一次挪動有無吃敌方的棋子,主如果用在實現悔棋操作。

c) 若是方针位置唯一一個棋子,且该棋子是當前棋子本身,此時,也應當设置eatenTag為false,由於本身不克不及吃本身。

d) 若是方针位置有棋子,但方针位置的棋子是己方棋子,也不克不及吃。

e) 若是方针位置是敌方棋子,則先保留该棋子,然後吃掉。

f) 设置eatenFlag為ture。

3) 當放下當前棋子時,更新棋盘矩阵對應位置的值

​      ///
​      /// 當放下當前棋子時,更新棋盘矩阵對應位置的值
​      ///
​      public void UpdateMatrix()
​      {
​            ChessBoard.Matrix = 1;
​            if (FloatMetaPosition != FixedMetaPosition)
​                ChessBoard.Matrix = 0;
​      }
4) 使该棋子的汗青轨迹進栈

咱们必要给棋子類添加一個属性HistorySteps,這是一個仓库類型,用於记實该棋子的汗青位置轨迹,界說以下。

///
​      /// 用仓库记實下该棋子的汗青位置轨迹
​      ///
​      public Stack HistorySteps { set; get; }
有了這個属性後,經由過程压栈,来保留近来一次的汗青位置,即:

HistorySteps.Push(FixedMetaPosition);
5) 保留旧位置

OldMetaPosition = FixedMetaPosition;
6) 當前棋子挪動到新位置後,更新FixedMetaPosition

FixedMetaPosition = FloatMetaPosition;
7) 保留當前棋局中所有走過的棋子记實

起首咱们必要在棋盘類ChessBoard

​      ///
​      /// 當前棋局中所有走過的棋子记實
​      ///
​      public static Stack HistoryStack
​      {
​            get
​            {
​                return _HistoryStack;
​            }
​            set
​            {
​                _HistoryStack = value;
​            }
​      }
8) 果断是不是将军

9) 果断是不是明将

到今朝為止,咱们只是在算法层面實現了分歧棋子的走棋法則,但尚未實現用鼠標拖動棋子的功效呢!也就是說,咱们今朝只是完成為了走棋的逻辑,但尚未實現界面操作。正所谓“磨刀不误砍柴工”,有了上面這些铺垫,完成界面逻辑即是瓜熟蒂落的事變了。

在本课程剛起頭的時辰,咱们說過,之以是用GDI+来画棋子,而不消Button或PictureBox控件来作為棋子。如许做的益處就是界面衬着效力高、不會產生卡顿征象;错误谬误就是鼠標事務在實現上不太直接,由於控件都有現成的鼠標事務,而GDI+画图則必要用間接的法子来實現鼠標事務了。

因為棋子是用GDI+画出来的,那末怎样實現鼠標按下、拖動、松開的事務呢?基来源根基理就是,經由過程果断當前鼠標的位置是不是位於某個棋子的内部,是的话,此時若是按下鼠標拖動,咱们的步伐就應當相應该事務。

那末怎样果断當前鼠標的位置是不是位於某個棋子的内部呢?法子也很简略,就是计较棋子中間點和當前鼠標地點位置處之間的間隔,若是该間隔小於或即是棋子的半径,則阐明鼠標位於棋子的内部,反之則在外部。

好了,有了上面的根本,在步伐主界面中,當在某個棋子内部按下鼠標時,應當显示该棋子可以挪動到的所有位置的预览,也就是雷同图9中的那些绿色圆圈。此外,當鼠標在某個棋子内部按下的時辰,必要在该棋子外围画一個赤色的圆圈,以表白這個棋子是玩家選中的棋子。

當鼠標按下某個棋子并拖動的時辰,该棋子應當跟從鼠標挪動;鼠標松開後,在合适走棋法則的条件下,该棋子應當落到當前鼠標地點的位置。若是该位置處有敌方棋子,則吃掉它。有了之前的筹备,實現這些事務就很简略了,只必要填充MouseDown、MouseMove、MouseUp這些事務相應便可以了。

咱们晓得,中國象棋遊戲中,應當是红黑两邊轮番走棋,即你走一步、我走一步,不克不及一方持续走棋。是以,咱们在棋盘類ChessBoard中界說一個bool類型的属性PlayToken,當應當红方走棋時,PlayToken為true;黑方走棋時,PlayToken為false。每當有一方走棋後,都要對PlayToken的值取反,以動态跟踪當前走棋次序。

當玩家走棋後懊悔了,想退回到走棋前的状况,咱们的步伐供给悔棋功效。

假如红方棋子“炮”從A點挪動到了B點,吃掉了本来在B點的黑方的“马”。此時,要想實現悔棋功效,大致分两步走:一是把红方“炮”的位置從B點改變成本来的A點,二是規复B點本来被吃掉的黑方的“马”。固然,棋盘矩阵Matrix對應位置處的元素也要随着更新。

要想實現悔棋,這就必要用一個仓库變量来记實某個棋子走過的位置、吃掉的棋子等信息。當必要悔棋時,只需挨次弹出仓库栈顶的元素,便可获得该棋子走棋前的位置、吃子等信息,從而举行規复。

這個功效是咱们項目標一個特點功效,就是當敌方走棋後,體系會主動提醒己方下一步有哪些着法可以用,在主界面右邊以列表的情势显現给玩家。

這個功效另有此外一個感化,就是检测死局,即若是敌方走棋後,發明己方已無路可走,則阐明當前已經是死局,己方输。

码字不容易,若是你喜好,别忘了點個赞成、加個存眷,博主後续會延续更新雷同文章和開源項目,感谢!
頁: [1]
查看完整版本: 中國象棋遊戲開發實战(C#)