【小游戏】怀旧经典之五子棋 | 追忆童年

1,339 阅读6分钟
  • 欢迎点赞 👍 收藏 ⭐留言 📝 如有错误请指正,火速修改!
  • 一直在学习的路上,有好文、好书推荐戳我!📧📧📧
  • 赠言: 人但有恒,事无不成!💪💪💪

前言

我正在参与掘金创作者训练营第6期, 点击了解活动详情

五子棋起源于中国,是全国智力运动会竞技项目之一,是一种两人对弈的纯策略型棋类游戏。双方分别使用黑白两色的棋子,下在棋盘直线与横线的交叉点上,先形成五子连珠者获胜。

  • 闲暇时候跟朋友来两盘是不是休闲又益智 🌟
  • 还记得初中高中喜欢在格子本本上面随时来一把真是怀旧又经典 😄
  • 在女朋友面前展示一下是不是很酷 😎

正文

实现思路 💬

为了更好的还原,我使用.NET Windows窗体应用程序来实现。 实现主要分为以下几步:

  • 绘制棋盘
  • 创建棋子逻辑
  • 实现取胜规则
  • 平局逻辑
  • 信息提示
  • 重置游戏
  • 测试

1️⃣ :准备素材 ✨

  • 需要木制背景,使棋盘更加真实
  • 一黑一白两色棋子

image.png

2️⃣ :使用Visual Studio创建windows窗体应用程序✨

  • 往窗体里面拖入一个PictureBox 并且为其添加背景图片

image.png

3️⃣ :使用C# 自带GDI绘制棋盘✨

👉 绘制主要分为三步即你需要告诉计算机,你要在哪里画?用什么画?画什么?

  • 在哪里画,我们的PictureBox组件有一个Event叫做外观-Paint双击绑定方法即可
  • 用什么画,C#提供了一个Pen类定义用于绘制直线和曲线的对象。
Pen pen = new Pen(Color.Black, 2); //两个参数为笔的颜色与粗细
  • 画什么,主要是使用Graphics类的DrawLine方法,该方法主要做的是将两个坐标点连接成一条直线
// Point类为坐标类即x\y轴坐标
e.Graphics.DrawLine(pen, new Point(50, 50), new Point(50, 450));

👉 棋盘分为横竖多条直线组成,我们了解了相关类之后,只需要遍历循环生成多条横竖线条即可组成棋盘。

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
    int Size = Const.BoardSize; // 定义坐标点大小
    int Count = Const.BoardGrid; // 定义棋盘多少X多少
    Pen pen = new Pen(Color.Black, 3);//笔的颜色与粗细
    Pen pen_edge = new Pen(Color.Black, 5);// 绘制边线
    for (int i = 1; i <= Count; i++)
    {
        // 定义横竖线条
        var across1 = new Point(Size, Size * i);
        var across2 = new Point(Size * Count, Size * i);
        var vertical1 = new Point(Size * i, Size * Count);
        var vertical2 = new Point(Size * i, Size);

        if (i == 1 || i == Count) //四周线条加粗,使棋盘更加形象
        {
            e.Graphics.DrawLine(pen_edge, across1, across2);
            e.Graphics.DrawLine(pen_edge, vertical1, vertical2);
        }
        else
        {
            e.Graphics.DrawLine(pen, across1, across2);
            e.Graphics.DrawLine(pen, vertical1, vertical2);
        }
    }
}

现在棋盘效果已经出来啦,我们一起看看我们的效果!👇

image.png

4️⃣ : 绘制棋子,点击棋盘正确落到对应格子上。✨

👉 需要解决的问题如下

  • 首先我们要考虑点击到格子边缘,如果精准落到我们的网格交叉点。
  • 点击的时候判断是否存在棋子
/// <summary>
/// 根据坐标计算当前棋子落点坐标
/// </summary>
/// <param name="x">鼠标点击X轴点</param>
/// <param name="y">鼠标点击Y轴点</param>
/// <returns></returns>
public static Point GetPos(int x, int y)
{
    // 点击边界不做处理
    if (x < Const.BoardSize || x > Const.BoardSize * Const.BoardGrid || y < Const.BoardSize || y > Const.BoardSize * Const.BoardGrid)
        return new Point(0,0);

    int half = Const.BoardInterval / 2;
    int remainderX = x % Const.BoardInterval;int remainderY = y % Const.BoardInterval;
    if (remainderX >= half) //以格子中心点为标识,超过格子半径即落在隔壁中心点
        x += (Const.BoardInterval - remainderX);
    else
        x -= remainderX;
    if (remainderY >= half)
        y += (Const.BoardInterval - remainderY);
    else
        y -= remainderY;
    return new Point(x/Const.BoardInterval, y/Const.BoardInterval);
}

