C# 实现2048核心算法(附思路)

通过游戏玩法来思考,首先最容易想到的是,2048只有四个移动方向,可以用差不多的方法来实现这四种操作。玩家选定一个移动方向之后,该方向上相同的数要进行一次相加操作,且只能加一次,然后所有的非零数堆积到移动方向上。

2048的游戏界面可以看做是一个二维数组。我们的所有操作,实际上都是针对这一个二维数组的。我们可以将二维数组看成多个一维数组来处理,比如左右移动时一行一行的处理,上下移动时一列一列的处理。接下来以向右移动为例去实现它。

假设当前在某一行,我们希望实现数字的相加。首先将这一行读取出来,当做一维数组来处理。逐个枚举每一个数字,如果有相邻且相等的数就直接相加。这时候问题就来了,相等的数中间隔着0怎么处理?我们可以选择用一个变量记录之前的非零数,然后跳过0,继续枚举后面的数,遇到相同的再相加,最后将一整行的非零数都移到右边即可。在这里不妨换种思路,为何不先将所有的零数移动到最左边后再去执行加法呢?
读取一行数字后,我们先将所有的零存进进一个新数组的左侧,2 0 2 0 就成了 0 0 2 2。然后从最右边开始,将相同且相邻的数字相加,后一个数置0,防止相加后又参与了相加的问题;接着继续枚举下一个数字,处理完后再进行一次移0操作,然后将结果返回给二维数组。

确定了相加的算法后,继续思考其过程可以发现:数字是往玩家操作的方向堆积的,但相加的方向是反过来的。例如数字向右移动,但却是从最右边开始往左相加的;数字向上移动,但却是从最上面开始往下相加的。不管向哪个方向移动,移0和复制的操作都是一样的,因此在考虑这一块时只需要注意实现上的细微差别即可。

另外就是需要设计随机数的生成。随机数是在空白格子上随机生成的,因此要定义一个空白格子结构体,结构体内存的是空白格子在二维数组中的下标。用一个结构体数组来存储每次移动后的所有空白格子,随机挑选一个空白格子,随机生成2或者4(两者生成概率最好不要完全一样)即可。

大概思路就是酱了,语文不好,表述能力不是很强,但是想法都是在代码中的。因为自学的是U3D游戏开发,所以用的是 C# 语言。很容易换成 C/C++ 来实现,所以仍然有借鉴的价值。不多说啦,结合文字和代码能够理解的更快,加油同学!

using System;
using System.Collections.Generic;

namespace Console2048
{
    /// <summary>
    /// 游戏核心类,负责处理游戏核心算法,与界面无关
    /// </summary>

    // 定义枚举类型表示移动方向
    enum MoveDirection
    {
        Up = 0,
        Down = 1,
        Left = 2,
        Right = 3
    }

    // 空白格子结构体
    struct BlankLocation
    {
        public int RIndex { get; set; }
        public int CIndex { get; set; }

        public BlankLocation(int rIndex, int cIndex) : this()
        { // 结构体自动属性要求给字段赋值
            this.RIndex = rIndex;
            this.CIndex = cIndex;
        }
    }

    class GameCore // 核心类不适合做成静态类,因此方法也不做成静态类
    {
        #region 字段定义
        private int[,] map;
        private int[] mergeArray;
        private int[] removeZeroArray;
        private Random random;
        private int[,] oldMap;
        public bool IsChange { get; set; } // IsChange属性标记是否产生了合并操作
        // public bool IsFull { get; set; } // IsFull属性标记数字是否已经满了
        public bool IsWin { get; set; } // IsWin属性标记玩家是否获胜
        public bool CanChange { get; set; } // CanChange属性标志是否还能合并
        public int Score { get; set; } // Score属性记录玩家最终得分
        public List<BlankLocation> emptyBlankList;
        #endregion


        #region 属性
        public int[,] Map
        {
            get
            { return this.map; }
        }
        #endregion

        #region 构造函数
        public GameCore()
        {
            map = new int[4, 4];
            mergeArray = new int[4]; // 暂存每一行或每一列移动之前的数据
            removeZeroArray = new int[4]; // 保存移动后的数据
            emptyBlankList = new List<BlankLocation>(16); // 初始化格子列表,长度为16
            random = new Random(); // 随机数
            oldMap = new int[4, 4]; // 记录每次移动之前所有数据的数组
        }
        #endregion

        #region 数据合并
        private void RemoveZero() // 后移0
        { // 将0全部后移,转换一下思路就是将所有非0元素赋值给一个新的数组
            Array.Clear(removeZeroArray, 0, 4); // 每次都清零数组
            for (int i = 0, j = 0; i < mergeArray.Length; i++) // 长度用 mergeArray.Length 可以让2048扩展到 n x n
            {
                if (mergeArray[i] != 0)
                    removeZeroArray[j++] = mergeArray[i];
            }
            removeZeroArray.CopyTo(mergeArray, 0); // 将新数组的数据复制给mergeArray
        }
        private void Merge() // 相加
        { // 将相邻且相同的数据相加,已经做过加法的就不加
            RemoveZero(); // 先将所有0后移
            for (int i = 0; i < mergeArray.Length - 1; i++)
            {
                if (mergeArray[i] != 0 && mergeArray[i] == mergeArray[i + 1]) // 非0数据才做相加
                {
                    mergeArray[i] *= 2; // 相同且相邻则相加
                    Score += mergeArray[i] * mergeArray[i]; // 更新得分
                    mergeArray[i + 1] = 0; // 相邻数据置0
                }
            }
            RemoveZero(); // 再次将所有0后移
        }
        #endregion

