[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());}
}

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

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

相关文章

OCR让纸质文档秒变电子文档,让自动驾驶成为现实

OCR文字识别具有广泛的应用范围&#xff0c;以下是一些常见和广泛应用的领域&#xff1a; 1. 文档数字化&#xff1a;OCR可以将印刷的文档、书籍、报纸等纸质文档转换为可编辑和可搜索的电子文本&#xff0c;从而实现文档的数字化存储和管理。这在图书馆、档案馆、企业和政府机…

关于eclipse导入部署具有增删改查的项目

目录 前言&#xff1a;当我们刚刚进入公司的第一步就是去部署当前公司的项目&#xff0c;本博客就是详细介绍怎么去部署当前公司的项目。 一&#xff0c;开发工具&#xff1a; 二&#xff0c;具体步骤&#xff1a; 2.1导入公司的项目 打开eclipse开发工具 2.2配置当前的环…

Django实现音乐网站 ⑺

使用Python Django框架制作一个音乐网站&#xff0c; 本篇主要是后台对歌手原有实现功能的基础上进行优化处理。 目录 新增编辑 表字段名称修改 隐藏单曲、专辑数 姓名首字母 安装xpinyin 获取姓名首字母 重写保存方法 列表显示 图片显示处理 引入函数 路径改为显示…

oracle sql developer批量删除某个用户

随着navicate收费&#xff0c;还得破解&#xff0c;pl/sql developer配置麻烦&#xff0c;最近使用oracle sql developer来试试oracle的操作如何&#xff1b; 用着还行&#xff0c;没有卡顿现象&#xff0c; 最近要oracle sql developer批量删除某个用户下所有的表&#xff0…

如何恢复已删除的 PDF 文件 - Windows 11、10

在传输数据或共享专业文档时&#xff0c;大多数人依赖PDF文件格式&#xff0c;但很少知道如何恢复意外删除或丢失的PDF文件。这篇文章旨在解释如何有效地恢复 PDF 文件。如果您身边有合适的数据恢复工具&#xff0c;PDF 恢复并不像看起来那么复杂。 便携式文档格式&#xff08…

原型模式与享元模式:提升系统性能的利器

原型模式和享元模式&#xff0c;前者是在创建多个实例时&#xff0c;对创建过程的性能进行调优&#xff1b;后者是用减 少创建实例的方式&#xff0c;来调优系统性能。这么看&#xff0c;你会不会觉得两个模式有点相互矛盾呢&#xff1f; 在有些场景下&#xff0c;我们需要重复…

23款奔驰GLS450时尚型升级原厂香氛负离子系统,清香宜人,久闻不腻

奔驰原厂香氛合理性可通过车内空气调节组件营造芳香四溢的怡人氛围。通过更换手套箱内香氛喷雾发生器所用的香水瓶&#xff0c;可轻松选择其他香氛。香氛的浓度和持续时间可调。淡雅的香氛缓缓喷出&#xff0c;并且在关闭后能够立刻散去。车内气味不会永久改变&#xff0c;香氛…

解数独(Java)

题目链接&#xff1a; 力扣 题目详情&#xff1a; 37. 解数独t编写一个程序&#xff0c;通过填充空格来解决数独问题。 数独的解法需 遵循如下规则&#xff1a; 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3 宫内只…

nodejs实现解析chm文件列表,无需转换为PDF文件格式,在线预览chm文件以及目录,不依赖任何网页端插件

特性: 1、支持任意深度的chm文件解析 2、解析后内容结构转换为tree数据呈现 3、点击树节点可以在html实时查看数据 4、不依赖任何浏览器端插件,兼容性较好 nodejs端核心代码 const $g = global.SG.$g, fs = global.SG.fs, router = global.SG.router, xlsx = global.SG.xl…

【ADS】在同一个symbol层次化实现理想器件和EM仿真的切换

