前端项目2048小游戏 您所在的位置:网站首页 优课达的课程有用吗 前端项目2048小游戏

前端项目2048小游戏

2023-09-16 06:32| 来源: 网络整理| 查看: 265

第一章 2048游戏介绍 1.1 项目介绍

相信大家都玩过2048这个游戏,这次我们就一步步地来完成2048的开发,这个项目它需要用到html css和js的知识。

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

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

2048游戏链接

第二章 2048静态页面开发 2.1 静态页面开发

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

|-- images |-- style |-- index.scss // scss入口文件 + footer |-- nav.scss // 头部区域文件 |-- main.scss // 主体区域文件 |-- desc.scss // 描述区域文件 |-- index.html

我们利用scss 变量声明中心方格区域的长宽,间隔等属性,如果以后我们需要适配移动端,只需要修改这里的变量值即可, 这就是scss的优势—可编程的 CSS。

利用html和css完成下面静态页面的开发

接下来我们渲染加入了方块的静态页面:如下图 我们来分析一下方块的相同点和不同点: 1 它们都有一样的大小,圆角,动效。所以我们需要设置一个统一的 class 为tile。 2 它们每个数字颜色和字体大小都不同,因此我们需要为每个值设置单独的样式,class 为tile-(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用于元素放缩。 我们完成静态页面的渲染:

第三章 2048 对象设计

###3.1 2048 对象设计

tile对象,我们编写js的时候考虑文件分离,考虑tile对象时,把每个方格当成一个tile对象。那么每个Tile应该有 row,column,value三个属性分别表示行、列、值,对应的 JS 代码为:

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

grid对象,它是管理tile对象的,它是4x4的方格。

// grid.js function Grid(size = 4) { this.size = size; }

用grid对象来存储Tile内容,在这里我们可以使用二维数组来存储:

//grid.js function Grid(size = 4) { this.size = size; this.cells = []; this.init(); } // prototype 设置方法 Grid.prototype.init = function(size) { for (let row = 0; row this.cells[row].push(null); } } };

引入js

... ...

往grid添加tile

//grid.js //... Grid.prototype.add = function(tile) { this.cells[tile.row][tile.column] = tile; };

uml图: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XhTi1Shm-1604221108564)(https://style.youkeda.com/img/course/f10/3/7.svg)]

3.2 2048 对象渲染

我们增加一个render对象来完成渲染

//render.js function Render() {} // 渲染整个grid Render.prototype.render = function(grid) { for (let row = 0; row // 如果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); }; 3.3 2048随机初始化

我们来看看需求:游戏开始能随机出现 2 个 Tile,每个的值 90%可能为 2,10%可能为 4 所以我们需要Grid所有的空闲方格,然后利用随机数,随机获取其中一个方格,创建Tile对象,并且设置 Value 值 所有可用的方格:

// grid.js // 获取所有可用方格的位置 Grid.prototype.availableCells = function() { const availableCells = []; for (let row = 0; row // 如果当前方格没有内容,则其可用(空闲) 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 this.size = size; this.grid = new Grid(size); this.render = new Render(); this.start(); } Manager.prototype.start = function() { for (let i = 0; i console.log(e.keyCode); });

###4.2监听回调控制

我们需要添加一个监听器:

function Listener() { window.addEventListener('keyup', function(e) { switch (e.keyCode) { case 38: console.log('向上'); break; case 37: console.log('向左'); break; case 39: console.log('向下'); break; case 40: console.log('向右'); break; } }); }

事件回调: 为了Listener响应键盘事件以后,能回传到Manager进行操作控制,我们需要给Listener传递一个回调函数。

//listener.js function Listener({ move: moveFn }) { window.addEventListener('keyup', function(e) { switch (e.keyCode) { case 38: moveFn('向左'); break; case 37: moveFn('向上'); break; case 39: moveFn('向下'); break; case 40: moveFn('向右'); break; } }); }

这就是一个非常典型的回调函数的用法,我们可以通过传递moveFn到Listener,当Listener触发键盘事件以后,回调Manager

4.3 方向向量化

向量化:方向在计算机之中一般用向量来表示。 我们来看看朝不同方向移动时,向量的变化:

左 => {row: 0, column: -1} //行不变,列减一 右 => {row: 0, column: 1} 上 => {row: -1, column: 0} 下 => {row: 1, column: 0} // 原始位置 + 向量 = 现在位置 // {row: 1, column: 1} + {row: 0, column: -1} = {row: 1, column: 0} //朝左移动 4.4移动位置计算

在不考虑方块合并的情况下,我们来看看移动规则: 规则 1:同一排或同一列的方块移动顺序跟随具体的方向,比如上图中:向上移动 2 先移动,4 后移动;向下移动 4 先移动,2 后移动 规则 2:每个方格都是移动到该方向的最后一个空白位置。

代码实现,遍历顺序: 我们设置一个方法,根据方向返回移动的路径。

//manager.js Manager.prototype.getPaths(direction){ let rowPath = []; let columnPath = []; return {rowPath, columnPath} //返回行遍历顺序和列遍历顺序 }

小知识:key和value 如果相同,可以简写

{ rowPath, columnPath; } // 等同于 { rowPath: rowPath, columnPath: columnPath }

我们加入正常的从左上到右下的顺序:

Manager.prototype.getPaths = function(direction) { let rowPath = []; let columnPath = []; for (let i = 0; i rowPath, columnPath }; }; 优化上面的代码: Manager.prototype.getPaths = function(direction) { let rowPath = []; let columnPath = []; for (let i = 0; i 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 }; }; 4.5tile移动处理

方块移动的思路: 根据方向获取遍历顺序,跟随顺序进行遍历 遍历时候,如果此位置上有 Tile,则进行移动 根据当前 Tile 的位置和方向,获取目标移动位置 进行 Tile 移动 只要有一个节点产生移动,则重新调用渲染器渲染 grid

// manager.js Manager.prototype.listenerFn = function(direction) { // 定义一个变量,判断是否引起移动 let moved = false; const { rowPath, columnPath } = this.getPaths(direction); for (let i = 0; i 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); } };

Render渲染器渲染时,扫描Grid中所有的Tile,动态生成 class。因此我们只需要改变Grid中Tile的 元素位置,页面当然重新渲染。思路如下: Tile对应的Grid原始位置设置为 null 更新Tile的 position 将更新后的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); } });