        #region 移动和检查
        private void MoveToUp() // 上移
        {
            for (int c = 0; c < map.GetLength(1); c++) // GetLength(1) 返回的是二维数组的列数
            {
                for (int r = 0; r < map.GetLength(0); r++)
                    mergeArray[r] = map[r, c]; // 将 map 中的一列数据复制给 mergeArray
                Merge(); // 进行相加操作
                for (int r = 0; r < map.GetLength(0); r++) // 将相加后的结果返回给二维数组
                    map[r, c] = mergeArray[r];
            }
        }
        private void MoveToDown() // 下移
        {
            for (int c = 0; c < map.GetLength(1); c++)
            {
                for (int r = map.GetLength(0) - 1; r >= 0; r--) // 倒着复制
                    mergeArray[3 - r] = map[r, c];
                Merge(); // 进行相加操作
                for (int r = map.GetLength(0) - 1; r >= 0; r--) // 将相加后的结果返回给二维数组
                    map[r, c] = mergeArray[3 - r];
            }
        }
        private void MoveToLeft() // 左移
        {
            for (int r = 0; r < map.GetLength(0); r++)
            {
                for (int c = 0; c < map.GetLength(1); c++)
                    mergeArray[c] = map[r, c]; // 将 map 中的一行数据复制给 mergeArray
                Merge(); // 进行相加操作
                for (int c = 0; c < map.GetLength(1); c++) // 将相加后的结果返回给二维数组
                    map[r, c] = mergeArray[c];
            }
        }
        private void MoveToRight() // 右移
        {
            for (int r = 0; r < map.GetLength(0); r++)
            {
                for (int c = map.GetLength(1) - 1; c >= 0; c--) // 倒着复制
                    mergeArray[3 - c] = map[r, c];
                Merge(); // 进行相加操作
                for (int c = map.GetLength(1) - 1; c >= 0; c--) // 将相加后的结果返回给二维数组
                    map[r, c] = mergeArray[3 - c];
            }
        }
        private void Check() // 检查是否产生不了合并
        {
            CanChange = false;
            for (int r = 0; r < map.GetLength(0); r++) // 外层 for 循环按行枚举
            {
                for (int c = 0; c < map.GetLength(1); c++) // 内层 for 循环按列枚举
                {
                    if (r != map.GetLength(1) - 1 && c != map.GetLength(1) - 1)
                    {
                        if (map[r, c] == map[r, c + 1] || map[r, c] == map[r + 1, c])
                        { // 对于中间3x3的格子只需比较向后和向下的元素
                            CanChange = true;
                            return;
                        }
                    }
                    else if (r == map.GetLength(1) - 1 && c != map.GetLength(1) - 1)
                    { // 对于处于第四行但不在第四列的数据只需同后一列数据比较
                        if (map[r, c] == map[r, c + 1])
                        { 
                            CanChange = true;
                            return;
                        }
                    }
                    else if (r != map.GetLength(1) - 1 && c == map.GetLength(1) - 1)
                    {
                        if (map[r, c] == map[r + 1, c])
                        { // 对于处于第四列但不在第四行的数据只需同后一列数据比较
                            CanChange = true;
                            return;
                        }
                    }  // 剩下一个就是在第四行第四列的元素就不用继续和谁比较了
                }
            }
        }
        public void Move(MoveDirection direction)
        { // 移动前记录map
            Array.Copy(map, oldMap, map.Length);
            IsChange = false; // 移动之前赋值“无变化”
            IsWin = false;
            CanChange = true;

            switch (direction)
            {
                case MoveDirection.Up:
                    MoveToUp();
                    break;
                case MoveDirection.Down:
                    MoveToDown();
                    break;
                case MoveDirection.Left:
                    MoveToLeft();
                    break;
                case MoveDirection.Right:
                    MoveToRight();
                    break;
            }

            // 用户操作之后对比map是否产生变化
            for (int r = 0; r < map.GetLength(0); r++)
            {
                for (int c = 0; c < map.GetLength(1); c++)
                {
                    if (map[r, c] == 2048) // 如果出现了2048,游戏胜利就结束
                    {
                        IsWin = true;
                        return;
                    }
                    if (map[r, c] != oldMap[r, c])
                    {
                        IsChange = true; // 产生变化返回true指示产生随机数
                        return;
                    }
                    if (!IsChange)
                        Check();
                }
            }
        }
        #endregion

