澳门新浦京电子游戏2小时完成HTML5拼图小游戏

50~90min

这40分钟的时间,是最关键时期,期间我们要完成整个游戏的主体部分。首先,我们需要用代码来实现以下过程:

初始化游戏界面数据(如游戏时间、所用步数)和显示一些UI部件(如图样)
|
-> 获取随机的拼图块位置
|
-> 显示打乱后的拼图块

我们将这些步骤做成一个个的函数方便我们统一调用:

function startGame () {
    isGameOver = false;

    /** 初始化时间和步数 */
    startTime = (new Date()).getTime();
    time = 0;
    steps = 0;
    /** 初始化拼图块列表 */
    initBlockList();
    /** 打乱拼图 */
    getRandomBlockList();
    /** 显示拼图 */
    showBlock();
    /** 显示缩略图 */
    showThumbnail();
    /** 显示时间 */
    addTimeTxt();
    /** 显示步数 */
    addStepsTxt();

    stageLayer.addEventListener(LEvent.ENTER_FRAME, onFrame);
}

函数一开始,我们把isGameOver变量设定为false代表游戏未结束,在后期的代码里,我们会看到这个变量的作用。接着我们初始化了用于表示时间和步数的timesteps这两个全局变量,另外初始化变量startTime的值用于后面计算游戏时间。
接下来,我们就要开始初始化拼图块了。见initBlockList里的代码:

function initBlockList () {
    blockList = new Array();

    for (var i = 0; i < 9; i++) {
        /** 根据序号计算拼图块图片显示位置 */
        var y = (i / 3) >>> 0, x = i % 3;

        blockList.push(new Block(i, x, y));
    }
}

这里我们使用了一个Block类,这个类用于显示拼图块和储存拼图块的数据,并提供了一些方法来操控拼图块,下面是其构造器的代码:

function Block (index, x, y) {
    LExtends(this, LSprite, []);

    var bmpd = imgBmpd.clone();
    bmpd.setProperties(x * 130, y * 130, 130, 130);
    this.bmp = new LBitmap(bmpd);
    this.addChild(this.bmp);

    var border = new LShape();
    border.graphics.drawRect(3, "#CCCCCC", [0, 0, 130, 130]);
    this.addChild(border);

    this.index = index;

    this.addEventListener(LMouseEvent.MOUSE_UP, this.onClick);
}

Block类继承自LSprite,属于一个显示对象,所以我们在这个类中添加了一个位图对象用于显示拼图块对应的图片。除此之外,我们还为拼图块添加了一个边框,在显示时用于隔开周围的拼图块。Block类有一个index属性,代表拼图块在拼图块列表blockList中的正确位置。最后,我们为此类添加了一个鼠标按下事件,用于处理鼠标按下后移动图块操作。

接下来我们还要介绍这个类的一个方法setLocation

Block.prototype.setLocation = function (x, y) {
    this.locationX = x;
    this.locationY = y;

    this.x = x * 130;
    this.y = y * 130;
};

这个方法用于设置拼图块对象的显示位置以及保存拼图块的“数组位置”。什么是“数组位置”呢?各位看官可以通过下面的图片加以了解:

澳门新浦京电子游戏 1

可以看到,“数组位置”就类似于二维数组中的元素下标。储存这个位置的作用在于可以很方便地从blockList中获取到附近的其他拼图块。这个方法在我们显示拼图时有调用到,在显示拼图之前,我们得先打乱拼图,见如下代码:

function getRandomBlockList () {
    /** 随机打乱拼图 */
    blockList.sort(function () {
        return 0.5 - Math.random();
    });

    /** 计算逆序和 */
    var reverseAmount = 0;

    for (var i = 0, l = blockList.length, preBlock = null; i < l; i++) {
        if (!preBlock) {
            preBlock = blockList[0];

            continue;
        }

        var currentBlock = blockList[i];

        if (currentBlock.index < preBlock.index) {
            reverseAmount++;
        }

        preBlock = currentBlock;
    }

    /** 检测打乱后是否可还原 */
    if (reverseAmount % 2 != 0) {
        /** 不合格,重新打乱 */
        getRandomBlockList();
    }
}

打乱拼图部分直接用数组的sort方法进行随机打乱:

blockList.sort(function () {
    return 0.5 - Math.random();
});

其实打乱算法有很多种,我这里采用最粗暴的方法,也就是随机打乱。这种算法简单是简单,坏在可能出现无法复原的现象。针对这个问题,就有配套的检测打乱后是否可还原的算法,具体的算法理论我摘用lufy大神的评论:

此类游戏能否还原关键是看它打乱后的逆序次数之和是否为偶数
假设你打乱后的数组中的每一个小图块为obj0obj1obj2,…它们打乱之前的序号分别为obj0.numobj1.num
接下来循环数组,如果前者的序号比后者大,如obj0.num > obj1.num,这表示一个逆序
当全部的逆序之和为奇数时表示不可还原,重新打乱即可,打乱后重新检测,直到逆序之和为偶数为止