👉核心:我们用当前坐标 模(%) 棋盘间隔,以每个格子中心为标识,超过格子半径即落点在隔壁点的中心。

/// <summary>
/// 下棋子方法
/// </summary>
/// <param name="type">棋子类型:黑、白</param>
/// <param name="pic">棋盘容器</param>
/// <param name="PlacementX">鼠标点击X轴点</param>
/// <param name="PlacementY">鼠标点击Y轴点</param>
public static void PlayingChess(PiecesType type, PictureBox pic, int PlacementX, int PlacementY)
{
    if (PlacementX == 0 || PlacementY == 0)
        return;
    int AccurateX = PlacementX*Const.BoardInterval - 37; // 精确棋子的X中心位置
    int AccurateY = PlacementY*Const.BoardInterval - 37; // 精确棋子的Y中心位置
    var graphic = pic.CreateGraphics();
    graphic.SmoothingMode = SmoothingMode.HighQuality;//去除图片锯齿,提高质量
    graphic.DrawImage(resizeImage(type == PiecesType.Black ? Resources.black : Resources.white, new Size(80, 80)), new Point(AccurateX, AccurateY));
}

//鼠标点击棋盘事件
private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
{
    // 获取精确落点坐标
    var point = GetPos(e.X, e.Y);
    if (CheckBoard[point.X, point.Y] != PiecesType.None)//是否重复下棋
        return;

    CheckBoard[point.X, point.Y] = piecesType;// 存储当前坐标棋子颜色
    PlayingChess(piecesType, pictureBox1, point.X, point.Y);

    piecesType = piecesType == PiecesType.Black ? PiecesType.White : PiecesType.Black;
}

👉为什么计算了落点坐标还要-37 ❓ resizeImage() 是什么方法 ❓

因为上面用了DrawImage()方法,我们指定了图片大小,这里要把误差补好,否则落点会出现偏差。 resizeImage()方法,是处理图片分辨率的方法,素材图片比例较大!

image.png

👉下棋子我们采用一个二维数组用于记录当前坐标下棋子的颜色PiecesType为枚举,存储棋子颜色相关信息

5️⃣ : 五子棋的核心:胜负判断算法!✨

规则: 我们五子棋胜利的条件为横向、纵向、斜向五连同色棋子即取胜,棋盘满无胜负即为平局。
思路: 我们棋盘为横向和纵向如果用坐标表示则为X轴Y轴,我们只需要嵌套循环将每一个坐标点的颜色标注,如果连续五个坐标点存在同一颜色即取胜。

public bool VictoryJudgment()
{
    // 横向判断
    for (int i = 0; i <= Const.BoardGrid - 4; i++)
    {
        for (int j = 0; j <= Const.BoardGrid; j++)
        {
            if (CheckBoard[i, j] == piecesType && CheckBoard[i + 1, j] == piecesType && CheckBoard[i + 2, j] == piecesType && CheckBoard[i + 3, j] == piecesType && CheckBoard[i + 4, j] == piecesType)
            {
                return true;
            }
        }
    }
    // 纵向判断
    for (int i = 0; i <= Const.BoardGrid; i++)
    {
        for (int j = 0; j <= Const.BoardGrid - 4; j++)
        {
            if (CheckBoard[i, j] == piecesType && CheckBoard[i, j + 1] == piecesType && CheckBoard[i, j + 2] == piecesType && CheckBoard[i, j + 3] == piecesType && CheckBoard[i, j + 4] == piecesType)
            {
                return true;
            }
        }
    }
    // 斜向判断
    for (int i = 0; i <= Const.BoardGrid - 4; i++)
    {
        for (int j = 0; j <= Const.BoardGrid - 4; j++)
        {
            if (CheckBoard[i, j] == piecesType && CheckBoard[i + 1, j + 1] == piecesType && CheckBoard[i + 2, j + 2] == piecesType && CheckBoard[i + 3, j + 3] == piecesType && CheckBoard[i + 4, j + 4] == piecesType)
            {
                return true;
            }
        }
    }
    // 反斜线判断
    for (int i = Const.BoardGrid; i >= 4; i--)
    {
        for (int j = 1; j < Const.BoardGrid - 3; j++)
        {
           if (CheckBoard[i, j] == piecesType && CheckBoard[i - 1, j + 1] == piecesType && CheckBoard[i - 2, j + 2] == piecesType && CheckBoard[i - 3, j + 3] == piecesType && CheckBoard[i - 4, j + 4] == piecesType)
            {
                return true;
            }
        }
    }
    return false;
}

