freeCodeCamp 每日编程挑战解析:用 JavaScript 实现象棋兵的合法走法(Valid Pawn Moves)
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
本文以 freeCodeCamp 开源仓库中的每日编程挑战(Daily Coding Challenge)第 173 题「Valid Pawn Moves」为蓝本,完整还原题面、棋盘规则、测试用例与官方参考解法,并结合仓库中的板块配置、种子脚本与 API 实现,从"一道题"延伸到"这套每日挑战系统如何运转"的源码级解析。读完本文,你将掌握 8×8 棋盘坐标建模、按规则枚举合法走法、边界条件处理等实战能力,也能独立在本地运行并验证这套每日挑战的完整数据链路。
挑战背景:Daily Coding Challenge 与第 173 题
freeCodeCamp 仓库中有一个名为daily-coding-challenges-javascript的课程板块,其中按编号存放了超过 240 道短小精悍的算法题(截至当前仓库,challengeOrder已排到 Challenge 240+),第 173 题正是其中之一,源文件位于 challenge-173 源文件。
从板块配置文件 daily-coding-challenges-javascript.json 可以看出这套题目的设计特点:
helpCategory: "JavaScript":按 JavaScript 语言组织分类;usesMultifileEditor: true:使用多文件编辑器承载代码;disableLoopProtectTests: true:禁用循环保护测试,允许自由编写循环逻辑;isUpcomingChange: true:标记为即将发布的新内容;- 题目以
Challenge N: 标题的形式顺序排列,例如 Challenge 171(Flatten the Array)、Challenge 172(Letters-Numbers)、Challenge 173(Valid Pawn Moves)、Challenge 174(Zodiac Finder)。
这套挑战还有一个重要特性:同一道题同时存在 JavaScript 与 Python 两个版本(仓库中的daily-coding-challenges-javascript与daily-coding-challenges-python两个板块成对出现),因此本篇文章的解法同样可以作为两种语言解题思路的参考起点。
题目描述:给定兵的位置,返回所有合法走法
棋盘坐标系统
题面给出的前提是标准国际象棋棋盘(8×8),列从左到右标记为A到H,行从下到上标记为1到8,完整坐标如下:
|A8|B8|C8|D8|E8|F8|G8|H8| | - | - | - | - | - | - | - | - | |A7|B7|C7|D7|E7|F7|G7|H7| |A6|B6|C6|D6|E6|F6|G6|H6| |A5|B5|C5|D5|E5|F5|G5|H5| |A4|B4|C4|D4|E4|F4|G4|H4| |A3|B3|C3|D3|E3|F3|G3|H3| |A2|B2|C2|D2|E2|F2|G2|H2| |A1|B1|C1|D1|E1|F1|G1|H1|
注意这里的坐标约定是"字母在前、数字在后",例如D4表示第 D 列第 4 行。这是整个题目的核心数据结构:一个坐标由"列字母(A–H)+ 行数字(1–8)"两个分量拼接而成。
走法规则
本挑战对兵的移动做了简化设定,只需遵守三条规则:
- 你是棋盘底部的玩家(即兵只能向上方移动,行号递增方向);
- 兵通常每次只能向上移动一格;
- 例外情况:如果兵位于起始行(第 2 行),它可以向上移动一格或两格。
换句话说,合法的目标格集合是:
- 第 8 行:没有合法走法(已到棋盘顶端,无法再向上);
- 第 2 行(起始行):
{上一行, 上两行},例如B2→["B3", "B4"]; - 其余行(第 1 行外的 3–7 行):只有
{上一行},例如D4→["D5"]。
说明:本挑战刻意省略了"兵吃子走斜线""过路兵(en passant)""兵升变"等真实国际象棋规则,聚焦于"直线前进 + 起始行两格特权"这一最小规则集,因此返回的目标格之间没有棋子阻挡的判定。
函数签名与示例
要求实现函数findPawnMoves(position),输入一个坐标字符串,返回一个按升序排列的合法目标格数组。题面给出的两个示例:
| 输入 | 输出 | 原因 | | - | - | - | |"D4"|["D5"]| 普通行,只能前进一格 | |"B2"|["B3", "B4"]| 起始行,可前进一格或两格,升序排列 |
测试用例(hints)全览
源文件中的--hints--部分是这道题的全部验收标准,本质上是五组断言。它们在本地校验和后续种子入库时都会被原样执行:
| 断言 | 期望结果 | | - | - | |findPawnMoves("D4")|["D5"]| |findPawnMoves("B2")|["B3", "B4"]| |findPawnMoves("A7")|["A8"]| |findPawnMoves("G2")|["G3", "G4"]| |findPawnMoves("E3")|["E4"]|
这五组用例刻意覆盖了关键分支:普通行(D4、E3)、起始行两格特权(B2、G2)、以及最顶端行边界(A7只能走到A8,验证了行号上限为 8 的边界约束)。
种子代码与官方参考解法
种子代码(起点)
源文件的--seed-contents--部分给出了解题起点,函数体默认直接返回传入的position,等待补充实现:
function findPawnMoves(position) { return position; }官方参考解法(--solutions--)
源文件同时提供了完整的参考实现,这也是本挑战最简洁直观的解法:
function findPawnMoves(position) { const column = position[0].toUpperCase(); const row = parseInt(position[1]); const moves = []; if (row >= 8) return moves; moves.push(`${column}${row + 1}`); if (row === 2) moves.push(`${column}${row + 2}`); return moves; }逐行拆解这段解法:
- 解析坐标:
position[0]取列字母(并用toUpperCase()兼容小写输入),parseInt(position[1])把行字符解析为数字; - 初始化结果数组
moves = []; - 边界处理:
if (row >= 8) return moves;处理第 8 行(含越界输入),此时兵已在棋盘顶端,直接返回空数组; - 前进一格:把
${column}${row + 1}拼进结果; - 起始行特权:
if (row === 2)时再前进一格,得到${column}${row + 2}; - 返回结果:由于实现顺序天然是先
row + 1后row + 2,输出自然满足题面要求的升序排列。
边界情况与正确性分析
把官方解法放到全输入空间审视,可以确认以下边界全部被覆盖:
row === 8:第 8 行无合法走法,返回[](例如输入"H8"得到空数组);row === 2:起始行返回两个目标格(例如"G2"→["G3", "G4"]);- 列字母大小写:
toUpperCase()使得"b2"与"B2"得到相同结果,增强了对输入格式的鲁棒性; - 数字字符解析:
parseInt("2")得到数字 2,字符串模板${row + 1}会把它拼回目标格字符串。
从时间复杂度看,该解法是严格O(1)的:无论输入是什么,都只执行常数次字符串操作,没有任何循环或递归,这是此类"规则枚举型"题目的最优形态。相比需要遍历棋盘或构建映射表的方案,直接基于行号做分支判断既简单又不易出错。
解法变体:正则与字符码
理解了解析逻辑后,可以自然衍生出等价的变体写法,供对比学习:
// 变体一:正则提取字母与数字 function findPawnMoves(position) { const match = position.match(/^([A-Ha-h])([1-8])$/); if (!match) return []; const column = match[1].toUpperCase(); const row = Number(match[2]); const moves = row < 8 ? [`${column}${row + 1}`] : []; if (row === 2) moves.push(`${column}${row + 2}`); return moves; } // 变体二:字符码转换列号 function findPawnMoves(position) { const colCode = position.charCodeAt(0) - 65; // 0 ~ 7 const row = +position[1]; const column = String.fromCharCode(65 + colCode); const moves = []; if (row < 8) moves.push(`${column}${row + 1}`); if (row === 2) moves.push(`${column}${row + 2}`); return moves; }变体一额外增加了输入格式校验(非法坐标返回空数组),变体二则展示了字符码与 ASCII 之间的换算技巧——这类"字母 ↔ 数字"的双向转换在第 172 题(Letters-Numbers)等相邻挑战中同样适用。
从一道题到一套系统:挑战文件的完整数据链路
这道题的 Markdown 源文件不仅仅是一个静态文档,它处于 freeCodeCamp 每日挑战系统的源头位置。理解这条链路,能让你真正看懂"挑战题面 → 数据库 → 线上接口"的运转方式。
第一步:Curriculum 源文件 → 种子脚本
tools/daily-challenges 目录 下的 seed-daily-challenges.ts 会把dev-playground超级板块下daily-coding-challenges-javascript与daily-coding-challenges-python两个板块的挑战批量导入 MongoDB 的DailyCodingChallenges集合。其中几个关键事实(均来自源码):
- 数量校验:脚本硬编码
EXPECTED_CHALLENGE_COUNT = 365,两个语言的挑战数量必须一致,否则直接抛错; - 按天排期:从
START_DATE = 2025-08-11起,每天一道,new Date(START_DATE.getTime() + i * ONE_DAY_IN_MS); - 数据合并:helpers.ts 中的
combineChallenges会把同一道题的 JavaScript 与 Python 两个版本的tests、challengeFiles合并成一个文档,同时校验两个版本的标题、描述、测试数量完全一致; - 题面清洗:
removeSection会剥掉解析器添加的<section id="description">包裹标签,只保留纯题面; - 文档主键:直接复用挑战的
id(如本道的69738771fb5a7b8b24cca29f)作为 MongoDB_id,该 ID 同时用于记录用户已完成挑战的completedDailyCodingChallenges[]数组。
第二步:数据库 → API 接口
api/src/daily-coding-challenge 目录 下的 README.md 明确说明:该模块提供"获取每日编程挑战信息"的接口,而提交评测逻辑仍留在 API 主模块中。路由与校验分别位于 routes/daily-coding-challenge.ts 与 schemas/daily-coding-challenge.ts。
日期处理工具 utils/helpers.ts 体现了这套系统的几个设计细节:
getNowUsCentral():按美国中部时区(America/Chicago)计算"今天"——每日挑战以美中时间切日;dateStringToUtcMidnight()与monthDayStringToUtcDate():分别把YYYY-MM-DD与MM-DD格式的日期字符串解析为 UTC 午夜,且做了正则格式校验和"非法日期回卷"防御;getSourceDate():由于只生成了 2025-08-11 至 2026-08-10 一年的题库,该函数会把任意请求日期映射回原始题库日期(含 2 月 29 日落到 2 月 28 日的特殊处理),实现"题库循环复用"。
第三步:API → 前端组件
前端在 client/src/components/daily-coding-challenge 目录下有对应的 widget.tsx、calendar.tsx 等组件,用于在页面上展示每日一题;同时 e2e/daily-coding-challenge.spec.ts 提供了端到端测试覆盖。整个链路可以概括为:
curriculum 挑战 Markdown(含 --hints-- / --seed-contents-- / --solutions--) │ seed-daily-challenges.ts(tools/daily-challenges) ▼ MongoDB DailyCodingChallenges 集合(365 条,JS+Python 双语合并) │ api/src/daily-coding-challenge(路由 + 时区/日期处理) ▼ 前端 widget / calendar 组件(client/src/components/daily-coding-challenge)学习要点总结
- 坐标建模:把"字母 + 数字"的棋盘坐标拆成独立的列分量与行分量处理,是这类题目的通用第一步;
- 规则最小化:题面明确省略了吃子、升变等复杂规则,解题时应严格按题面规则实现,不自行扩展;
- 边界先行:第 8 行(无走法)与第 2 行(起始行特权)两个分支是本体的全部逻辑,
row >= 8的提前返回让代码更清晰; - 结果有序性:按
row + 1→row + 2的构造顺序天然满足升序要求,无需额外排序; - 可测试性:五组 hints 断言就是验收标准,把每个分支都映射到至少一个用例,是编写高质量挑战题(以及编写任何函数)的良好示范。
如果你想在本地完整跑通这套系统,可以按 tools/daily-challenges/README.md 的指引操作:复制sample.env为.env、安装依赖、以"显示即将发布内容"的方式启动主客户端(使 GraphQL 端点可用)、进入tools/daily-challenges目录后执行pnpm seed-daily-challenges,即可把题库灌入本地的freecodecamp数据库。而在此之前,不妨先在浏览器控制台或 Node REPL 中运行本文的参考解法,用五组测试用例验证findPawnMoves的全部分支——这本身就是对"输入 → 规则 → 输出"这一编程思维的最小练习。
【免费下载链接】freeCodeCampfreeCodeCamp.org's open-source codebase and curriculum. Learn math, programming, and computer science for free.项目地址: https://gitcode.com/GitHub_Trending/fr/freeCodeCamp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考