C#实现五子棋详细教程

C#实现五子棋详细教程

一、引言

算是做的第一个小游戏。

任务

创建一个五子棋程序

二、实验环境

Visual stdio 2019
Windows窗体应用

三、实验过程

思路

0.导入资源
1.棋盘棋子分析
2.创建棋子
3.寻找最接近鼠标位置的棋子中心位置
4.得到真实坐标,正确放置棋子
5.代码重构,继承重写
6.简单胜利判断
7.所有种胜利判断
8.玩家与赢家提示

0.导入资源

如下图操作,制作并导入棋盘和棋子资源

(properties–>resources.resx–>现有资源–>添加现有文件)

1.棋盘棋子分析

打开图片属性

通过分析棋盘与棋子,明确放置棋子时的坐标

棋子:50*50(留出上下10的空格)

棋盘:630*630= 70 * 9(8个子+边缘半个+边缘半个)

加入棋盘

容器panel–>放入棋盘(SIZE 630*630)

加入棋子

控件picturebox–>放入棋子(SIZE 50*50)

定位点是在框的左上角

左上角这个点是(10,10) 往右一个棋子+70 往下一个棋子+70

请添加图片描述

2.创建棋子

这里我学到继承:

class Piece : PictureBox

冒号表示继承 子类继承父类 自动获取父类的功能

2.1代码创建棋子
类 Piece

新建一个类 Piece 表示 棋子 下面代码很多由后面步骤填上~

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Drawing;


namespace 五子棋
{
    class Piece:PictureBox
    {
        private const int IMAGE_WIDTH = 50;
        private const int IMAGE_HEIGHT = 50;//定义图片高度
        public Piece(int x,int y)
        {
            //this.Image =Properties.Resources.white;//黑棋白棋得再用子类
            this.Location = new Point(x- IMAGE_WIDTH/2, y- IMAGE_HEIGHT/2);//位置指定
            this.Size = new Size(IMAGE_WIDTH, IMAGE_HEIGHT);
        }
       // public abstract PieceType GetPieceType();//抽象方法 实现多态
        //【在第八步中加入以下代码】
       public Image GetNextImage()//直接判断 没有使用抽象方法实现多态 
        {
            if(this is BlackPiece)//is 判断是什么类型的数据 x is int 返回bool数据
            {
                return Properties.Resources.white;
            }
            else
            {
                return Properties.Resources.black;
            }
        }
        //public abstract Image GetNextImage();
        public Image GetImage()
        {
            return this.Image;
        }
    }
}
类BlackPiece 与 类WhitePiece(Piece的子类)

新建 类 BlackPiece{作为 黑棋 } 与 类 WhitePiece{作为 白棋 }(作为Piece的子类)

   class BlackPiece:Piece 
    {
        public BlackPiece(int x,int y):base(x,y)//调用父亲
        {
            this.Image = Properties.Resources.black;
        }
    }

类的关系: PictureBox**—>Piece–>**BlackPiece/WhitePiece

PictureBox:系统定义控件 -----> Piece:自定义 获取 Piece 功能 指定宽与高 **-----> ** BlackPiece/WhitePiece:指定图片为黑棋/白棋

