天天看點

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#