第 6 周:JavaScript 基础与控制台版 2048

本周目标

本周完成后,应能够:

  • 使用变量、数据类型和运算符表达程序状态与计算过程
  • 使用条件和循环解决基础流程问题
  • 使用数组和对象组织一组数据
  • 使用函数拆分逻辑并减少重复代码
  • 使用常用字符串、数组、Math 和日期能力处理数据
  • 阅读基础错误信息,并使用控制台和断点定位问题
  • 综合使用数组、对象和纯函数完成可操作的控制台版 2048

学习资料

JavaScript 参考视频待定。当前按以下知识范围和练习任务执行,确定课程后再补充视频章节映射。

需要掌握的知识点

运行环境与语法基础

  • JavaScript 在浏览器中的运行方式
  • 内部脚本和外部脚本的引入方式
  • console.log()、浏览器控制台和基础断点
  • 语句、表达式、注释和代码块
  • letconst 的使用场景和命名规范

数据类型与运算

  • 字符串、数字、布尔值、undefinednull
  • typeof 和基础类型判断
  • 显式与隐式类型转换
  • 算术、赋值、比较和逻辑运算符
  • ===== 的区别,默认使用严格相等
  • 模板字符串和字符串拼接

流程控制

  • ifelse ifelse
  • switch 的使用场景
  • forwhile 循环
  • breakcontinue
  • 循环边界、初始化条件和终止条件
  • 使用流程图或伪代码描述复杂判断

数组

  • 数组创建、索引、长度和遍历
  • 数组元素的增加、删除和修改
  • pushpopshiftunshiftsplice
  • 查找、筛选、转换和汇总的基本思路
  • 二维数组的结构和遍历

纯函数与二维数组处理

  • 函数的结果只由参数决定,不依赖或修改外部状态
  • 返回新数组,不直接修改传入的数组
  • 使用嵌套循环遍历二维数组
  • 比较两个二维数组的内容是否一致
  • 将复杂规则拆分为多个可单独测试的小函数

函数

  • 函数声明、函数表达式和箭头函数
  • 参数、默认值、返回值和提前返回
  • 局部作用域与外部变量
  • 将复杂问题拆分为多个职责单一的函数
  • 回调函数的基本概念

对象与常用能力

  • 对象、属性、方法和遍历
  • 数组与对象组合的数据结构
  • 字符串常用方法
  • Math 的随机数、取整和极值能力
  • 日期对象的基础使用
  • 解构和展开语法

调试与代码质量

  • SyntaxErrorReferenceErrorTypeError 等基础错误
  • 使用日志缩小问题范围
  • 使用断点观察变量和执行顺序
  • 变量、函数命名和适当的函数长度
  • 为练习准备输入、预期输出和实际输出

控制台输入与输出

  • 使用 console.table() 输出二维棋盘
  • 使用 console.clear() 清理上一轮输出
  • 使用 prompt() 接收玩家指令
  • 对输入进行大小写转换和合法性检查
  • 使用循环维持游戏流程,并提供明确的退出条件

必做练习

以下练习用于掌握 JavaScript 基础,同时为控制台版 2048 准备可复用函数。

  1. 方向指令判断:实现 isValidDirection(input),判断输入是否为 WASD,不区分大小写。
  2. 创建空棋盘:实现 createEmptyBoard(size = 4),使用循环返回指定尺寸的二维数组,所有位置初始值为 0。
  3. 统计空位数量:实现 countEmptyCells(board),使用嵌套循环统计二维棋盘中 0 的数量。
  4. 查找最大数字:实现 getMaxTile(board),返回棋盘中的最大数字;空棋盘返回 0。
  5. 反转一行数字的位置:实现 reverseLine(line),将数组元素按照原位置从后往前重新排列。反转只改变元素顺序,不修改数字本身,也不是按数字大小排序。例如,输入 [2, 0, 4, 8],输出 [8, 4, 0, 2];输入 [2, 2, 4, 0],输出 [0, 4, 2, 2]。函数必须返回新数组,不能修改传入数组。
  6. 复制二维棋盘:实现 cloneBoard(board),修改返回的棋盘时不能影响原棋盘。
  7. 提取指定列:实现 getColumn(board, colIndex),把二维棋盘中的指定列提取为一维数组。
  8. 写回指定列:实现 setColumn(board, colIndex, column),把一维数组写入指定列,并返回新棋盘。
  9. 随机整数:实现 randomIndex(length),返回 0length - 1 之间的随机整数;length 不合法时返回明确结果。
  10. 记录测试结果:为以上每个函数至少准备一个正常用例和一个边界用例,记录输入、预期输出和实际输出。

