【C#】手把手系列|实现五子棋对战(第一章)

本人(ID:蒸发杰作)旗下所有文章均放弃版权,请任意使用。只是如果您觉得,看了我的文章,有所收获的话,不妨点个赞,写个评论。这是对我最大的支持。

开博客的时间也不短了。同样的,学习编程的时间也不短了。但相比于程序编写的进步,博客撰写的速度却还有些落后。这里开启一个新的专题,内容分为三步走,首先实现五子棋(第一章到第二章),进而实现电脑下五子棋(第三章到第四章),最后带你领略一下其他编程高手的五子棋作品。(第五章)

既然说了是手把手系列。所以在前景引入,材料准备上我都会为您先准备好。但读者的自主思考仍旧是必要的。编程的本质就是解决实际问题罢了,一个问题总会有很多解决方法,欢迎大家提出同一个问题的更好解决方法。

本系列适合于新手按部就班照猫画虎的学习,也适合老手从中取用所需要的章节。

使用工具:C#&&VS2017以及PS。

第一章

1.素材准备

说到五子棋,大家肯定都不陌生。五黑或五白连成一条线,相应方就胜利。

在C#实现五子棋界面的方法有两种,第一种种是不使用底图:使用C#自带的绘图工具绘制底图。这样绘制出来的五子棋的如下

这里写图片描述

因为没有底色,所以机器感很明显。

第二种方式就是先导入一张底图,然后在底图上添加新的图片。素材可以从网络上找,只要是方方正正的棋盘就可以了。当然也可以用PS来自己绘制。例如我从网上找到了的如下棋盘:

和如下包含棋子的图片:

在PS中截取出需要的黑棋和白棋,导出为.PNG格式(没有白色背景的格式)。

然后就可以开始工作了。

大家可以从我的网盘中下载素材包。

链接:https://pan.baidu.com/s/1osZuH2TSojmDaZcO18fhUg 提取码:vg4a

2.素材导入

VS早就提供了导入素材的支持,导入后的素材可以像是内部变量一样使用。但很多人导入素材的方式还和过去一样原始,不得不感慨,教材没有讲到的东西,会的人真是要少一大截呢。

新建C#的winform程序后,在任务管理器的Properties(英文:所有物)之下,双击Resources.resx(资源文件)

进入如右所视界面。点击添加资源右边的下三角,打开从现有文件添加,依次添加对应资源就OK了。

3.将棋盘显示到屏幕上

在winform的From1,从工具箱将picturebox控件拖动一个到Form1设计板块上。为了能显示任意大小的图片,在属性中将picturebox的大小调整成自动大小。(不会请百度,时刻记得:面向搜索编程)

然后在form的初始化函数(Form1_Load)中写入picturebox1.image=Properties.Resources.maps;(即Properties命名空间下的资源类的图片变量:maps)

一幅地图便会显示在桌面上。这就是目前的界面,后期可以将form1设置成长度固定,然后把灰白部分用“木色”填充。在加上双方交手的计分和时间等,一个五子棋游戏就这样被做好。

4.获取位置信息

如我们前面所言,我们要图像上绘制图像,那么应该怎么做呢?首先得要获取得到棋盘可以落子的点的位置。然后在落子点位置再生成一个小的picturebox对象。picturebox.image再设置成要么=黑子,要么=白子。

那么像素的位置是不可或缺的。为了获得像素,为该picturebox添加添加一个鼠标移动事件,并再拖一个textbox到界面中来,显示坐标的位置。

效果如图所示:只要光标划过就可以显示出对应光标位置的X,Y(相对于picturebox这个容器,而不是相对于Form这个容器的距离)

量测后得到:(以左上角为(0,0),向右为x轴正方向,向下为Y轴正方向)

左上角位置:(27,27)    右上角:(458,27) 左下角(27,458)

每行,每列十九个落子点,十八个格子。  (458-27)/18=24

则每个小正方形为24*24 (像素)

5.结构设计

