JavaScript-小游戏-2048

需求

开局生成随机靠边方块 点击方向键移动方块 相邻方块且数值相同发生合并 方块占满界面触发游戏结束提示

游戏界面

标签结构HTML

area区域和death区域分别表示游戏区域和死亡提示区域

js 复制代码
 <!-- 页面 -->
  <div class="area"></div>
  <!-- 死亡提示区域 -->
  <div class="death"></div>

层叠样式 css

设置游戏区域为flex布局 主轴默认是水平 flex-wrap: wrap;设置主轴自动换行 注意这里游戏区域的大小是200px*200px的 后面可以算出每个方块的大小

js 复制代码
 .area {
      display: flex;
      flex-wrap: wrap;
      width: 200px;
      height: 200px;
      font-size: 30px;
    }

获取元素 js

获取两个游戏区域和游戏结束提示区域的div

js 复制代码
   const area = document.querySelector('.area')
    const death = document.querySelector('.death')

数据分析

二维数组arr[y][x]第一个索引表示y坐标 第二个索引表示x坐标 数组的值表示方块的数值(2,4,8...)没有值就表示这个坐标位置没有方块 这里还要初始化一个rArr数组是用来将数组旋转结果存入新数组防止重复处理的

后面的移动和合并都是需要旋转数组进行操作

js 复制代码
let arr = [] //方块坐标和数值
    let rArr = [] //旋转数组
    //初始化
    for (let i = 0; i < 5; i++) {
      arr[i] = []
      rArr[i] = []
    }

功能实现

渲染方法

  • 像素点的思路渲染游戏区域为5*5个方块 所以算出每个方块 的大小为40px*40px
  • 如果有方块 就渲染为粉色 方块的数值${arr[y][x]}也渲染在div
  • 其余部分就是画布
  • block字符串累加完毕之后渲染到页面上
js 复制代码
 //渲染页面
    function render() {
      block = ''
      for (let y = 0; y < 5; y++) {
        for (let x = 0; x < 5; x++) {
          if (arr[y][x]) {
            block += `<div class="square"style="width: 40px;height: 40px;text-align: center; line-height: 40px; background-color: pink;">${arr[y][x]}</div>`
          }
          else {
            block += ' <div class="block" style="width: 40px;height: 40px;background-color: antiquewhite;"></div>'
          }
        }
      }
      area.innerHTML = block
    }
    render()

游戏区域页面如下

开局随机生成两个方块

这里的方块坐标不能重复

  • 声明两个数字类型的全局变量 用来表示两个随机生成的坐标
  • 这里的randomStart函数用来随机生成不重复的两个坐标 ac变量表示随机生成[0,4]之间的整数坐标 如果生成的随机整数坐标a c重复 就重新调用randomStart函数生成
  • 这里运用了三元运算符 来根据 Math.random() 随机生成[0,1)范围内的两个小数的大小 决定这两个方块的坐标位置 这里坐标位置可以重复
  • 第一个方块Math.random()> 0.5 执行前面的表达式arr[0][a] = 2 方块随机在上面的边 否则执行后面的表达式 arr[a][0] = 2方块随机在左边的边 第二个方块同理

三元运算符

条件?表达式A:表达式B 条件为真执行前面的表达式A 条件为假执行后面的表达式B 常见用法 是处理null值 这里表示 如果传入的参数是假的话name的值为 'stranger' 如果是真的话就是参数name属性的值

js 复制代码
 //三元运算符
    //以箭头函数形式 定义greeting函数 这里传入的参数是对象
    const greeting = (person) => {
      const name = person ? person.name : 'stranger'
      return `hello ${name}`
    }
    console.log(greeting({ age: 18, name: 'a' })); //hello a

    //这里三元运算符是真 
    // 但是字符串本身没有name属性
    console.log(greeting('b')); //undefined

    //传入的都是字面量 是最简单的表达式 可以通过代码直接看出值
    //这些是假值 
    //数字字面量
    console.log(greeting(0)); //hello stranger
    //null字面量
    console.log(greeting(null)); //hello stranger
    //字符串字面量
    console.log(greeting('')); //hello stranger

最终开局随机生成两个方块功能代码如下

js 复制代码
//随机生成两个不相同随机坐标
    let a = 0, c = 0
    function randomStart() {
      //生成的随机数不能重复 连续两次的话
      a = Math.floor(Math.random() * 5)  //0-5
      c = Math.floor(Math.random() * 5)
      if (c === a) {
        randomStart()
      }
    }
    //开局生成两个不重复的方块
    randomStart()
    Math.random() > 0.5 ? arr[0][a] = 2 : arr[a][0] = 2
    Math.random() > 0.5 ? arr[0][c] = 2 : arr[c][0] = 2