为什么要定义let self = this?1 这涉及到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 = ''; };

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-67M2H056-1604221108567)(https://style.youkeda.com/img/course/f10/4/3.svg)]

第五章 2048合并处理 5.1 合并处理

tile什么时候合并:方格移动到不能移动为止,并且下一个位置的 value 值和该方格 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 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); } }; 5.2 完善游戏步骤

tile合并后置逻辑; Tile 合并或移动之后,游戏还得继续,因此每次移动之后,我们让游戏随机再次生成一个Tile。 随机生成Tile的代码,在初始化的时候已经实现过了,我们需要将这段代码抽离成一个函数,代码如下:

//manager.js // 随机添加一个节点 Manager.prototype.addRandomTile = function() { const position = this.grid.randomAvailableCell(); if (position) { // 90%概率为2,10%为4 const value = Math.random() this.addRandomTile(); } this.render.render(this.grid); }; // 移动核心逻辑 Manager.prototype.listenerFn = function(direction) { // ... if (moved) { this.addRandomTile(); this.render.render(this.grid); } }; 第六章 2048动画效果

###6.1 移动动画 回顾一下之前的代码逻辑 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QioNI8yS-1604221108568)(https://style.youkeda.com/img/course/f10/6/1.svg)] 方块移动动画: 1 使用CSS特性transition: transform 100ms ease-in-out,给transform加入动画效果。 2 因为我们的每个Tile节点是临时创建的,并不会出现class切换的效果,当然也不会出现transform值变化过程, 无法使用动画。我们可以使用一个猥琐逻辑,首先将Tileclass 设置为原始位置,然后延迟设置为当前位置。 示例:

开始时Tile在 1 行 1 列,16ms 后位置变成了 1 行 4 列 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; }; 6.2 移动动画(二)

为了在merge的时候也保留移动动画,我们需要保留merge的两个原始Tile,才能实现方块移动效果。 我们继续在Tile里增加属性,代码如下:

// 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; } // ... }; 第七章 2048 储存

###7.1 本地储存 我们先来看几个问题: 1 有哪些信息需要被储存? 当前分数 最高分数 当前方格面板中的每一个方格数字 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() { // 获取方格状态 }; 7.2本地存储(二)

序列化和反序列化 将对象信息变成字符串信息,我们通常叫做序列化。在这里我们分两步进行: 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.js Grid.prototype.serialize = function() { const cellState = []; // cellState 是一个二维数组,分别存储整个Grid信息。 // 如果该位置有Tile, 则返回 Tile序列化结果 // 如果该位置没有Tile,则存储null for (let row = 0; row 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 const cell = cells[row][column]; if (cell) { this.cells[row][column] = new Tile(cell.position, cell.value); } } }

历史进度流程:

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; }; 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 }); }; // 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(); } }; 第八章 项目完整代码

