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丑陋画图不要介意)


这时 我们需要一个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;//中心点的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窗体可视化编程的特点,与传统编程相结合,不断迭代,止于至善。