效果如下

点击方向键移动方块

分析点击方向键之后方块变化

这里有上下左右四个方向 每个方向都要写移动逻辑的话代码冗余了 所以可以根据要旋转的方向不同对数组进行旋转处理 然后再左移 这时候再进行左移这个方向的合并判断 最后把数组旋转回去 移动处理完毕之后还需要生成随机新方块

这里的设计不太符合单一职责(一个函数 模块只负责一件事)事件监听承受了太多功能 目前还不知道怎么优化比较好

根据分析将功能拆分

  • 除了向左移动的情况 其他方向都是 点击键盘方向键之后先更改type的值再根据type调用rotate()函数旋转数组 move()往左移动 add()判断合并最后再rotate()旋转回去

这里的rotate()要传入具体的type参数 因为在上下移动过程中出现了顺时针和逆时针的旋转 这里将数组旋转回去的时候要传入的type就不同了

  • 向左移动的情况很简单 只需要直接move()移动再执行合并 移动完之后
  • 生成新随机方块 random()
  • 此时arr已经处理完毕 rander()渲染页面
  • 判断是否死亡 deathJudge() 键盘按键事件如下
js 复制代码
let type = ''//移动方向
    //每次移动完生成一个边缘新方块
    document.addEventListener('keydown', function (e) {
      if (e.key === 'ArrowUp') {
        type = 'up'
        rotate('up')//旋转
        move()//移动
        add()
        rotate('down')//旋转回去
      }

      //这里处理错了down
      else if (e.key === 'ArrowDown') {
        type = 'down'
        rotate('down')//旋转
        move()//移动
        add()
        rotate('up')//旋转回去
      }
      else if (e.key === 'ArrowLeft') {
        type = 'left'
        move()
        add()
      }
      else if (e.key === 'ArrowRight') {
        type = 'right'
        rotate('right')//旋转
        move()//移动
        add()
        //因为这里是翻转 所以和之前一样处理翻转回去就行
        rotate('right')
      }
      //移动处理完毕生成新随机方块
      random()
      render()
      deathJudge()
    })

旋转处理

  • 如果有值就根据type不同旋转 right 这里相当于翻转 所以旋转回去只需要再翻转一次rotate('right')就行 down 这里相当于顺时针旋转90度 所以旋转回去需要逆时针旋转90度也就是rotate(up)

up 这里相当于逆时针旋转90度 所以旋转回去需要顺时针旋转90度也就是rotate('down')

  • 旋转完毕后 删除已经处理完旋转的元素 便于后面将旋转之后的数组给arr

这里删除处理也可以用delete 不会破坏索引!

  • 然后遍历rArr把数值给arr 再把rArr相应的元素删除方便下次rotate()旋转

放进新数组是为了防止旋转后的元素被重复旋转遍历 最终代码如下

js 复制代码
  //翻转数组 变成左移
    function rotate(type) {
      console.log('rotate', type);
      for (let y = 0; y < 5; y++) {
        for (let x = 0; x < 5; x++) {
          if (arr[y][x]) {
            //翻转
            if (type === 'right') {
              //这里旋转结果必须放进新数组防止重复处理
              rArr[y][4 - x] = arr[y][x]
            }
            //顺时针90
            else if (type === 'down') {
              rArr[x][4 - y] = arr[y][x]
            }
            //逆时针90
            else if (type === 'up') {
              rArr[4 - x][y] = arr[y][x]
            }
            //删除原位置元素
            // arr[y][x] = 0
            delete arr[y][x]
          }
        }
      }
      //把旋转之后的数组给arr 方便转回去
      for (let j = 0; j < 5; j++) {
        for (let i = 0; i < 5; i++) {
          if (rArr[j][i]) {
            arr[j][i] = rArr[j][i]
            delete rArr[j][i]
          }
        }
      }
      // console.log('rotate旋转处理之后的arr', arr);
    }

移动处理

旋转完毕后 数组只需要左移动就可以

  • 首先遍历arr数组 如果这行出现空位 !arr[y][x]true 表示 arr[y][x]为false也就是为空和0的时候 执行后面的语句
  • 遍历这一行空位后面的部分 这里循环的起始点是i = x 如果后面有值就给前面空位 这里记得删掉后面的值 然后直接break跳出空位后元素遍历循环继续对这一行进行空位搜索

补充说明breakcontinue的区别 break只跳出一层循环 这里相当于 x>=1的情况下都不执行内层循环了 continue跳过当前 然后进行下一个迭代 这里相当于只有在x===1的情况下才不执行内层循环 其他情况是正常执行的

