C#实现五子棋游戏

时间:2019-04-13
本文章向大家介绍C#实现五子棋游戏,主要包括C#实现五子棋游戏使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

曾经自学C#做计算机图形学的作业,GDI+画图确实好用,目前在找.NET的实习,尝试做了一个最基本的五子棋,复习一下C#的基本语法,目前只能当跟基友一起玩的单机小游戏,之后再加入AI和联网对战功能。目前我还是小菜鸟,过程设计和代码有不合理或者能优化的地方欢迎各位大神指正。

首先是界面设计,最后就是这样,控件一共有一个PictureBox;一个开始Button,命名为btnStart;一个重置Button,命名为btnReset;一个Label,用于显示游戏状态。

五子棋界面

然后是各基本类。新建一个MainSize类用于存放界面上的可能用到的参数,主框体大小520*460,棋盘是一个PictureBox控件,大小401*401,棋盘20行20列,每个格子边长20,棋子直径16。新建一个ChessBoard类表示棋盘,有一个静态函数DrawBoard。之前做计算机图形学作业画函数时用到的画坐标系方法在这里同样适用,函数代码如下。

class ChessBoard
  {

    static readonly Color color = Color.Black;
    static readonly float penWid = 1.0f;
    static readonly Pen pen = new Pen(color, penWid);    

    public static void DrawCB(Graphics gra,PictureBox pic)
    {
      //每排数量
      int horC = MainSize.CBWid / MainSize.CBGap;
      //间隔
      int gap = MainSize.CBGap;
      Image img = new Bitmap(MainSize.CBWid, MainSize.CBHei);
      gra = Graphics.FromImage(img);
      gra.Clear(Color.White);
      gra.DrawRectangle(pen, 0, 0, MainSize.CBWid, MainSize.CBHei);
      //画棋盘
      for (int i = 0; i < horC; i++)
      {
        gra.DrawLine(pen, 0, i * gap, MainSize.CBWid, i * gap);
        gra.DrawLine(pen, i * gap, 0, i * gap, MainSize.CBHei);
      }
      gra.DrawLine(pen, 0, horC * gap, MainSize.CBWid, horC * gap - 1);
      gra.DrawLine(pen, horC * gap - 1, 0, horC * gap, MainSize.CBHei);
      pic.Image = img;
    }

  }

还有一个基本类Chess,用来表示棋子,有一个静态函数DrawChess,代码如下。这里代码有点乱,写的时候没加注释。bool型变量用于表示下棋的双方,pen1和pen2用于绘制双方的棋子,颜色设置为红蓝,因为自古红蓝出CP【其实是本来想用黑白的但picturebox背景白色再画白色棋子就看不出来】。整型变量nexX和newY用于表示棋子在棋盘上的坐标,根据四舍五入就近原则落点。这里的四舍五入我花了很长时间,写了很长的一段判断代码但都会出错,最后借鉴了github上Xu Pu同学的数据结构假期作业中的方法才完成了这个函数,在此感谢这位同学~

class Chess
  {
    public static void DrawChess(bool type,PictureBox pic,Graphics graphic,MouseEventArgs e)
    {
      graphic = pic.CreateGraphics();
      Pen pen1 = new Pen(Color.Red, 1);
      Brush bru1 = new SolidBrush(Color.Red);
      Pen pen2 = new Pen(Color.Blue, 1);
      Brush bru2 = new SolidBrush(Color.Blue);
      int newX = (int)((e.X + MainSize.CBGap / 2) / MainSize.CBGap) * MainSize.CBGap - MainSize.ChessRadious / 2;
      int newY = (int)((e.Y + MainSize.CBGap / 2) / MainSize.CBGap) * MainSize.CBGap - MainSize.ChessRadious / 2;
      if (type)
      {
        graphic.DrawEllipse(pen1, newX, newY, MainSize.ChessRadious, MainSize.ChessRadious);
        graphic.FillEllipse(bru1, newX, newY, MainSize.ChessRadious, MainSize.ChessRadious);
      }
      if (!type)
      {
        graphic.DrawEllipse(pen2, newX, newY, MainSize.ChessRadious, MainSize.ChessRadious);
        graphic.FillEllipse(bru2, newX, newY, MainSize.ChessRadious, MainSize.ChessRadious);
      }
      graphic.Dispose();
    }

  }

最后是主程序,一共设置了四个全局变量,Graphics graphic用于画图,bool type用于表示下棋双方,bool start表示游戏是否开始,二维数组ChessBack用于模拟下棋场景并进行计算。

主程序的构造函数对主框体和PictureBox的大小进行初始化,在Form1_Load函数中添加函数InitializeThis()对游戏进行初始化,包括将ChessBack数组全部置0,type设为true,start设为false,绘制棋盘,按键开始的enabled属性设为true,按键重置设为false。

按键开始和重置的功能较为简单,代码如下。

private void btnStart_Click(object sender, EventArgs e)
    {
      start = true;
      label1.Text = "游戏开始!";
      btnStart.Enabled = false;
      btnReset.Enabled = true;
    }

    private void btnReset_Click(object sender, EventArgs e)
    {

      if (MessageBox.Show("确定要重新开始?", "提示", MessageBoxButtons.YesNo) == DialogResult.Yes)
      {              
        InitializeThis();
      }
    }