这些练习分别对应后续综合任务中的输入校验、棋盘初始化、空位查找、左右移动、上下移动、状态复制和随机数字生成。

本周综合任务:控制台版 2048

使用原生 JavaScript 实现一个可以在浏览器控制台中运行的基础可玩版 2048。游戏使用 console.table() 显示棋盘,使用 prompt() 接收操作。

难度评估:中等,约为完整版本的 60%-70%。完成上述必做练习后,大部分辅助函数可以直接复用。应按以下任务顺序逐步实现,每完成一个函数先通过测试,再进入下一项任务。

输入约定:本综合任务按标准 2048 棋盘验收。line 参数为长度 4 的一维数组,元素为 0 或正整数;board 参数为 4×4 二维数组,每个格子为 0 或正整数;玩家指令为字符串或关闭输入框得到的 null

任务一:压缩一行数字

实现 compactLine(line),将所有非零数字移动到左侧,并在右侧补零。

  • 函数签名:compactLine(line)
  • 输入:长度 4 的一维数组 line,表示棋盘中的一行,例如 [2, 0, 2, 4]
  • 输出:新的同长度一维数组,非零数字保持原相对顺序并移动到左侧,例如 [2, 2, 4, 0]
  • 约束:返回新数组,保持传入的 line 不变
查看提示

先使用 filter() 或循环取出所有非零数字,再根据缺少的数量补 0。复制数组后再处理,避免修改参数。

任务二:向左合并一行数字

实现 mergeLineLeft(line),返回合并后的数组。一次移动中,每个数字最多参与一次合并。

  • 函数签名:mergeLineLeft(line)
  • 输入:长度 4 的一维数组 line,表示移动前的一行数字
  • 输出:新的同长度一维数组,表示向左移动并合并后的结果
  • 示例:[2, 2, 2, 2] 返回 [4, 4, 0, 0]
  • 示例:[2, 2, 4, 0] 返回 [4, 4, 0, 0]
  • 示例:[4, 0, 4, 4] 返回 [8, 4, 0, 0]
  • 示例:[0, 0, 0, 0] 返回 [0, 0, 0, 0]
  • 约束:返回新数组,保持传入的 line 不变
查看提示

先压缩,再从左向右扫描;发现相邻数字相同时合并,并让索引额外前进一步,避免同一个数字连续合并两次。

任务三:查找所有空位