js 复制代码
    // 移动逻辑
    //所有数组都旋转处理成左移判断
    function move() {
      console.log('move');
      for (let y = 0; y < 5; y++) {
        for (let x = 0; x < 5; x++) {
          if (!arr[y][x]) {
            //后面如果后面有值就给前面空位
            for (let i = x; i < 5; i++) {
              //只执行一次 找出最靠近空位的值
              if (arr[y][i]) {
                arr[y][x] = arr[y][i]
                arr[y][i] = 0
                //这里用哪个符号比较好
                break //跳出for i 循环么
              }
            }
          }
        }
      }
    }

点击方向键移动方块效果如下

合并判断

这里合并判断也会根据方向不同 判断的方向不同 所以这里放在数组旋转回去之前 所以无论方向如何都是对左移进行合并判断

  • 合并 这里是向左合并的 判断的是arr[j][i] 和右边相邻位置 arr[j][i + 1]的元素数值 这里的判断范围缩小i < 4 如果相等就左边方块数值累加然后右边方块数值清空

注意 这里只完成了数值的合并 合并完之后还要向左移动 如图例

  • 全部合并完之后要都向左移动 move()
js 复制代码
 //合并
    function add() {
      console.log('合并add', type, arr);
      for (let j = 0; j < 5; j++) {
        for (let i = 0; i < 4; i++) {
          //相邻检测
          if (arr[j][i]) {
            if (arr[j][i] === arr[j][i + 1]) {
              console.log('找到左移方块', j, i);
              arr[j][i] += arr[j][i]
              arr[j][i + 1] = 0
            }
          }
        }
      }
      //全部合并完之后再向左移动
      move()
    }

合并效果如下

移动完毕生成新随机方块

这些方块是从没有方块的坐标中随机生成的

  • scope数组表示没有方块的坐标 第一个索引表示方块序号可以用来随机选取方块 第二个索引表示方块的x或者y值
  • 循环遍历arr把没有值的坐标放进scope数组
  • index表示随机的scope索引 范围为[0,scope.length-1]

Math.floor Math.random 表示从[a,b]随机整数 Math.floor(Math.random() * (b - a + 1)) + a; Math.random()随机生成[0,1)之间的随机小数 Math.random() * (b - a + 1) 生成[0,b-a+1) Math.floor 对小数向下取整得到[0,b-a]之间的整数 最后再加上a 范围偏移成[a,b]

  • 这里randomY表示随机到的y坐标 也就是scope[index][0]index个方块的第0个坐标
  • 最后随机生成的方块数值可能为2和4 继续用Math.random()随机生成的小数和0.5的大小比较决定随机生成方块的数值
js 复制代码
//移动之后再随机生成一个方块 数值可能是4或者2
    function random() {
      let scope = []
      //因为可能后面没有值 长度实际上小于10
      for (let j = 0; j < 5; j++) {
        for (let i = 0; i < 5; i++) {
          //方块不重复
          //把没有值的坐标放进数组 然后再随机索引进行选择
          if (!arr[j][i]) {
            scope.push([j, i])
          }
        }
      }
      //随机索引
      let index = Math.floor(Math.random() * scope.length)
      let randomY = scope[index][0]
      let randomX = scope[index][1]

      //随机数值
      Math.random() > 0.5 ? arr[randomY][randomX] = 2 : arr[randomY][randomX] = 4
    }

移动完毕生成新随机方块效果如下

死亡功能

死亡判断

开局只生成两个方块不需要判断 之后每次移动完毕都要判断一次

  • death表示是否出现死亡 Boolean类型初始值是true
  • 遍历arr 如果位置上元素没有值 就还没有死亡 death = false 这个是要找的非常态 出现这种情况就知道最终结果了 所以初始值是常态true 元素上有值还要继续往下找

这里不能用!arr[j].every((value) => value > 1) 因为every对稀疏数组的空槽是不执行 的 在random()randomStart()给随机坐标赋值的时候造成了arr[j]是稀疏数组 会导致判断失误 补充说明 稀疏数组创建方式

  • 最终的death值就能表示死亡情况
  • 如果death === true就执行死亡效果
js 复制代码
   //死亡判断
    function deathJudge() {
      let death = true //表示是否进入死亡
      for (let y = 0; y < 5; y++) {
        //如果这一行有空位 就跳出循环
        //这里不是表示每个都大于
        //不能用every因为稀疏数组不执行
        for (let x = 0; x < 5; x++) {
          //有一个位置上的元素没有值 就不是死亡
          if (!arr[y][x]) {
            death = false
          }
        }
      }
      console.log(death, '死亡判断');
      if (death === true) {
        deachCss()  //死亡效果
      }
    }

