文章目录
一、前言
连连看规则要求
1,相同的卡片可以消除
2,两张卡片间连线的拐弯不能超过两个
3,用户操作和消除要有较为友好的动画
4,游戏最后要有解
这个是3年前在新光供应链任职,子公司技术总监让写的一份程序,当然最后承诺的机械键盘还是没给我们😅
二、思路分析
2.1,相同的卡片可以消除
首先这条是相对容易实现的,我们需要用到的仅仅是绘图与点击事件。考虑使用canvas画布绘制出对应的图案即可。
这里我们用大家最喜欢的微软旗下的mspaint软件以6*6的方格绘制大致模型。四周的方块我们做预留,主要用在下一步的连线绘制上。
具体操作上我们需要自定义View,在onDraw方法中绘制即可。核心代码如下
/**
* 绘制棋盘的所有图标 当这个坐标内的值大于0时绘制
*/
for (int x = 0; x < map.length; x += 1) {
for (int y = 0; y < map[x].length; y += 1) {
if (map[x][y] > 0) {
Point p = indextoScreen(x, y);
canvas.drawBitmap(icons[map[x][y]], p.x, p.y, null);
}
}
}
实现效果如图
2.2,两张卡片间连线的拐弯不能超过两个
这里是实现思路的核心,我们在考虑连接卡片的时候需要分析下,这里的连通算法要求至多两个拐点表示相连,也就是最多3条直线。这里我们需要用到分类算法与广度优先算法。
1,广度优先:两个节点node1和node2,将能和node1直接相连的节点加入集合S,然后再将能和S集合中节点直接相连的节点加入S(去重复),最后再将能和S集合中节点直接相连的节点加入S(去重复),如果node2在S集合中,则表示两者相连。
2,分类算法:(此程序采用)首先判断两个节点是否可以直接相连,如果否,则两个节点是否可以通过一个拐角相连(两个节点是一个正方形的对角线),如果还是否,则判断两个节点是否可以通过两个拐角相连。
具体分析如下
1,两个节点是否可以直接相连,
2,如果否,则考虑能否通过2条直线相连,
3,如果否,则考虑3条直线相连
4,否则就是不可连接
1)两个节点是否可以直接相连
这种情况我们需要考虑竖直与水平两种情况
比如Point(1,1)到Point(3,1)为水平方向,Point(1,2)到Point(1,4)为竖直方向,核心算法如下
//这是针对一条线的情况
private boolean linkD(Point p1, Point p2) {
//case 1:在同一条垂直线上
if (p1.x == p2.x) {
int y1 = Math.min(p1.y, p2.y);
int y2 = Math.max(p1.y, p2.y);
boolean flag = true;
for (int y = y1 + 1; y < y2; y++) {
if (map[p1.x][y] != 0) {
flag = false;
break;
}
}
if (flag) {
return true;
}
}
//case 2:在同一条水平线上
if (p1.y == p2.y) {
int x1 = Math.min(p1.x, p2.x);
int x2 = Math.max(p1.x, p2.x);
boolean flag = true;
for (int x = x1 + 1; x < x2; x++) {
if (map[x][p1.y] != 0) {
flag = false;
break;
}
}
if (flag) {
return true;
}
}
return false;
}
2)如果否,则考虑能否通过2条直线相连
这种情况我们需要考虑"﹂“型、”﹁"型两种情况
比如从Point(1,1)到Point(3,2)的路径上,我们需要定位到中间拐点,即Point(3,1)、Point(1,2),其中任意一点能通则认为是连通的。核心算法如下
//判断两个点是否可以连接,并将可以连接的中间点全部记录在path中,用于连线动画
private boolean link(Point p1, Point p2) {
//如果是同一个点的话就返回false
if (p1.equals(p2)) {
return false;
}
//下面都不是同一个点
path.clear();
if (map[p1.x][p1.y] == map[p2.x][p2.y]) {
//case 1:一条线可以连接的情况
if (linkD(p1, p2)) {
path.add(p1);
path.add(p2);
return true;
}
//case 2:两条线可以连接的情况
//2.1"﹂"型
Point p = new Point(p1.x, p2.y);
if (map[p.x][p.y] == 0) {
if (linkD(p1, p) && linkD(p, p2)) {
path.add(p1);
path.add(p);
path.add(p2);
return true;
}
}
//2.2"﹁"型
p = new Point(p2.x, p1.y);
if (map[p.x][p.y] == 0) {
if (linkD(p1, p) && linkD(p, p2)) {
path.add(p1);
path.add(p);
path.add(p2);
return true;
}
}
expandX(p1, p1E);//加载p1水平方向的所有点
expandX(p2, p2E);//加载p2水品方向的所有点
for (Point pt1 : p1E) {
for (Point pt2 : p2E) {
if (pt1.x == pt2.x) {//如果水平值相等,即在同一垂直线上就有可能连接
if (linkD(pt1, pt2)) {
path.add(p1);
path.add(pt1);
path.add(pt2);
path.add(p2);
return true;
}
}
}
}
//换成垂直方向上的同理可得
expandY(p1, p1E);
expandY(p2, p2E);
for (Point pt1 : p1E) {
for (Point pt2 : p2E) {
if (pt1.y == pt2.y) {
if (linkD(pt1, pt2)) {
path.add(p1);
path.add(pt1);
path.add(pt2);
path.add(p2);
return true;
}
}
}
}
return false;
}
return false;
}
3)如果否,则考虑3条直线相连
这种情况我们仍然要考虑水平与竖直方向,这里仅以水平方向阐述思维方式
- 这里从Point(2,1)到Point(1,3)的路径上,我们获取Point(2,1)在水平方向上的延伸区域,得到点的集合Point(0,1)、Point(1,1)、Point(5,1)共计3个点
- 同理我们获取Point(1,3)在水平方向上的延伸区域,得到点的集合Point(0,3)共计1个点
- 然后使用双层循环判断,Point(2,1)在水平方向上的延伸点与Point(1,3)在水平方向上的延伸点是否可以用直线连接。
- 这里我们找到Point(0,1)与Point(0,3)是直线连通点,即从Point(2,1)到Point(1,3)的路径上可以3条直线相连
核心算法如下
//将p点左右的点都加到l中去
private void expandX(Point p, List<Point> l) {
l.clear();
for (int x = p.x + 1; x < xCount; x++) {
if (map[x][p.y] != 0) {
break;
}
l.add(new Point(x, p.y));
}
for (int x = p.x - 1; x >= 0; x--) {
if (map[x][p.y] != 0) {
break;
}
l.add(new Point(x, p.y));
}
}
2.2,用户操作和消除要有较为友好的动画
我们在进行连通判断的时候,保存了有用户连通点的路径path.add(p1)。这里我们仅仅需要用这些连通点在方块中间绘制直线即可
代码实现如下
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* 绘制连通路径,然后将路径以及两个图标清除
*/
if (path != null && path.length >= 2) {
for (int i = 0; i < path.length - 1; i++) {
Paint paint = new Paint();
paint.setColor(Color.CYAN);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(3);
Point p1 = indextoScreen(path[i].x, path[i].y);
Point p2 = indextoScreen(path[i + 1].x, path[i + 1].y);
canvas.drawLine(p1.x + iconSize / 2, p1.y + iconSize / 2,
p2.x + iconSize / 2, p2.y + iconSize / 2, paint);
}
/**
* 消除连线上的第一个path[0]
*/
Point p = path[0];
map[p.x][p.y] = 0;
/**
* 消除连线上的最后一个path[0]
*/
p = path[path.length - 1];
map[p.x][p.y] = 0;
/**
* 消除
*/
selected.clear();
/**
* 1,消除线条:(path == null && path.length < 2)
* 2,由后面的this.invalidate()方法刷新
*/
path = null;
}
/**
* 绘制棋盘的所有图标 当这个坐标内的值大于0时绘制
*/
for (int x = 0; x < map.length; x += 1) {
for (int y = 0; y < map[x].length; y += 1) {
if (map[x][y] > 0) {
Point p = indextoScreen(x, y);
canvas.drawBitmap(icons[map[x][y]], p.x, p.y, null);
}
}
}
/**
* 绘制选中图标,当选中时图标放大显示
*/
for (Point position : selected) {
Point p = indextoScreen(position.x, position.y);
if (map[position.x][position.y] >= 1) {
canvas.drawBitmap(icons[map[position.x][position.y]], null, new Rect(p.x - 5, p.y - 5, p.x + iconSize + 5, p.y + iconSize + 5), null);
}
}
}
具体效果(这里特意放大便于效果图)
4)否则就是不可连接
在判断第一个Point不可连接后,我们需要循环判断棋盘上所有的Point是否可以连接,如果都不能连接。则分析问题出在
1,棋盘初始化本就无解
2,本来整个棋盘是可以完全消除所有单元格的,但是由于我们操作的顺序发生了变化,最终导致棋盘无解。