html

优课达-2048 2048 SCORE 0 BEST 0 Play 2048 Game onlineJoin the numbers and get to the 2048 tile! New Game 出品 Game Over! Try again

scss

//desc.scc .desc { display: flex; padding: 0 42px; align-items: center; p { font-size: 15px; color: #635545; flex: 1; } button { margin: 0; padding: 0; width: 98px; height: 44px; border-radius: 4px; background-color: #8f7a66; font-size: 14px; color: #fff; line-height: 44px; text-align: center; font-weight: 700; font-family: Arial-Black; } } // index.scss p { padding: 0; margin: 0; } button { cursor: pointer; &:focus { outline: none; } } @import './nav.scss'; @import './desc.scss'; @import './main.scss'; .body { margin: 0; padding: 0; font-family: 'Clear Sans', 'Helvetica Neue', Arial, sans-serif; color: #776e65; } .container { position: relative; width: 375px; height: 667px; position: fixed; background-color: #faf8ef; left: 50%; top: 50%; transform: translate(-50%, -50%); } footer { display: flex; align-items: flex-end; justify-content: center; margin-top: 78px; img { width: 100px; height: 36px; } span { margin-left: 16px; font-size: 14px; color: #8f7a67; font-weight: 500; } } .mask { position: absolute; left: 0; right: 0; top: 0; bottom: 0; display: none; flex-direction: column; align-items: center; justify-content: center; background-color: rgba(255, 255, 255, 0.4); .content { font-family: Arial-Black; font-size: 26px; color: #8f7a67; text-align: center; } button { margin-top: 20px; width: 98px; height: 44px; background: #8f7a67; border-radius: 4px; font-size: 14px; color: #ffffff; text-align: center; font-family: Arial-Black; font-weight: 700; line-height: 44px; } } //main.scss $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 { margin-bottom: $grid-spacing; display: flex; .grid-cell { width: $tile-size; height: $tile-size; margin-right: $grid-spacing; float: left; border-radius: 3px; background: rgba(238, 228, 218, 0.35); &:last-child { margin-right: 0; } } &:last-child { margin-bottom: 0; } } } .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; } @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-merged .tile-inner { z-index: 20; animation: pop 200ms ease 100ms; animation-fill-mode: backwards; } .tile-new .tile-inner { animation: appear 200ms ease-in-out; animation-delay: 100ms; animation-fill-mode: backwards; } .tile.tile-2 .tile-inner { background: #eee4da; } .tile.tile-4 .tile-inner { background: #ede0c8; } .tile.tile-8 .tile-inner { color: #f9f6f2; background: #f2b179; } .tile.tile-16 .tile-inner { color: #f9f6f2; background: #f59563; } .tile.tile-32 .tile-inner { color: #f9f6f2; background: #f67c5f; } .tile.tile-64 .tile-inner { color: #f9f6f2; background: #f65e3b; } .tile.tile-128 .tile-inner { color: #f9f6f2; background: #edcf72; font-size: 30px; } .tile.tile-256 .tile-inner { color: #f9f6f2; background: #edcc61; font-size: 30px; } .tile.tile-512 .tile-inner { color: #f9f6f2; background: #edc850; font-size: 30px; } .tile.tile-1024 .tile-inner { color: #f9f6f2; background: #edc53f; font-size: 22px; } .tile.tile-2048 .tile-inner { color: #f9f6f2; background: #edc22e; font-size: 22px; } } } @keyframes appear { 0% { opacity: 0; transform: scale(0); } 100% { opacity: 1; transform: scale(1); } } @keyframes pop { 0% { transform: scale(0); } 50% { transform: scale(1.2); } 100% { transform: scale(1); } } //nav.scss nav { height: 68px; padding: 30px 42px; display: flex; justify-content: space-between; align-items: center; h1 { margin: 0; font-size: 34px; font-weight: 700px; color: #635545; } .score { display: flex; > div { width: 68px; height: 68px; margin-left: 10px; border-radius: 6px; background-color: #bbada0; display: flex; flex-direction: column; align-items: center; justify-content: center; .label { font-size: 15px; font-weight: bold; color: #eee4d9; } .value { font-size: 28px; font-weight: 700; color: #ffffff; } } } }

js