死亡效果

要找出方块数值最大值打印在游戏结束区域

  • maxArr数组用来放每行的最大值
  • maxX表示每行的最大值 找出来之后放进maxArr数组
  • arr[y]采用线性遍历 像直线一样逐个排查 这里默认每行的第一个元素为最大值 然后遍历后面的元素 如果比maxX大 就更新 把值给maxX

后面还会更新数组最大值的一些比较方法

  • 把每行的最大值pushmaxArr数组
  • 然后再对maxArr进行线性遍历 从而找到整个arr数组的最大值
  • 最后把最大数值渲染到页面
js 复制代码
 function deachCss() {
      //找到数组中的最大数值
      let maxArr = []
      let maxX = 0
      for (let y = 0; y < 5; y++) {
        maxX = arr[y][0]
        for (let x = 1; x < 5; x++) {
          if (arr[y][x] > maxX) {
            maxX = arr[y][x]
          }
        }
        maxArr.push(maxX) //每行的最大
      }
      console.log(maxArr);
      //再从每行的最大里面找
      let max = maxArr[0]
      for (let a = 1; a < maxArr.length; a++) {
        if (max < maxArr[a]) {
          max = maxArr[a]
        }
      }
      //死亡效果
      death.innerText = `游戏结束 最大方块为${max}`
    }

死亡效果如下

最终代码

js 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>2048</title>
</head>

