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;//边界值  我:这个就像defineprivate 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,yPoint realPoint = new Point(OFFSET + NODE_DISTANCEA * nodeID.X, OFFSET + NODE_DISTANCEA * nodeID.Y);//边界35+3*70return realPoint;
}

放入一颗棋子( x,y ,枚举类型的type)
        //放入一颗棋子 x,y ,枚举类型的typepublic 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丑陋画图不要介意)

这时 我们需要一个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为止

例如:我把棋子下在这里

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

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

1.同色棋子个数

2.距离绝对值

3.循环次数

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

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

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

我们需要变量

1.同色棋子个数 count

2.距离绝对值 distance

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

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

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

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

 public void CheckWinner(){int centerX = borad.LlastPlaceNode.X;//中心点的Xint 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;//这个棋子有一个了 反正是某一种 黑/白/NULLint step = 0;//考察次数/移动的步子的次数 最多不能超过4【❤新加入】int distance = 1;//考察点和中心点的距离 绝对值      【❤新加入】while (step < 4)//默认循环次数是4次 五颗同色的 则count=5{step++;//先往某一个方向走1步int targetX = centerX + distance * checkDx;//考察点的Xint targetY = centerY + distance * checkDy;考察点的Y//如果找到的 超出了边界 或者不是同色的棋子 那么需要反向 这一步不算数 (中心和考察的)距离回到1if (   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 距离加1else{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窗体可视化编程的特点,与传统编程相结合,不断迭代,止于至善。

C#实现五子棋详细教程相关推荐

  1. XP母盘制作详细教程(完全版)

    XP母盘制作详细教程(完全版) 现在XP系统已经成为很多网吧的选择,但一直没有网友提供完整的方案.春节放假期间做了几套系统,顺便把过程记录如下 希望能给那些想学习网吧系统的朋友一点帮助 安装Windo ...

  2. 手把手从零开始搭建k8s集群超详细教程

    本教程根据B站课程云原生Java架构师的第一课K8s+Docker+KubeSphere+DevOps同步所做笔记教程 k8s集群搭建超详细教程 1. 基本环境搭建 1. 创建私有网络 2. 创建服务 ...

  3. win10系统优化计算机,全面优化win10电脑系统详细教程 | 专业网吧维护

    全面优化win10电脑系统详细教程 以下针对win10系统的电脑全面优化的步骤: 步骤1:禁止开机启动项 1.首先我们先来优化开机速度,拖慢开机速度的首先是开机自启动项,Ctrl + Shift + ...

  4. GPU运行Tensorflow详细教程及错误解决

    GPU运行Tensorflow详细教程及错误解决 前提条件 配置GPU运行 确认是否成功配置 出现的错误及解决方案 前提条件 最重要的一点:CUDA与tensorflow的版本一点要对应,不然用不了! ...

  5. VMware虚拟机安装黑苹果MacOS Mojave系统详细教程

    更多资源请百度搜索:前端资源网 欢迎关注我的博客:www.w3h5.com 最近遇到一个H5页面的 iPhone X 刘海兼容问题.查到一个 XCode 编辑器,可以模拟 iPhone X 环境运行. ...

  6. [分享] 从定制Win7母盘到封装详细教程 By BILL ( 10月23日补充说明 )

    [分享] 从定制Win7母盘到封装详细教程 By BILL ( 10月23日补充说明 ) billcheung 发表于 2011-10-23 00:07:49 https://www.itsk.com ...

  7. win七系统如何卸载MySQL_win7系统卸载SQL2008R2数据库的详细教程

    用过SQL2008R2数据库的朋友都知道,安装起来容易卸起来麻烦,可是在win7 32位旗舰版系统就不知道怎么卸载SQL2008R2数据库了.其实卸载SQL2008R2数据库的方法也很简单,可直接通过 ...

  8. Ubuntu系统安装搜狗输入法详细教程

    Ubuntu16.04系统安装搜狗输入法详细教程 解决Ubuntu 18.04中文输入法的问题,安装搜狗拼音

  9. PHP7Grafika,PHP图片处理库Grafika详细教程(3):图像属性处理

    该文章是接着上篇文章,<PHP极其强大的图片处理库Grafika详细教程(2):图像特效处理模块>,由于grafika功能太多,所以分开写,其他的点击这里 该文章主要写grafika的图像 ...

最新文章

  1. Windows Server 2012 之配置AD DS
  2. 如何高效安全的将资源同步到本地数据库
  3. java做 excel文件的 导入导出 (SSM+layer)
  4. Wget用法、参数解释的比较好的一个文章
  5. 一张图看懂微软Power BI系列组件
  6. android自动化测试之robotium初探(三),Android自动化测试之Robotium--基础操作.pdf
  7. CodeIgniter典型的表单提交验证代码
  8. jetson nano 相关设置(开机自动登录、取消休眠和屏保、开机自启动程序)
  9. Maximo 7.5 集成方式 去掉主菜单
  10. amd860k能装黑苹果吗_我的电脑可以安装黑苹果吗?
  11. 编译GDAL支持ArcObjects
  12. Python第七天 函数 函数参数 函数里的变量 函数返回值 多类型传值 函数递归调用 匿名函数 内置函数 列表表达式/列表重写...
  13. ICMP(网际控制报文协议)
  14. 使用Microsoft Network Monitor分析Wireshark无法解析的SSL流量包
  15. 电商产品设计:如何设计产品分销体系
  16. Python3,4行代码给图片加美颜,拍照再也不需要开美颜滤镜了。
  17. FSK过零检测技术软件实现
  18. 物料分类帐环境下:物料下一期间标准价的维护及批准发布时点
  19. 健康医疗数据安全指南内容
  20. Redis分布式锁/Redis的setnx命令如何设置key的失效时间(同时操作setnx和expire)

热门文章

  1. emc整改措施及案例_EMC整改方案
  2. substance painter中的粒子笔刷重力方向错乱问题
  3. ALM(application lifecycle management)介绍
  4. CISSP第5/8知识点错题集
  5. MVC 音乐商店 第 6 部分: 使用数据批注模型验证
  6. hihoCoder挑战赛16 王胖浩与三角形
  7. Java实现人民币大小写转换
  8. OGR几何要素处理(创建编辑点线面)——Python地理数据处理学习分享
  9. Edxposed学习研究(四)Magisk(面具)源码下载编译详细实战教程
  10. 《MAC OS X 技术内幕》读书笔记第一章:MAC OS X的起源