[C#] 简单的俄罗斯方块实现
一个控制台俄罗斯方块游戏的简单实现. 已在 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)
{// 如果当前没形状, 返回 falseif (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;}// 当前形状设为 nullCurrentShape = 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 就会被设为 falsefor (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());}
}
相关文章:

[C#] 简单的俄罗斯方块实现
一个控制台俄罗斯方块游戏的简单实现. 已在 github.com/SlimeNull/Tetris 开源. 思路 很简单, 一个二维数组存储当前游戏的方块地图, 用 bool 即可, true 表示当前块被填充, false 表示没有. 然后, 抽一个 “形状” 类, 形状表示当前玩家正在操作的一个形状, 例如方块, 直线…...

postman官网下载安装登录详细教程
目录 一、介绍 二、官网下载 三、安装 四、注册登录postman账号(不注册也可以) postman注册登录和不注册登录的使用区别 五、关于汉化的说明 一、介绍 简单来说:是一款前后端都用来测试接口的工具。 展开来说:Postman 是一个…...

(贪心) 剑指 Offer 14- I. 剪绳子 ——【Leetcode每日一题】
❓剑指 Offer 14- I. 剪绳子 难度:中等 给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n > 1 并且 m > 1),每段绳子的长度记为 k[0],k[1]...k[m-1] 。请问 k[0]*k[1]*...*k[m…...

如何将Linux上的cpolar内网穿透设置成 - > 开机自启动
如何将Linux上的cpolar内网穿透设置成 - > 开机自启动 文章目录 如何将Linux上的cpolar内网穿透设置成 - > 开机自启动前言一、进入命令行模式二、输入token码三、输入内网穿透命令 前言 我们将cpolar安装到了Ubuntu系统上,并通过web-UI界面对cpolar的功能有…...
50.两数之和(力扣)
目录 问题描述 核心代码解决 代码思想 时间复杂度和空间复杂度 问题描述 给定一个整数数组 和一个整数目标值 ,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。numstarget 你可以假设每种输入只会对应一个答案。但是&am…...

k8s基础
k8s基础 文章目录 k8s基础一、k8s组件二、k8s组件作用1.master节点2.worker node节点 三、K8S创建Pod的工作流程?四、K8S资源对象1.Pod2.Pod控制器3.service && ingress 五、K8S资源配置信息六、K8s部署1.K8S二进制部署2.K8S kubeadm搭建 七、K8s网络八、K8…...

【自然语言处理】大模型高效微调:PEFT 使用案例
文章目录 一、PEFT介绍二、PEFT 使用2.1 PeftConfig2.2 PeftModel2.3 保存和加载模型 三、PEFT支持任务3.1 Models support matrix3.1.1 Causal Language Modeling3.1.2 Conditional Generation3.1.3 Sequence Classification3.1.4 Token Classification3.1.5 Text-to-Image Ge…...

FFmpeg将编码后数据保存成mp4
以下测试代码实现的功能是:持续从内存块中获取原始数据,然后依次进行解码、编码、最后保存成mp4视频文件。 可保存成单个视频文件,也可指定每个视频文件的总帧数,保存多个视频文件。 为了便于查看和修改,这里将可独立的…...

设置VsCode 将打开的多个文件分行(栏)排列,实现全部显示
目录 1. 前言 2. 设置VsCode 多文件分行(栏)排列显示 1. 前言 主流编程IDE几乎都有排列切换选择所要查看的文件功能,如下为Visual Studio 2022的该功能界面: 图 1 图 2 当在Visual Studio 2022打开很多文件时,可以按照图1、图2所示找到自…...

Vue.js2+Cesium1.103.0 六、标绘与测量
Vue.js2Cesium1.103.0 六、标绘与测量 点,线,面的绘制,可实时编辑图形,点击折线或多边形边的中心点,可进行添加线段移动顶点位置等操作,并同时计算出点的经纬度,折线的距离和多边形的面积。 De…...
【redis 延时队列】使用go-redis的list做异步,生产消费者模式
分享一个用到的,使用go-redis的list做异步,生产消费者模式,接着再用 go 协程去检测队列里是否有东西去消费 如果队列为空,就会一直pop,空轮询导致 cpu 资源浪费和redis qps无效升高,所以可以通过 time.Sec…...

激光焊接塑料多点测试全画面穿透率测试仪
工程塑料由于其具有高比强度、电绝缘性、耐磨性、耐腐蚀性等优点,已广泛应用于各个重要领域。另一方面,工程塑料还具有良好的焊接性,是制成复合材料的基体材料的优良选择,因此目前已成为国内外新型复合材料的研究热点。 工程塑料…...
用 Uno 当烧录器给 atmega328 烧录 bootloader
用 Uno 当烧录器给 atmega328 烧录 bootloader date: 2023-8-10 https://backmountaindevil.github.io/#/hackaday/arduino/isp 引脚接线 把两个板子的 11(MOSI)、12(MISO)、13(SCK)、5V、GND 两两相连,还要把 Uno(烧录器)的 10 接到atmeg…...

spring boot策略模式实用: 告警模块为例
spring boot策略模式实用: 告警模块 0 涉及知识点 策略模式, 模板方法, 代理, 多态, 反射 1 需求概括 场景: 每隔一段时间, 会获取设备运行数据, 如通过温湿度计获取到当前环境温湿度;需求: 对获取回来的进行分析, 超过配置的阈值需要产生对应的告警 2 方案设计 告警的类…...
Camunda 7.x 系列【10】使用 Rest API 运行流程实例
有道无术,术尚可求,有术无道,止于术。 本系列Spring Boot 版本 2.7.9 本系列Camunda 版本 7.19.0 源码地址:https://gitee.com/pearl-organization/camunda-study-demo 文章目录 1. 前言2. 官方接口文档3. 本地接口文档3.1 Postman3.2 Camunda Platform Run Swagger3.3 S…...

Python-OpenCV中的图像处理-边缘检测
Python-OpenCV中的图像处理-边缘检测 边缘检测Canny算子 边缘检测Canny算子 Canny 边缘检测是一种非常流行的边缘检测算法,是 John F.Canny 在 1986 年提出的。它是一个有很多步构成的算法:噪声去除、计算图像梯度、非极大值抑制、滞后阀值等。 Canny(i…...

一文了解Java序列化和反序列化:对象的存储与传输
一文了解Java序列化和反序列化:对象的存储与传输 作者:Stevedash 发布时间:2023年8月9日 21点30分 前言 Java序列化是一项强大而重要的技术,它允许我们将对象转换为字节流,以便在存储、传输和重建时使用。在本文中&…...
react-codemirror2 编辑器需点击一下或者延时才显示数据的问题
现象: <Codemirror/>组件的数据已经赋上值的情况下,初始状态不渲染数据,需要点击编辑框获取焦点后才展示,或者延迟了几秒才显示出来。 原因: 指定了一些依赖的版本,可能不兼容了一些功能,…...

火山引擎联合Forrester发布《中国云原生安全市场现状及趋势白皮书》,赋能企业构建云原生安全体系
国际权威研究咨询公司Forrester 预测,2023年全球超过40%的企业将会采用云原生优先战略。然而,云原生在改变企业上云及构建新一代基础设施的同时,也带来了一系列的新问题,针对涵盖云原生应用、容器、镜像、编排系统平台以及基础设施…...

需要数电发票接口的,先熟悉下数电发票基本常识
最近有一些技术小伙伴来咨询数电发票接口的时候,对数电发票的一些常识不太了解, 导致沟通起来比较困难。比较典型的这三个问题: 一、开具数电票时,如何设置身份认证频次? 请公司的法定代表人或财务负责人登录江苏省电…...

测试微信模版消息推送
进入“开发接口管理”--“公众平台测试账号”,无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息: 关注测试号:扫二维码关注测试号。 发送模版消息: import requests da…...
Ubuntu系统下交叉编译openssl
一、参考资料 OpenSSL&&libcurl库的交叉编译 - hesetone - 博客园 二、准备工作 1. 编译环境 宿主机:Ubuntu 20.04.6 LTSHost:ARM32位交叉编译器:arm-linux-gnueabihf-gcc-11.1.0 2. 设置交叉编译工具链 在交叉编译之前&#x…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...
基于Uniapp开发HarmonyOS 5.0旅游应用技术实践
一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架,支持"一次开发,多端部署",可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务,为旅游应用带来…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...

【单片机期末】单片机系统设计
主要内容:系统状态机,系统时基,系统需求分析,系统构建,系统状态流图 一、题目要求 二、绘制系统状态流图 题目:根据上述描述绘制系统状态流图,注明状态转移条件及方向。 三、利用定时器产生时…...

ios苹果系统,js 滑动屏幕、锚定无效
现象:window.addEventListener监听touch无效,划不动屏幕,但是代码逻辑都有执行到。 scrollIntoView也无效。 原因:这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作,从而会影响…...

Mac下Android Studio扫描根目录卡死问题记录
环境信息 操作系统: macOS 15.5 (Apple M2芯片)Android Studio版本: Meerkat Feature Drop | 2024.3.2 Patch 1 (Build #AI-243.26053.27.2432.13536105, 2025年5月22日构建) 问题现象 在项目开发过程中,提示一个依赖外部头文件的cpp源文件需要同步,点…...
今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存
文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...