天天看点

c#撸的控制台版2048小游戏

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