连连看游戏思维拓展(java版)

一、前言

连连看规则要求
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下载地址,免积分下载,如果需要积分可以留言邮箱。
这个确实是做前端程序好啊,想写个什么出来玩,就可以真的弄个出来玩。对于文中处理方式,大家如果有更好的方法,或者觉得哪里有问题的欢迎留言。如果觉得有用,欢迎收藏或者打赏

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流星雨在线

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值