上面我给出的getRandomBlockList里的代码就是在实现打乱算法和检测是否可还原算法。

还有一种打乱方式,大家可以尝试尝试:和复原拼图一样,将空白块一步一步地与周围的拼图随机交换顺序。这个打乱算法较上一种而言,不会出现无法复原的现象,而且可以根据打乱的步数设定游戏难度。

在完成打乱拼图块后,如期而至的是显示拼图块:

function showBlock() {
    for (var i = 0, l = blockList.length; i < l; i++) {
        var b = blockList[i];

        /** 根据序号计算拼图块位置 */
        var y = (i / 3) >>> 0, x = i % 3;

        b.setLocation(x, y);

        gameLayer.addChild(b);
    }
}

显示了拼图块后,我们要做的就是添加操作拼图块的功能。于是需要拓展Block类,为其添加事件监听器onClick方法:

Block.prototype.onClick = function (e) {
    var self = e.currentTarget;

    if (isGameOver) {
        return;
    }

    var checkList = new Array();

    /** 判断右侧是否有方块 */
    if (self.locationX > 0) {
        checkList.push(Block.getBlock(self.locationX - 1, self.locationY));
    }

    /** 判断左侧是否有方块 */
    if (self.locationX < 2) {
        checkList.push(Block.getBlock(self.locationX + 1, self.locationY));
    }

    /** 判断上方是否有方块 */
    if (self.locationY > 0) {
        checkList.push(Block.getBlock(self.locationX, self.locationY - 1));
    }

    /** 判断下方是否有方块 */
    if (self.locationY < 2) {
        checkList.push(Block.getBlock(self.locationX, self.locationY + 1));
    }

    for (var i = 0, l = checkList.length; i < l; i++) {
        var checkO = checkList[i];

        /** 判断是否是空白拼图块 */
        if (checkO.index == 8) {
            steps++;
            updateStepsTxt();

            Block.exchangePosition(self, checkO);

            break;
        }
    }
};

首先,我们在这里看到了isGameOver全局变量的作用,即在游戏结束后,阻断点击拼图块后的操作。

在点击了拼图块后,我们先获取该拼图块周围的拼图块,并将它们装入checkList,再遍历checkList,当判断到周围有空白拼图块后,即周围有index属性等于8的拼图块后,先更新操作步数,然后将这两个拼图块交换位置。具体交换拼图块位置的方法详见如下代码:

Block.exchangePosition = function (b1, b2) {
    var b1x = b1.locationX, b1y = b1.locationY,
        b2x = b2.locationX, b2y = b2.locationY,
        b1Index = b1y * 3 + b1x,
        b2Index = b2y * 3 + b2x;

    /** 在地图块数组中交换两者位置 */
    blockList.splice(b1Index, 1, b2);
    blockList.splice(b2Index, 1, b1);

    /** 交换两者显示位置 */
    b1.setLocation(b2x, b2y);
    b2.setLocation(b1x, b1y);

    /** 判断游戏是否结束 */
    Block.isGameOver();
};

还有就是Block.getBlock静态方法,用于获取给定的“数组位置”下的拼图块:

Block.getBlock = function (x, y) {
    return blockList[y * 3 + x];
};

Block.exchangePosition中,我们通过Block.isGameOver判断玩家是否已将拼图复原:

Block.isGameOver = function () {
    var reductionAmount = 0, l = blockList.length;

    /** 计算还原度 */
    for (var i = 0; i < l; i++) {
        var b = blockList[i];

        if (b.index == i) {
            reductionAmount++;
        }
    }

    /** 计算是否完全还原 */
    if (reductionAmount == l) {
        /** 游戏结束 */
        gameOver();
    }   
};

到这里,我们就实现了打乱和操作拼图块部分。

五,算法&代码讲解

先来个游戏初始化:

init(30,"mylegend",390,420,main);

为了方便操作游戏中的一些数据,我们设定许多变量:

var backLayer,chessLayer,overLayer;
var statusText = new LTextField();
var statusContent="您先请吧……";
var matrix = [
    [0,0,0],
    [0,0,0],
    [0,0,0]
];
var usersTurn = true;
var step = 0;
var title = "井字棋";
var introduction = ""
var infoArr = [title,introduction];

第一行是层变量;第二行是实例化的文本框对象,用来显示文字;第三行是当前显示信息的文字,比如该哪方走,哪方赢了等,会根据不同情况改变。

 

matrix是用来保存当前棋盘数据的数组,如果下一步棋,就会更改其中数据,顺便也说一下,为了区分【空白格子】,【我方下的位置】,【电脑下的位置】,我们用-1来代表【我方下的位置】,用0来代表【空白格子】,1来代表【电脑下的位置】;看官且记,这-1,0,1在棋盘数组中便各有了代表意义。