👉下棋子的时候我们定义了二维数组,每次落棋子的时候我们会在二维数组即棋盘坐标标记这个二维数组上棋子的颜色。由于有四种情况我们这边写四层循环,处理我们的四种取胜情况。

6️⃣ : 平局判断,即考虑棋盘满了,但是胜负未分的情况!✨

⛳逻辑: 二维数据所有坐标点是否填充满!
☀️方法: GetLength()方法获取二维数组长度,参数为1则是第二个数组。
⭐测试: 偷懒了设置小棋盘测试,达到预期提示!

public bool IsDogfall()
{
    for (int i = 1; i < CheckBoard.GetLength(0); i++)
        for (int j = 1; j < CheckBoard.GetLength(1); j++)
        {
            if (CheckBoard[i, j] == PiecesType.None)
                return false;
        }
    return true;
}

7️⃣ : 重置游戏 ✨

👉我们棋盘是画在PictureBox组件上的,它提供了一个刷新方法Refresh(),我们直接调用即可!
注意: 需要将定义的二维数组清空,否则会导致无法落棋子!

private void button2_Click(object sender, EventArgs e)
{
    pictureBox1.Refresh();// 刷新棋盘控件,即清除所有棋子
    CheckBoard = new PiecesType[Const.BoardGrid + 1, Const.BoardGrid + 1];//初始化棋子坐标二维数组,下标+1
}

8️⃣ : 输赢测试 ✨

横竖测试斜向测试
image.pngimage.png
image.pngimage.png

image.png

注意事项(一) 参数 ⚡⚡⚡

👉记录的一些变量,以及常量。如棋盘大小,间距可能会变动,直接常量存储方便改动!

//记录各个坐标点的二维数组
private PiecesType[,] CheckBoard = new PiecesType[Const.BoardGrid + 1, Const.BoardGrid + 1];
private PiecesType piecesType = PiecesType.Black; // 默认先手下棋棋子颜色
private bool StartGame = false;//判断是否开始游戏

publish class Const
{
    // 棋盘大小
    public const int BoardSize = 50;
    // 棋盘间隔
    public const int BoardInterval = 50;
    // 棋盘格子数
    public const int BoardGrid = 10;
}

注意事项(二) 调试 ⚡⚡⚡

为了方便及时获取鼠标点坐标调试,我们可以在PictureBox组件加上一个事件 MouseMove监听鼠标经过的坐标点!

private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    var point = GetPos(e.X, e.Y);
    this.label1.Text = $"监听鼠标经过棋盘坐标 X:{e.X} Y:{e.Y}";
    this.label2.Text = $"计算坐标所在格子 X:{point.X} Y:{point.Y}";
}

🌈🌈🌈效果展示

为了直观提示游戏者,我使用Label组件,实时提示更加友好,毕竟Winform的弹框很丑!!

Rec 0003_114205.gif

总结 📌

  • 算法: 在判断算法上感觉还有很多优化的地方,也希望大佬们能为我指导一下!
  • 寄言: 写任何业务逻辑的时候,先思考再行动,把东西都考虑到。
  • 感悟: 开始写的时候,其实是没有什么思路,无从下手,觉得很复杂,当有条理的把所有逻辑考虑之后,你会觉得其实很简单。从绘制棋盘到棋子,再到算法,一步一步从无到有。当然在实现的过程中要考虑扩展,也要考虑代码可读可维护。
  • 优化: 可以加入人机对战,以及局域网对战!