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]

      

      因为涉及到取值和赋值,用到了指针,也可以用两个方法替代取值和赋值。

      代码如下

 private unsafe bool Move(int loop, int si, int ei, int step, Func<int, int, IntPtr> getInt)
{
//算法基于左向移动 bool moved = false; for (int x = ; x < loop; x++)
{
//第一步 合并
for (int y = si; y * step < ei; y+=step)
{
var val1 = (int*)getInt(x, y); if (*val1 != )
{
for (var y2 = y + step; y2 != ei + step; y2 += step)
{
var val2 = (int*)getInt(x, y2);
//忽略0
if (*val2 == ) continue;
//合并
if (*val1 == *val2)
{
*val1 *= ;
*val2 = ;
moved = true; Score += *val1; if (*val1 == ) 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 == )
{
var y2 = lastY ?? y + step;
for (; y2 != ei + step; y2 += step)
{
var val2 = (int*)getInt(x, y2); if (*val2 != )
{
*val1 = *val2;
*val2 = ;
moved = true; lastY = y2 + step;
break;
}
}
//最后一列了
if (y2 == ei) break;
}
}
} return moved;
}

    调用的核心代码:

 switch (direction)
{
case MoveDirection.Up:
move = Move(C, , R - , , (x, y) => {
fixed (int* _ = &_bs[, ])
{
return (IntPtr)(_ + y * C + x);
}
});
break;
case MoveDirection.Down:
move = Move(C, R - , , -, (x, y) => {
fixed (int* _ = &_bs[,])
{
return (IntPtr)(_ + y * C + x);
}
});
break;
case MoveDirection.Left:
move = Move(R, , C - , , (x, y) => {
fixed (int* _ = &_bs[, ])
{
return (IntPtr)(_ + x * C + y);
}
});
break;
case MoveDirection.Right:
move = Move(R, C - , , -, (x,y)=> {
fixed(int* _ = &_bs[, ])
{
return (IntPtr)(_ + x * C + y);
}
});
break;
}

  2.3结果判定

    网上大多数的算法都是复制一份矩阵数据,然后依次从各个方向上进行合并和移动,之后和原矩阵进行比较,如果数据相同则说明没有变化,从而判定失败。

    这种太复杂,太死板了,太低效了。仔细分析可知,失败的判定其实很简单:

    1.已经没有空位可以随机数字了,说明不可移动。

    2.每个坐标的数字和它旁边的数字都不相等。说明不可合并。

    

    代码如下:

 /// <summary>
/// 判断是否可以合并
/// </summary>
private void CheckGame()
{
//是否已经填满 并且无法移动
for (int x = ; x < R; x++)
{
for (int y = ; y < C; y++)
{
if (y < C - && _bs[x, y] == _bs[x, y + ]) return;
if (x < R - && _bs[x, y] == _bs[x + , 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 = ; x < R; x++)
{
for (int y = ; y < C; y++)
{
if (_bs[x, y] == ) ls.Add((x,y));
}
} var xy = ls[_rnd.Next(ls.Count)];
_bs[xy.x, xy.y] = _rnd.Next() == ? : ;
return ls.Count - ; }

      因为这个判定必然发生中随机生成数字之后,即上面move返回true时,那么调用代码:

    if (move && State != GameState.Succ)
{
//有移动 随机在空位生成数字
var emptyNum = GenerateNum(); //判断是否结束
if(emptyNum == ) 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 = , C = ; 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, , R - , , (x, y) => {
fixed (int* _ = &_bs[, ])
{
return (IntPtr)(_ + y * C + x);
}
});
break;
case MoveDirection.Down:
move = Move(C, R - , , -, (x, y) => {
fixed (int* _ = &_bs[,])
{
return (IntPtr)(_ + y * C + x);
}
});
break;
case MoveDirection.Left:
move = Move(R, , C - , , (x, y) => {
fixed (int* _ = &_bs[, ])
{
return (IntPtr)(_ + x * C + y);
}
});
break;
case MoveDirection.Right:
move = Move(R, C - , , -, (x,y)=> {
fixed(int* _ = &_bs[, ])
{
return (IntPtr)(_ + x * C + y);
}
});
break;
} if (move && State != GameState.Succ)
{
Steps++; Log = (direction, bs); //有移动 随机中空位生成数字
var emptyNum = GenerateNum(); //判断是否结束
if(emptyNum == ) CheckGame();
}
} /// <summary>
/// 判断是否可以合并
/// </summary>
private void CheckGame()
{
//是否已经填满 并且无法移动
for (int x = ; x < R; x++)
{
for (int y = ; y < C; y++)
{
if (y < C - && _bs[x, y] == _bs[x, y + ]) return;
if (x < R - && _bs[x, y] == _bs[x + , 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 = ; x < R; x++)
{
for (int y = ; y < C; y++)
{
if (_bs[x, y] == ) ls.Add((x,y));
}
} Shuffle(ls); var xy = ls[_rnd.Next(ls.Count)];
_bs[xy.x, xy.y] = _rnd.Next() == ? : ;
return ls.Count - ; } private IList<T> Shuffle<T>(IList<T> arr)
{
for (var i = ; 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 = ; x < loop; x++)
{
//第一步 合并
for (int y = si; y * step < ei; y+=step)
{
var val1 = (int*)getInt(x, y); if (*val1 != )
{
for (var y2 = y + step; y2 != ei + step; y2 += step)
{
var val2 = (int*)getInt(x, y2);
//忽略0
if (*val2 == ) continue;
//合并
if (*val1 == *val2)
{
*val1 *= ;
*val2 = ;
moved = true; Score += *val1; if (*val1 == ) 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 == )
{
var y2 = lastY ?? y + step;
for (; y2 != ei + step; y2 += step)
{
var val2 = (int*)getInt(x, y2); if (*val2 != )
{
*val1 = *val2;
*val2 = ;
moved = true; lastY = y2 + step;
break;
}
}
//最后一列了
if (y2 == ei) break;
}
}
} return moved;
} /// <summary>
/// 重启游戏
/// </summary>
public void Restart()
{
Score = Steps = ;
State = GameState.None;
Log = null; _bs = new int[R, C]; for (int i = ; i < ; i++)
{
var x = _rnd.Next(R);
var y = _rnd.Next(C);
if (_bs[x, y] == ) _bs[x, y] = _rnd.Next() == ? : ;
else i--;
}
} public void RandNum()
{
for (int x = ; x < R; x++)
{
for (int y = ; y < C; y++)
{
_bs[x, y] = (int)Math.Pow(, _rnd.Next());
}
}
} public void Show()
{
Console.SetCursorPosition(, ); Console.WriteLine($"得分:{Score} 步数:{Steps} [R]键显示上一步操作记录(当前:{ShowPre}) "); Console.WriteLine(); Console.WriteLine(new string('-', C * ));
for (int x = ; x < R; x++)
{
for (int y = ; y < C; y++)
{
var b = _bs[x, y];
Console.Write($"{(b == 0 ? " " : b.ToString()),4}|");
}
Console.WriteLine();
Console.WriteLine(new string('-', C * ));
} if (ShowPre && Log != null)
{
Console.WriteLine();
Console.WriteLine(new string('=', ));
Console.WriteLine(); var bs = Log?.data; Console.WriteLine($"方向:{Log?.direction} ");
Console.WriteLine(); Console.WriteLine(new string('-', C * ));
for (int x = ; x < R; x++)
{
for (int y = ; y < C; y++)
{
var b = bs[x, y];
Console.Write($"{(b == 0 ? " " : b.ToString()),4}|");
}
Console.WriteLine();
Console.WriteLine(new string('-', C * ));
}
} } }
}

Main入口:

         static void Main(string[] args)
{
Game.R = ;
Game.C = ; 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#撸的控制台版2048小游戏的更多相关文章

  1. jQuery实践-网页版2048小游戏

    ▓▓▓▓▓▓ 大致介绍 看了一个实现网页版2048小游戏的视频,觉得能做出自己以前喜欢玩的小游戏很有意思便自己动手试了试,真正的验证了这句话-不要以为你以为的就是你以为的,看视频时觉得看懂了,会写了, ...

  2. .NET手撸2048小游戏

    .NET手撸2048小游戏 2048是一款益智小游戏,得益于其规则简单,又和2的倍数有关,因此广为人知,特别是广受程序员的喜爱. 本文将再次使用我自制的"准游戏引擎"FlysEng ...

  3. 2048小游戏代码解析 C语言版

    2048小游戏,也算是风靡一时的益智游戏.其背后实现的逻辑比较简单,代码量不算多,而且趣味性强,适合作为有语言基础的童鞋来加强编程训练.本篇分析2048小游戏的C语言实现代码. 前言 游戏截图:  游 ...

  4. 基于jQuery的2048小游戏设计(网页版)

    上周模仿一个2048小游戏,总结一下自己在编写代码的时候遇到的一些坑. 游戏规则:省略,我想大部分人都玩过,不写了 源码地址:https://github.com/xinhua6/2048game.g ...

  5. 如何在CentOS上安装一个2048小游戏

    如何在centos上安装一个2048小游戏 最近在学习CentOS系统,就琢磨着玩点什么,然后我看到有人在玩2048小游戏,所有我就在想,为啥不装一个2048小游戏搞一下嘞,于是乎,我就开始工作啦 由 ...

  6. js、jQuery实现2048小游戏

    2048小游戏 一.游戏简介:  2048是一款休闲益智类的数字叠加小游戏 二. 游戏玩法: 在4*4的16宫格中,您可以选择上.下.左.右四个方向进行操作,数字会按方向移动,相邻的两个数字相同就会合 ...

  7. C# 开发2048小游戏

    这应该是几个月前,闲的手痒,敲了一上午代码搞出来的,随之就把它丢弃了,当时让别人玩过,提过几条更改建议,但是时至今日,我也没有进行过优化和更改(本人只会作案,不会收场,嘎嘎),下面的建议要给代码爱好的 ...

  8. Swift实战之2048小游戏

    上周在图书馆借了一本Swift语言实战入门,入个门玩一玩^_^正好这本书的后面有一个2048小游戏的实例,笔者跟着实战了一把. 差不多一周的时间,到今天,游戏的基本功能已基本实现,细节我已不打算继续完 ...

  9. 用js实现2048小游戏

    用js实现2048小游戏 笔记仓库:https://github.com/nnngu/LearningNotes 1.游戏简介 2048是一款休闲益智类的数字叠加小游戏.(文末给出源代码和演示地址) ...

随机推荐

  1. node mysql模块写入中文字符时的乱码问题

    刚刚发现一个奇怪的问题: 在node上用mysql模块将数据写入数据库的时候,在cmd上打开mysql发现select出来的中文是乱码.但这就奇怪了. 因为本机在安装mysql的时候就已经在配置文件将 ...

  2. React-Redux填坑

    这篇东西以后慢慢补充. Q:今天遇到一个问题是 TypeError:dispatch is not a function A:一直报这个type error,调试了好一阵,最后在tof上看到网友说co ...

  3. Hills And Valleys 杭电多校第五场

    题意:长度为n的序列,有一次翻转区间的机会,问最长不减序列 题解:如果没有翻转区间的机会,有两个做法. 一是dp[i]表示以i结尾的最长序列 dp[i]=max(dp[i],dp[j]+1)  (j& ...

  4. Python 绘制全球疫情地图

    国内疫情得到控制后,我就没怎么再关心过疫情,最近看到一条新闻,全球疫情累计确诊人数已经突破 500w 大关,看到这个数字我还是有点吃惊的. 思来想去,还是写一篇全球疫情的分析的文章,本文包括网络爬虫. ...

  5. MSC添加shard节点

    1.MSC添加shard节点 mkdir -p /mongodb/38027/conf  /mongodb/38027/log  /mongodb/38027/datamkdir -p /mongod ...

  6. 这次终于可以愉快的进行 appium 自动化测试了

    appium 是进行 app 自动化测试非常成熟的一套框架.但是因为 appium 设计到的安装内容比较多,很多同学入门都跪在了环境安装的部分.本篇讲述 appium 安卓环境的搭建,希望让更多童鞋轻 ...

  7. 【Java】向*.txt文档里面重复添加同一个字符串

    闺蜜说让我用代码写五万个对不起给她~~ import java.io.FileWriter; import java.io.IOException; /** * Created by lenovo o ...

  8. JVM调优总结(七)-调优方法

    JVM调优工具 Jconsole,jProfile,VisualVM Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用.对垃圾回收算法有很详细的跟踪.详细说明参考这里 ...

  9. idea 开发 webpack项目时,只要已加入SVN 版本控制 一直 updating 问题解决

    场景描述,这是一个困扰我很久的一个问题,一直百度,都解决不了,今天自己通过设置终于解决了,慢慢的都是辛酸泪,赶快写个笔记记录一下. 对于idea 开发 vue-cli+webpack 项目,idea  ...

  10. fix元素居中

    今天的一个面试题,我是这么写的: div{ position:fixed; margin:auto; left:; right:; top:; bottom:; width:200px; height ...