最重要的部分是点击picturebox的函数,先判断游戏是否开始,否则不会有反应。游戏开始后点击即可落子,并修改ChessBack矩阵,红色为1,蓝色为2,如果已经有棋子则返回,即落子失败。如果棋盘已满但没有分出胜负则弹出平局的提示框并给出提示。之后判断是否分出胜负,添加函数bool Victory(int bx,int by),分出胜负后提示胜利,如果没有则返回。最后换人,type=!type即可,然后修改label的文字表面到哪一方落子了。代码如下。

private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
    {
      if (start)
      {
        //在计算矩阵中的位置
        int bX = (int)((e.X + MainSize.CBGap / 2) / MainSize.CBGap);
        int bY = (int)((e.Y + MainSize.CBGap / 2) / MainSize.CBGap);
        //防止在同一个位置落子
        if (ChessBack[bX, bY] != 0)
          return;
        Chess.DrawChess(type, pictureBox1, graphic, e);
        ChessBack[bX,bY] = type?1:2;
        //判断棋盘是否满了

        if (IsFull() && !Victory(bX,bY))
        {
          if (MessageBox.Show("游戏结束,平局") == DialogResult.OK)
            InitializeThis();
          return;
        }
        //判断胜利
        if (Victory(bX,bY))
        {
          string Vic = type ? "红" : "蓝";
          if (MessageBox.Show(Vic + "方胜利!") == DialogResult.OK)
            InitializeThis();
          return; 
        }

        //换人
        type = !type;
        label1.Text = type ? "红方's trun!" : "蓝方's turn!";
      }
      else
        return;
    }

判断胜负的函数有点复杂,我自己是用递归的方式判断,先写了一个横向的进行测试,如果横向两端的值与当前值相同则变量count++,最后返回count的值,如果>4则表示胜利。但是这个函数运行出错,显示为stackoverflow,但我不知道错误在哪,只好换一种判断方法。后来才想明白两端的值都是0则会溢出,应该判断两端的值是否为1或2而不是当前值。此处借鉴了实验楼网站上的C语言版五子棋的判断胜负方式,从当前落子的矩阵中,横竖斜4个方向任意一个方向有连续5个数的值与当前的值相同则胜利,实现也不复杂,细分为三个函数实现。代码如下。

#region 判断胜利
    private bool Victory(int bx,int by)
    {
      if (HorVic(bx, by))
        return true;
      if (VerVic(bx, by))
        return true;
      if (Vic45(bx, by))
        return true;
      else
        return false;
    }

    private bool Vic45(int bx, int by)
    {

      int b1 = (bx - 4) > 0 ? bx - 4 : 0;
      int b2 = (by - 4) > 0 ? by - 4 : 0;
      //int buttom = b1 > b2 ? b2 : b1;
      int val = ChessBack[bx, by];
      for (int i = b1,j=b2; i < 16&&j<16; i++,j++)
      {
        if (ChessBack[i, j] == val && ChessBack[i + 1, j + 1] == val &&
          ChessBack[i + 2, j + 2] == val && ChessBack[i + 3, j + 3] == val
          && ChessBack[i + 4, j + 4] == val)
          return true;
      }
      for (int i = b1, j = b2; i < 16 && j < 16; i++, j++)
      {
        if (ChessBack[i, j] == val && ChessBack[i + 1, j - 1] == val &&
          ChessBack[i + 2, j - 2] == val && ChessBack[i + 3, j - 3] == val
          && ChessBack[i - 4, j - 4] == val)
          return true;
      }
      return false;
    }

    private bool VerVic(int bx, int by)
    {
      int buttom = (by - 4) > 0 ? by - 4 : 0;
      int val = ChessBack[bx, by];
      for (int i = buttom; i < 16; i++)
      {
        if (ChessBack[bx, i] == val && ChessBack[bx, i+1] == val &&
          ChessBack[bx, i+2] == val && ChessBack[bx ,i+3] == val
          && ChessBack[bx, i+4] == val)
          return true;
      }
      return false;
    }

    private bool HorVic(int bx, int by)
    {
      int left = (bx-4)>0?bx-4:0;
      int val = ChessBack[bx,by];
      for (int i = left; i < 16; i++)
      {
        if (ChessBack[i, by] == val && ChessBack[i + 1, by] == val &&
          ChessBack[i + 2, by] == val && ChessBack[i + 3, by] == val
          && ChessBack[i + 4, by] == val)
          return true;
      }
      return false;
    }
    #endregion

完成后进行测试,都没有问题,即认为大功告成了。总结了一下编写过程中问题,变量命名不太好,type,start等变量容易与关键词混淆;主函数代码行数还是太多,不方便阅读,或许应该把判定胜负和判定棋盘是否已满也移到棋盘类中;之后添加新游戏模式不方便,比如添加AI和联机对战,需要修改的代码有点多,个人想法是分别新建一个带AI的框体和联机的框体,然后修改基本类,在这种情况下最大化的代码复用。

回想一个这个小程序编写并不复杂,但我花了很多时间在改错上,落子的函数和判断胜利的函数花的时间最多,事前用笔进行一下简单的演算再写或许能省一点时间。这是我的第一篇博客,暂时当实验报告来写吧,虽然是出于兴趣做的,但还是希望各位大神能指出不足,给出建议,我会虚心学习的。最后再次感谢实验楼网站【不是软广】和github上的Xu Pu同学,还有各位看到最后的朋友们~