前端纯原生代码实现2048

代码织雪鹤
• 阅读 5186

前言

为什么闲着没事要做一个2048呢?还不是360前端星计划(2018春招实习生)要我做的。然后就花了几天时间做了一个2048小游戏,兼容到pc端和部分移动端(设备有限,有的移动浏览器真的没兼容到或者是真的不想做兼容了)。仅供大家看看就好哈。

github地址: https://github.com/GDUTxxZ/20...

在线预览:http://47.94.199.75/index.html (这个网址暂时有效。。以后点进去不知道又是我的什么实验作品。)

游戏介绍

我做完给朋友看之后发现不是每个人都玩过这个游戏。简单介绍一下游戏内容好了。

前端纯原生代码实现2048

获胜条件: 拼凑出一个2048方块
失败条件: 当前没有可用方块,并且所有方块都不可以和临近方块合并

代码结构

index.html:

<div id="bg"></div><!-- 背景 -->
<div id="main"></div><!-- 实体 -->
<div id="alert">
    <span>游戏失败</span>
    <button>再来一局</button>
<div>

#bg为背景图,也就是空的灰色方块,因为方块移动的时候不能露出底下的空白
#main为实体,也就是游戏中我们看见的包含数字的方块
#alert为提示框,一开始display:none,当游戏胜利或者结束的时候,display:block
#alert span 失败消息或者胜利消息

css的话,主要是关于动画元素的设置:

base.css

#main .item {
    transition: all .3s ease-out;
    -moz-transition: all .3s ease-out; /* Firefox 4 */
    -webkit-transition: all .3s ease-out; /* Safari 和 Chrome */
    -o-transition: all .3s ease-out; /* Opera */
    left: 0px;
    top: 0px;
}

js的话主要是两块,util.js负责了一些外围函数(重要的是关于移动端滑动事件的封装)的处理,2048.js就是页面整体逻辑

util.js // 关于移动端滑动事件的封装

const touchManager = (function () {
    let start = []
    let end = []
    let timeStamp = 0

    let manager = {}
    manager.touchstart = function (event) { // 记录下开始位置
        event.stopPropagation()

        timeStamp = event.timeStamp // 获取点击时的时间
        let target = event.targetTouches[0]

        start = [target.pageX, target.pageY]
        end = [target.pageX, target.pageY]
        console.log('start')
    }
    manager.touchmove = function (event) { // 记录下移动位置
        event.stopPropagation()
        event.preventDefault()

        let target = event.targetTouches[0]

        end = [target.pageX, target.pageY]
        console.log('move')
    }
    manager.touchend = function (event) { // 处理开始位置和移动位置给出滑动方向
        event.stopPropagation()
        event.preventDefault()

        const abs = Math.abs

        let time = event.timeStamp - timeStamp // 获取滑动操作使用的时间
        let moveX = end[0] - start[0]
        let moveY = end[1] - start[1]

        if (time > 500 || (abs(moveX) < 50 && abs(moveY) < 50)) { // 移动距离不够或时间太长就不认为是滑动
            return false
        } else {
            if (abs(moveX) >= abs(moveY)) { // 横向移动距离较长
                console.log(moveX)
                return moveX > 0 ? 'right' : 'left'
            } else { // 纵向移动距离较长
                console.log(moveY)
                return moveY > 0 ? 'down' : 'up' 
            }
        }
    }
    return manager
})()

2048.js 主要由以下几个数据结构和函数构成:

数据结构:

let data = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]] // 初始化
let emptyList = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] // 当前没有元素的格子
const container = document.querySelector('#main') // 操作实体
const bg = document.querySelector('#bg') // 背景板
const alert = document.querySelector('#alert') // 提醒框
const alertText = alert.querySelector('span') // 提醒框里的文字容器
const baseSize = parseInt(getDomStyle(container).width) // 基础画板的size
let gameOver = false // 标志游戏是否结束
const animateTime = 300 // 单位ms, 动画时长

这个地方保存了大量dom元素的引用,为了以后操作的时候减少获取dom的性能消耗

另外还有以下几个函数:

main // 主程序
resize(el) // 调整元素宽高一致
createElement() // 在emptyList里找1-2个下标出来,给data添加新的元素,取值为{2, 4}
paint(el, data) // 用data在el里画出每一个格子
animate(move, arrow) // 传入一个移动队列,和移动方向‘left’代表横向,‘top’代表纵向
isWin(data) isLost(data, emptyList) // 判断游戏胜负
win() lost() // 显示胜负消息
replay() // 再来一局
moveHandle = {...} // 封装了计算移动结果的函数

然后再从主程序看函数的流程:

function main () { // 主程序
    // 调整背景和实体宽高
    resize(container)
    resize(bg)
    
    // 初始化背景和实体元素
    paint(bg, data)
    // 创建1-2个初始元素
    createNewElement()
    paint(container, data)

    // 绑定事件监听器
    addEvent(window, 'keydown', function (event) { // 按键监听
        if (gameOver) {
            return
        }
        let arrow = keyCodeMap[event.keyCode]
        switch (arrow) {
            case 'left':
            case 'up':
            case 'right':
            case 'down': {
                moveHandle.move(arrow)
                break
            }
        }
    })

    addEvent(alert.querySelector('button'), 'click', replay) // 再玩一次

    addEvent(container, 'touchstart', touchManager.touchstart)
    addEvent(container, 'touchmove', touchManager.touchmove)
    addEvent(container, 'touchend', function (event) {
        let arrow = touchManager.touchend(event)
        if (arrow) {
            moveHandle.move(arrow)
        }
    })
}

也即是:1.初始化 2. 绑定事件监听

然后就是如何计算出移动结果,以下用一个左滑计算(moveHandle.moveleft)为例子

moveleft: function () { // 向左移动
    // 计算移动后的data
    // 要移动的元素的移动坐标
    // 没有元素的格子
    let newData = copy(data) // 获取当前数据的一个copy
    let move = [] // 方块移动队列
    emptyList = []
    for (let i = 0; i < 4; i++) { // 一行行处理
        let newList = [] // 新行
        let oldList = data[i]

        for (let j = 0; j < 4; j++) { // 找到所有非0单元
            let value = newData[i][j]
            if (value !== 0) {
                newList.push(value)
            }
        }

        if (newList.length > 1) { // 合并同类项
            for (let j = 0, len = newList.length; j < len - 1; j++) {
                if (newList[j] === newList[j + 1]) {
                    newList[j] *= 2
                    newList[j + 1] = 0
                    j++
                }
            }
            newList = newList.filter(item => item !== 0) // 过滤掉上一步产生的0
        }

        for (let j = newList.length; j < 4; j++) { // 补全数列尾部的0
            emptyList.push(i * 4 + j)
            newList.push(0)
        }

        newData[i] = newList

        // 产生每位元素移动的坐标
        for (let j = 0, k = 0,tag = false; j < 4; j++) { // j为旧元素位置,k为移动到的位置
            if (newList[k] === 0) { // 如果没有要移动的位置了
                break
            } else if (oldList[j] === newList[k]) { // j移动到k位置
                if (j !== k) {
                    move.push({
                        start: [i, j],
                        end: [i, k]
                    })
                }
                k++
            } else if (oldList[j] === newList[k] / 2) { // 两个元素合成k位置的元素
                move.push({
                    start: [i, j],
                    end: [i, k]
                })
                if (tag) {
                    k++
                }
                tag = !tag
            }
        }
    }

    return {
        newData: newData,
        move: move
    }
}

这个函数最后产出的是 newData 计算后的 data, move 方块的移动队列,形如[{start: [x1, y1], end: [x2, y2]}, ... ]

然后怎么利用这个计算结果呢,看moveHandle.move.(moveHandle中有三个私有变量,moving锁定句柄,防止动画过程中用户再次滑动,win是否胜利,lost时候失败)

move: function (arrow) { // arrow = 移动方向
    if (this.moving) { // 如果正在进行动画,返回移动失败
        return false
    }

    let result = this['move' + arrow]() // 获取移动计算后的结果
    let newData = result.newData // 移动后的数据矩阵
    let move = result.move // 移动元素列表

    // 根据移动元素列表判断该操作是否有效
    if (move.length === 0) { // 没有可以移动的元素,则无效
        console.log('本次移动无效')
        return false
    }

    // 进行0.3秒动画
    data = newData // 修改全局数据矩阵
    createNewElement() // 创造新元素

    // 判断游戏胜负
    this.win = isWin(newData)
    if (!this.win) {
        this.lost = isLost(newData, emptyList)
    }

    this.moving = true // 锁定该事件句柄


    setTimeout((function (self) {
        animate(move, arrow)
        return function () {
            // 足够时间后
            self.moving = false // 终止动画
            paint(container, data) // 重绘视图

            // 判断游戏胜负
            if (self.win) { // 赢得了游戏
                win()
            } else if (self.lost) {
                lost()
            }
        }
    })(this), animateTime)
}

我自认为我的注释内容还是挺多的,应该还是能看懂。这次分享就到这了。欢迎评论区留言讨论。发现有什么bug也尽可能跟我说把。

BUG

目前已知的是:
1.微信内置浏览器的转码问题:

这个因为我懒得整一个域名,所以它为了安全就会进行转码,没法游戏。也就不修复了。。只是个小玩具。

2.ios长按会选取文字而且无法取消:

这个问题我已经做了一定的修复,但是我没复现这个问题的方法,也没再处理

3.夸克浏览器自带手势导致左滑右滑会进行系统行为:

没想到办法,如果有人有办法请告诉我,谢谢。
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Karen110 Karen110
3年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
1年前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Stella981 Stella981
3年前
JS 苹果手机日期显示NaN问题
问题描述newDate("2019122910:30:00")在IOS下显示为NaN原因分析带的日期IOS下存在兼容问题解决方法字符串替换letdateStr"2019122910:30:00";datedateStr.repl
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这