2.4,游戏最后要有解
关于连连看有解,我查阅了下文档加上自己思索了下,大概有以下几种处理方法
1)当出现所有图案均无法连时,游戏会自动洗牌。
这种处理方法demo中有加上,好处是棋子的随机性特别强,对于玩家来说难度挑战可以最大化,缺点是打乱图案是一种极不友好的交互方式。参考新浪微博、参考NGA
2)使用拉斯维加斯算法+回溯法初始化有解的棋盘
1)清空地图
2)随机生成一个图块,并执行下一行:随机在另一处生成同样的图块,如果之间有通路,就保留,否则回到上一行重新生成;
3)如果这样下去能生成整张地图,就结束,否则回溯继续试探。 也就是随机试探着一对一对地增加图块。
这是个拉斯维加斯算法+回溯法。又因为连连看破解的时候是从外向内的,类似拓扑排序,所以从简单往复杂方向生成的话,可以保证最后可破解的。
这个意义不大
1,算法时间复杂度开销太大。需要不停的回溯。
2,从概率的角度讲,实现一种生成有效棋盘的算法没有多大意义,因为即使产生的棋盘有效,我们在游戏时,最后也有可能将游戏的棋盘玩死。参考知乎
3)内置多种怎么操作都可解的棋盘
4)游戏体验上给玩家新增道具消除
这个在棋子比较多的时候体验还行,像上图只有4个的情况下也会显得非常尴尬
5)有规律的初始化棋盘
规律可以是在保证有解的情况下轻微的颠倒棋子顺序,或者初始化棋盘的时候先有规律地初始化棋盘中心等方式,这种处理方法demo中也有加上。体验上可以保证有解,但是对于玩家来说难度挑战就难以最大化。
三、写在最后的话
demo下载地址,免积分下载,如果需要积分可以留言邮箱。
这个确实是做前端程序好啊,想写个什么出来玩,就可以真的弄个出来玩。对于文中处理方式,大家如果有更好的方法,或者觉得哪里有问题的欢迎留言。如果觉得有用,欢迎收藏或者打赏