[C#] 简单的俄罗斯方块实现

news2024/11/25 8:23:28

一个控制台俄罗斯方块游戏的简单实现. 已在 github.com/SlimeNull/Tetris 开源.
在这里插入图片描述


思路

很简单, 一个二维数组存储当前游戏的方块地图, 用 bool 即可, true 表示当前块被填充, false 表示没有.

然后, 抽一个 “形状” 类, 形状表示当前玩家正在操作的一个形状, 例如方块, 直线, T 形什么的. 一个形状又有不同的样式, 也就是玩家可以切换的样式. 每一个样式都是原来样式旋转之后的结果. 为了方便, 可以直接使用硬编码的方式存储所有样式中方块的相对坐标.

一个形状有一个自己的坐标, 并且它包含很多方块. 在绘制的时候, 获取它每一个方块的坐标, 转换为地图内的绝对坐标, 然后使用 StringBuilder 拼接字符串, 即可.


资料

俄罗斯方块中总共有这七种方块

在这里插入图片描述


类型定义

一个简单的二维坐标

/// <summary>
/// 表示一个坐标
/// </summary>
/// <param name="X"></param>
/// <param name="Y"></param>
record struct Coordinate(int X, int Y)
{
    /// <summary>
    /// 根据基坐标和相对坐标, 获取一个绝对坐标
    /// </summary>
    /// <param name="baseCoord"></param>
    /// <param name="relativeCoord"></param>
    /// <returns></returns>
    public static Coordinate GetAbstract(Coordinate baseCoord, Coordinate relativeCoord)
    {
        return new Coordinate(baseCoord.X + relativeCoord.X, baseCoord.Y + relativeCoord.Y);
    }
}

形状的一个样式, 单纯使用坐标数组存储即可.

record struct ShapeStyle(Coordinate[] Coordinates);

形状

/// <summary>
/// 形状基类
/// </summary>
abstract class Shape
{
    /// <summary>
    /// 名称
    /// </summary>
    public abstract string Name { get; }

    /// <summary>
    /// 形状的位置
    /// </summary>
    public Coordinate Position { get; set; }

    /// <summary>
    /// 形状所有的样式
    /// </summary>
    protected abstract ShapeStyle[] ShapeStyles { get; }

    /// <summary>
    /// 当前使用的样式索引
    /// </summary>
    private int _currentStyleIndex = 0;

    /// <summary>
    /// 从坐标构建一个新形状
    /// </summary>
    /// <param name="position"></param>
    public Shape(Coordinate position)
    {
        Position = position;
    }

    /// <summary>
    /// 获取当前形状的当前所有方块 (相对坐标)
    /// </summary>
    /// <returns></returns>
    public IEnumerable<Coordinate> GetBlocks()
    {
        return ShapeStyles[_currentStyleIndex].Coordinates;
    }

    /// <summary>
    /// 获取当前形状下一个样式的所有方块 (相对坐标)
    /// </summary>
    /// <returns></returns>
    public IEnumerable<Coordinate> GetNextStyleBlocks()
    {
        return ShapeStyles[(_currentStyleIndex + 1) % ShapeStyles.Length].Coordinates;
    }

    /// <summary>
    /// 改变样式
    /// </summary>
    public void ChangeStyle()
    {
        _currentStyleIndex = (_currentStyleIndex + 1) % ShapeStyles.Length;
    }
}

一个 T 形状的实现

class ShapeT : Shape
{
    public ShapeT(Coordinate position) : base(position)
    {
    }

    public override string Name => "T";

    protected override ShapeStyle[] ShapeStyles { get; } = new ShapeStyle[]
    {
        new ShapeStyle(
            new Coordinate[]
            {
                new Coordinate(-1, 0),
                new Coordinate(0, 0),
                new Coordinate(1, 0),
                new Coordinate(0, 1),
            }),
        new ShapeStyle(
            new Coordinate[]
            {
                new Coordinate(-1, 0),
                new Coordinate(0, -1),
                new Coordinate(0, 0),
                new Coordinate(0, 1),
            }),
        new ShapeStyle(
            new Coordinate[]
            {
                new Coordinate(-1, 0),
                new Coordinate(0, 0),
                new Coordinate(1, 0),
                new Coordinate(0, -1),
            }),
        new ShapeStyle(
            new Coordinate[]
            {
                new Coordinate(1, 0),
                new Coordinate(0, -1),
                new Coordinate(0, 0),
                new Coordinate(0, 1),
            }),
    };
}

主逻辑

上面的定义已经写好了, 接下来就是写游戏主逻辑.

主逻辑包含每一回合自动向下移动形状, 如果无法继续向下移动, 则把当前的形状存储到地图中. 并进行一次扫描, 将所有的整行全部消除.

抽一个 TetrisGame 的类用来表示俄罗斯方块游戏, 下面是这个类的基本定义.

class TetrisGame
{
    /// <summary>
    /// x, y
    /// </summary>
    private readonly bool[,] map;

    private readonly Random random = new Random();


    public TetrisGame(int width, int height)
    {
        map = new bool[width, height];

        Width = width;
        Height = height;
    }

    public Shape? CurrentShape { get; set; }

    public int Width { get; }
    public int Height { get; }
}

判断当前形状是否可以进行移动的方法

/// <summary>
/// 判断是否可以移动 (移动后是否会与已有方块重合, 或者超出边界)
/// </summary>
/// <param name="xOffset"></param>
/// <param name="yOffset"></param>
/// <returns></returns>
private bool CanMove(int xOffset, int yOffset)
{
    // 如果当前没形状, 返回 false
    if (CurrentShape == null)
        return false;

    foreach (var block in CurrentShape.GetBlocks())
    {
        Coordinate coord = Coordinate.GetAbstract(CurrentShape.Position, block);

        coord.X += xOffset;
        coord.Y += yOffset;

        // 如果移动后方块坐标超出界限, 不能移动
        if (coord.X < 0 || coord.X >= Width ||
            coord.Y < 0 || coord.Y >= Height)
            return false;

        // 如果移动后方块会与地图现有方块重合, 则不能移动
        if (map[coord.X, coord.Y])
            return false;
    }

    return true;
}

判断当前形状是否能够切换到下一个样式的方法

/// <summary>
/// 判断是否可以改变形状 (改变形状后是否会和已有方块重合, 或者超出边界)
/// </summary>
/// <returns></returns>
private bool CanChangeShape()
{
    // 如果当前没形状, 当然不能切换样式
    if (CurrentShape == null)
        return false;

    // 获取下一个样式的所有方块
    foreach (var block in CurrentShape.GetNextStyleBlocks())
    {
        Coordinate coord = Coordinate.GetAbstract(CurrentShape.Position, block);

        // 如果超出界限, 不能切换
        if (coord.X < 0 || coord.X >= Width ||
            coord.Y < 0 || coord.Y >= Height)
            return false;

        // 如果与现有方块重合, 不能切换
        if (map[coord.X, coord.Y])
            return false;
    }

    return true;
}

把当前形状存储到地图中

/// <summary>
/// 将当前形状存储到地图中
/// </summary>
private void StorageShapeToMap()
{
    // 没形状, 存寂寞
    if (CurrentShape == null)
        return;

    // 所有方块遍历一下
    foreach (var block in CurrentShape.GetBlocks())
    {
        // 转为绝对坐标
        Coordinate coord = Coordinate.GetAbstract(CurrentShape.Position, block);

        // 超出界限则跳过
        if (coord.X < 0 || coord.X >= Width ||
            coord.Y < 0 || coord.Y >= Height)
            continue;

        // 存地图里
        map[coord.X, coord.Y] = true;
    }

    // 当前形状设为 null
    CurrentShape = null;
}

生成一个新形状

/// <summary>
/// 生成一个新形状
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
private void GenerateShape()
{
    int shapeCount = 7;
    int randint = random.Next(shapeCount);

    Coordinate initCoord = new Coordinate(Width / 2, 0);

    Shape newShape = randint switch
    {
        0 => new ShapeI(initCoord),
        1 => new ShapeJ(initCoord),
        2 => new ShapeL(initCoord),
        3 => new ShapeO(initCoord),
        4 => new ShapeS(initCoord),
        5 => new ShapeT(initCoord),
        6 => new ShapeZ(initCoord),

        _ => throw new InvalidOperationException()
    };

    CurrentShape = newShape;
}

扫描地图, 消除所有整行

/// <summary>
/// 扫描, 消除掉可消除的行
/// </summary>
private void Scan()
{
    for (int y = 0;  y < Height; y++)
    {
        // 设置当前行是整行
        bool ok = true;

        // 循环当前行的所有方块, 如果方块为 false, ok 就会被设为 false
        for (int x = 0; x < Width; x++)
            ok &= map[x, y];

        // 如果当前行确实是整行
        if (ok)
        {
            // 所有行全部往下移动
            for (int _y = y; _y > 0; _y--)
                for (int x = 0; x < Width; x++)
                    map[x, _y] = map[x, _y - 1];
            
            // 最顶行全设为空
            for (int x = 0; x < Width; x++)
                map[x, 0] = false;
        }
    }
}

封装一些用户操作使用的方法

/// <summary>
/// 根据指定偏移, 进行移动
/// </summary>
/// <param name="xOffset"></param>
/// <param name="yOffset"></param>
public void Move(int xOffset, int yOffse
{
    lock (this)
    {
        if (CurrentShape == null)
            return;

        if (CanMove(xOffset, yOffset))
        {
            var newCoord = CurrentShape.
            newCoord.X += xOffset;
            newCoord.Y += yOffset;

            CurrentShape.Position = newC
        }
    }
}

/// <summary>
/// 向左移动
/// </summary>
public void MoveLeft()
{
    Move(-1, 0);
}

/// <summary>
/// 向右移动
/// </summary>
public void MoveRight()
{
    Move(1, 0);
}

/// <summary>
/// 向下移动
/// </summary>
public void MoveDown()
{
    Move(0, 1);
}

/// <summary>
/// 改变形状样式
/// </summary>
public void ChangeShapeStyle()
{
    lock (this)
    {
        if (CurrentShape == null)
            return;

        if (CanChangeShape())
            CurrentShape.ChangeStyle();
    }
}

/// <summary>
/// 降落到底部
/// </summary>
public void Fall()
{
    lock (this)
    {
        while (CanMove(0, 1))
        {
            Move(0, 1);
        }
    }
}

游戏每一轮的主逻辑

/// <summary>
/// 下一个回合
/// </summary>
public void NextTurn()
{
    lock (this)
    {
        // 如果当前没有存在的形状, 则生成一个新的, 并返回
        if (CurrentShape == null)
        {
            GenerateShape();
            return;
        }

        // 如果可以向下移动
        if (CanMove(0, 1))
        {
            // 直接改变当前形状的坐标
            var newCoord = CurrentShape.Position;
            newCoord.Y += 1;

            CurrentShape.Position = newCoord;
        }
        else
        {
            // 将当前的形状保存到地图中
            StorageShapeToMap();
        }

        // 扫描, 判断某些行可以被消除
        Scan();
    }
}

将地图渲染到控制台

public void Render()
{
    StringBuilder sb = new StringBuilder();

    bool[,] mapCpy = new bool[Width, Height];
    Array.Copy(map, mapCpy, mapCpy.Length);

    if (CurrentShape != null)
    {
        foreach (var block in CurrentShape.GetBlocks())
        {
            Coordinate coord = Coordinate.GetAbstract(CurrentShape.Position, block);
            if (coord.X < 0 || coord.X >= Width ||
                coord.Y < 0 || coord.Y >= Height)
                continue;

            mapCpy[coord.X, coord.Y] = true;
        }
    }

    sb.AppendLine("┌" + new string('─', Width * 2) + "┐");
    for (int y = 0; y < Height; y++)
    {
        sb.Append("|");

        for (int x = 0; x < Width; x++)
        {
            sb.Append(mapCpy[x, y] ? "##" : "  ");
        }

        sb.Append("|");

        sb.AppendLine();
    }

    sb.AppendLine("└" + new string('─', Width * 2) + "┘");

    lock (this)
    {
        Console.SetCursorPosition(0, 0);
        Console.Write(sb.ToString());
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/855834.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

力扣 474. 一和零

题目来源&#xff1a;https://leetcode.cn/problems/ones-and-zeroes/description/ C题解&#xff1a;本题其实是01背包问题&#xff01;只不过这个背包有两个维度&#xff0c;一个是m 一个是n&#xff0c;而不同长度的字符串就是不同大小的待装物品。动规五部曲&#xff1a; …

Java并发 | 常见线程安全容器

文章目录 简介一、Hash表&#x1f6a3;1、ConcurrentHashMap1.1 内部实现原理1.2 并发操作方法1.3 ConcurrentHashMap与Hashtable的比较 二、集合&#x1f6a3;2、CopyOnWriteArrayList2.1 内部实现原理2.2 Copy-On-Write(COW)设计思想2.3 实操 三、Map&#x1f6a3;3、Concurr…

C语言第十课----------------扫雷----------数组的经典练手题

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; &#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382;…

力扣120.三角形最小路径和(动态规划)

/*** author Limg* date 2022/08/09* 给定一个三角形 triangle &#xff0c;找出自顶向下的最小路径和。* 每一步只能移动到下一行中相邻的结点上。* 相邻的结点在这里指的是下标与上一层结点下标相同或者等于上一层结点下标 1 的两个结点。* 也就是说&#xff0c;如果正位于当…

(MVC)SpringBoot+Mybatis+Mapper.xml

前言&#xff1a;本篇博客主要对MVC架构、Mybatis工程加深下理解&#xff0c;前面写过一篇博客&#xff1a;SprintBoothtml/css/jsmybatis的demo&#xff0c;里面涉及到了Mybatis的应用&#xff0c;此篇博客主要介绍一种将sql语句写到了配置文件里的方法&#xff0c;即Mybatis里…

AVL树(二叉搜索树)

AVL树 1.1 AVL树的概念1.2 AVL树节点的定义1.3 AVL树的旋转1.3.1 右旋&#xff08;右单旋&#xff09;1.3.2 左旋&#xff08;左单旋&#xff09;1.3.3 左右双旋&#xff08;先左单旋再右单旋&#xff09;1.3.4 右左双旋&#xff08;先右单旋再左单旋&#xff09; 1.4 AVL树的插…

进程 的初识

程序和进程有什么区别 程序是静态的概念&#xff0c;gcc xxx.c -o pro 磁盘中生成的文件&#xff0c;叫做程序。进程是程序的一次运行活动&#xff0c;通俗点的意思就是程序跑起来了&#xff0c;系统中就多了一个进程。 如何查看系统中有哪些进程 使用 ps 指令&#xff08;完整…

❤ vue组件的生命周期

❤ vue组件的生命周期 介绍 在vue组件中&#xff0c;生命周期指的是从组件创建开始&#xff0c;到组件销毁&#xff0c;所经历的整个过程&#xff1b;在这个过程中的一些不同的阶段&#xff0c;vue会调用指定的一些组件方法。基本生命周期函数有下面几个阶段&#xff1a;创建…

Python中的dataclass:简化数据类的创建

Python中的dataclass是一个装饰器&#xff0c;用于自动添加一些常见的方法&#xff0c;如构造函数、__repr__、__eq__等。它简化了创建数据类的过程&#xff0c;减少了样板代码&#xff0c;提高了代码的可读性和可维护性。有点类似java里面的Java Bean。 让我们看一个简单的例子…

将.doc文档的默认打开方式从WPS修改为word office打开方式的具体方法(以win 10 操作系统为例)

将.doc文档的默认打开方式从WPS修改为word office打开方式的具体方法&#xff08;以win 10 操作系统为例&#xff09; 随着近几年WPS软件的不断完善和丰富&#xff0c;在某些方面取得了具有特色的优势。在平时编辑.doc文档时候也常常用到wps软件&#xff0c;不过WPS文献也存在…

【分布式技术专题】RocketMQ延迟消息实现原理和源码分析

痛点背景 业务场景 假设有这么一个需求&#xff0c;用户下单后如果30分钟未支付&#xff0c;则该订单需要被关闭。你会怎么做&#xff1f; 之前方案 最简单的做法&#xff0c;可以服务端启动个定时器&#xff0c;隔个几秒扫描数据库中待支付的订单&#xff0c;如果(当前时间-订…

EasyPoi导出 导入(带校验)简单示例 EasyExcel

官方文档 : http://doc.wupaas.com/docs/easypoi pom的引入: <!-- easyPoi--><dependency><groupId>cn.afterturn</groupId><artifactId>easypoi-spring-boot-starter</artifactId><version>4.0.0</version></dep…

分布式协调组件Zookeeper

Zookeeper介绍 什么是Zookeeper ZooKeeper 是⼀种分布式协调组件&#xff0c;用于管理大型主机。在分布式环境中协调和管理服务是一个复杂的过程。ZooKeeper 通过其简单的架构和 API 解决了这个问题。ZooKeeper 允许开发人员专注于核心应用程序逻辑&#xff0c;而不必担心应用…

【Linux】多线程——线程引入 | 线程控制

文章目录 一、Linux多线程1. 线程概念2. 线程创建3. 线程和进程4. 线程的优缺点 二、线程控制1. 线程创建2. 线程终止3. 线程等待4. 线程分离5. 线程局部存储 三、线程封装 一、Linux多线程 一级页表和二级页表都是key/val模型&#xff0c;一级页表的key是第一份的10个比特位&a…

(统计学习方法|李航)第一章统计学习方法概论——四五六节模型评估与模型选择,正则化与交叉验证,泛化能力

一&#xff0c;模型评估与模型选择 1.训练误差与测试误差 假如我们有100个数据。80条记录给训练集&#xff0c;10条记录给测试集&#xff0c;10条记录给验证集 先在训练集中训练模型&#xff0c; 再在验证集上测试看哪种模型更拟合 最后用测试集算出成绩 表示决策函数 模型…

数据清理在数据科学中的重要性

什么是数据清理&#xff1f; 推荐&#xff1a;使用 NSDT场景编辑器 助你快速搭建可编辑的3D应用场景 在数据科学中&#xff0c;数据清理是识别不正确数据并修复错误的过程&#xff0c;以便最终数据集可供使用。错误可能包括重复字段、格式不正确、字段不完整、数据不相关或不准…

基于kettle实现pg数据定时转存mongodb

mogodb 待创建 基于kettle实现pg数据定时转存mongodb_kettle 实时迁移 mongodb_呆呆的私房菜的博客-CSDN博客

链表和哈希Set

1 LinkedList集合类 LinkedList集合类底层是使用双向链表实现的&#xff0c;相较于ArrayList&#xff0c;更方便进行增删操作。 在增删查改方面&#xff0c;新增了头尾操作&#xff0c;比如从头部插入、尾部插入、头部删除、尾部删除、头部查询和尾部查询等操作。由于有头尾的…

SpringCloud实用篇3----Docker

1.初识Docker 1.1 什么是Docker 微服务虽然具备各种各样的优势&#xff0c;但服务的拆分通用给部署带来了很大的麻烦。 分布式系统中&#xff0c;依赖的组件非常多&#xff0c;不同组件之间部署时往往会产生一些冲突。在数百上千台服务中重复部署&#xff0c;环境不一定一致…

gitblit windows部署

1.官网下载 往死慢&#xff0c;我是从百度找的1.9.1&#xff0c;几乎就是最新版 http://www.gitblit.com/ 2.解压 下载下来是一个zip压缩包&#xff0c;直接解压即可 3.配置 3.1.配置资源库路径 找到data文件下的gitblit.properties文件&#xff0c;用Notepad打开 **注意路…