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 //忽略020 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 }

View Code

 

    调用的核心代码:

技术图片
技术图片

 1 switch (direction) 2  { 3 case MoveDirection.Up: 4 move = Move(C, 0, R - 1, 1, (x, y) => { 5 fixed (int* _ = &_bs[0, 0]) 6  { 7 return (IntPtr)(_ + y * C + x); 8  } 9  });10 break;11 case MoveDirection.Down:12 move = Move(C, R - 1, 0, -1, (x, y) => {13 fixed (int* _ = &_bs[0,0])14  {15 return (IntPtr)(_ + y * C + x);16  }17  });18 break;19 case MoveDirection.Left:20 move = Move(R, 0, C - 1, 1, (x, y) => {21 fixed (int* _ = &_bs[0, 0])22  {23 return (IntPtr)(_ + x * C + y);24  }25  });26 break;27 case MoveDirection.Right:28 move = Move(R, C - 1, 0, -1, (x,y)=> { 29 fixed(int* _ = &_bs[0, 0])30  {31 return (IntPtr)(_ + x * C + y);32  }33  });34 break;35 }

View Code

 

  2.3结果判定

    网上大多数的算法都是复制一份矩阵数据,然后依次从各个方向上进行合并和移动,之后和原矩阵进行比较,如果数据相同则说明没有变化,从而判定失败。

    这种太复杂,太死板了,太低效了。仔细分析可知,失败的判定其实很简单:

    1.已经没有空位可以随机数字了,说明不可移动。

    2.每个坐标的数字和它旁边的数字都不相等。说明不可合并。

    

    代码如下:

技术图片
技术图片

 1 /// <summary> 2 /// 判断是否可以合并 3 /// </summary> 4 private void CheckGame() 5  { 6 //是否已经填满 并且无法移动 7 for (int x = 0; x < R; x++) 8  { 9 for (int y = 0; y < C; y++)10  {11 if (y < C - 1 && _bs[x, y] == _bs[x, y + 1]) return;12 if (x < R - 1 && _bs[x, y] == _bs[x + 1, y]) return;13  }14  }15 16 State = GameState.Fail;17  }18 19 /// <summary>20 /// 随机在空位生成一个数字21 /// </summary>22 /// <returns></returns>23 private int GenerateNum()24  {25 var ls = new List<(int x, int y)>(R * C);26 for (int x = 0; x < R; x++)27  {28 for (int y = 0; y < C; y++)29  {30 if (_bs[x, y] == 0) ls.Add((x,y));31  }32  } 33 34 var xy = ls[_rnd.Next(ls.Count)];35 _bs[xy.x, xy.y] = _rnd.Next(10) == 9 ? 4 : 2;36 return ls.Count - 1;37 38 }

View Code

 

      因为这个判定必然发生在随机生成数字之后,即上面move返回true时,那么调用代码:

技术图片
技术图片

