2048小游戏(前端三板斧详细笔记,JavaScript初步详细教程)

目录

项目源码:GitHub - Atopos-suyu/256game: 这是改编 的2048小游戏,为防止有人通关不成功,让他们体验下通关的满足感,写了256小游戏

2048设计稿:腾讯 CoDesign - 腾讯 CoDesign

页面效果展示:

游戏开始界面:

游戏结束页面:

游戏胜利页面:

实战需求:

实战技术知识点:

静态页面开发

2048静态页面一:

静态页面二

我们分析下其相同点和不同点:

2048模型设计,随机渲染

2048移动处理

2048合并处理

2048动画效果

2048本地存储

项目源码:GitHub - Atopos-suyu/256game: 这是改编 的2048小游戏,为防止有人通关不成功,让他们体验下通关的满足感,写了256小游戏
2048设计稿:腾讯 CoDesign - 腾讯 CoDesign

页面效果展示:

游戏开始界面:

游戏结束页面:

游戏胜利页面:

实战需求:

1.游戏是一个4x4的方格,每个方格我们称作为一个Tile或者Cel。
2.游戏开始能随机出现2个Tile,每个的值90%可能为2,10%可能为4。
3.可以通过上、下、左、右键盘操作,每个Tle按照方向移动到不可移动为止。
4.如果移动以后两个Tile的内容值一样,则进行合并。
5.每个Tile移动会有100ms的移动动画。
6.每个Tile的出现有个短暂的放大效果。
7.每次Tile的合并有个短暂的放大回弹效果。
8.J顶部Score记录当前分数,BestScore记录有史以来最高
分,每次合并都会产生分数的变化,分数计算规则为:分数
=原来分数+合并后的值。
9.游戏将时时刻刻记录进度,刷新页面重现游戏进度。
10.当某个Tile的值为2048,游戏胜利。
11.当每个方格都有值,并且相邻两个方格无法再进行合并,则游戏结束。

实战技术知识点:

1.静态页面渲染:需要HTML、CSS基础知识,包括学习的SCSS知识。
2.开始游戏等事件处理:需要使用DOM监听事件。
3.Tle移动处理:需要监听键盘事件(暂时不处理H5中手势事件的情况)。
4.Tile动态随机添加:需要使用DOM动态操作。
5.Tile移动,合并:需要使用Javascript列表,对象,方法等数据结构和常用技巧。
6.Tile动画:需要使用CSS的transform和animation等动画效果。
7.本地缓存:需要使用Javascript localStorage浏览器缓存。
8…

静态页面开发

2048静态页面一:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>优课达-2048</title>
    <link rel="stylesheet" href="./style/index.css" />
  </head>
  <body>
    <div class="container">
      <nav>...</nav>
      <div class="desc">...</div>
      <main>
        <div class="game-grid">...</div>
        <div class="tile-container">...</div>
      </main>
      <footer>...</footer>
    </div>
  </body>
</html>

整个页面的CSS文件较大,为了更加清晰的理解CSS文件。我们利用scss@import特性对文件进行分离,如下文件目录。

|-- images
|-- style
|-- index.scss  // scss入口文件 + footer
|-- nav.scss    // 头部区域文件
|-- main.scss   // 主体区域文件
|-- desc.scss   // 描述区域文件
|-- index.html
$field-width: 290px;
$grid-spacing: 10px;
$grid-row-cells: 4;
$tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells;
$tile-border-radius: 3px;

main {
  margin-top: 20px;
  margin-left: auto;
  margin-right: auto;
  box-sizing: border-box;
  width: $field-width;
  height: $field-width;
  position: relative;
  padding: $grid-spacing;
  background: #bbada0;
  border-radius: 8px;

  .game-grid {
    .grid-row {
      .grid-cell {
      }
    }
  }
}
静态页面二

为了方便以后Tile方块动态渲染,我们加入2、4、8到2048,11个方块静态页面,最终效果如下:

我们分析下其相同点和不同点:

1.它们都有一样的大小,圆角,动效。所以我们需要设置一个统一的class为tile

2.它们每个数字颜色和字体大小都不同,因此我们需要为每个值设置单独的样式,class为ti1e-(x)(x为2、4、8、16.2048)。

3.它们的位置可以总结为*行(roW),列(column),因此我们可以使用绝对定位进行布局class为title-position-(row)-(column)

4.每个元素都有移动(translate)和放缩(scale)动画,因为两个动画都是transform的一个属性,会出现冲突。因此我们将每个Tile分为外框tile和tile-inner两个部分,tile用于元素移动,title-inner用于元素放缩。

<div class="tile tile-2 tile-position-1-1">
  <div class="tile-inner">2</div>
</div>
.tile-container {
  position: absolute;
  left: 0;
  top: 0;

  .tile {
    position: absolute;
    width: $tile-size;
    height: $tile-size;
    border-radius: 4px;
    transition: transform 100ms ease-in-out;
  }

  .tile-inner {
    width: 100%;
    height: 100%;
    line-height: $tile-size;
    background: #eee4da;
    text-align: center;
    font-weight: bold;
    font-size: 34px;
    color: #776e65;
  }
//通过scss的循环和变量,动态生成4行4列的Tile Position样式
  @for $x from 1 through $grid-row-cells {
    @for $y from 1 through $grid-row-cells {
      .tile-position-#{$x}-#{$y} {
      $xPos: $grid-spacing + floor(($tile-size + $grid-spacing) * ($y - 1));
      $yPos: $grid-spacing + floor(($tile-size + $grid-spacing) * ($x - 1));
      transform: translate($xPos, $yPos);
    }
  }
}

.tile.tile-2 .tile-inner {
  background: #eee4da;
}
}

<div class="tile-container"><!--存放游戏中出现的小方块-->
  <!-- <div class="tile tile-2 tile-position-1-1">
  <div class="tile-inner">2</div>
</div>
  <div class="tile tile-4 tile-position-1-2">
  <div class="tile-inner">4</div>
</div>
  <div class="tile tile-8 tile-position-1-3">
  <div class="tile-inner">8</div>
</div>
  <div class="tile tile-16 tile-position-1-4">
  <div class="tile-inner">16</div>
</div>
  <div class="tile tile-32 tile-position-2-1">
  <div class="tile-inner">32</div>
</div>
  <div class="tile tile-64 tile-position-2-2">
  <div class="tile-inner">64</div>
</div>
  <div class="tile tile-128 tile-position-2-3">
  <div class="tile-inner">128</div>
</div>
  <div class="tile tile-256 tile-position-2-4">
  <div class="tile-inner">256</div>
</div>
  <div class="tile tile-512 tile-position-3-1">
  <div class="tile-inner">512</div>