ADS层次化切换理想器件和电磁仿真 1.需求描述2.实现步骤2.1 层次结构2.2 新建schematic2.3 新建symbol2.4 使用演示 3.后言 1.需求描述 在使用ADS做电磁仿真时&#xff0c;得到的结果需要多次迭代去接近原理图&#xff0c;那么就需要反复与原理图切换进行对比。 比较直接的方法…

canvas实现代码雨

学习抖音&#xff1a; 渡一前端必修课 效果图&#xff1a; 全部代码&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge">&…

flinksql sink to sr often fail because of nullpoint

flinksql or DS sink to starrocks often fail because of nullpoint flink sql 和 flink ds sink starrocks 经常报NullpointException重新编译代码 并上传到flink 集群 验证&#xff0c;有效 flink sql 和 flink ds sink starrocks 经常报NullpointException 使用flink-sta…

PHP流浪动物招领网站mysql数据库web结构apache计算机软件工程网页wamp

一、源码特点 PHP流浪动物招领网站 是一套完善的web设计系统&#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 下载链接 nullhttps://download.csdn.net/download/qq_41221322/88190168视频演示 …

观察混合云环境中 Kubernetes 可观测性的 6 种有效策略

在混合云环境中观察Kubernetes需要理解分布式系统的行为和性能。我下面这篇文章中的六个策略可以帮助实现这一目标。 2023年&#xff0c;原生云应用和平台迅速增长。组织不断努力最大化其应用程序的潜力&#xff0c;确保无缝的用户体验&#xff0c;并推动业务增长。混合云环境…

O2OA开发平台实施入门指南

O2OA&#xff08;翱途&#xff09;开发平台&#xff0c;是一款适用于协同办公系统开发与实施的基础平台&#xff0c;说到底&#xff0c;它也是一款快速开发平台。开发者可以基于平台提供的能力完成门户、流程、信息相关的业务功能开发。 既然定位为开发平台&#xff0c;那么开…

RocketMQ 5.x如何使用GRPC方式发送消费消息

这里是weihubeats,觉得文章不错可以关注公众号小奏技术&#xff0c;文章首发。拒绝营销号&#xff0c;拒绝标题党 RocketMQ版本 5.1.0 背景 我们都知道RocketMQ 5.x新增了proxy模式部署方式&#xff0c;也就是支持了GRPC的消费方式消费&#xff0c;所以今天我们来试试 本次…

SAN共享存储架构

SAN共享存储架构 概述 近年在高性能专用存储网络需求的驱使下&#xff0c;许多SAN存储系统应用于高性能计算网络系统、大型网站系统、非线性编辑系统等网络系统中&#xff0c;存储设备与计算机主机系统之间一对一的关系&#xff0c;被可供多个计算机主机共享读写同一个存储设…

探究Vue源码:mustache模板引擎(10) 解决不能用连续点符号找到多层对象问题,为编译循环结构做铺垫

上文 探究Vue源码:mustache模板引擎(9) 将单层无喜欢结果tokens转为dom字符串 我们简单处理了 token转字符串的业务逻辑 但是 我们只处理了最贱的花括号 接下来 带着大家将井号的也处理一下 我们打开项目 将 www中的index.html代码改回之前的这样 <!DOCTYPE html> <h…

改进的麻雀算法优化最大相关峭度解卷积(SCSSA-MCKD),实现早期微弱故障诊断,MATLAB代码实现

01 引言 由于一些设备的早期故障产生的冲击十分微弱&#xff0c;易被系统噪声干扰&#xff0c;如何有效地对设备的原始故障信号进行降噪并增强信号中微弱冲击成分&#xff0c;是进行该类部件早期故障诊断的关键。 最大相关峭度解卷积&#xff08;MCKD&#xff09;通过解卷积运算…

leetcode 399-除法求值

法一&#xff1a;并查集 分析示例1&#xff1a; a / b 2.0 a/ b 2.0 a/b2.0&#xff0c;说明 a 2 b a2b a2b&#xff0c; a a a和 b b b在同一个集合中 b / c 3.0 b/c3.0 b/c3.0&#xff0c;说明 b 3 c b3c b3c&#xff0c; b b b和 c c c在同一个集合中 求 a / c a/…