userTurn是用来判断玩家是否可以下棋;step是用来表示走的步数,用来判断棋盘是否下满;title,introduction还有infoArr原本是用来制作关于界面的,结果做到最后就算了,大家直接忽视掉吧。

接下来就是main函数,由于没有图片,所以就没有加载部分了:

function main(){
    gameInit();
    addText();
    addLattice();    
}

main调用的几个函数如下:

function gameInit(){
    initLayer();
    addEvent();
}

function addText(){
    statusText.size = 15;    
    statusText.weight = "bold";
    statusText.color = "white";
    statusText.text = statusContent;
    statusText.x = (LGlobal.width-statusText.getWidth())*0.5;
    statusText.y = 393;

    overLayer.addChild(statusText);
}

function addLattice(){
    backLayer.graphics.drawRect(10,"dimgray",[0,0,390,420],true,"dimgray");
    backLayer.graphics.drawRect(10,"dimgray",[0,0,390,390],true,"lavender");
    for(var i=0;i<3;i++){
        backLayer.graphics.drawLine(3,"dimgray",[130*i,0,130*i,390]);
    }
    for(var i=0;i<3;i++){
        backLayer.graphics.drawLine(3,"dimgray",[0,130*i,390,130*i]);
    }
}

解释一下他们的功能。首先,gameInit是用来初始化游戏的,包括初始化层一类的东西。addText是用来加下面文字的。addLattice使用来画棋盘的。代码很简单,参照lufylegend
API文档看一下就能看懂。

 

接下来我们来看gameInit里调用的函数:

function initLayer(){
    backLayer = new LSprite();
    addChild(backLayer);

    chessLayer = new LSprite();
    backLayer.addChild(chessLayer);

    overLayer = new LSprite();
    backLayer.addChild(overLayer);
}
function addEvent(){
    backLayer.addEventListener(LMouseEvent.MOUSE_DOWN,onDown);
}

initLayer是用来实例化层的,说明了一点就是实例化LSprite。addEvent用来加点击事件。

 

然后接下来就来看看事件触发的onDown:

function onDown(){
    var mouseX,mouseY;
    mouseX = event.offsetX;
    mouseY = event.offsetY;

    var partX = Math.floor(mouseX/130);
    var partY = Math.floor(mouseY/130);
    if(matrix[partX][partY]==0){
        usersTurn=false;
        matrix[partX][partY]=-1;
        step++;
        update(partX,partY);

        if(win(partX,partY)){
            statusContent = "帅呆了,你赢啦!点击屏幕重开游戏。";
            gameover();
            addText();
        }else if(isEnd()){
            statusContent = "平局啦~~点击屏幕重开游戏。";
            gameover();
            addText();
        }else{
            statusContent = "电脑正在思考中……";
            addText();
            computerThink();
        }
    }
}

这个函数要做的就是先取出点击位置,然后根据点的位置下一颗棋,然后将在棋盘数组中相应的位置设为-1,表示是我方走的,然后判断:下了这一步棋后的胜负或者平局情况,并且调用相应的函数和显示相应的文字。判断赢,我们用win函数,代码如下:

function win(x,y){
    if(Math.abs(matrix[x][0]+matrix[x][1]+matrix[x][2])==3){
        return true;
    }
    if(Math.abs(matrix[0][y]+matrix[1][y]+matrix[2][y])==3){
        return true;
    }
    if(Math.abs(matrix[0][0]+matrix[1][1]+matrix[2][2])==3){
        return true;
    }
    if(Math.abs(matrix[2][0]+matrix[1][1]+matrix[0][2])==3){
        return true;
    }
    return false;
}

首先我们判断第x行,第0,1,2列的数字相加的绝对值是否为3(由于这个函数在下面还要用到,所以我们要做得通用性,所以就用了绝对值)。为什么等于3呢?因为看官是否记得我们上面说的:-1代表【我方下的位置】,0代表【空白格子】,1代表【电脑下的位置】。但凡是下了棋的地方,值总是1或者-1,所以假如有三个同一方棋子连在一起,那这几个值加起来的绝对值一定是3。因此就返回true代表赢了。如果一直判断到最后都没有,就返回false,代表还没有赢。

 

我们用isEnd判断平局,代码如下:

function isEnd(){
    return step>=9;
}

代码很简单,就是判断棋盘占满没有。

其中用到updata负责更新棋盘。代码如下:

function update(x,y){
    var v = matrix[x][y];
    if(v>0){
        chessLayer.graphics.drawArc(10,"green",[x*130+65,y*130+65,40,0,2*Math.PI]);
    }else if(v<0){
        chessLayer.graphics.drawLine(20,"#CC0000",[130*x+30,130*y+30,130*(x+1)-30,130*(y+1)-30]);
        chessLayer.graphics.drawLine(20,"#CC0000",[130*(x+1)-30,130*y+30,130*x+30,130*(y+1)-30]);
    }
}