</div>
  <div class="tile tile-1024 tile-position-3-2">
  <div class="tile-inner">1024</div>
</div>
  <div class="tile tile-2048 tile-position-3-3">
  <div class="tile-inner">2048</div>
</div> -->
</div>

main .tile-container {
  position: absolute;
  left: 0;
  top: 0;
}
main .tile-container .tile {
  position: absolute;
  width: 60px;
  height: 60px;
  border-radius: 4px;
  transition: transform 100ms ease-in-out;
}
main .tile-container .tile-inner {
  width: 100%;
  height: 100%;
  line-height: 60px;
  background: #eee4da;
  text-align: center;
  font-weight: bold;
  font-size: 34px;
  color: #776e65;
}
main .tile-container .tile-position-1-1 {
  transform: translate(10px, 10px);
}
main .tile-container .tile-position-1-2 {
  transform: translate(80px, 10px);
}
main .tile-container .tile-position-1-3 {
  transform: translate(150px, 10px);
}
main .tile-container .tile-position-1-4 {
  transform: translate(220px, 10px);
}
main .tile-container .tile-position-2-1 {
  transform: translate(10px, 80px);
}
main .tile-container .tile-position-2-2 {
  transform: translate(80px, 80px);
}
main .tile-container .tile-position-2-3 {
  transform: translate(150px, 80px);
}
main .tile-container .tile-position-2-4 {
  transform: translate(220px, 80px);
}
main .tile-container .tile-position-3-1 {
  transform: translate(10px, 150px);
}
main .tile-container .tile-position-3-2 {
  transform: translate(80px, 150px);
}
main .tile-container .tile-position-3-3 {
  transform: translate(150px, 150px);
}
main .tile-container .tile-position-3-4 {
  transform: translate(220px, 150px);
}
main .tile-container .tile-position-4-1 {
  transform: translate(10px, 220px);
}
main .tile-container .tile-position-4-2 {
  transform: translate(80px, 220px);
}
main .tile-container .tile-position-4-3 {
  transform: translate(150px, 220px);
}
main .tile-container .tile-position-4-4 {
  transform: translate(220px, 220px);
}
main .tile-container .tile-merged .tile-inner {
  z-index: 20;
  animation: pop 200ms ease 100ms;
  animation-fill-mode: backwards;
}
main .tile-container .tile-new .tile-inner {
  animation: appear 200ms ease-in-out;
  animation-delay: 100ms;
  animation-fill-mode: backwards;
}
main .tile-container .tile.tile-2 .tile-inner {
  background: #eee4da;
}
main .tile-container .tile.tile-4 .tile-inner {
  background: #ede0c8;
}
main .tile-container .tile.tile-8 .tile-inner {
  color: #f9f6f2;
  background: #f2b179;
}
main .tile-container .tile.tile-16 .tile-inner {
  color: #f9f6f2;
  background: #f59563;
}
main .tile-container .tile.tile-32 .tile-inner {
  color: #f9f6f2;
  background: #f67c5f;
}
main .tile-container .tile.tile-64 .tile-inner {
  color: #f9f6f2;
  background: #f65e3b;
}
main .tile-container .tile.tile-128 .tile-inner {
  color: #f9f6f2;
  background: #edcf72;
  font-size: 30px;
}
main .tile-container .tile.tile-256 .tile-inner {
  color: #f9f6f2;
  background: #edcc61;
  font-size: 30px;
}
main .tile-container .tile.tile-512 .tile-inner {
  color: #f9f6f2;
  background: #edc850;
  font-size: 30px;
}
main .tile-container .tile.tile-1024 .tile-inner {
  color: #f9f6f2;
  background: #edc53f;
  font-size: 22px;
}
main .tile-container .tile.tile-2048 .tile-inner {
  color: #f9f6f2;
  background: #edc22e;
  font-size: 22px;
}

2048模型设计,随机渲染

2048对象设计

Tile对象
// tile.js
function Tile(position, value) {
  this.row = position.row;
  this.column = position.column;
  this.value = value;
}
Grid对象
// grid.js
function Grid(size = 4) {
  this.size = size;
}
//grid.js
function Grid(size = 4) {
  this.size = size;
  this.cells = [];
  this.init(size);
}

// prototype 设置方法
Grid.prototype.init = function (size) {
  for (let row = 0; row < size; row++) {
    this.cells.push([]);
    for (let column = 0; column < size; column++) {
      this.cells[row].push(null);
    }
  }
};
//grid.js
function Grid(size = 4, state) {
  this.size = size; 
  this.cells = [];  
  this.init(size);  //调用init方法进行初始化
  // 如果有之前的进度,则恢复
  if (state) {
    this.recover(state);
  }
}  //Grid对象表示一个游戏棋盘,大小为size*size的方格,参数state恢复游戏进度

Grid.prototype.recover = function({ size, cells }) {
  this.size = size;
  //通过遍历cells二维数组,如果某个单元格存在内容,则新建一个Tile对象并放在Grid对象的位置上
  for (let row = 0; row < this.size; row++) {
    for (let column = 0; column < this.size; column++) {
      const cell = cells[row][column];
      if (cell) {
        this.cells[row][column] = new Tile(cell.position, cell.value);
      }
    }
  }
};//recover方法用于恢复Grid对象的状态

Grid.prototype.init = function(size) {
  for (let row = 0; row < size; row++) {
    this.cells.push([]);
    for (let column = 0; column < size; column++) {
      this.cells[row].push(null);
    }
  }
};//定义一个Grid对象的初始化方法,创建指定大小的二维数组,遍历循环将每个位置的值初始化为null

Grid.prototype.add = function(tile) {
  this.cells[tile.row][tile.column] = tile;
};//add方法将tile对象添加到网格的对应单元格,存储在对应位置的二维数组中

Grid.prototype.remove = function(tile) {
  this.cells[tile.row][tile.column] = null;
};//remove方法用于从网格中移除tile对象,将对应单元格的值设置为null

// 获取Grid所有可用方格的位置
Grid.prototype.availableCells = function() {
  const availableCells = [];
  for (let row = 0; row < this.cells.length; row++) {
    for (let column = 0; column < this.cells[row].length; column++) {
      // 如果当前方格没有内容,则其可用(空闲)
      if (!this.cells[row][column]) {
        availableCells.push({ row, column });
      }//将这个单元格的行和列的值作为一个对象推入'availableCells'数组
    }
  }
  return availableCells;//即所有空闲单元格的列表
};

//从给定的Grid对象中获取所有空闲方格并随机返回其中一个方格
Grid.prototype.randomAvailableCell = function() {
  // 获取到所有的空闲方格
  const cells = this.availableCells();
  if (cells.length > 0) {
    //如果存在空闲方格,利用Math.random()随机获取其中的某一个
    return cells[Math.floor(Math.random() * cells.length)];
  }
};