至此我们对五子棋设计已经有了个大致的了解。下面进行编程的结构设计。

程序任务:实现一款可以单机进行双人对局的五子棋。

~题外话:首先界面逻辑和业务逻辑一定要分离。这样即便更换一个显示平台,更换一个全新的界面,也可以飞速完成。

——类型抽象:纵观游戏,有如下事物:棋子,棋盘,时间,棋局本身,棋手,游戏规则。棋子直接用图像代替,不设立新类。棋盘同理。时间暂时不考虑,属于锦上添花的功能,后续加入很方便。棋局本身,记录棋局是否结束(如果结束胜利者是谁),当前棋局的位置信息的类。棋手,可落子,可在每次落子之后判断是否胜利。规则类,如果将之抽象为了一个类的话,那么可以很便捷的通过修改规则类来修改游戏规则,例如要把五子棋换成围棋,或者其他棋类游戏。只需要修改规则类就好。落子和判断胜利的规则会随着规则改变而改变。

这儿为了简化,暂时只采用一个下棋类涵盖上述所有类型。

下棋类中将包括:


棋局胜者(包含三个状态:None,Black,White)

记录棋子位置和颜色的棋谱二维表Maps[19,19]。(其中每个位置都存储一个None,Black,White,读取时显示为对应图片,none无图片)

落子函数(在内部棋谱二维表中落子,每次落子后调用结算函数结算)

结算函数(结算是否有胜利者)


结构很简单,照着实现就好。中间有需要再做微调。

6.业务逻辑实现

准备已经很周全,可以开始最后也是最重要的工作,开始编程。

将先前设计好的结构摆在自己面前,照着实现。

首先先实现一个枚举:

/// <summary>
/// 棋子类别的枚举
/// </summary>
public enum Kind { None, Black, White }

然后新建一个Chess类,向其中添加上面过程中我们提到的变量和函数:

        public static int lenth = 19;
        public Kind Winner = Kind.None;

        /// <summary>
        /// 存储棋子信息的二维表
        /// </summary>
        public Kind[,] Maps = new Kind[lenth, lenth];

        /// <summary>
        /// 落子函数(落子类型,位置)
        /// </summary>
        public void DoLuoZi(Kind kind0, int i, int j)
        {
            //于地图上改写
            Maps[i, j] = kind0;
            JieSuan(kind0, i, j);
        }

以及最重要的检查游戏是否结束的函数Jiesuan(结算):

        /// <summary>
        /// 结算胜利的函数
        /// </summary>
        private void JieSuan(Kind kind0, int row, int col)
        {

            //不要想什么屏幕位置,行就是行,列就是列
            //于落子位置结算横,竖,斜,四个位置4-1-4的长度的连子情况。
            //检查同一横(变列)
            int n = 0;//计数变量,记录最多几个为kind0类

            for (int j = col - 4; j <= col + 4; j++)
            {
                //如果超过索引则跳过
                if (j < 0 || j >= 19)
                    continue;
                //否则检查连子情况
                if (Maps[row, j] == kind0)
                {
                    n++;
                }
                else
                {
                    n = 0;
                }
                if (n == 5) Winner = kind0;
            }

            //检查同一竖排(变行)
            n = 0;
            for (int i = row - 4; i <= row + 4; i++)
            {
                //如果超过索引则跳过
                if (i < 0 || i >= 19)
                    continue;
                if (Maps[i, col] == kind0)
                {
                    n++;
                }
                else
                {
                    n = 0;
                }
                if (n == 5) Winner = kind0;

            }

            //检查左上到右下斜
            n = 0;
            for (int i = row - 4, j = col - 4; i <= row + 4; i++, j++)
            {
                //如果超过索引则跳过
                if (i < 0 || i >= 19 || j < 0 || j >= 19)
                    continue;
                if (Maps[i, j] == kind0)
                {
                    n++;
                }
                else
                {
                    n = 0;
                }
                if (n == 5) Winner = kind0;
            }

            //检查左下到右上
            //检查左上到右下斜
            n = 0;
            for (int i = row + 4, j = col - 4; i >= row - 4; i--, j++)
            {
                //如果超过索引则跳过
                if (i < 0 || i >= 19 || j < 0 || j >= 19)
                    continue;
                if (Maps[i, j] == kind0)
                {
                    n++;
                }
                else
                {
                    n = 0;
                }
                if (n == 5) Winner = kind0;
            }
        }