<body>
  <!-- 页面 -->
  <div class="area"></div>
  <!-- 死亡提示区域 -->
  <div class="death"></div>
  <style>
    .area {
      display: flex;
      flex-wrap: wrap;
      width: 200px;
      height: 200px;
      font-size: 25px;
    }
  </style>
  <script>
    const area = document.querySelector('.area')
    const death = document.querySelector('.death')
    let arr = [] //方块坐标和数值
    let rArr = [] //旋转数组
    //初始化
    for (let i = 0; i < 5; i++) {
      arr[i] = []
      rArr[i] = []
    }
    deathJudge() //开局只随机生成两个方块不需要判断死亡

    //随机生成两个不相同随机坐标
    let a = 0, c = 0
    function randomStart() {
      //生成的随机数不能重复 连续两次的话
      a = Math.floor(Math.random() * 5)  //0-5
      c = Math.floor(Math.random() * 5)
      if (c === a) {
        randomStart()
      }
    }
    //开局生成两个不重复的方块
    randomStart()
    Math.random() > 0.5 ? arr[0][a] = 2 : arr[a][0] = 2
    Math.random() > 0.5 ? arr[0][c] = 2 : arr[c][0] = 2

    //移动之后再随机生成一个方块 数值可能是4或者2
    function random() {
      let scope = []
      for (let j = 0; j < 5; j++) {
        for (let i = 0; i < 5; i++) {
          //方块不重复
          //把没有值的坐标放进数组 然后再随机索引进行选择
          if (!arr[j][i]) {
            scope.push([j, i])
          }
        }
      }
      //随机索引
      let index = Math.floor(Math.random() * scope.length)
      let randomY = scope[index][0]
      let randomX = scope[index][1]

      //随机数值
      Math.random() > 0.5 ? arr[randomY][randomX] = 2 : arr[randomY][randomX] = 4
    }


    let type = ''//移动方向
    //每次移动完生成一个边缘新方块

    document.addEventListener('keydown', function (e) {
      if (e.key === 'ArrowUp') {
        type = 'up'
        rotate('up')//旋转
        move()//移动
        add()
        rotate('down')//旋转回去

      }

      //这里处理错了down
      else if (e.key === 'ArrowDown') {
        type = 'down'
        rotate('down')//旋转
        move()//移动
        add()
        rotate('up')//旋转回去

      }
      else if (e.key === 'ArrowLeft') {
        type = 'left'
        move()
        add()
      }
      else if (e.key === 'ArrowRight') {
        type = 'right'
        rotate('right')//旋转
        move()//移动
        add()
        //因为这里是翻转 所以和之前一样处理翻转回去就行
        rotate('right')
      }
      //移动处理完毕生成新随机方块
      random()
      render()
      deathJudge()
    })

    //翻转数组 变成左移
    function rotate(type) {
      console.log('rotate', type);
      for (let y = 0; y < 5; y++) {
        for (let x = 0; x < 5; x++) {
          if (arr[y][x]) {
            //翻转
            if (type === 'right') {
              //这里旋转结果必须放进新数组防止重复处理
              rArr[y][4 - x] = arr[y][x]
            }
            //顺时针90
            else if (type === 'down') {
              rArr[x][4 - y] = arr[y][x]
            }
            //逆时针90
            else if (type === 'up') {
              rArr[4 - x][y] = arr[y][x]
            }
            //删除原位置元素
            // arr[y][x] = 0
            delete arr[y][x]
          }
        }
      }
      //把旋转之后的数组给arr 方便转回去
      for (let j = 0; j < 5; j++) {
        for (let i = 0; i < 5; i++) {
          if (rArr[j][i]) {
            arr[j][i] = rArr[j][i]
            delete rArr[j][i]
          }
        }
      }
      // console.log('rotate旋转处理之后的arr', arr);
    }


    // 移动逻辑
    //所有数组都旋转处理成左移判断
    function move() {
      console.log('move');
      for (let y = 0; y < 5; y++) {
        for (let x = 0; x < 5; x++) {
          if (!arr[y][x]) {
            //后面如果后面有值就给前面空位
            for (let i = x; i < 5; i++) {
              //只执行一次 找出最靠近空位的值
              if (arr[y][i]) {
                arr[y][x] = arr[y][i]
                arr[y][i] = 0
                //这里用哪个符号比较好
                break //跳出for i 循环么
              }
            }
          }
        }
      }
    }

    //合并
    function add() {
      console.log('合并add', type, arr);
      for (let j = 0; j < 5; j++) {
        for (let i = 0; i < 4; i++) {
          //相邻检测
          if (arr[j][i]) {
            if (arr[j][i] === arr[j][i + 1]) {
              console.log('找到左移方块', j, i);
              arr[j][i] += arr[j][i]
              arr[j][i + 1] = 0
            }
          }
        }
      }
      //全部合并完之后再向左移动
      move()
    }

    //死亡判断
    function deathJudge() {
      let death = true //表示是否进入死亡
      for (let y = 0; y < 5; y++) {
        //如果这一行有空位 就跳出循环
        //这里不是表示每个都大于
        //不能用every因为稀疏数组不执行
        for (let x = 0; x < 5; x++) {
          //有一个位置上的元素没有值 就不是死亡
          if (!arr[y][x]) {
            death = false
          }
        }
      }
      console.log(death, '死亡判断');
      if (death === true) {
        deachCss()  //死亡效果
      }
    }


    function deachCss() {
      //找到数组中的最大数值
      let maxArr = []
      let maxX = 0
      for (let y = 0; y < 5; y++) {
        maxX = arr[y][0]
        for (let x = 1; x < 5; x++) {
          if (arr[y][x] > maxX) {
            maxX = arr[y][x]
          }
        }
        maxArr.push(maxX) //每行的最大
      }
      console.log(maxArr);
      //再从每行的最大里面找
      let max = maxArr[0]
      for (let a = 1; a < maxArr.length; a++) {
        if (max < maxArr[a]) {
          max = maxArr[a]
        }
      }
      //死亡效果
      death.innerText = `游戏结束 最大方块为${max}`
    }


    //渲染页面
    function render() {
      block = ''
      for (let y = 0; y < 5; y++) {
        for (let x = 0; x < 5; x++) {
          if (arr[y][x]) {
            block += `<div class="square"style="width: 40px;height: 40px;text-align: center; line-height: 40px; background-color: pink;">${arr[y][x]}</div>`
          }
          else {
            block += ' <div class="block" style="width: 40px;height: 40px;background-color: antiquewhite;"></div>'

          }
        }
      }
      area.innerHTML = block
    }
    render()


  </script>
</body>

</html>
相关推荐
爱心发电丶1 小时前
基于UniappX开发电销APP,实现CRM后台控制APP自动拨号
javascript
地狱恶犬萨煤耶1 小时前
JavaScript-实现函数方法-改变this指向call apply bind
javascript
地狱恶犬萨煤耶1 小时前
JavaScript-小游戏-单词消消乐
javascript
tyro曹仓舒2 小时前
干了10年前端,才学会使用IntersectionObserver
前端·javascript
mine_mine3 小时前
油猴脚本拦截fetch和xhr请求,实现修改服务端接口功能
javascript
一 乐3 小时前
考公|考务考试|基于SprinBoot+vue的考公在线考试系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot·课程设计
林太白4 小时前
跟着TRAE SOLO全链路看看项目部署服务器全流程吧
前端·javascript·后端
特级业务专家4 小时前
把 16MB 中文字体压到 400KB:我写了一个 Vite 字体子集插件
javascript·vue.js·vite
先生沉默先4 小时前
NodeJs 学习日志(8):雪花算法生成唯一 ID
javascript·学习·node.js