实现 getEmptyCells(board),返回 4×4 棋盘中所有值为 0 的位置。每个位置使用 { row, col } 表示。

  • 函数签名:getEmptyCells(board)
  • 输入:4×4 二维数组 board,表示当前棋盘
  • 输出:数组,每一项是一个空位坐标对象 { row, col }
  • 示例:输入 [[2, 0, 0, 0], [0, 4, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],输出包含 { row: 0, col: 1 }{ row: 1, col: 0 } 等空位
  • 边界:没有空位时输出空数组 []
查看提示

在必做练习 countEmptyCells() 的两层循环基础上,把“数量加一”改为将 { row, col } 加入结果数组。

任务四:比较两个棋盘

实现 boardsEqual(boardA, boardB),逐项比较两个二维数组。尺寸或任意数字不同都返回 false

  • 函数签名:boardsEqual(boardA, boardB)
  • 输入:两个二维数组 boardAboardB;主要用于比较两个 4×4 棋盘,也要能识别尺寸不同的情况
  • 输出:布尔值;完全一致返回 true,否则返回 false
  • 示例:boardsEqual([[2, 0]], [[2, 0]]) 返回 true
  • 示例:boardsEqual([[2, 0]], [[0, 2]]) 返回 false
  • 边界:行数不同、列数不同都返回 false
查看提示

先比较行数和每行长度,再逐项比较;发现不同后立即返回 false

任务五:判断是否达到目标数字

实现 hasTarget(board, target = 2048),判断棋盘中是否存在目标数字。

  • 函数签名:hasTarget(board, target = 2048)
  • 输入:4×4 二维数组 board;可选正整数 target,默认值为 2048
  • 输出:布尔值;棋盘中存在目标数字返回 true,否则返回 false
  • 示例:hasTarget([[2, 4, 8, 16], [32, 64, 128, 256], [512, 1024, 2048, 0], [0, 0, 0, 0]]) 返回 true
  • 示例:hasTarget([[2, 4, 8, 16], [32, 64, 128, 256], [512, 1024, 0, 0], [0, 0, 0, 0]], 2048) 返回 false
查看提示

可以使用两层循环,也可以组合 some();找到目标后不需要继续遍历。

任务六:判断是否还可以移动

实现 canMove(board)。满足以下任意条件时返回 true

  • 棋盘中仍有空位
  • 水平方向存在相邻且相同的数字
  • 垂直方向存在相邻且相同的数字

只需要向右和向下检查相邻位置,避免重复判断或数组越界。

  • 函数签名:canMove(board)
  • 输入:4×4 二维数组 board,表示当前棋盘
  • 输出:布尔值;仍可移动返回 true,不能移动返回 false
  • 示例:canMove([[2, 0, 4, 8], [16, 32, 64, 128], [256, 512, 1024, 2], [4, 8, 16, 32]]) 返回 true
  • 示例:canMove([[2, 4, 2, 4], [4, 2, 4, 2], [2, 4, 2, 4], [4, 2, 4, 2]]) 返回 false
  • 示例:canMove([[2, 2, 4, 8], [16, 32, 64, 128], [256, 512, 1024, 2], [4, 8, 16, 32]]) 返回 true
查看提示

先调用 getEmptyCells() 判断是否存在空位;没有空位时,只比较当前位置与右侧、下方的数字。

任务七:判断游戏是否失败

实现 isGameOver(board)。棋盘没有空位,并且水平方向、垂直方向都不存在可合并数字时,返回 true

  • 函数签名:isGameOver(board)
  • 输入:4×4 二维数组 board,表示当前棋盘
  • 输出:布尔值;游戏失败返回 true,否则返回 false
  • 示例:isGameOver([[2, 4, 2, 4], [4, 2, 4, 2], [2, 4, 2, 4], [4, 2, 4, 2]]) 返回 true
  • 示例:isGameOver([[2, 2, 4, 8], [16, 32, 64, 128], [256, 512, 1024, 2], [4, 8, 16, 32]]) 返回 false
  • 示例:isGameOver([[2, 0, 4, 8], [16, 32, 64, 128], [256, 512, 1024, 2], [4, 8, 16, 32]]) 返回 false
查看提示

不要重复编写判断规则,可以直接复用 canMove() 并对结果取反。

任务八:实现四个方向移动

mergeLineLeft() 的基础上实现 moveLeft()moveRight()moveUp()moveDown()。每次移动返回新棋盘及棋盘是否变化。

  • 函数签名:moveLeft(board)moveRight(board)moveUp(board)moveDown(board)
  • 输入:4×4 二维数组 board,表示移动前的棋盘
  • 输出:对象 { board: newBoard, changed }
  • 输出说明:newBoard 是移动后的新棋盘;changed 是布尔值,表示棋盘是否发生变化
  • 示例:moveLeft([[2, 0, 2, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]) 输出 { board: [[4, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], changed: true }
  • 示例:moveLeft([[2, 4, 8, 16], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]) 输出 changed: false
  • 约束:返回新棋盘,保持传入的 board 不变
查看提示

复用必做练习中的 reverseLine()getColumn()setColumn()。向右移动可以先反转每一行,左移后再反转;上下移动可以先提取一列,按一行处理后再写回新棋盘。先完成左右,再处理上下。

任务九:生成随机数字

实现 addRandomTile(board),随机选择一个空位并生成数字 2

  • 函数签名:addRandomTile(board)
  • 输入:4×4 二维数组 board,表示生成新数字前的棋盘
  • 输出:新的二维数组,在一个随机空位上写入数字 2
  • 示例:输入一个全 0 的 4×4 棋盘,输出棋盘中应只有一个位置是 2,其他位置仍是 0
  • 边界:如果没有空位,返回与输入内容一致的新棋盘
  • 约束:返回新棋盘,保持传入的 board 不变
查看提示

复用 getEmptyCells()randomIndex()cloneBoard(),先选择随机空位并复制棋盘,再写入数字。

任务十:初始化游戏

创建 4×4 空棋盘,并随机生成两个初始数字。

  • 函数签名:initGame()
  • 输入:无
  • 输出:4×4 二维数组,包含两个随机生成的数字 2
  • 示例:输出棋盘中应有 2 个非零格子,并且这 2 个格子的值都为 2
  • 约束:两个初始数字不能出现在同一个格子
查看提示

复用必做练习中的 createEmptyBoard(),再连续调用两次 addRandomTile() 生成初始棋盘。

任务十一:实现控制台游戏循环

使用循环输出棋盘和操作提示,并通过 prompt() 接收指令:W 上、A 左、S 下、D 右。指令不区分大小写。

  • 函数建议:startGame()
  • 输入:玩家通过 prompt() 输入的字符串,例如 WASD;关闭输入框时输入值为 null
  • 输出:通过 console.table() 输出当前棋盘;通过 alert()console.log() 输出无效输入、胜利或失败提示
  • 流程输出:每次有效移动后展示更新后的棋盘
  • 边界:无效指令保持棋盘不变;无效移动保持棋盘不变;关闭输入框时结束脚本
查看提示

先将输入统一转换为大写,再使用对象映射或 switch 选择移动函数。根据移动结果中的 changed 判断是否生成新数字;关闭输入框时可以结束脚本。

任务十二:完成胜负流程

出现 2048 时提示胜利;棋盘无法继续移动时提示失败,随后结束游戏循环。

  • 函数建议:在 startGame() 中调用 hasTarget()isGameOver()
  • 输入:每次有效移动并生成新数字后的棋盘
  • 输出:胜利时输出明确胜利提示并结束循环;失败时输出明确失败提示并结束循环
  • 示例:棋盘中出现 2048 时,输出 你赢了
  • 示例:isGameOver(board)true 时,输出 游戏结束
  • 约束:胜负判断应发生在棋盘状态更新之后
查看提示

一次有效移动的顺序应为“移动 -> 判断棋盘变化 -> 生成数字 2 -> 检查胜负 -> 输出新棋盘”。

实现要求

  • 每个函数只负责一个明确任务
  • 使用控制台完成输入输出
  • 通过函数参数和返回值传递游戏状态
  • 不直接修改传入的数组或棋盘
  • 核心函数至少包含一个正常用例和一个边界用例
  • 游戏逻辑与控制台输入输出分开组织
  • 游戏代码放入 console-2048.js
  • 核心逻辑测试放入 console-2048.test.js 或独立测试区域

本周产出

  • 2048 JavaScript 基础练习及测试记录
  • 可实际游玩的控制台版 2048
  • 2048 核心逻辑测试记录
  • 一份错误与调试记录
  • 一份 JavaScript 与 C 语言差异笔记

完成检查

  • 能够区分 letconst
  • 能够正确使用条件、循环和函数
  • 能够选择数组或对象组织数据
  • 能够把长逻辑拆成多个职责清晰的函数
  • 能够使用控制台和断点定位基础错误
  • 已完成可实际游玩的控制台版 2048
  • 支持四向移动、固定生成数字 2 及胜负判断
  • 无效输入和无效移动不会改变棋盘
  • 核心逻辑未直接修改输入数据
  • 每个 2048 核心函数均具有可重复执行的测试用例