        #region 生成随机数字
        // 生成数字
        // 需求:在空白位置,随机产生一个2(90%)或者4(10%)
        // 分析:先统计所有空白位置,再随机选择一个位置随机填入2或4
        private void CalculateEmpty()
        {
            emptyBlankList.Clear(); // 每次统计空位置前先清空列表
            for (int r = 0; r < map.GetLength(0); r++)
            {
                for (int c = 0; c < map.GetLength(1); c++)
                {
                    if (map[r, c] == 0)
                    {
                        // 记录空白位置的索引,因为个数不确定,所以用集合
                        // 类是将多个基本数据类型,封装为一个自定义类型
                        emptyBlankList.Add(new BlankLocation(r, c));
                    }
                }
            }
        }
        public void GenerateNumber()
        {
            CalculateEmpty();
            //IsFull = true;
            if (emptyBlankList.Count > 0) // 如果有空位置的话
            {
                // 随机挑选一个空位置,然后把它的索引返回给loc
                //IsFull = false;
                int randomIndex = random.Next(0, emptyBlankList.Count);
                BlankLocation loc = emptyBlankList[randomIndex];

                // 生成4的概率是30%,相当于0~9生成0,1,2
                int ran = random.Next(0, 10);
                if (ran == 0 || ran == 1 || ran == 2)
                    map[loc.RIndex, loc.CIndex] = 4;
                else
                    map[loc.RIndex, loc.CIndex] = 2;
            }
            /*else
                IsFull = true;*/
        }
        #endregion
    }

    class Program
    {
        static void Main()
        {
            GameCore core = new GameCore();

            // 先生成两个随机数
            core.GenerateNumber();
            core.GenerateNumber();
            // 将控制台背景色改成白色
            Console.BackgroundColor = ConsoleColor.White;
            // 显示游戏界面
            DrawMap(core.Map);

            while (true)
            { // 用户操作
                UserAction(core);
                core.GenerateNumber();
                DrawMap(core.Map);
                if (core.IsWin)
                { // 合出了2048,游戏胜利
                    Console.WriteLine("\t\t\t\t\t恭喜你,游戏胜利!");
                    Console.WriteLine("\t\t\t\t\t您最终的分数是:{0}", core.Score);
                }
                if (core.emptyBlankList.Count == 0 && !core.CanChange)
                { // 如果没有空位置生成随机数,且在各个方向上都已经无法合并(即没有任何改变了)了则表明游戏结束
                    Console.WriteLine("\t\t\t\t\t没有空位置了!");
                    Console.WriteLine("\t\t\t\t\t  游戏结束!");
                    Console.WriteLine("\t\t\t\t\t您最终的分数是:{0}", core.Score);
                    break;
                }
            }
        }

        private static void DrawMap(int[,] map)
        { // 因为 Main 函数是静态函数,调用不了实例,索引函数声明为静态的
            Console.Clear();
            Console.WriteLine("\n\n\n\n\n");
            int number = 0;
            for (int r = 0; r < 4; r++)
            {
                Console.Write("\t\t\t\t\t");
                for (int c = 0; c < 4; c++)
                {
                    number = map[r, c]; // 数字暂存二维数组中的数,防止每个分支都访问一次二维数组
                    // 根据数字不同选择不同的颜色输出
                    if (number == 0)
                        Console.ForegroundColor = ConsoleColor.Black;
                    else if (number == 2)
                        Console.ForegroundColor = ConsoleColor.Gray;
                    else if (number == 4)
                        Console.ForegroundColor = ConsoleColor.Red;
                    else if (number == 8)
                        Console.ForegroundColor = ConsoleColor.Green;
                    else if (number == 16)
                        Console.ForegroundColor = ConsoleColor.Yellow;
                    else if (number == 32)
                        Console.ForegroundColor = ConsoleColor.Blue;
                    else if (number == 64)
                        Console.ForegroundColor = ConsoleColor.Magenta;
                    else if (number == 128)
                        Console.ForegroundColor = ConsoleColor.Cyan;
                    else if (number == 256)
                        Console.ForegroundColor = ConsoleColor.DarkYellow;
                    else if (number == 512)
                        Console.ForegroundColor = ConsoleColor.DarkBlue;
                    else if (number == 1024)
                        Console.ForegroundColor = ConsoleColor.DarkGreen;
                    else
                        Console.ForegroundColor = ConsoleColor.DarkRed;
                    Console.Write(number + "\t");
                }
                Console.WriteLine("\n"); // 换行
            }
        }

        private static void UserAction(GameCore core)
        {
            Console.Write("\t\t\t\t\t");
            switch (Console.ReadLine())
            {
                case "w":
                case "W":
                    core.Move(MoveDirection.Up);
                    break;
                case "s":
                case "S":
                    core.Move(MoveDirection.Down);
                    break;
                case "a":
                case "A":
                    core.Move(MoveDirection.Left);
                    break;
                case "d":
                case "D":
                    core.Move(MoveDirection.Right);
                    break;
            }
        }
    }
}

  • 13
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值