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