以上的代码也很好理解,就是先取出画的那一点是什么,如果是我方画的(在棋盘数组就是-1),在判断时,取出的值如果小于0,就画个叉叉。如果大于0也就是代表电脑画的(在棋盘数组中代表1),就画个圆。

onDown中还用到了gameOver函数,代码如下:

function gameover(){
    backLayer.removeEventListener(LMouseEvent.MOUSE_DOWN,onDown);
    backLayer.addEventListener(LMouseEvent.MOUSE_DOWN,function(){
        chessLayer.removeAllChild();
        backLayer.removeChild(chessLayer);
        backLayer.removeChild(overLayer);
        removeChild(backLayer);
        matrix = [
            [0,0,0],
            [0,0,0],
            [0,0,0]
        ];
        step = 0;
        main();
        statusContent = "您先请吧……";
        addText();
    });
}

看似代码有点长,其实很简单,就是简单的移除界面上的一切对象,并且把一些值恢复为默认值。还有onDown中的computerThink函数,代码如下:

function computerThink(){
    var b = best();
    var x = b.x;
    var y = b.y;
    matrix[x][y]=1;
    step++;
    update(x,y);

    if(win(x,y)){
        statusContent = "哈哈你输了!点击屏幕重开游戏。";
        gameover();
        addText();
    }else if(isEnd()){
        statusContent = "平局啦~~点击屏幕重开游戏。";
        gameover();
        addText();
    }else{
        statusContent = "该你了!!!";
        addText();
    }
}

首先这个函数用了best函数,这个函数会返回一个要下的位置,然后我们把在棋盘数组中相应的位置设置为1,并且把走的步数+1。然后在相应位置画上。然后判断是否赢了或者平局,或者没赢没输没平局。

best是电脑AI算法部分,代码如下:

function best(){
    var bestx;
    var besty;
    var bestv=0;
    for(var x=0;x<3;x++){
        for(var y=0;y<3;y++){
            if(matrix[x][y]==0){
                matrix[x][y] = 1;
                step++;
                if(win(x,y)){
                    step--;
                    matrix[x][y] = 0;    
                    return {'x':x,'y':y,'v':1000};
                }else if(isEnd()){
                    step--;
                    matrix[x][y]=0;    
                    return {'x':x,'y':y,'v':0};
                }else{
                    var v=worst().v;
                    step--;
                    matrix[x][y]=0;
                    if(bestx==null || v>=bestv){
                        bestx=x;
                        besty=y;
                        bestv=v;
                    }
                }
            }
        }
    }
    return {'x':bestx,'y':besty,'v':bestv};
}

算法的思路如下:首先我们遍历棋盘数组,然后判断遍历到的那格如果是空的(也就值是0)就先假设画上,并且将在棋盘数组中相应的位置设为1,表示电脑是下的,然后将走的步数+1。普通的操作就完了,接下来就是给下的这一步评分阶段,代码如下:

if(win(x,y)){
    step--;
    matrix[x][y] = 0;    
    return {'x':x,'y':y,'v':1000};
}else if(isEnd()){
    step--;
    matrix[x][y]=0;    
    return {'x':x,'y':y,'v':0};
}else{
    var v=worst().v;
    step--;
    matrix[x][y]=0;
    if(bestx==null || v>=bestv){
        bestx=x;
        besty=y;
        bestv=v;
    }
}

首先我们判断一下如果下了这一步,是否就赢了,如果是,就先把步数改回去,并且把棋盘数组改为下这一步之前的棋盘数组(因为我们在computerThink里要改一道,所以先改回去,避免改重了),然后返回这一步的位置,并且评分为1000。最后这个过程用return来实现,return是神马,我想就不用说了吧。判断是否赢了,我们用了win函数,上面已经说过了。

 

但是万一下了这一步没赢怎么办,就接着判断是否下了成平局,怎么才能成平局呢?就是把整个棋盘占满且对方没有赢,自己也没有赢就是平局。由于如果别人赢了,就不会进行电脑AI,也就不会调用best函数,换句话说就是不可能进行到这一步;如果是电脑赢了,在上级判断中已经做了相应操作而且用return已经推出函数了,也不会运行到此步,因此直接判断占满没有就可以了。因此用到isEnd函数,上面也用到过,并且讲到过,这里不罗嗦。

万一上面的两种情况都不对怎么办?那就随便下个吧。但是随便下也不能乱下。因此用到了worst来选择“随便下”最好的位置。代码如下:

function worst(){
    var bestx;
    var besty;
    var bestv = 0;
    for(var x=0;x<3;x++){
        for(var y=0;y<3;y++){
            if(matrix[x][y] == 0){
                matrix[x][y] = -1;
                step++;
                if(win(x,y)){
                    step--;
                    matrix[x][y] = 0;    
                    return {'x':x,'y':y,'v':-1000};
                }else if(isEnd()){
                    step--;
                    matrix[x][y]=0;    
                    return {'x':x,'y':y,'v':0};;
                }else{
                    var v=best().v;
                    step--;
                    matrix[x][y]=0;
                    if(bestx==null || v<=bestv){
                        bestx=x;
                        besty=y;
                        bestv=v;
                    }
                }

            }
        }
    }
    return {'x':bestx,'y':besty,'v':bestv};
}

这个函数和best是反着来的,它是假设下了某一步后,别人会赢或者平局。如果别人走那步会赢,就返回这个位置,把这个位置先占住。平局和对方赢是一样的原理,就是见哪里不对就填哪里。最后的判读是在对方不可能赢的情况下采取的,就是通过best函数取最好的。这个best函数在上面讲过了。不作解释了~~

通过worst这个函数会返回几个值,第一个和第二个是随便下的位置,最后一个是评分。在best中我们把这几个返回值接收到,并且通过评分判断这个选择是否比平局的结果还要差,再返回给computerThink这个函数来绘画布局。因此这个过程很绕。大家要搞清楚关系,搞清楚了就不难了。

写在前面

上一篇文章我写了一个简单的iOS
拼图游戏(童年的记忆——拼图游戏),现在我要让这个游戏聪明起来,帮助你来完成拼图。写这篇文章的时候正好在看《最强大脑》,节目里的第一个PK就是复原这种拼图(非图而是数字,数字华容道),节目营造了非常紧张的气氛,其实这种拼图复原算是比较简单的。
不再前戏,直接进入正题:游戏源码点这里(拼图游戏),您可以从这份源码中get到的技术点:

> 设置代理类为控制器瘦身
> A*算法(含借助完全二叉树实现优先队列)
> GCD信号量控制数组遍历流量
> 定时器+GCD信号量实现数组遍历的暂停、继续

游戏效果:

澳门新浦京电子游戏 2

游戏效果.gif

源代码下载

最后奉上源代码:点击下载

二,游戏在哪里玩?

相信大家看了介绍就对井字棋有了了解。现在我用HTML5配合开源游戏引擎lufylegend开发出了这一款游戏,并实现了人工智能(AI)确保游戏中玩家能棋缝对手。

接下来是游戏在线试玩和下载源码的地址:

下载地址(含源代码):

在线试玩地址:

 

20922789888000,广搜算法已基本不能搜出结果,直到爆内存。拼图为5阶方阵时,状态数(5*5)!

1.551121004333098e25,10的25次方。先别着急,我们可以选择性能较高的A*算法为拼图游戏助力。

  • A*算法简介:
    A*(A-Star)算法是一种静态路网中求解最短路径最有效的直接搜索方法,也是解决许多搜索问题的有效算法。它并不是搜索每一种可能的状态,而是有选择地启发式搜索,每一次在多个状态中选择可能最接近目标状态的一个。依此层层搜索,显然相比暴力搜索,少走了很多的弯路。
    本文并不打算深入描述该算法技术细节。推荐阅读A*算法详解。

对于该游戏,对每一个状态到目标状态的“距离”进行估算,将每一个方块偏离它正确位置的距离进行累加,取偏离值最小者择优录取。图示说明:

澳门新浦京电子游戏 3

图3.png

当前状态估价(本例中也就是曼哈顿距离)计算方式:
4距离它正确的位置也就是上图中7的位置:横向1+纵向1=2;
1距离它正确的位置0;
3距离它正确的位置也就是上图中2的位置:横向2+纵向1=3;
2距离它正确的位置也就是上图中3的位置:横向2+纵向1=3;
7距离它正确的位置也就是上图中5的位置:横向0+纵向1=1;
0距离它正确的位置也就是上图中4的位置:横向2+纵向1=3;
6距离它正确的位置0;
我们给予空格位特权,不对它考核。
5距离它正确的位置也就是上图中0的位置:横向0+纵向1=1;
然后将上述距离值累加。
当然实际还可以在这个基础上对估价进行调整,比如乘以一定系数。

准备阶段

准备lufylegend游戏引擎,大家可以去官方网站下载:

lufylegend.com/lufylegend

引擎文档地址:

lufylegend.com/lufylegend/api

可以说,如果没有强大的lufylegend引擎,这种html5小游戏用原生canvas制作,少说要一天呢。

一,什么是TicTacToe(井字棋)

本游戏为在下用lufylegend开发的第二款小游戏。此游戏是大家想必大家小时候都玩过,因为玩它很简单,只需要一张草稿纸和一只笔就能开始游戏,所以广受儿童欢迎。可能我说了半天,对它名字不熟悉的朋友也不懂我在说神马。那没关系,我就引用Wiki(维基百科)的介绍作为大家对它名字的认识,顺便也勾起我们儿时的回忆:

井字棋,大陆、台湾又称为井字游戏、圈圈叉叉;另外也有打井游戏、OX棋的称呼,香港多称井字过三关、过三关,是种纸笔游戏。两个玩家,一个打圈(O),一个打叉(X),轮流在3乘3的格上打自己的符号,最先以横、直、斜连成一线则为胜。如果双方都下得正确无误,将得和局。这种游戏实际上是由第一位玩家所控制,第一位玩家是攻,第二位玩家是守。第一位玩家在角位行第一子的话赢面最大(见图一),第二位玩家若是在边,角位下子,第一位玩家就可以以两粒连线牵制着第二位玩家,然后制造“两头蛇”。

澳门新浦京电子游戏 4

图一

 

点我)。拼图为4阶方阵时,拼图状态数(4*4)!

30~50min

开发开始界面。游戏不能没有开始界面所以我们首先实现这部分代码。在此之前是index.html里的代码,代码如下:

<!DOCTYPE html>
<html>
<head>
    <title>Puzzle</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
    <script type="text/javascript" src="./lib/lufylegend-1.10.1.simple.min.js"></script>
    <script type="text/javascript" src="./js/Main.js"></script>
</head>
<body style="margin: 0px; font-size: 0px; background: #F2F2F2;">
    <div id="mygame"></div>
</body>
</html>

主要是引入一些js文件,不多说。然后准备一个Main.js文件,在这个文件里添加初始化界面和加载资源的代码:

/** 初始化游戏 */
LInit(60, "mygame", 390, 580, main);

var imgBmpd;
/** 游戏层 */
var stageLayer, gameLayer, overLayer;
/** 拼图块列表 */
var blockList;
/** 是否游戏结束 */
var isGameOver;
/** 用时 */
var startTime, time, timeTxt;
/** 步数 */
var steps, stepsTxt;

function main () {
    /** 全屏设置 */
    if (LGlobal.mobile) {
        LGlobal.stageScale = LStageScaleMode.SHOW_ALL;
    }
    LGlobal.screen(LGlobal.FULL_SCREEN);

    /** 添加加载提示 */
    var loadingHint = new LTextField();
    loadingHint.text = "资源加载中……";
    loadingHint.size = 20;
    loadingHint.x = (LGlobal.width - loadingHint.getWidth()) / 2;
    loadingHint.y = (LGlobal.height - loadingHint.getHeight()) / 2;
    addChild(loadingHint);

    /** 加载图片 */
    LLoadManage.load(
        [
            {path : "./js/Block.js"},
            {name : "img", path : "./images/img.jpg"}
        ],
        null,
        function (result) {
            /** 移除加载提示 */
            loadingHint.remove();

            /** 保存位图数据,方便后续使用 */
            imgBmpd = new LBitmapData(result["img"]);

            gameInit();
        }
    );
}

function gameInit (e) {
    /** 初始化舞台层 */
    stageLayer = new LSprite();
    stageLayer.graphics.drawRect(0, "", [0, 0, LGlobal.width, LGlobal.height], true, "#EFEFEF");
    addChild(stageLayer);

    /** 初始化游戏层 */
    gameLayer = new LSprite();
    stageLayer.addChild(gameLayer);

    /** 初始化最上层 */
    overLayer = new LSprite();
    stageLayer.addChild(overLayer);

    /** 添加开始界面 */
    addBeginningUI();
}

以上代码有详细注释,大家可以对照引擎文档和注释进行阅读。有些全局变量会在以后的代码中使用,大家可以先忽略。接下来是addBeginningUI函数里的代码,用于实现开始界面:

function addBeginningUI () {
    var beginningLayer = new LSprite();
    beginningLayer.graphics.drawRect(0, "", [0, 0, LGlobal.width, LGlobal.height], true, "#EDEDED");
    stageLayer.addChild(beginningLayer);

    /** 游戏标题 */
    var title = new LTextField();
    title.text = "拼图游戏";
    title.size = 50;
    title.weight = "bold";
    title.x = (LGlobal.width - title.getWidth()) / 2;
    title.y = 160;
    title.color = "#FFFFFF";
    title.lineWidth = 5;
    title.lineColor = "#000000";
    title.stroke = true;
    beginningLayer.addChild(title);

    /** 开始游戏提示 */
    var hint = new LTextField();
    hint.text = "- 点击屏幕开始游戏 -";
    hint.size = 25;
    hint.x = (LGlobal.width - hint.getWidth()) / 2;
    hint.y = 370;
    beginningLayer.addChild(hint);

    /** 开始游戏 */
    beginningLayer.addEventListener(LMouseEvent.MOUSE_UP, function () {
        beginningLayer.remove();

        startGame();
    });
}

到此,运行代码,得到我们的开始界面:

澳门新浦京电子游戏 5

看到这个画面,其实我自己都想吐槽一下实在是太“朴素”了,囧……