// 获取某个位置的Tile
Grid.prototype.get = function(position) {
  if (this.outOfRange(position)) {
    return null;
  }
  return this.cells[position.row][position.column];
};

// 判断某个位置是否超出边界
Grid.prototype.outOfRange = function(position) {
  return (
    position.row < 0 ||
    position.row >= this.size ||
    position.column < 0 ||
    position.column >= this.size
  );
};//满足以下任何条件,则判断true

Grid.prototype.serialize = function() {
  const cellState = [];

  // cellState 是一个二维数组,分别存储整个Grid信息。
  // 如果该位置有Tile, 则返回 Tile序列化结果
  // 如果该位置没有Tile,则存储null
  for (let row = 0; row < this.size; row++) {
    cellState[row] = [];
    for (let column = 0; column < this.size; column++) {
      cellState[row].push(
        this.cells[row][column] ? this.cells[row][column].serialize() : null
      );//如果该位置有tile则调用Tile对象的serialize方法将Tile序列化的结果存入cellState
    }//如果没有则存储null
  }

  return {
    size: this.size,
    cells: cellState  //cells表示存储了所有格子状态的二维数组
  };
};
引入js
<html>
  ...
  <body>
    ...
    <script src="./scripts/tile.js"></script>
    <script src="./scripts/grid.js"></script>
    <script src="./scripts/index.js"></script>
  </body>
</html>

2048对象渲染

Render对象用于页面渲染

//render.js
function Render() {}

// 渲染整个grid
Render.prototype.render = function(grid) {
  for (let row = 0; row < grid.size; row++) {
    for (let column = 0; column < grid.size; column++) {
      // 如果grid中某个cell不为空,则渲染这个cell
      if (grid.cells[row][column]) {
        this.renderTile(grid.cells[row][column]);
      }
    }
  }
};

// 渲染单个tile
Render.prototype.renderTile = function(tile) {};
渲染每一个Tile
//render.js
function Render() {
  this.tileContainer = document.querySelector('.tile-container');
}

// 渲染单个tile
Render.prototype.renderTile = function(tile) {
  // 创建一个tile-inner
  const tileInner = document.createElement('div');
  tileInner.setAttribute('class', 'tile-inner');
  tileInner.innerHTML = tile.value;

  // 创建一个tile
  const tileDom = document.createElement('div');
  let classList = [
    'tile',
    `tile-${tile.value}`,
    `tile-position-${tile.row + 1}-${tile.column + 1}`
  ];
  tileDom.setAttribute('class', classList.join(' '));
  tileDom.appendChild(tileInner);
  this.tileContainer.appendChild(tileDom);
};

效果如下:

2048随机初始化
所有可用方格
// grid.js
// 获取所有可用方格的位置
Grid.prototype.availableCells = function() {
  const availableCells = [];
    for (let row = 0; row < this.cells.length; row++) {
    for (let column = 0; column < this.cells[row].length; column++) {
      // 如果当前方格没有内容,则其可用(空闲)
      if (!this.cells[row][column]) {
        availableCells.push({ row, column });
      }
    }
  }
  return availableCells;
};
随机某个可用方格
// grid.js
// 随机获取某个可用方格的位置
Grid.prototype.randomAvailableCell = function() {
  // 获取到所有的空闲方格
  const cells = this.availableCells();
  if (cells.length > 0) {
    // 利用Math.random()随机获取其中的某一个
    return cells[Math.floor(Math.random() * cells.length)];
  }
};
index.js利用随机空闲位置创建节点
let grid = new Grid();
let render = new Render();
for (let i = 0; i < 2; i++) {
  // 90%概率为2,10%为4
  const value = Math.random() < 0.9 ? 2 : 4;
  // 随机一个方格的位置
  const position = grid.randomAvailableCell();
  // 添加到grid中
  grid.add(new Tile(position, value));
}
render.render(grid);

2048移动处理

重构-Manager

//manager.js
function Manager(size = 4) {
  this.size = size;
  this.grid = new Grid(size);
  this.render = new Render();
  this.start();
}

Manager.prototype.start = function () {
  for (let i = 0; i < 2; i++) {
    // 90%概率为2,10%为4
    const value = Math.random() < 0.9 ? 2 : 4;
    // 随机一个方格的位置
    const position = this.grid.randomAvailableCell();
    // 添加到grid中
    this.grid.add(new Tile(position, value));
  }
  this.render.render(this.grid);
};
键盘监听
window.addEventListener('keyup', function (e) {
  console.log(e.code);
});
监听回调设置(监听器Listener)
function Listener() {
  window.addEventListener('keyup', function (e) {
    switch (e.code) {
      case 'ArrowUp':
        console.log('向上');
        break;
      case 'ArrowLeft':
        console.log('向左');
        break;
      case 'ArrowRight':
        console.log('向右');
        break;
      case 'ArrowDown':
        console.log('向下');
        break;
    }
  });
}
事件回调
//listener.js
function Listener({ move: moveFn }) {
  window.addEventListener('keyup', function (e) {
    switch (e.code) {
      case 'ArrowLeft':
        moveFn('向左');
        break;
      case 'ArrowUp':
        moveFn('向上');
        break;
      case 'ArrowDown':
        moveFn('向下');
        break;
      case 'ArrowRight':
        moveFn('向右');
        break;
    }
  });
}

Manager修改

//manager.js
this.listener = new Listener({
  move: function (direction) {
    console.log(direction);
  },
});
方向向量化(方向上下左右)
左 => row: 1, column: 0  // 列数索引 -1, 也就是column -1
右 => row: 1, column: 2  // 列数索引 +1, 也就是column +1
上 => row: 0, column: 1  // 行数索引 -1, 也就是row -1
下 => row: 2, column: 1  // 行数索引 +1, 也就是row +1
//listener.js
function Listener({ move: moveFn }) {
  window.addEventListener('keyup', function (e) {
    switch (e.code) {
      case 'ArrowUp':
        moveFn({ row: -1, column: 0 });
        break;
      case 'ArrowLeft':
        moveFn({ row: 0, column: -1 });
        break;
      case 'ArrowRight':
        moveFn({ row: 0, column: 1 });
        break;
      case 'ArrowDown':
        moveFn({ row: 1, column: 0 });
        break;
    }
  });
}
移动位置计算
Tile移动

代码实现-遍历顺序
//manager.js
Manager.prototype.getPaths = function (direction) {
  let rowPath = [];
  let columnPath = [];
  return { rowPath, columnPath };
};

加入正常的坐下到右下的遍历顺序