#思考题:本代码中写了四个方向的判断,0度,45度,90度,135度,把重复的工作做了四遍,能否把他们用一个循环替代呢?

6.界面逻辑和实现

业务逻辑的实现靠其内在逻辑的抽象,页面的实现则更多是对对业务逻辑函数的调用。

1.每开一个棋局就新建一个棋局类。

2.落子均采用LuoZi函数。

3.显示通过在Picturebox.Controls.Add()中添加新的picturebox实现(这点请百度)绘图

4.比较重要的是落子不是一定要点到正好的位置上,而是大致在这一范围就可以了。

5.设置一个当前棋手(用上前边的kind枚举),每下完一个棋子之后就交换当前棋手。

比较重要的是第四点,这儿不详细诉说,你可以查看我下边的界面代码:

namespace 五子棋
{
    public partial class Form1 : Form
    {
        Chess chess = new Chess();
        Kind nowKind = Kind.Black;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            pictureBox1.Image = Properties.Resources.maps;
        }


        //27,27 左上角 右下角 458 458 长度24*24,location是相对于控件的位置。十九个落子点,18个长度
        //点击事件,存储落子信息,落子
        private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
        {
            //如果落在屏幕外围
            if (e.X < 23 || e.X > 462 || e.Y < 23 || e.Y > 462)
            {
                return;
            }
            // 量化
            int j;
            int i;
            //若果余数小于6则向下取整,如果大于18则向上取整,中间则无效。
            if ((e.X - 27) % 24 < 6)
                j = (e.X - 27) / 24;
            else if ((e.X - 27) % 24 > 18)
                j = (e.X - 27) / 24 + 1;
            else return;

            if ((e.Y - 27) % 24 < 6)
                i = (e.Y - 27) / 24;
            else if ((e.Y - 27) % 24 > 18)
                i = (e.Y - 27) / 24 + 1;
            else return;

            //落子
            chess.DoLuoZi(nowKind, i, j);

            //于对应位置绘制图形
            PictureBox pictureBox = new PictureBox();
            pictureBox.SizeMode = PictureBoxSizeMode.AutoSize;
            pictureBox.BackColor = Color.Transparent;
            pictureBox.Location = new Point((int)(27 + 24 * j - 7.5), (int)(27 + 24 * i - 7.5));
            if (nowKind == Kind.Black)
            {
                pictureBox.Image = Properties.Resources.Black;
                nowKind = Kind.White;
            }
            else
            {
                pictureBox.Image = Properties.Resources.White;
                nowKind = Kind.Black;
            }
            pictureBox1.Controls.Add(pictureBox);
          
            if (chess.Winner != Kind.None)
            {
                MessageBox.Show($"{chess.Winner}WIN!");

            }
        }

        private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            textBox1.Text = $"e.x={e.X},e.y={e.Y}";
        }
    }
}

7.结果

你可以将textbox删除。也可自己添加想要的功能,如撤销(添加一个存储所有下棋历史信息的list即可),如显示当前正在进行操作的一方,也可以为游戏做一个主界面(开始游戏,载入游戏等),还可以把对局信息保存起来。(你也可用相同界面对规则略作更改使得用户可以在进行游戏的时候选择下五子棋还是围棋。)

下一章节我将主要讲解界面设计和对局保存。以及落子时调用声音等。你可以自己实现一些功能,然后再和第二章的功能相比对。

猜你喜欢

转载自blog.csdn.net/qq_40938169/article/details/83683964