JavaScript-小游戏-2048
需求
开局生成随机靠边方块 点击方向键移动方块 相邻方块且数值相同发生合并 方块占满界面触发游戏结束提示
游戏界面
标签结构HTML
area区域和death区域分别表示游戏区域和死亡提示区域
<!-- 页面 -->
<div class="area"></div>
<!-- 死亡提示区域 -->
<div class="death"></div>
层叠样式 css
设置游戏区域为flex布局 主轴默认是水平 flex-wrap: wrap;设置主轴自动换行 注意这里游戏区域的大小是200px*200px的 后面可以算出每个方块的大小
.area {
display: flex;
flex-wrap: wrap;
width: 200px;
height: 200px;
font-size: 30px;
}
获取元素 js
获取两个游戏区域和游戏结束提示区域的div
const area = document.querySelector('.area')
const death = document.querySelector('.death')
数据分析
二维数组 用arr[y][x]第一个索引表示y坐标 第二个索引表示x坐标 数组的值表示方块的数值(2,4,8...)没有值就表示这个坐标位置没有方块
这里还要初始化一个rArr数组是用来将数组旋转结果存入新数组防止重复处理的
后面的移动和合并都是需要旋转数组进行操作
let arr = [] //方块坐标和数值
let rArr = [] //旋转数组
//初始化
for (let i = 0; i < 5; i++) {
arr[i] = []
rArr[i] = []
}
功能实现
渲染方法
- 像素点的思路渲染游戏区域为
5*5个方块 所以算出每个方块 的大小为40px*40px - 如果有方块 就渲染为粉色 方块的数值
${arr[y][x]}也渲染在div上 - 其余部分就是画布
-
block字符串累加完毕之后渲染到页面上
//渲染页面
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函数用来随机生成不重复的两个坐标a和c变量表示随机生成[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属性的值
//三元运算符
//以箭头函数形式 定义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
最终开局随机生成两个方块功能代码如下
//随机生成两个不相同随机坐标
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()键盘按键事件如下
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()旋转
放进新数组是为了防止旋转后的元素被重复旋转遍历 最终代码如下
//翻转数组 变成左移
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跳出空位后元素遍历循环继续对这一行进行空位搜索
补充说明
break和continue的区别break只跳出一层循环 这里相当于x>=1的情况下都不执行内层循环了continue跳过当前 然后进行下一个迭代 这里相当于只有在x===1的情况下才不执行内层循环 其他情况是正常执行的
// 移动逻辑
//所有数组都旋转处理成左移判断
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()
//合并
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的大小比较决定随机生成方块的数值
//移动之后再随机生成一个方块 数值可能是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就执行死亡效果
//死亡判断
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
后面还会更新数组最大值的一些比较方法
- 把每行的最大值
push进maxArr数组 - 然后再对
maxArr进行线性遍历 从而找到整个arr数组的最大值 - 最后把最大数值渲染到页面
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}`
}
死亡效果如下
最终代码
<!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>