c#撸的控制台版2048小游戏
1.分析
最近心血来潮,突然想写一个2048小游戏。于是搜索了一个在线2048玩玩,熟悉熟悉规则。
只谈核心规则:(以左移为例)
1.1合并
以行为单位,忽略0位,每列依次向左进行合并,且每列只能合并一次。被合并列置0。
1.2移动
每列依次向左往0位上移动,不限次数。
1.3判定
[成功]就是合并后值为2048,[失败]则是没有任何一个方向上能进行合并或者移动了。
2.实现
一样只谈核心的东西。网上大多数的实现算法有这么几种。
2.1为每个方向上的合并和移动实现一个算法。
这种太过繁琐,其实算法逻辑都差不多,只是方向不同而已,冗余代码太多
2.2以某一个方向作为算法基础,其他方向进行矩阵旋转,直到和基础算法方向一致,处理完成之后,再旋转矩阵到原来方向。
这种做到了各个方向上一定的通用,但是增加了额外的两次矩阵运算。
其实只需实现一个方向的算法,然后抽离出和方向有关的变量,封装为参数,通过参数控制方向。
比如左方向:以行为单位,处理每列的数据。那么第一层循环将是按行的数量进行迭代。处理列索引将上0-最后一列。
比如右方向:以行为单位,处理每列的数据。那么第一层循环将是按行的数量进行迭代。处理列索引将上最后一列-0。
比如上方向:以列为单位,处理每行的数据。那么第一层循环将是按列的数量进行迭代。处理列索引将上0-最后一行。
比如下方向:以列为单位,处理每行的数据。那么第一层循环将是按列的数量进行迭代。处理列索引将上最后一行-0。
变量抽取为:
第一层循环的loop,可以传入行或者列数量。
第二层循环的起始值starti,结束值endi,因为有正和反两个方向,所以还需要一个步长step来控制方向,+1为正,-1为反。
因为是二维数组,所以还需要一个委托,来重定义[x,y]的取值和设置值。比如以行为外层循环的,返回[x,y],以列为外层循环的,返回[y,x]
因为涉及到取值和赋值,用到了指针,也可以用两个方法替代取值和赋值。
代码如下
1 private unsafe bool Move(int loop, int si, int ei, int step, Func<int, int, IntPtr> getInt) 2 { 3 //算法基于左向移动 4 5 bool moved = false; 6 7 for (int x = 0; x < loop; x++) 8 { 9 //第一步 合并 10 for (int y = si; y * step < ei; y+=step) 11 { 12 var val1 = (int*)getInt(x, y); 13 14 if (*val1 != 0) 15 { 16 for (var y2 = y + step; y2 != ei + step; y2 += step) 17 { 18 var val2 = (int*)getInt(x, y2); 19 //忽略0 20 if (*val2 == 0) continue; 21 //合并 22 if (*val1 == *val2) 23 { 24 *val1 *= 2; 25 *val2 = 0; 26 moved = true; 27 28 Score += *val1; 29 30 if (*val1 == 2048) State = GameState.Succ; 31 32 //移动处理列索引 33 y = y2; 34 } 35 else y = y2 - step;//不相等 36 break; 37 } 38 } 39 40 } 41 42 //第二步 往0位上移动 43 int? lastY = null; 44 for (int y = si; y != ei; y += step) 45 { 46 var val1 = (int*)getInt(x, y); 47 48 if (*val1 == 0) 49 { 50 var y2 = lastY ?? y + step; 51 for (; y2 != ei + step; y2 += step) 52 { 53 var val2 = (int*)getInt(x, y2); 54 55 if (*val2 != 0) 56 { 57 *val1 = *val2; 58 *val2 = 0; 59 moved = true; 60 61 lastY = y2 + step; 62 break; 63 } 64 } 65 //最后一列了 66 if (y2 == ei) break; 67 } 68 } 69 } 70 71 return moved; 72 }
调用的核心代码:
switch (direction) { case MoveDirection.Up: move = Move(C, 0, R - 1, 1, (x, y) => { fixed (int* _ = &_bs[0, 0]) { return (IntPtr)(_ + y * C + x); } }); break; case MoveDirection.Down: move = Move(C, R - 1, 0, -1, (x, y) => { fixed (int* _ = &_bs[0,0]) { return (IntPtr)(_ + y * C + x); } }); break; case MoveDirection.Left: move = Move(R, 0, C - 1, 1, (x, y) => { fixed (int* _ = &_bs[0, 0]) { return (IntPtr)(_ + x * C + y); } }); break; case MoveDirection.Right: move = Move(R, C - 1, 0, -1, (x,y)=> { fixed(int* _ = &_bs[0, 0]) { return (IntPtr)(_ + x * C + y); } }); break; }
2.3结果判定
网上大多数的算法都是复制一份矩阵数据,然后依次从各个方向上进行合并和移动,之后和原矩阵进行比较,如果数据相同则说明没有变化,从而判定失败。
这种太复杂,太死板了,太低效了。仔细分析可知,失败的判定其实很简单:
1.已经没有空位可以随机数字了,说明不可移动。
2.每个坐标的数字和它旁边的数字都不相等。说明不可合并。
代码如下:
/// <summary> /// 判断是否可以合并 /// </summary> private void CheckGame() { //是否已经填满 并且无法移动 for (int x = 0; x < R; x++) { for (int y = 0; y < C; y++) { if (y < C - 1 && _bs[x, y] == _bs[x, y + 1]) return; if (x < R - 1 && _bs[x, y] == _bs[x + 1, y]) return; } } State = GameState.Fail; } /// <summary> /// 随机在空位生成一个数字 /// </summary> /// <returns></returns> private int GenerateNum() { var ls = new List<(int x, int y)>(R * C); for (int x = 0; x < R; x++) { for (int y = 0; y < C; y++) { if (_bs[x, y] == 0) ls.Add((x,y)); } } var xy = ls[_rnd.Next(ls.Count)]; _bs[xy.x, xy.y] = _rnd.Next(10) == 9 ? 4 : 2; return ls.Count - 1; }
因为这个判定必然发生中随机生成数字之后,即上面move返回true时,那么调用代码:
if (move && State != GameState.Succ) { //有移动 随机在空位生成数字 var emptyNum = GenerateNum(); //判断是否结束 if(emptyNum == 0) CheckGame(); }
3.完整的代码如下:
Game类:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _2048 { public enum MoveDirection{ Up, Down, Left, Right } public enum GameState { None, Fail, Succ, } public class Game { public static int R = 4, C = 4; private int[,] _bs; private Random _rnd = new Random(); public GameState State = GameState.None; public int Score, Steps; public (MoveDirection direction, int[,] data)? Log; public bool ShowPre; public Game() { Restart(); } public unsafe void Move(MoveDirection direction) { if (State != GameState.None) return; var move = false; var bs = (int[,])_bs.Clone(); switch (direction) { case MoveDirection.Up: move = Move(C, 0, R - 1, 1, (x, y) => { fixed (int* _ = &_bs[0, 0]) { return (IntPtr)(_ + y * C + x); } }); break; case MoveDirection.Down: move = Move(C, R - 1, 0, -1, (x, y) => { fixed (int* _ = &_bs[0,0]) { return (IntPtr)(_ + y * C + x); } }); break; case MoveDirection.Left: move = Move(R, 0, C - 1, 1, (x, y) => { fixed (int* _ = &_bs[0, 0]) { return (IntPtr)(_ + x * C + y); } }); break; case MoveDirection.Right: move = Move(R, C - 1, 0, -1, (x,y)=> { fixed(int* _ = &_bs[0, 0]) { return (IntPtr)(_ + x * C + y); } }); break; } if (move && State != GameState.Succ) { Steps++; Log = (direction, bs); //有移动 随机中空位生成数字 var emptyNum = GenerateNum(); //判断是否结束 if(emptyNum == 0) CheckGame(); } } /// <summary> /// 判断是否可以合并 /// </summary> private void CheckGame() { //是否已经填满 并且无法移动 for (int x = 0; x < R; x++) { for (int y = 0; y < C; y++) { if (y < C - 1 && _bs[x, y] == _bs[x, y + 1]) return; if (x < R - 1 && _bs[x, y] == _bs[x + 1, y]) return; } } State = GameState.Fail; } /// <summary> /// 随机在空位生成一个数字 /// </summary> /// <returns></returns> private int GenerateNum() { var ls = new List<(int x, int y)>(R * C); for (int x = 0; x < R; x++) { for (int y = 0; y < C; y++) { if (_bs[x, y] == 0) ls.Add((x,y)); } } Shuffle(ls); var xy = ls[_rnd.Next(ls.Count)]; _bs[xy.x, xy.y] = _rnd.Next(10) == 9 ? 4 : 2; return ls.Count - 1; } private IList<T> Shuffle<T>(IList<T> arr) { for (var i = 0; i < arr.Count; i++) { var index = _rnd.Next(arr.Count); var tmp = arr[i]; arr[i] = arr[index]; arr[index] = tmp; } return arr; } /// <summary> /// /// </summary> /// <param name="si">开始索引</param> /// <param name="ei">结束索引</param> /// <param name="step">方向</param> /// <param name="getInt">取值(重定义[x,y]可以保持算法通用 同时满足x,y方向的移动)</param> /// <returns></returns> private unsafe bool Move(int loop, int si, int ei, int step, Func<int, int, IntPtr> getInt) { //算法基于左向移动 bool moved = false; for (int x = 0; x < loop; x++) { //第一步 合并 for (int y = si; y * step < ei; y+=step) { var val1 = (int*)getInt(x, y); if (*val1 != 0) { for (var y2 = y + step; y2 != ei + step; y2 += step) { var val2 = (int*)getInt(x, y2); //忽略0 if (*val2 == 0) continue; //合并 if (*val1 == *val2) { *val1 *= 2; *val2 = 0; moved = true; Score += *val1; if (*val1 == 2048) State = GameState.Succ; //移动处理列索引 y = y2; } else y = y2 - step;//不相等 break; } } } //第二步 往0位上移动 int? lastY = null; for (int y = si; y != ei; y += step) { var val1 = (int*)getInt(x, y); if (*val1 == 0) { var y2 = lastY ?? y + step; for (; y2 != ei + step; y2 += step) { var val2 = (int*)getInt(x, y2); if (*val2 != 0) { *val1 = *val2; *val2 = 0; moved = true; lastY = y2 + step; break; } } //最后一列了 if (y2 == ei) break; } } } return moved; } /// <summary> /// 重启游戏 /// </summary> public void Restart() { Score = Steps = 0; State = GameState.None; Log = null; _bs = new int[R, C]; for (int i = 0; i < 2; i++) { var x = _rnd.Next(R); var y = _rnd.Next(C); if (_bs[x, y] == 0) _bs[x, y] = _rnd.Next(10) == 0 ? 4 : 2; else i--; } } public void RandNum() { for (int x = 0; x < R; x++) { for (int y = 0; y < C; y++) { _bs[x, y] = (int)Math.Pow(2, _rnd.Next(12)); } } } public void Show() { Console.SetCursorPosition(0, 0); Console.WriteLine($"得分:{Score} 步数:{Steps} [R]键显示上一步操作记录(当前:{ShowPre}) "); Console.WriteLine(); Console.WriteLine(new string('-', C * 5)); for (int x = 0; x < R; x++) { for (int y = 0; y < C; y++) { var b = _bs[x, y]; Console.Write($"{(b == 0 ? " " : b.ToString()),4}|"); } Console.WriteLine(); Console.WriteLine(new string('-', C * 5)); } if (ShowPre && Log != null) { Console.WriteLine(); Console.WriteLine(new string('=', 100)); Console.WriteLine(); var bs = Log?.data; Console.WriteLine($"方向:{Log?.direction} "); Console.WriteLine(); Console.WriteLine(new string('-', C * 5)); for (int x = 0; x < R; x++) { for (int y = 0; y < C; y++) { var b = bs[x, y]; Console.Write($"{(b == 0 ? " " : b.ToString()),4}|"); } Console.WriteLine(); Console.WriteLine(new string('-', C * 5)); } } } } }
Main入口:
static void Main(string[] args) { Game.R = 4; Game.C = 4; var game = new Game(); while (true) { game.Show(); var key = Console.ReadKey(); switch (key.Key) { case ConsoleKey.UpArrow: game.Move(MoveDirection.Up); break; case ConsoleKey.DownArrow: game.Move(MoveDirection.Down); break; case ConsoleKey.RightArrow: game.Move(MoveDirection.Right); break; case ConsoleKey.LeftArrow: game.Move(MoveDirection.Left); break; case ConsoleKey.R: game.ShowPre = !game.ShowPre; break; } if (game.State == GameState.None) continue; game.Show(); var res = MessageBox.Show("需要重新开始吗?", game.State == GameState.Succ ? "恭喜你!!!成功过关!!!" : "很遗憾!!!失败了!!!",MessageBoxButtons.YesNo); if (res == DialogResult.Yes) { game.Restart(); continue; } break; } Console.ReadKey(); }