不过我这次图个制作速度,所以还望各位看官海量。

三,游戏截图

澳门新浦京电子游戏 6

澳门新浦京电子游戏 7

 

四,游戏引擎

 

本游戏运用国产的lufylegend引擎,版本为1.6.1,如果大家感兴趣可以去官网看看

lufylegend官方网站:

lufylegend API文档:

上面有此引擎的下载和API介绍。关于用lufylegend开发游戏的其他文章:

【HTML5游戏开发】简单的《找不同汉字版》,来考考你的眼力吧

 

* 方案三

对于方案三,容易联想到尝试每种步骤可能性,最终选出可以复原的步骤链。

澳门新浦京电子游戏 8

图2.png

上图即表示每一个状态衍生出的可能路径,排除了重复的状态。对于这种暴力搜索算法,性能是较低的(关于搜索算法,我此前的文章有介绍过最短路径的两个经典算法

澳门新浦京电子游戏 9

性能优化(TODO)

暂时想到了以下优化方向:

  • 当前源码每次UICollectionView数据源刷新时,为全局刷新方式,实际除了重置游戏,每次都只是2个格子在变化。可以修改为局部刷新。
  • 当前源码为搜索完成后再进行拼图复原。需要耗费一定的时间。尝试边搜索边拼图的方式(边“生产”边“消费”)。
  • 根据方格数的多少(难易程度)灵活调整A*算法的估价,意在优化游戏还原步数。

0~30min

准备素材(10min) +
修改素材(20min)。由于在下实在手残,不善于P图,修改图片用了大约20min,囧……

* 方案一

方案一实现起来较为简单,假定我们现在已经将有序的(处于复原态)拼图移动了数次,也就是拼图打乱了(如下图左)。接下来将空位移动到右下角。

澳门新浦京电子游戏 10

图1.png

显然,移动的策略有很多种,我们只需要一种,比如这样:空位先逐步横向移动到最右侧,再逐步纵向移动到最下侧。你看,可以啦。(上图右边)

算法分解:

1。求出当前空位所在行、列。
2。当前空位与其右侧格子换位。
3。重复1、2,直到空位位于最右侧(最大列)。
4。当前空位与其下侧格子换位。
5。重复1、4,直到空位位于最下侧(最大行)。

如此,我们已经完成了打乱步骤。当然,我们移动的每一步都需要记录在案,以便按照记录逆步骤进行还原。注:我提供的游戏源码中并没有包含该方案(方案一)的代码实现,感兴趣的读者可以自行实现。

以下是游戏地址:

* 方案二

对于方案二,写这篇文章的时候才临时考虑加入这个方案。基本思路上文中其实已经阐明,暂不展开讨论。

这是我的游戏记录,欢迎各位挑战:

方案解读

基本思路

那么怎样让原本屌丝气质的游戏具备人工智能(AI),自动还原拼图,从此华丽变身高大上呢?

  • 方案一:
    我们很容易想到的方法,按打乱的顺序逆序还原。此方案的打乱顺序即将原本有序的图片按照游戏规则移动数次,从而实现打乱效果。但是我想你们已经发现了一个问题,我所设计的游戏打乱后空位设置在右下角,我们还需要想办法将空位移动到右下角的目标位置。
  • 方案二:
    不关心打乱的过程,依次将编号0、1、2…
    回归到正确位置,逐渐缩小乱序图区域。不过往往最后两行的几块需要稍微调整下策略。
  • 方案三:
    不关心打乱的过程,从打乱后的状态开始,根据一定约束条件,对下一步的多种可能性进行搜索判断,逐步演进,从而找出复原步骤。我选用的是A*搜索算法。

接下来就来讲讲如何开发完成这款游戏的。(按“编年体”)

鸣谢

本文配套源码中直接套用了《拼图游戏和它的AI算法》该文作者封装的A*算法,在此对原作者表示感谢。

初学lufylegend.js之日,我用lufylegend.js开发了第一个HTML5小游戏——拼图游戏,还写了篇博文来炫耀一下:HTML5小游戏《智力大拼图》发布,挑战你的思维风暴。不过当时初学游戏开发,经验浅薄,所以没有好好专研游戏里的算法和代码的缺陷,导致游戏出现了很多bug,甚至拼图打乱后很可能无法复原。最近经常有朋友问起这个游戏,希望我能把代码里的bug改一下方便初学者学习,顺便我也打算测试一下自己写这种小游戏的速度,所以就抽出了一些时间将这个游戏从头到尾重新写了一遍,计算了一下用时,从准备、修改素材到最后完成游戏,一共用了大约2h的时间。

90~120min

最后30min用于细枝末节上的处理,如显示拼图缩略图、显示&更新时间和步数,以及添加游戏结束画面,这些就交给如下冗长而简单的代码来完成吧:

function showThumbnail() {
    var thumbnail = new LBitmap(imgBmpd);
    thumbnail.scaleX = 130 / imgBmpd.width;
    thumbnail.scaleY = 130 / imgBmpd.height;
    thumbnail.x = (LGlobal.width - 100) /2;
    thumbnail.y = 410;
    overLayer.addChild(thumbnail);
}

function addTimeTxt () {
    timeTxt = new LTextField();
    timeTxt.stroke = true;
    timeTxt.lineWidth = 3;
    timeTxt.lineColor = "#54D9EF";
    timeTxt.color = "#FFFFFF";
    timeTxt.size = 18;
    timeTxt.x = 20;
    timeTxt.y = 450;
    overLayer.addChild(timeTxt);

    updateTimeTxt();
}

function updateTimeTxt () {
    timeTxt.text = "时间:" + getTimeTxt(time);
}

function getTimeTxt () {
    var d = new Date(time);

    return d.getMinutes() + " : " + d.getSeconds();
};

function addStepsTxt () {
    stepsTxt = new LTextField();
    stepsTxt.stroke = true;
    stepsTxt.lineWidth = 3;
    stepsTxt.lineColor = "#54D9EF";
    stepsTxt.color = "#FFFFFF";
    stepsTxt.size = 18;
    stepsTxt.y = 450;
    overLayer.addChild(stepsTxt);

    updateStepsTxt();
}

function updateStepsTxt () {
    stepsTxt.text = "步数:" + steps;

    stepsTxt.x = LGlobal.width - stepsTxt.getWidth() - 20;
}

function onFrame () {
    if (isGameOver) {
        return;
    }

    /** 获取当前时间 */
    var currentTime = (new Date()).getTime();

    /** 计算使用的时间并更新时间显示 */
    time = currentTime - startTime;
    updateTimeTxt();
}

function gameOver () {
    isGameOver = true;

    var resultLayer = new LSprite();
    resultLayer.filters = [new LDropShadowFilter()];
    resultLayer.graphics.drawRoundRect(3, "#BBBBBB", [0, 0, 350, 350, 5], true,"#DDDDDD");
    resultLayer.x = (LGlobal.width - resultLayer.getWidth()) / 2;
    resultLayer.y = LGlobal.height / 2;
    resultLayer.alpha = 0;
    overLayer.addChild(resultLayer);

    var title = new LTextField();
    title.text = "游戏通关"
    title.weight = "bold";
    title.stroke = true;
    title.lineWidth = 3;
    title.lineColor = "#555555";
    title.size = 30;
    title.color = "#FFFFFF";
    title.x = (resultLayer.getWidth() - title.getWidth()) / 2;
    title.y = 30;
    resultLayer.addChild(title);

    var usedTimeTxt = new LTextField();
    usedTimeTxt.text = "游戏用时:" + getTimeTxt(time);
    usedTimeTxt.size = 20;
    usedTimeTxt.stroke = true;
    usedTimeTxt.lineWidth = 2;
    usedTimeTxt.lineColor = "#555555";
    usedTimeTxt.color = "#FFFFFF";
    usedTimeTxt.x = (resultLayer.getWidth() - usedTimeTxt.getWidth()) / 2;
    usedTimeTxt.y = 130;
    resultLayer.addChild(usedTimeTxt);

    var usedStepsTxt = new LTextField();
    usedStepsTxt.text = "所用步数:" + steps;
    usedStepsTxt.size = 20;
    usedStepsTxt.stroke = true;
    usedStepsTxt.lineWidth = 2;
    usedStepsTxt.lineColor = "#555555";
    usedStepsTxt.color = "#FFFFFF";
    usedStepsTxt.x = usedTimeTxt.x;
    usedStepsTxt.y = 180;
    resultLayer.addChild(usedStepsTxt);

    var hintTxt = new LTextField();
    hintTxt.text = "- 点击屏幕重新开始 -";
    hintTxt.size = 23;
    hintTxt.stroke = true;
    hintTxt.lineWidth = 2;
    hintTxt.lineColor = "#888888";
    hintTxt.color = "#FFFFFF";
    hintTxt.x = (resultLayer.getWidth() - hintTxt.getWidth()) / 2;
    hintTxt.y = 260;
    resultLayer.addChild(hintTxt);

    LTweenLite.to(resultLayer, 0.5, {
        alpha : 0.7,
        y : (LGlobal.height - resultLayer.getHeight()) / 2,
        onComplete : function () {
            /** 点击界面重新开始游戏 */
            stageLayer.addEventListener(LMouseEvent.MOUSE_UP, function () {
                gameLayer.removeAllChild();
                overLayer.removeAllChild();

                stageLayer.removeAllEventListener();

                startGame();
            });
        }
    });
}

Ok,2h下来,整个游戏就搞定咯~不得不表扬一下lufylegend这个游戏引擎,实在是可以大幅提升开发效率。

发表评论

电子邮件地址不会被公开。 必填项已用*标注