freeCodeCamp 每日编程挑战解析:用 JavaScript 实现象棋兵的合法走法(Valid Pawn Moves)
本文以 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 的全部分支——这本身就是对"输入 → 规则 → 输出"这一编程思维的最小练习。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0632
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00