2.2鼠标创建棋子
分析:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mgOiLNkv-1629278808843)(C#实现五子棋详细教程.assets/图5.png)]
picturebox坐标在左上角(蓝色) 我们要的是中间(绿色) 因此

this.Location = new Point(x- IMAGE_WIDTH/2, y- IMAGE_HEIGHT/2);

完整:

 private const int IMAGE_WIDTH = 50;
        private const int IMAGE_HEIGHT = 50;//定义图片高度
        public Piece(int x,int y)
        {
            //this.Image =Properties.Resources.white;//黑棋白棋得再用子类
            this.Location = new Point(x- IMAGE_WIDTH/2, y- IMAGE_HEIGHT/2);//位置指定
            this.Size = new Size(IMAGE_WIDTH, IMAGE_HEIGHT);
        }

在棋盘中加入MouseDown事件

private void pnlGobang_MouseDown_1(object sender, MouseEventArgs e)
        {//此代码是暂时的前期的 文档后面我们即将根据需要继续改进
             if(isBlack==true)//黑白棋交替放置
             {
                 this.pnlGobang.Controls.Add(new BlackPiece(e.X, e.Y));
                 isBlack = false;
             }
             else
             {
                 this.pnlGobang.Controls.Add(new WhitePiece(e.X, e.Y));
                 isBlack = true;
             }
		}

终于 我们能在上面放棋子了

3.寻找最接近鼠标位置的棋子中心位置

问题:我发现玩家竟然能在棋盘上随便放棋 但这是不行的棋子只能放在棋子该放的地方
请添加图片描述

目的:棋子只能放在棋子该放的地方 ->定位棋盘交叉点–> 寻找最接近鼠标位置的棋子中心位置

分析:

1.鼠标在框内时,才能放置棋子

请添加图片描述

2.(能放置光标显示小手 不能放置的地方光标显示禁止)

3.靠近左边 放入左边(紫色)

中间 无效
请添加图片描述

靠近右边 放入右边(红色)

变量声明

public static readonly int NODE_COUNT = 9; // public类外面要用//棋子/交叉点个数
        //private const Point NO_MATCH_NODE=new Point(-1,-1);//
        private static readonly Point NO_MATCH_NODE = new Point(-1, -1);//记录一个无效的点 //改成?//改为静态才能赋值
        private readonly int OFFSET = 35;//边界值  我:这个就像define
        private readonly int NODE_DISTANCEA = 70;//交叉点距离 也是棋子空间  
        private readonly int NODE_RADIUS = 20;//交叉点附近有效区间点的半径
        
board类中一维查找交叉点
 //一维查找
        public int FindTheClosestNode(int pos)
        {
            if (pos < OFFSET - NODE_RADIUS)//在边界
            {
                return -1;
            }
            pos -= OFFSET;
            int quotient = pos / NODE_DISTANCEA;//求商 恰恰是对应的第几个70的坐标
            int remainder = pos % NODE_DISTANCEA;//求余数 恰恰是这个点和左边交叉点的距离
            
            if (remainder<=NODE_RADIUS)
            {
                return quotient;//第几个棋子 返回左边的棋子 左边交叉点下标
            }
            else if(remainder>=NODE_DISTANCEA- NODE_RADIUS)
            {
                return quotient+1;//返回右边的棋子 右边交叉点下标
            }
            else
            {
                return -1;//有效下标 从零开始 返回-1是无效
            }
        }
board类中维查找交叉点
  //二维查找交叉点
        public Point FindTheClosestNode(int x,int y)
        {
            int nodeIdx = FindTheClosestNode(x);
            if(nodeIdx==-1)
            {
                return NO_MATCH_NODE;
            }
            int nodeIdy = FindTheClosestNode(y);
            if (nodeIdy == -1)
            {
                return NO_MATCH_NODE;
            }
            //升维
            return new Point(nodeIdx, nodeIdy);
        }
board类中 判断能否放置
 public bool CanBePlaced(int x,int y)
        {
            Point nodeId = FindTheClosestNode(x,y);
            if(nodeId==NO_MATCH_NODE)
            {
                return false;
            }
            return true;
        }
返回界面类中 显示能否放置

(能放置光标显示小手 不能放置的地方光标显示禁止)

 private void pnlGobang_MouseMove(object sender, MouseEventArgs e)
        {
            //鼠标移动事件判断能不能放棋子
            //通过切换鼠标样子
            if(game.CanBePlaced(e.X,e.Y)==true)//两个参数不变 都是界面代码
            {
                this.Cursor = Cursors.Hand;//设置鼠标样式 出现小手
            }
            else
            {
                this.Cursor = Cursors.No;
            }
        }

4.得到真实坐标,正确放置棋子

终于 我们得到最接近的棋子,我们的现在拥有了他的坐标(例如[ 4,4 ] / [ 0,5 ] / [ 3 ,3 ])

但是 真正放在棋盘上确实需要真实的坐标,比如[290,290]

那么接下来我们就来得到他的真实坐标,正确放置棋子

需要用二维数组来存放已经下过的棋子

 private Piece[,] pieces =new Piece[9,9];

1 数组的下标正好和交叉点的下标一一对应
2 交叉点转化为真真实的棋子位置坐标
3 通过数组可以判断有没有下过棋子

转化为真实棋子位置
private Point ConvertToRealPosition(Point nodeID)//参数是找到交叉点 //M:NODE_DISTANCEA=70
{
//nodeID.X nodeID.Y 获取真实下标x,y
    Point realPoint = new Point(OFFSET + NODE_DISTANCEA * nodeID.X, OFFSET + NODE_DISTANCEA * nodeID.Y);//边界35+3*70
    return realPoint;       
}

请添加图片描述

放入一颗棋子( x,y ,枚举类型的type)
        //放入一颗棋子 x,y ,枚举类型的type
        public Piece PlaceAPiece(int x,int y,PieceType type)

新生成一个类 枚举类型的type PieceType type

enum  PieceType
    {
           NONE,BLACK,WHITE
    }

回到PlaceAPiece

[nodeId.X, nodeId.Y] 是[5,3]这种 而不是真实下标
[realPoint.X, realPoint.Y] 是真实下标

 public Piece PlaceAPiece(int x,int y,PieceType type)
	{
            //获取最近的交叉点
            Point nodeId = FindTheClosestNode(x, y);
            //如果是无效的交叉点 则产生一个空的棋子
            //交叉点不存在 棋子也不存在
            if (nodeId == NO_MATCH_NODE)
                return null;

            //避免同一位置重复放入棋子
            
            if (pieces[nodeId.X, nodeId.Y] != null)
                return null;
            Point realPoint = ConvertToRealPosition(nodeId);
            if(type==PieceType.BLACK)
            {
                //如果是黑 数组中放入黑棋
                //数组下标与点的下标一一对应
                pieces[nodeId.X, nodeId.Y] = new BlackPiece(realPoint.X, realPoint.Y);
            }
            else if(type== PieceType.WHITE)
            {
                pieces[nodeId.X, nodeId.Y] = new WhitePiece(realPoint.X, realPoint.Y);
            }
            lastPlaceNode = nodeId;//如果成功放入黑棋/白棋 记录最新的交叉点 
            return pieces[nodeId.X, nodeId.Y];//返回之前判断黑或白
        }

回到主设计界面吧,来运用PlaceAPiece

将e.X e.Y 替换成相应的交叉点来添加新的棋子 -》真实点

    Piece  piece= borad.PlaceAPiece(x,y,currentPlayer);
            if(piece!=null)
            {
                //生成新的棋子后 马上判断有没有赢家
                CheckWinner();
                if(currentPlayer==PieceType.BLACK)//PieceType enum 枚举
                {
                    currentPlayer = PieceType.WHITE;
                }
                else if(currentPlayer == PieceType.WHITE)
                {
                    currentPlayer = PieceType.BLACK;
                }
            }

OK 我们终于可以放棋子

请添加图片描述

5.代码重构,继承重写

如果所有代码只放在一个类中 ,不利于分解和封装,不利于重用

所以我们模块化出新类 让 Game类 负责游戏逻辑 即方法重构,方法重写

因为游戏的平台和界面是多样化的,所以就希望将我们的【后台逻辑】和【前端界面】分离,后台写好了,前端可以改变 ,后台固定。

所以就把FrmGobang的代码的逻辑部分放到专门的游戏类 game类,进行重构,收拾一下

例如:

请添加图片描述

左边的这段代码是从FrmGobang窗体代码中抽出来的逻辑代码 封装到【Game】中 不抽离界面代码 即重构

右边是FrmGobang窗体界面代码

后面我们还要进行胜负判断,确定谁该下棋,确实谁赢了

所以我们得知道xy的位置上放置什么棋子—GetPieceType
是黑棋 ,白棋,还是 没东西
对应的是枚举类【PieceType】
BLACK,WHITE 还是 NONE

同样 GetPieceType 也能用来确定谁该下棋,确实谁赢了

得到xy的位置上放置什么棋子
 public PieceType GetPieceType(int nodeIdX, int nodeIdY )
        {
            if(pieces[nodeIdX, nodeIdY]==null)
            {
                return PieceType.NONE;
            }
            if (pieces[nodeIdX, nodeIdY] is BlackPiece)//is 判断是什么类型
            {
                return PieceType.BLACK;
            }
            if (pieces[nodeIdX, nodeIdY] is WhitePiece)//is 判断是什么类型
            {
                return PieceType.WHITE;
            }
            
            return PieceType.NONE;
        }

6.简单胜负判断

漫长的重构之后,我们终于要进行简单的一个横竖胜负判断!

要如何简单判断胜负呢?

1.五子连心
2.同色 和最后一步同色
3.最后一步的棋子 冠军得出

请添加图片描述

记录最后一步棋子的交叉点

private Point lastPlaceNode =  NO_MATCH_NODE;//NO_MATCH_NODE;改为静态才能赋值
 public Point LlastPlaceNode//公有只读的属性
        {
            get
            {
                return lastPlaceNode;//记录最后一步棋子的交叉点
            }
        }

在 PlaceAPiece里面 “return pieces[nodeId.X, nodeId.Y];//返回之前判断黑或白”之前加入:

 lastPlaceNode = nodeId;//如果成功放入黑棋/白棋 记录最新的交叉点 

就成功记录最后一步棋子的交叉点请添加图片描述

CheckWinner()判断五子连珠 【横向 】

最后下的这个棋子

 int centerX = borad.LlastPlaceNode.X;//中心点的X 
 int centerY = borad.LlastPlaceNode.Y;//中心点的Y

考察他旁边有没有相连接的,考察他旁边的子

 checkDx, checkDy

不是同色 结束

while (count<5)
 if (  borad.GetPieceType(targetX, targetY) != currentPlayer)
      break;
 else count++;

五子连心

                  if (count == 5)                   {                       winner = currentPlayer;                   }

Orz 好像可以判断很简单的单边的横着的判断了

CheckWinner()判断五子连珠 【八方 】

以下图表示一个子往不同方向走,坐标的变化(ps丑陋画图不要介意)

图14 图15

这时 我们需要一个count来记录连心的次数

count作用:

1.记录同色的棋子个数----从一到五 有五子连心就是赢了

2.考察点targe和中心点center的距离 绝对值

3.循环次数 考察次数

一个人扮演了三个角色

上代码和注释!

if (game.Winner != PieceType.NONE)//已经决出胜负!比赛结束啦 不再下棋了
            {
                return;
            } 
if (piece!=null)
            {
                this.pnlGobang.Controls.Add(piece);//放置 某方下完 可以提示下一个
                this.picCurrentPlayer.Image = piece.GetNextImage();//该谁下棋的照片切换
                if(game.Winner==PieceType.BLACK)
                {
                    this.lblCurrentPlayer.Text="你赢啦";
                    this.picCurrentPlayer.Image = piece.GetImage();//赢了就固定
                    MessageBox.Show("恭喜你!黑棋胜利啦!!!");
                }
                else if (game.Winner == PieceType.WHITE)
                {
                    this.lblCurrentPlayer.Text = "你赢啦";
                    this.picCurrentPlayer.Image = piece.GetImage();//赢了就固定
                    MessageBox.Show("恭喜你!白棋胜利啦!!!");
                }

所有种胜利判断

但是这个代码并不能考虑,形如oo@oo的情况下,o表示放了棋子,@表示没放,但最后一个子放在@的位置,放在中间就不行啦

因此,中心点在中间,方向就不是单一的
当一个方向找不到同色的棋子时,需要转向 反向找
距离回复到从1开始找,直到同色的棋子总数为5为止

例如:我把棋子下在这里

图16

开始寻找啦,左边找不到了,往右边找

图17

找了五次的过程中,两边都没有同色的棋子
这时我们要记录的是:

1.同色棋子个数

2.距离绝对值

3.循环次数

所以count就不能一个人扮演3个角色,所以要拆成三个变量

如果例如左右方向都没有满足五子连珠的。就看看上下等其他三个方向,把以下看做四个方向

在这里插入图片描述

一个方向走不通 ,就换一个,不要把鸡蛋放在一个盒子里,直到走通就往这方向走下去,

在这里插入图片描述

我们需要变量

1.同色棋子个数 count

2.距离绝对值 distance

3.循环次数 step 循环四次就要结束了,不然就转向重复了

4.防止反向重复reverse,如下图

在这里插入图片描述

​ 要避免不断反向 用reverse记录

那么就要修改CheckWinner()了,上新的改进的代码!【❤新加入】表示比上个代码新加入的变量

 
  public void CheckWinner()
        {
            int centerX = borad.LlastPlaceNode.X;//中心点的X
            int centerY = borad.LlastPlaceNode.Y;//中心点的Y

            //dx,dy循环固定的方向  checkDx checkDy 是查找的方向 可能反向
            for (int dx = -1; dx <= 1; dx++)//dx方向变量 通过正负 0 控制增加 减少 不动
            {
                for (int dy = -1; dy <= 1; dy++)
                {
                    if (dx == 0 && dy == 0) //自己和自己不用判断 只要判断其他八个方向
                    {
                        continue;
                    }
                    int reverse = 0;//反向的次数 默认为0               【❤新加入】
                    int checkDx = dx;//动态考察 /移动的方向X 可能出现反向 【❤新加入】
                    int checkDy = dy;//动态考察 /移动的方向Y 可能出现反向 【❤新加入】

                    int count = 1;//这个棋子有一个了 反正是某一种 黑/白/NULL
                    int step = 0;//考察次数/移动的步子的次数 最多不能超过4【❤新加入】
                    int distance = 1;//考察点和中心点的距离 绝对值      【❤新加入】

                    while (step < 4)//默认循环次数是4次 五颗同色的 则count=5
                    {
                        step++;//先往某一个方向走1步

                        int targetX = centerX + distance * checkDx;//考察点的X
                        int targetY = centerY + distance * checkDy;考察点的Y
                        //如果找到的 超出了边界 或者不是同色的棋子 那么需要反向 这一步不算数 (中心和考察的)距离回到1
                       
                            if (   targetX <0  || targetX >= Borad.NODE_COUNT
                                || targetY < 0 || targetY >= Borad.NODE_COUNT
                                || borad.GetPieceType(targetX, targetY) != currentPlayer)
                            {
                                reverse++;
                                checkDx = -checkDx;
                                checkDy = -checkDy;
                                step--;
                                distance = 1;
                            }
                            //如果没有出边界 且是同色的 那么同色的棋子个数+1 距离加1
                            else
                            {
                                count++;
                                distance++;
                            }
                            if(reverse==2)
                            {
                                break;//得到一颗不同色的棋子 达不到5个同色子
                            }                     
                        
                    }
                    if (count == 5)
                    {
                        winner = currentPlayer;
                    }
                }
            
               
            }
 }

终于可以玩并且准确判断胜负
请添加图片描述

8.玩家与赢家提示

我发现 ,有时候自己和自己玩的时候,常常忘记现在该谁下棋,白子还是黑子呢,所以我决定加一个判断框;来判断该谁下棋了。

完成后如下:

请添加图片描述

窗体中加入label,与picturebox,分别放字和棋子

判断当前玩家

黑棋白棋交替

当前玩家是黑棋 (图片:黑棋)文字:该你下棋啦
下完 (点完)
当前玩家变白棋 文字:(图片:白棋)文字:该你下棋啦
下完 (点完)
当前玩家变黑棋 文字:(图片:黑棋)文字:该你下棋啦
。。。。
结束循环:有赢家
(图片:* (赢的) 棋) 文字:(* (赢的)棋)赢啦

窗体代码中

if (piece!=null)
            {
                this.pnlGobang.Controls.Add(piece);//放置 某方下完 可以提示下一个
                this.picCurrentPlayer.Image = piece.GetNextImage();//该谁下棋的照片切换
                if(game.Winner==PieceType.BLACK)
                {
                    this.lblCurrentPlayer.Text="你赢啦";
                    this.picCurrentPlayer.Image = piece.GetImage();//赢了就固定
                    MessageBox.Show("恭喜你!黑棋胜利啦!!!");
                }
                else if (game.Winner == PieceType.WHITE)
                {
                    this.lblCurrentPlayer.Text = "你赢啦";
                    this.picCurrentPlayer.Image = piece.GetImage();//赢了就固定
                    MessageBox.Show("恭喜你!白棋胜利啦!!!");
                }
markdown语法学习小记

记得右下方给代码选择语言 才会有高亮

四、总结

完成五子棋的步骤包括:0.导入资源 —>1.棋盘棋子分析—> 2.创建棋子—> 3.寻找最接近鼠标位置的棋子中心位置 —>4.得到真实坐标,正确放置棋子—> 5.代码重构,继承重写—> 6.简单胜利判断—> 7.所有种胜利判断—> 8.玩家与赢家提示

首先我学到如果所有代码只放在一个类中 ,不利于分解和封装,不利于重用。就像不同抽屉里要放不同的东西,才会井井有条,需要时好及时拿出来。

其次我学会利用Windows窗体可视化编程的特点,与传统编程相结合,不断迭代,止于至善。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值