Manager.prototype.getPaths = function (direction) {
  let rowPath = [];
  let columnPath = [];
  for (let i = 0; i < this.size; i++) {
    rowPath.push(i);
    columnPath.push(i);
  }
  return {
    rowPath,
    columnPath,
  };
};
Manager.prototype.getPaths = function (direction) {
  let rowPath = [];
  let columnPath = [];
  for (let i = 0; i < this.size; i++) {
    rowPath.push(i);
    columnPath.push(i);
  }

  // 向右的时候
  if (direction.column === 1) {
    columnPath = columnPath.reverse();
  }

  // 向下的时候
  if (direction.row === 1) {
    rowPath = rowPath.reverse();
  }
  return {
    rowPath,
    columnPath,
  };
};
代码实现-找寻目标位置
// 寻找移动方向目标位置
Manager.prototype.getNearestAvaibleAim = function (aim, direction) {
  // 位置 + 方向向量的计算公式
  function addVector(position, direction) {
    return {
      row: position.row + direction.row,
      column: position.column + direction.column,
    };
  }
  aim = addVector(aim, direction);

  // 获取grid中某个位置的元素
  let next = this.grid.get(aim);

  // 如果next元素存在(也就是此目标位置已经有Tile),或者是超出游戏边界,则跳出循环。目的:就是找到最后一个空白且不超过边界的方格
  while (!this.grid.outOfRange(aim) && !next) {
    aim = addVector(aim, direction);
    next = this.grid.get(aim);
  }

  // 这时候的aim总是多计算了一步,因此我们还原一下
  aim = {
    row: aim.row - direction.row,
    column: aim.column - direction.column,
  };

  return {
    aim,
    next,
  };
};
通过循环,找寻最后一个空白位置
// 如果next元素存在(也就是此目标位置已经有Tile,
// 或者是超出游戏边界,则跳出循环。
// 目的:就是找到最后一个空白且不超过边界的方格
while (!this.grid.outOfRange(aim) && !next) {
  aim = addVector(aim, direction);
  next = this.grid.get(aim);
}
Grid新增两个方法
// grid.js

// 获取某个位置的Tile
Grid.prototype.get = function (position) {
  if (this.outOfRange(position)) {
    return null;
  }
  return this.cells[position.row][position.column];
};

// 判断某个位置是否超出边界
Grid.prototype.outOfRange = function (position) {
  return (
    position.row < 0 ||
    position.row >= this.size ||
    position.column < 0 ||
    position.column >= this.size
  );
};
Tile移动
Tile移动处理

1.根据方向获取遍历顺序,跟随顺序进行遍历

2.遍历时候,如果此位置上有Tie,则进行移动

3.根据当前Tle的位置和方向,获取目标移动位置

4.进行Tile移动

5.只要有一个节点产生移动,则重新调用渲染器渲染grid

// manager.js
Manager.prototype.listenerFn = function(direction) {
  // 定义一个变量,判断是否引起移动
  let moved = false;

  const { rowPath, columnPath } = this.getPaths(direction);
  for (let i = 0; i < rowPath.length; i++) {
    for (let j = 0; j < columnPath.length; j++) {
      const position = { row: rowPath[i], column: columnPath[j] };
      const tile = this.grid.get(position);
      if (tile) {
        // 当此位置有Tile的时候才进行移动
        // 移动时,首先获取目标移动位置
        const { aim, next } = this.getNearestAvaibleAim(position, direction);
        this.moveTile(tile, aim);
        moved = true;
      }
    }
  }

  // 移动以后进行重新渲染
  if (moved) {
    this.render.render(this.grid);
  }
};

MoveFile处理

1.Tile对应的Grid原始位置设置为nul

2.更新Tile的position

3.将更新后的Tile设置到Grid新的位置

// manager.js

// 移动Tile,先将grid中老位置删除,在添加新位置
Manager.prototype.moveTile = function(tile, aim) {
  this.grid.cells[tile.row][tile.column] = null;
  tile.updatePosition(aim);
  this.grid.cells[aim.row][aim.column] = tile;
};
// tile.js

// 更新Tile的位置
Tile.prototype.updatePosition = function(position) {
  this.row = position.row;
  this.column = position.column;
};
监听联调

Listener监听回调中调用listenerFn方法,代码如下:

let self = this;
this.listener = new Listener({
  move: function(direction) {
    self.listenerFn(direction);
  }
});

这涉及到JS作用域,因为回调函数function(direction)是由Listener调用的,因此this会指向Listener,并不是Manager。在这种情况下,如果使用this.listenerFn将无法找到listenerFn方法,因此我们需要在方法调用之前(this还未改变之前)将this先保存到self

Render
// render.js

// 渲染整个grid, 在之前先清空所有的Tile
Render.prototype.render = function(grid) {
  this.empty();
  ...
};

Render.prototype.empty = function() {
  this.tileContainer.innerHTML = '';
};
总结:最终类图

2048合并处理

Tile合并规则

规则只有一个:方格移动到不能移动为止,并且下一个位置的Vaue

值和该方格value值一样。

// 寻找移动方向目标位置
Manager.prototype.getNearestAvaibleAim = function(aim, direction) {
  //...
  return {
    aim,
    next
  };
};
Tile合并代码
// 移动核心逻辑
Manager.prototype.listenerFn = function(direction) {
  // 定义一个变量,判断是否引起移动
  let moved = false;

  const { rowPath, columnPath } = this.getPaths(direction);
  for (let i = 0; i < rowPath.length; i++) {
    for (let j = 0; j < columnPath.length; j++) {
      const position = { row: rowPath[i], column: columnPath[j] };
      const tile = this.grid.get(position);
      if (tile) {
        // 当此位置有Tile的时候才进行移动
        const { aim, next } = this.getNearestAvaibleAim(position, direction);

        // 区分合并和移动,当next值和tile值相同的时候才进行合并
        if (next && next.value === tile.value) {
          // 合并位置是next的位置,合并的value是tile.value * 2
          const merged = new Tile(
            {
              row: next.row,
              column: next.column
            },
            tile.value * 2
          );
          //将合并以后节点,加入grid
          this.grid.add(merged);
          //在grid中删除原始的节点
          this.grid.remove(tile);
          moved = true;
        } else {
          this.moveTile(tile, aim);
          moved = true;
        }
      }
    }
  }

  // 移动以后进行重新渲染
  if (moved) {
    this.render.render(this.grid);
  }
};
新增Grid的remove方法,删除某个TIle
// grid.js
Grid.prototype.remove = function(tile) {
  this.cells[tile.row][tile.column] = null;
};
完善游戏步骤
TIle合并后置逻辑

当T11e合并或移动之后,游戏还得继续,因此每次移动之后,我们让游戏随机再次生成一个Ti1e。

