房產(chǎn)網(wǎng)站定制秦皇島網(wǎng)站seo
一個控制臺俄羅斯方塊游戲的簡單實現(xiàn). 已在 github.com/SlimeNull/Tetris 開源.
思路
很簡單, 一個二維數(shù)組存儲當前游戲的方塊地圖, 用 bool
即可, true
表示當前塊被填充, false
表示沒有.
然后, 抽一個 “形狀” 類, 形狀表示當前玩家正在操作的一個形狀, 例如方塊, 直線, T 形什么的. 一個形狀又有不同的樣式, 也就是玩家可以切換的樣式. 每一個樣式都是原來樣式旋轉(zhuǎn)之后的結(jié)果. 為了方便, 可以直接使用硬編碼的方式存儲所有樣式中方塊的相對坐標.
一個形狀有一個自己的坐標, 并且它包含很多方塊. 在繪制的時候, 獲取它每一個方塊的坐標, 轉(zhuǎn)換為地圖內(nèi)的絕對坐標, 然后使用 StringBuilder
拼接字符串, 即可.
資料
俄羅斯方塊中總共有這七種方塊
類型定義
一個簡單的二維坐標
/// <summary>
/// 表示一個坐標
/// </summary>
/// <param name="X"></param>
/// <param name="Y"></param>
record struct Coordinate(int X, int Y)
{/// <summary>/// 根據(jù)基坐標和相對坐標, 獲取一個絕對坐標/// </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);}
}
形狀的一個樣式, 單純使用坐標數(shù)組存儲即可.
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>/// 從坐標構(gòu)建一個新形狀/// </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 形狀的實現(xiàn)
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),}),};
}
主邏輯
上面的定義已經(jīng)寫好了, 接下來就是寫游戲主邏輯.
主邏輯包含每一回合自動向下移動形狀, 如果無法繼續(xù)向下移動, 則把當前的形狀存儲到地圖中. 并進行一次掃描, 將所有的整行全部消除.
抽一個 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;// 如果移動后方塊會與地圖現(xiàn)有方塊重合, 則不能移動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;// 如果與現(xiàn)有方塊重合, 不能切換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()){// 轉(zhuǎn)為絕對坐標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;}// 當前形狀設(shè)為 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++){// 設(shè)置當前行是整行bool ok = true;// 循環(huán)當前行的所有方塊, 如果方塊為 false, ok 就會被設(shè)為 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];// 最頂行全設(shè)為空for (int x = 0; x < Width; x++)map[x, 0] = false;}}
}
封裝一些用戶操作使用的方法
/// <summary>
/// 根據(jù)指定偏移, 進行移動
/// </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());}
}