//grid.js //grid.js 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 const cell = cells[row][column]; if (cell) { this.cells[row][column] = new Tile(cell.position, cell.value); } } } }; // prototype 设置方法 Grid.prototype.init = function(size) { for (let row = 0; row this.cells[row].push(null); } } }; Grid.prototype.add = function(tile) { this.cells[tile.row][tile.column] = tile; }; Grid.prototype.remove = function(tile) { this.cells[tile.row][tile.column] = null; }; // 获取所有可用方格的位置 Grid.prototype.availableCells = function() { const availableCells = []; for (let row = 0; row // 如果当前方格没有内容,则其可用(空闲) if (!this.cells[row][column]) { availableCells.push({ row, column }); } } } return availableCells; }; // 随机获取某个可用方格的位置 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 = this.size || position.column = this.size ); }; Grid.prototype.serialize = function() { const cellState = []; // cellState 是一个二维数组,分别存储整个Grid信息。 // 如果该位置有Tile, 则返回 Tile序列化结果 // 如果该位置没有Tile,则存储null for (let row = 0; row cellState[row].push( this.cells[row][column] ? this.cells[row][column].serialize() : null ); } } return { size: this.size, cells: cellState }; }; //index.js new Manager(); // listener.js function Listener({ move: moveFn, start: startFn }) { window.addEventListener('keyup', function(e) { switch (e.keyCode) { case 38: moveFn({ row: -1, column: 0 }); break; case 37: moveFn({ row: 0, column: -1 }); break; case 39: moveFn({ row: 0, column: 1 }); break; case 40: moveFn({ row: 1, column: 0 }); break; } }); const buttons = document.querySelectorAll('button'); for (let i = 0; i startFn(); }); } } //manager.js function Manager(size = 4, aim = 8) { this.size = size; this.aim = aim; this.render = new Render(); this.storage = new Storage(); let self = 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); this._render(); } else { this.start(); } }; Manager.prototype.start = function() { this.score = 0; this.status = 'DOING'; this.grid = new Grid(this.size); for (let i = 0; i // 渲染之前调用存储 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, { score: this.score, status: this.status, bestScore: this.bestScore }); }; // 随机添加一个节点 Manager.prototype.addRandomTile = function() { const position = this.grid.randomAvailableCell(); if (position) { // 90%概率为2,10%为4 const value = Math.random() rowPath, columnPath } = this.getPaths(direction); for (let i = 0; i 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); this.grid.cells[aim.row][aim.column] = tile; }; // 根据方向,确定遍历的顺序 Manager.prototype.getPaths = function(direction) { let rowPath = []; let columnPath = []; for (let i = 0; i 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 }; }; // 判断游戏是否失败 Manager.prototype.checkFailure = function() { // 获取空白的Cell const emptyCells = this.grid.availableCells(); // 如果存在空白,则游戏肯定没有失败 if (emptyCells.length > 0) { return false; } for (let row = 0; row 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 row: row + direction.row, column: column + direction.column }); // 判断Value是否相同 if (next && next.value === now.value) { return false; } } } } return true; }; //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'); } // 渲染整个grid 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中某个cell不为空,则渲染这个cell if (grid.cells[row][column]) { this.renderTile(grid.cells[row][column]); } } } }; Render.prototype.renderBestScore = function(bestScore) { this.bestScoreContainer.innerHTML = bestScore; }; Render.prototype.renderScore = function(score) { this.scoreContainer.innerHTML = score; }; Render.prototype.renderStatus = function(status) { if (status === 'DOING') { this.statusContainer.style.display = 'none'; return; } this.statusContainer.style.display = 'flex'; this.statusContainer.querySelector('.content').innerHTML = status === 'WIN' ? 'You Win!' : 'Game Over!'; }; // 清空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 = [ '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); } else if (tile.mergedTiles) { classList.push('tile-merged'); //如果有mergedTiles,则渲染mergedTile的两个Tile tileDom.setAttribute('class', classList.join(' ')); for (let i = 0; i classList.push('tile-new'); } tileDom.setAttribute('class', classList.join(' ')); tileDom.appendChild(tileInner); this.tileContainer.appendChild(tileDom); }; //storage.js // 历史最高分 const BestScoreKey = '2048BestScore'; // 方格状态和分数 const CellStateKey = '2048CellState'; function Storage() {} Storage.prototype.setBestScore = function(bestScore) { window.localStorage.setItem(BestScoreKey, bestScore); }; Storage.prototype.getBestScore = function() { return window.localStorage.getItem(BestScoreKey); }; // 存储方格状态和分数 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; }; //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 }; };


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有