//manager.js

// 随机添加一个节点
Manager.prototype.addRandomTile = function() {
  const position = this.grid.randomAvailableCell();
  if (position) {
    // 90%概率为2,10%为4
    const value = Math.random() < 0.9 ? 2 : 4;
    // 随机一个方格的位置
    const position = this.grid.randomAvailableCell();
    // 添加到grid中
    this.grid.add(new Tile(position, value));
  }
};

修改调用区域代码,如下:

// manager.js
Manager.prototype.start = function() {
  for (let i = 0; i < 2; i++) {
    this.addRandomTile();
  }
  this.render.render(this.grid);
};

// 移动核心逻辑
Manager.prototype.listenerFn = function(direction) {
  // ...
  if (moved) {
    this.addRandomTile();
    this.render.render(this.grid);
  }
};

2048动画效果

移动动画(UML图)

方块移动动画

1.使用CSS特性transition:transform 100ms ease-in-out给transform加入动画效果。

2.因为我们的每个T11e节点是临时创建的,并不会出现class切换的效果,当然也不会出现transform值变化过程,无法使用动画。我们可以使用一个猥琐逻辑,首先将Tile class设置为原始位置,然后延迟设置为当前位置。

const div = document.createElement('div');
div.setAttribute('class', 'tile-position-1-1');
setTimeout(() => {
  div.setAttribute('class', 'tile-posiiton-1-4');
}, 16);
// tile.js
function Tile(position, value) {
  this.row = position.row;
  this.column = position.column;
  this.value = value;

  // 新增prePosition属性
  this.prePosition = null;
}

Tile.prototype.updatePosition = function(position) {
  // 更新的时候,先将当前位置,保存为prePosition
  this.prePosition = { row: this.row, column: this.column };

  this.row = position.row;
  this.column = position.column;
};
// render.js
// 渲染单个tile
Render.prototype.renderTile = function(tile) {
  // 创建一个tile-inner
  const tileInner = document.createElement('div');
  tileInner.setAttribute('class', 'tile-inner');
  tileInner.innerHTML = tile.value;

  // 创建一个tile
  const tileDom = document.createElement('div');
  let classList = [
    'tile',
    `tile-${tile.value}`,
    `tile-position-${tile.row + 1}-${tile.column + 1}`
  ];

  if (tile.prePosition) {
    // 先设置之前的位置
    classList[2] = `tile-position-${tile.prePosition.row + 1}-${tile.prePosition
                                                                .column + 1}`;
    // 延迟设置当前的位置
    setTimeout(function() {
      classList[2] = `tile-position-${tile.row + 1}-${tile.column + 1}`;
      tileDom.setAttribute('class', classList.join(' '));
    }, 16);
  }

  tileDom.setAttribute('class', classList.join(' '));
  tileDom.appendChild(tileInner);
  this.tileContainer.appendChild(tileDom);
};
.tile {
  position: absolute;
  width: $tile-size;
  height: $tile-size;
  border-radius: 4px;

  /*transition 移动动画*/
  transition: transform 100ms ease-in-out;
}

移动动画(二)

// tile.js
function Tile(position, value) {
  this.row = position.row;
  this.column = position.column;
  this.value = value;

  // 新增prePosition属性
  this.prePosition = null;
  // 存储merged两个Tile
  this.mergedTiles = null;
}
// 移动核心逻辑
Manager.prototype.listenerFn = function(direction) {
  //...
  if (next && next.value === tile.value) {
    // 合并位置是next的位置,合并的value是tile.value * 2
    const merged = new Tile(
      {
        row: next.row,
        column: next.column
      },
      tile.value * 2
    );
    this.score += merged.value;

    //...
    if (merged.value === this.aim) {
      this.status = 'WIN';
    }

    // 特别注意下面两句话
    merged.mergedTiles = [tile, next];
    tile.updatePosition({ row: next.row, column: next.column });
    moved = true;
  }

  // ...
};
// 特别注意下面两句话
merged.mergedTiles = [tile, next];
tile.updatePosition({ row: next.row, column: next.column });

我们提前存储merged行为发生的两个原始节点,并更新原始TiLe的位置,让其产生移动效果,最后继续修改渲染代码:

