通过游戏玩法来思考,首先最容易想到的是,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
{
///
/// 游戏核心类,负责处理游戏核心算法,与界面无关
///
// 定义枚举类型表示移动方向
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 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(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;
}
}
}
}