1 if (move && State != GameState.Succ)2  { 3 //有移动 随机在空位生成数字4 var emptyNum = GenerateNum();5 6 //判断是否结束7 if(emptyNum == 0) CheckGame();8 }

View Code

 

3.完整的代码如下:

Game类:

技术图片
技术图片

 1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6  7 namespace _2048 8 { 9 public enum MoveDirection{ 10  Up, 11  Down, 12  Left, 13  Right 14  } 15  16 public enum GameState 17  { 18  None, 19  Fail, 20  Succ, 21  } 22  23 public class Game 24  { 25 public static int R = 4, C = 4; 26  27 private int[,] _bs; 28 private Random _rnd = new Random(); 29 public GameState State = GameState.None; 30 public int Score, Steps; 31 public (MoveDirection direction, int[,] data)? Log; 32 public bool ShowPre; 33  34 public Game() 35  { 36  Restart();  37  }  38  39 public unsafe void Move(MoveDirection direction) 40  { 41 if (State != GameState.None) return; 42  43 var move = false; 44 var bs = (int[,])_bs.Clone(); 45  46 switch (direction) 47  { 48 case MoveDirection.Up: 49 move = Move(C, 0, R - 1, 1, (x, y) => { 50 fixed (int* _ = &_bs[0, 0]) 51  { 52 return (IntPtr)(_ + y * C + x); 53  } 54  }); 55 break; 56 case MoveDirection.Down: 57 move = Move(C, R - 1, 0, -1, (x, y) => { 58 fixed (int* _ = &_bs[0,0]) 59  { 60 return (IntPtr)(_ + y * C + x); 61  } 62  }); 63 break; 64 case MoveDirection.Left: 65 move = Move(R, 0, C - 1, 1, (x, y) => { 66 fixed (int* _ = &_bs[0, 0]) 67  { 68 return (IntPtr)(_ + x * C + y); 69  } 70  }); 71 break; 72 case MoveDirection.Right: 73 move = Move(R, C - 1, 0, -1, (x,y)=> {  74 fixed(int* _ = &_bs[0, 0]) 75  { 76 return (IntPtr)(_ + x * C + y); 77  } 78  }); 79 break; 80  } 81  82 if (move && State != GameState.Succ) 83  {  84 Steps++; 85  86 Log = (direction, bs); 87  88 //有移动 随机中空位生成数字 89 var emptyNum = GenerateNum(); 90  91 //判断是否结束 92 if(emptyNum == 0) CheckGame(); 93  } 94  } 95  96 /// <summary> 97 /// 判断是否可以合并 98 /// </summary> 99 private void CheckGame()100  {101 //是否已经填满 并且无法移动102 for (int x = 0; x < R; x++)103  {104 for (int y = 0; y < C; y++)105  {106 if (y < C - 1 && _bs[x, y] == _bs[x, y + 1]) return;107 if (x < R - 1 && _bs[x, y] == _bs[x + 1, y]) return;108  }109  }110 111 State = GameState.Fail;112  }113 114 /// <summary>115 /// 随机在空位生成一个数字116 /// </summary>117 /// <returns></returns>118 private int GenerateNum()119  {120 var ls = new List<(int x, int y)>(R * C);121 for (int x = 0; x < R; x++)122  {123 for (int y = 0; y < C; y++)124  {125 if (_bs[x, y] == 0) ls.Add((x,y));126  }127  }128 129  Shuffle(ls);130 131 var xy = ls[_rnd.Next(ls.Count)];132 _bs[xy.x, xy.y] = _rnd.Next(10) == 9 ? 4 : 2;133 return ls.Count - 1;134 135  }136 137 private IList<T> Shuffle<T>(IList<T> arr)138  {139 for (var i = 0; i < arr.Count; i++)140  {141 var index = _rnd.Next(arr.Count);142 var tmp = arr[i];143 arr[i] = arr[index];144 arr[index] = tmp;145  }146 147 return arr;148  }149 150 /// <summary>151 /// 152 /// </summary>153 /// <param name="si">开始索引</param>154 /// <param name="ei">结束索引</param>155 /// <param name="step">方向</param>156 /// <param name="getInt">取值(重定义[x,y]可以保持算法通用 同时满足x,y方向的移动)</param>157 /// <returns></returns>158 private unsafe bool Move(int loop, int si, int ei, int step, Func<int, int, IntPtr> getInt)159  { 160 //算法基于左向移动161 162 bool moved = false; 163 164 for (int x = 0; x < loop; x++)165  { 166 //第一步 合并167 for (int y = si; y * step < ei; y+=step)168  {169 var val1 = (int*)getInt(x, y);170 171 if (*val1 != 0)172  {173 for (var y2 = y + step; y2 != ei + step; y2 += step)174  { 175 var val2 = (int*)getInt(x, y2);176 //忽略0177 if (*val2 == 0) continue;178 //合并179 if (*val1 == *val2)180  {181 *val1 *= 2;182 *val2 = 0;183 moved = true;184 185 Score += *val1;186 187 if (*val1 == 2048) State = GameState.Succ;188 189 //移动处理列索引190 y = y2;191  }192 else y = y2 - step;//不相等193 break;194  }195  } 196 197  }198 199 //第二步 往0位上移动 200 int? lastY = null;201 for (int y = si; y != ei; y += step)202  {203 var val1 = (int*)getInt(x, y);204 205 if (*val1 == 0)206  {207 var y2 = lastY ?? y + step;208 for (; y2 != ei + step; y2 += step)209  {210 var val2 = (int*)getInt(x, y2);211 212 if (*val2 != 0)213  {214 *val1 = *val2;215 *val2 = 0;216 moved = true;217 218 lastY = y2 + step;219 break; 220  }221  }222 //最后一列了223 if (y2 == ei) break;224  } 225  }226  } 227 228 return moved;229  } 230 231 /// <summary>232 /// 重启游戏233 /// </summary>234 public void Restart()235  {236 Score = Steps = 0;237 State = GameState.None;238 Log = null;239 240 _bs = new int[R, C];241 242 for (int i = 0; i < 2; i++)243  {244 var x = _rnd.Next(R);245 var y = _rnd.Next(C);246 if (_bs[x, y] == 0) _bs[x, y] = _rnd.Next(10) == 0 ? 4 : 2;247 else i--;248  }249  }250 251 public void RandNum()252  {253 for (int x = 0; x < R; x++)254  {255 for (int y = 0; y < C; y++)256  {257 _bs[x, y] = (int)Math.Pow(2, _rnd.Next(12));258  } 259  }260  }261 262 public void Show()263  {264 Console.SetCursorPosition(0, 0);265 266 Console.WriteLine($"得分:{Score} 步数:{Steps} [R]键显示上一步操作记录(当前:{ShowPre}) ");267 268  Console.WriteLine();269 270 271 Console.WriteLine(new string(-, C * 5));272 for (int x = 0; x < R; x++)273  {274 for (int y = 0; y < C; y++)275  {276 var b = _bs[x, y];277 Console.Write($"{(b == 0 ? " " : b.ToString()),4}|");278  } 279  Console.WriteLine();280 Console.WriteLine(new string(-, C * 5)); 281  }282 283 if (ShowPre && Log != null)284  {285  Console.WriteLine();286 Console.WriteLine(new string(=, 100));287  Console.WriteLine();288 289 var bs = Log?.data;290 291 Console.WriteLine($"方向:{Log?.direction} ");292  Console.WriteLine();293 294 Console.WriteLine(new string(-, C * 5));295 for (int x = 0; x < R; x++)296  {297 for (int y = 0; y < C; y++)298  {299 var b = bs[x, y];300 Console.Write($"{(b == 0 ? " " : b.ToString()),4}|");301  }302  Console.WriteLine();303 Console.WriteLine(new string(-, C * 5));304  } 305  }306 307  }308 309  }310 }

View Code

 

Main入口:

技术图片
技术图片

 1 static void Main(string[] args) 2  { 3 Game.R = 4; 4 Game.C = 4; 5  6 var game = new Game(); 7  8 while (true) 9  {10  game.Show();11 12 var key = Console.ReadKey();13 switch (key.Key)14  {15 case ConsoleKey.UpArrow:16  game.Move(MoveDirection.Up);17 break;18 case ConsoleKey.DownArrow:19  game.Move(MoveDirection.Down);20 break;21 case ConsoleKey.RightArrow:22  game.Move(MoveDirection.Right);23 break;24 case ConsoleKey.LeftArrow:25  game.Move(MoveDirection.Left);26 break;27 case ConsoleKey.R:28 game.ShowPre = !game.ShowPre;29 break;30 31  }32 if (game.State == GameState.None) continue;33 34  game.Show();35 36 var res = MessageBox.Show("需要重新开始吗?", game.State == GameState.Succ ? "恭喜你!!!成功过关!!!" : "很遗憾!!!失败了!!!",MessageBoxButtons.YesNo);37 if (res == DialogResult.Yes)38  {39  game.Restart();40 continue;41  }42 break;43  }44 45  Console.ReadKey();46 }

View Code

 

相关文章