@keyframes appear {
  0% {
    opacity: 0;
    transform: scale(0);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

2048本地存储

保存进度

1.想想有哪些信息需要被保存?

1.当前分数

2.历史最高分

3.当前的方格面板Grid,每一个方格里面的数字

2.想想在什么时候进行保存呢?

分数变化,方格里面数字变化都应该保存,所以应该在每一次移动之

后,渲染之前进行保存,也就是如下代码区域:

//manager.js

Manager.prototype._render = function() {
  // 添加在此处进行处理

  this.render.render(this.grid, { score: this.score, status: this.status });
};
3.想想使用什么技术进行保存呢?

毫无疑问,当然选择window.localStorage。

4.应该在什么时候恢复进度呢?

当页面重新加载的时候,初始化的时候,如果有历史进度,则加载历

史进度。
 

// 历史最高分
const BestScoreKey = '2048BestScore';
// 方格状态 和 分数
const CellStateKey = '2048CellState';

function Storage() {}

Storage.prototype.setCellState = function({ score, grid }) {
  // 存储方格状态 和 分数
};

Storage.prototype.getCellState = function() {
  // 获取方格状态
};

本地存储(二)

序列化和反序列化

将对象信息变成字符串信息,我们通常叫做序列化。在这里我们分两步进行:

1.将grid变成通用的json格式

2.利用Json.stringify()将json序列化为字符串。

//tile.js
Tile.prototype.serialize = function() {
  return {
    position: {
      row: this.row,
      column: this.column
    },
    value: this.value
  };
};

继续序列化grid

//grid.js

Grid.prototype.serialize = function() {
  const cellState = [];

  // cellState 是一个二维数组,分别存储整个Grid信息。
  // 如果该位置有Tile, 则返回 Tile序列化结果
  // 如果该位置没有Tile,则存储null
  for (let row = 0; row < this.size; row++) {
    cellState[row] = [];
    for (let column = 0; column < this.size; column++) {
      cellState[row].push(
        this.cells[row][column] ? this.cells[row][column].serialize() : null
      );
    }
  }

  return {
    size: this.size,
    cells: cellState
  };
};
反序列化
function Grid(size = 4, state) {
  this.size = size;
  this.cells = this.init(size);

  // 如果有之前的进度,则恢复
  if (state) {
    this.recover(state);
  }
}

Grid.prototype.recover = function({ size, cells }) {
  this.size = size;
  // 遍历这个二维数组,如果某个cell存在,则新建一个Tile节点。
  for (let row = 0; row < this.size; row++) {
    for (let column = 0; column < this.size; column++) {
      const cell = cells[row][column];
      if (cell) {
        this.cells[row][column] = new Tile(cell.position, cell.value);
      }
    }
  }
};
历史进度流程(
1. 获取storage.js)存储/获取逻辑
const CellStateKey = '2048CellState';

//...

// 存储方格状态和分数
Storage.prototype.setCellState = function({ score, grid }) {
  window.localStorage.setItem(
    CellStateKey,
    JSON.stringify({
      score,
      grid: grid.serialize()
    })
  );
};

// 获取方格信息
Storage.prototype.getCellState = function() {
  const cellState = window.localStorage.getItem(CellStateKey);
  return cellState ? JSON.parse(cellState) : null;
};
2. 完善listenerFn方法,每一步移动后,都进行存储。
function Manager(size = 4, aim = 2048) {
  //...
  // 新增storage属性
  this.storage = new Storage();
  //...
}

Manager.prototype._render = function() {
  // 渲染之前调用存储
  this.storage.setCellState({ score: this.score, grid: this.grid });
  this.render.render(this.grid, { score: this.score, status: this.status });
};
3. 完善恢复方格状态的逻辑
3.1. manager.js
// manager.js
Manager.prototype.defaultStart = function() {
  const state = this.storage.getCellState();
  // 如果存在缓存则恢复
  if (state) {
    this.score = state.score;
    this.status = 'DOING';
    this.grid = new Grid(this.size, state.grid);
    this._render();
  } else {
    this.start();
  }
};
3.2. grid.js
//grid.js
function Grid(size = 4, state) {
  this.size = size; 
  this.cells = [];  
  this.init(size);  //调用init方法进行初始化
  // 如果有之前的进度,则恢复
  if (state) {
    this.recover(state);
  }
}  //Grid对象表示一个游戏棋盘,大小为size*size的方格,参数state恢复游戏进度

Grid.prototype.recover = function({ size, cells }) {
  this.size = size;
  //通过遍历cells二维数组,如果某个单元格存在内容,则新建一个Tile对象并放在Grid对象的位置上
  for (let row = 0; row < this.size; row++) {
    for (let column = 0; column < this.size; column++) {
      const cell = cells[row][column];
      if (cell) {
        this.cells[row][column] = new Tile(cell.position, cell.value);
      }
    }
  }
};//recover方法用于恢复Grid对象的状态

Grid.prototype.init = function(size) {
  for (let row = 0; row < size; row++) {
    this.cells.push([]);
    for (let column = 0; column < size; column++) {
      this.cells[row].push(null);
    }
  }
};//定义一个Grid对象的初始化方法,创建指定大小的二维数组,遍历循环将每个位置的值初始化为null

Grid.prototype.add = function(tile) {
  this.cells[tile.row][tile.column] = tile;
};//add方法将tile对象添加到网格的对应单元格,存储在对应位置的二维数组中

Grid.prototype.remove = function(tile) {
  this.cells[tile.row][tile.column] = null;
};//remove方法用于从网格中移除tile对象,将对应单元格的值设置为null

// 获取Grid所有可用方格的位置
Grid.prototype.availableCells = function() {
  const availableCells = [];
  for (let row = 0; row < this.cells.length; row++) {
    for (let column = 0; column < this.cells[row].length; column++) {
      // 如果当前方格没有内容,则其可用(空闲)
      if (!this.cells[row][column]) {
        availableCells.push({ row, column });
      }//将这个单元格的行和列的值作为一个对象推入'availableCells'数组
    }
  }
  return availableCells;//即所有空闲单元格的列表
};

//从给定的Grid对象中获取所有空闲方格并随机返回其中一个方格
Grid.prototype.randomAvailableCell = function() {
  // 获取到所有的空闲方格
  const cells = this.availableCells();
  if (cells.length > 0) {
    //如果存在空闲方格,利用Math.random()随机获取其中的某一个
    return cells[Math.floor(Math.random() * cells.length)];
  }
};

// 获取某个位置的Tile
Grid.prototype.get = function(position) {
  if (this.outOfRange(position)) {
    return null;
  }
  return this.cells[position.row][position.column];
};

// 判断某个位置是否超出边界
Grid.prototype.outOfRange = function(position) {
  return (
    position.row < 0 ||
    position.row >= this.size ||
    position.column < 0 ||
    position.column >= this.size
  );
};//满足以下任何条件,则判断true

Grid.prototype.serialize = function() {
  const cellState = [];

  // cellState 是一个二维数组,分别存储整个Grid信息。
  // 如果该位置有Tile, 则返回 Tile序列化结果
  // 如果该位置没有Tile,则存储null
  for (let row = 0; row < this.size; row++) {
    cellState[row] = [];
    for (let column = 0; column < this.size; column++) {
      cellState[row].push(
        this.cells[row][column] ? this.cells[row][column].serialize() : null
      );//如果该位置有tile则调用Tile对象的serialize方法将Tile序列化的结果存入cellState
    }//如果没有则存储null
  }

  return {
    size: this.size,
    cells: cellState  //cells表示存储了所有格子状态的二维数组
  };
};
3.3. listener.js
function Listener({ move: moveFn, start: startFn }) {//接受对象中的两个属性作为参数
  window.addEventListener('keyup', function(e) {
    switch (e.code) {//注册键盘监听器,按键后会触发指定的事件
      case 'ArrowUp':
        moveFn({ row: -1, column: 0 });
        break;
      case 'ArrowLeft':
        moveFn({ row: 0, column: -1 });
        break;
      case 'ArrowRight':
        moveFn({ row: 0, column: 1 });
        break;
      case 'ArrowDown':
        moveFn({ row: 1, column: 0 });
        break;
    }//条件分支结构,用于根据不同的表达式值执行不同的代码块
  });

  const buttons = document.querySelectorAll('button');
  for (let i = 0; i < buttons.length; i++) {
    buttons[i].addEventListener('click', function() {
      startFn();
    });
  }//遍历所有的 <button> 元素,并为它们添加一个点击事件监听器
}//点击每个按钮时都会触发 startFn() 函数的执行
3.4. manage.js
function Manager(size = 4, aim = 256) {
  this.size = size;
  this.aim = aim;
  this.render = new Render(); //处理游戏界面的渲染
  this.storage = new Storage(); //处理游戏状态的存储
  let self = this; //便于在后续的回调函数内部访问到正确的this对象
  this.listener = new Listener({
    move: function (direction) {
      self.listenerFn(direction);
    }, //用户执行移动操作时调用方法
    start: function () {
      self.start(); //游戏开始时触发方法
    },
  });
  this.defaultStart(); //默认的初始化设置
} //以上为创建一个游戏管理器对象,方法绑定事件响应用户操作和游戏开始

Manager.prototype.defaultStart = function () {
  const state = this.storage.getCellState(); //获取储存对象的游戏状态
  let bestScore = this.storage.getBestScore(); //获取最佳得分
  if (!bestScore) {
    bestScore = 0;
  }
  this.bestScore = bestScore; //获取游戏管理器对象最佳得分
  // 如果存在缓存则恢复
  if (state) {
    this.score = state.score; //恢复游戏当前得分
    this.status = "DOING"; //游戏状态进行中
    this.grid = new Grid(this.size, state.grid);
    //传入游戏大小和从 state.grid 中获取的游戏棋盘格子状态
    //以恢复游戏棋盘
    this._render(); //进行页面渲染,将恢复的游戏状态显示在界面上
  } else {
    this.start();
  }
}; //根据存储的游戏状态或者新开始一个游戏,来进行默认的游戏初始化设置

Manager.prototype.start = function () {
  this.score = 0; //计分板清0
  this.status = "DOING"; //游戏状态设置为进行中
  this.grid = new Grid(this.size); //创建游戏盘面
  for (let i = 0; i < 2; i++) {
    //初始化
    this.addRandomTile();
  } //向盘面中添加两个数字方块
  this._render(); //渲染游戏盘面
};

Manager.prototype._render = function () {
  // 渲染之前调用存储
  this.storage.setCellState({ score: this.score, grid: this.grid });
  if (this.score > this.bestScore) {
    this.bestScore = this.score;
    this.storage.setBestScore(this.bestScore);
  }
  this.render.render(this.grid, {
    //调用render属性的render方法
    score: this.score,
    status: this.status,
    bestScore: this.bestScore,
  });
}; //将游戏盘面(grid)的状态渲染到前端界面上

// 随机添加一个节点
Manager.prototype.addRandomTile = function () {
  const position = this.grid.randomAvailableCell(); //获取可用的随机空白格子
  if (position) {
    // 90%概率为2,10%为4
    const value =
      Math.random() < 0.8
      ? 2
      : 4
    // 随机一个方格的位置
    const position = this.grid.randomAvailableCell();
    // 添加到grid中
    this.grid.add(new Tile(position, value));
  }
}; //该方法可在游戏盘面中随机生成一个新的数字方块

// 移动逻辑核心
Manager.prototype.listenerFn = function (direction) {
  // 定义一个变量,判断是否引起移动,初始值为false移动则为true
  let moved = false;
  //根据移动方向获取路径rowPath和columnPath
  const { rowPath, columnPath } = this.getPaths(direction);
  for (let i = 0; i < rowPath.length; i++) {
    for (let j = 0; j < columnPath.length; j++) {
      const position = { row: rowPath[i], column: columnPath[j] };
      const tile = this.grid.get(position);
      if (tile) {
        // 当此位置有Tile,根据移动方向获取最近可用位置和下一个位置
        const { aim, next } = this.getNearestAvaibleAim(position, direction);

        // 区分合并和移动,当next值和tile值相同的时候才进行合并
        if (next && next.value === tile.value) {
          // 合并位置是next的位置,合并的value是tile.value * 2
          const merged = new Tile(
            {
              row: next.row,
              column: next.column,
            },
            tile.value * 2
            );

          this.score += merged.value; //更新分数
          //将合并以后节点,加入grid
          this.grid.add(merged);
          //在grid中删除原始的节点
          this.grid.remove(tile);
          //判断游戏是否获胜
          if (merged.value === this.aim) {
            this.status = "WIN";
          } //将相邻的两个方块合并为一个,并进行相应的位置更新
          merged.mergedTiles = [tile, next];
          tile.updatePosition({ row: next.row, column: next.column });
          moved = true;
        } else {
          this.moveTile(tile, aim);
          moved = true; //表示进行了移动操作
        }
      }
    }
  }

  // 移动以后进行重新渲染
  if (moved) {
    this.addRandomTile(); //在游戏盘面上随机生成一个新的数字方块
    if (this.checkFailure()) {
      this.status = "FAILURE";
    }
    this._render(); //重新渲染游戏界面
  }
};

// 移动Tile,先将grid中老位置删除,再添加新位置
Manager.prototype.moveTile = function (tile, aim) {
  this.grid.cells[tile.row][tile.column] = null; //清除该位置上的方块
  tile.updatePosition(aim); //将方块位置更新为目标位置aim
  this.grid.cells[aim.row][aim.column] = tile; //将移动方块放置在游戏盘面上
};

// 根据方向,确定遍历的顺序
Manager.prototype.getPaths = function (direction) {
  let rowPath = [];
  let columnPath = []; //用于储存行和列的遍历顺序
  for (let i = 0; i < this.size; i++) {
    rowPath.push(i);
    columnPath.push(i);
  }

  // 向右的时候
  if (direction.column === 1) {
    columnPath = columnPath.reverse();
  } //将columnPath数组反转(倒序以便从右向左遍历游戏盘面的列

  // 向下的时候
  if (direction.row === 1) {
    rowPath = rowPath.reverse();
  } //将rowPath数组反转(倒序),以便从下往上遍历游戏盘面的行
  return {
    rowPath,
    columnPath,
  };
}; //根据给定的移动方向确定遍历游戏盘面的行和列的顺序

Manager.prototype.getNearestAvaibleAim = function (aim, direction) {
  // 寻找移动方向目标位置
  function addVector(position, direction) {
    return {
      row: position.row + direction.row,
      column: position.column + direction.column,
    };
  } // 位置 + 方向向量的计算公式
  aim = addVector(aim, direction);

  let next = this.grid.get(aim);
  // 获取游戏盘面上新目标位置的元素

  while (!this.grid.outOfRange(aim) && !next) {
    aim = addVector(aim, direction);
    next = this.grid.get(aim);
  } //判断新目标位置是否超出游戏边界且没有元素存在
  //满足条件则继续向移动方向前进一步,更新目标位置,并再次获取新位置上的元素。

  aim = {
    row: aim.row - direction.row,
    column: aim.column - direction.column,
  }; //目标位置(上述多计算的一步)减去移动方向向量,将其还原为最近可用的目标位置

  return {
    aim,
    next,
  };
};

// 判断游戏是否失败
Manager.prototype.checkFailure = function () {
  // 获取空白的Cell
  const emptyCells = this.grid.availableCells();
  // 如果存在空白,则游戏肯定没有失败
  if (emptyCells.length > 0) {
    return false;
  }

  for (let row = 0; row < this.grid.size; row++) {
    for (let column = 0; column < this.grid.size; column++) {
      let now = this.grid.get({ row, column });

      // 根据4个方向,判断临近的Tile的Value值是否相同
      let directions = [
        { row: 0, column: 1 },
        { row: 0, column: -1 },
        { row: 1, column: 0 },
        { row: -1, column: 0 },
      ];
      for (let i = 0; i < directions.length; i++) {
        const direction = directions[i];
        const next = this.grid.get({
          row: row + direction.row,
          column: column + direction.column,
        }); //计算出下一个相邻方格的位置
        // 判断当前方块和相邻方块的Value是否相同
        if (next && next.value === now.value) {
          return false;
        }
      }
    }
  }
  return true;
};
3.5. render.js
//render.js
function Render() {
  this.tileContainer = document.querySelector(".tile-container");
  this.scoreContainer = document.querySelector(".now .value");
  this.statusContainer = document.querySelector(".status");
  this.bestScoreContainer = document.querySelector(".best .value");
} //构造函数 Render(),用于初始化游戏界面的相关元素

// 渲染整个游戏界面
Render.prototype.render = function (grid, { score, status, bestScore }) {
  this.empty(); //用于清空游戏界面,即移除之前渲染的方块
  this.renderScore(score);
  this.renderBestScore(bestScore);
  this.renderStatus(status); //分别用于渲染得分、历史最高分和游戏状态
  for (let row = 0; row < grid.size; row++) {
    for (let column = 0; column < grid.size; column++) {
      // 如果grid中某个cell不为空,则渲染这个cell
      if (grid.cells[row][column]) {
        this.renderTile(grid.cells[row][column]);
      }
    }
  } //调用 this.renderTile(grid.cells[row][column]) 方法进行渲染,将方块显示在游戏界面上
};

Render.prototype.renderBestScore = function (bestScore) {
  this.bestScoreContainer.innerHTML = bestScore;
}; //用于渲染历史最高分,将传入的 bestScore 参数更新到界面上显示

Render.prototype.renderScore = function (score) {
  this.scoreContainer.innerHTML = score;
}; //用于渲染当前得分,将传入的 score 参数更新到界面上显示

Render.prototype.renderStatus = function (status) {
  if (status === "DOING") {
    //表示游戏正在进行中,此时隐藏游戏状态容器
    this.statusContainer.style.display = "none";
    return;
  } //用于渲染游戏状态,根据传入的 status 参数来显示相应的界面内容
  this.statusContainer.style.display = "flex";
  this.statusContainer.querySelector(".content").innerHTML =
    status === "WIN" ? "You Win!" : "Game Over!";
}; //表示游戏结束。此时显示游戏状态容器,并根据 status 的值设置状态内容

// 清空tileContainer
Render.prototype.empty = function () {
  this.tileContainer.innerHTML = "";
};

// 渲染单个tile
Render.prototype.renderTile = function (tile) {
  // 创建一个tile-inner
  const tileInner = document.createElement("div");
  tileInner.setAttribute("class", "tile-inner"); //设置类名
  tileInner.innerHTML = tile.value; //赋值

  // 创建一个tile
  const tileDom = document.createElement("div");
  let classList = [
    //数组中包含3个类名
    "tile",
    `tile-${tile.value}`, //根据方块的值动态生成,${}将包含变量或表达式
    `tile-position-${tile.row + 1}-${tile.column + 1}`, //根据方块的行和列值加1生成
  ];

  if (tile.prePosition) {
    // 先设置之前的位置
    classList[2] = `tile-position-${tile.prePosition.row + 1}-${
      tile.prePosition.column + 1
    }`;
    // 延迟设置当前的位置
    setTimeout(function () {
      classList[2] = `tile-position-${tile.row + 1}-${tile.column + 1}`;
      tileDom.setAttribute("class", classList.join(" "));
    }, 16); //将数组中的元素以空格分隔拼接成一个字符串:'tile tile-2 tile-position-3-4'
  } else if (tile.mergedTiles) {
    //表示该方块是由多个方块合并而成
    classList.push("tile-merged"); //添加类名
    //如果有mergedTiles,则渲染mergedTile的两个Tile
    tileDom.setAttribute("class", classList.join(" "));
    for (let i = 0; i < tile.mergedTiles.length; i++) {
      this.renderTile(tile.mergedTiles[i]);
    } //对合并的方块再调用renderTile方法进行渲染
  } else {
    classList.push("tile-new");
  }

  tileDom.setAttribute("class", classList.join(" "));
  tileDom.appendChild(tileInner); //将tileInner添加为tileDom的子元素
  this.tileContainer.appendChild(tileDom);
}; //这个方法通过创建和设置不同的类名来渲染方块的位置、合并状态和新创建状态
//并将渲染后的方块元素添加到指定的容器中
3.6. storage.js
// 历史最高分
const BestScoreKey = "2048BestScore";
// 方格状态和分数
const CellStateKey = "2048CellState";

function Storage() {}
//为 Storage 对象提供构造函数,并为该构造函数创建的对象实例提供方法
Storage.prototype.setBestScore = function (bestScore) {
  window.localStorage.setItem(BestScoreKey, bestScore);
}; //将指定键名和键值作为参数来设置本地存储

Storage.prototype.getBestScore = function () {
  return window.localStorage.getItem(BestScoreKey);
}; //返回本地存储中键名为BestScoresKey的值,即最高分

// 存储方格状态和分数
Storage.prototype.setCellState = function ({ score, grid }) {
  window.localStorage.setItem(
    CellStateKey,
    JSON.stringify({
      //转换为JSON字符串
      score,
      grid: grid.serialize(),
    })
  ); //以便下一次打开游戏页面时可以恢复之前的状态
}; //将方格状态和得分序列化成 JSON 字符串并将其保存到浏览器的本地存储中

// 获取方格信息
Storage.prototype.getCellState = function () {
  const cellState = window.localStorage.getItem(CellStateKey);
  return cellState ? JSON.parse(cellState) : null;
};
//从本地存储中获取之前保存的方格状态和得分,并将其解析为对象后返回
//如果之前没有保存过数据,则返回 null
3.7. tile.js
function Tile(position, value) {
  this.row = position.row;
  this.column = position.column;
  this.value = value;

  // 新增prePosition属性
  this.prePosition = null;
  // 存储merged两个Tile
  this.mergedTiles = null;
}

Tile.prototype.updatePosition = function(position) {
  // 更新的时候,先将当前位置保存为prePosition
  this.prePosition = { row: this.row, column: this.column };

  this.row = position.row;
  this.column = position.column;
};

Tile.prototype.serialize = function() {
  return {
    position: {
      row: this.row,
      column: this.column
    },
    value: this.value
  };
};

new Manager();


 
  • 27
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

柳智麒

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

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

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

打赏作者

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

抵扣说明:

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

余额充值