首页
/ freeCodeCamp 每日编程挑战解析:用 JavaScript 实现象棋兵的合法走法(Valid Pawn Moves)

freeCodeCamp 每日编程挑战解析:用 JavaScript 实现象棋兵的合法走法(Valid Pawn Moves)

2026-09-09 16:32:34作者:毕习沙Eudora

本文以 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-javascriptdaily-coding-challenges-python 两个板块成对出现),因此本篇文章的解法同样可以作为两种语言解题思路的参考起点。

题目描述:给定兵的位置,返回所有合法走法

棋盘坐标系统

题面给出的前提是标准国际象棋棋盘(8×8),列从左到右标记为 AH,行从下到上标记为 18,完整坐标如下:

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)"两个分量拼接而成。

走法规则

本挑战对兵的移动做了简化设定,只需遵守三条规则:

  1. 你是棋盘底部的玩家(即兵只能向上方移动,行号递增方向);
  2. 兵通常每次只能向上移动一格;
  3. 例外情况:如果兵位于起始行(第 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"]

这五组用例刻意覆盖了关键分支:普通行(D4E3)、起始行两格特权(B2G2)、以及最顶端行边界(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;
}

逐行拆解这段解法:

  1. 解析坐标position[0] 取列字母(并用 toUpperCase() 兼容小写输入),parseInt(position[1]) 把行字符解析为数字;
  2. 初始化结果数组 moves = []
  3. 边界处理if (row >= 8) return moves; 处理第 8 行(含越界输入),此时兵已在棋盘顶端,直接返回空数组;
  4. 前进一格:把 ${column}${row + 1} 拼进结果;
  5. 起始行特权if (row === 2) 时再前进一格,得到 ${column}${row + 2}
  6. 返回结果:由于实现顺序天然是先 row + 1row + 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-javascriptdaily-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 两个版本的 testschallengeFiles 合并成一个文档,同时校验两个版本的标题、描述、测试数量完全一致;
  • 题面清洗removeSection 会剥掉解析器添加的 <section id="description"> 包裹标签,只保留纯题面;
  • 文档主键:直接复用挑战的 id(如本道的 69738771fb5a7b8b24cca29f)作为 MongoDB _id,该 ID 同时用于记录用户已完成挑战的 completedDailyCodingChallenges[] 数组。

第二步:数据库 → API 接口

api/src/daily-coding-challenge 目录 下的 README.md 明确说明:该模块提供"获取每日编程挑战信息"的接口,而提交评测逻辑仍留在 API 主模块中。路由与校验分别位于 routes/daily-coding-challenge.tsschemas/daily-coding-challenge.ts

日期处理工具 utils/helpers.ts 体现了这套系统的几个设计细节:

  • getNowUsCentral():按美国中部时区(America/Chicago)计算"今天"——每日挑战以美中时间切日;
  • dateStringToUtcMidnight()monthDayStringToUtcDate():分别把 YYYY-MM-DDMM-DD 格式的日期字符串解析为 UTC 午夜,且做了正则格式校验和"非法日期回卷"防御;
  • getSourceDate():由于只生成了 2025-08-11 至 2026-08-10 一年的题库,该函数会把任意请求日期映射回原始题库日期(含 2 月 29 日落到 2 月 28 日的特殊处理),实现"题库循环复用"。

第三步:API → 前端组件

前端在 client/src/components/daily-coding-challenge 目录下有对应的 widget.tsxcalendar.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)

学习要点总结

  1. 坐标建模:把"字母 + 数字"的棋盘坐标拆成独立的列分量与行分量处理,是这类题目的通用第一步;
  2. 规则最小化:题面明确省略了吃子、升变等复杂规则,解题时应严格按题面规则实现,不自行扩展;
  3. 边界先行:第 8 行(无走法)与第 2 行(起始行特权)两个分支是本体的全部逻辑,row >= 8 的提前返回让代码更清晰;
  4. 结果有序性:按 row + 1row + 2 的构造顺序天然满足升序要求,无需额外排序;
  5. 可测试性:五组 hints 断言就是验收标准,把每个分支都映射到至少一个用例,是编写高质量挑战题(以及编写任何函数)的良好示范。

如果你想在本地完整跑通这套系统,可以按 tools/daily-challenges/README.md 的指引操作:复制 sample.env.env、安装依赖、以"显示即将发布内容"的方式启动主客户端(使 GraphQL 端点可用)、进入 tools/daily-challenges 目录后执行 pnpm seed-daily-challenges,即可把题库灌入本地的 freecodecamp 数据库。而在此之前,不妨先在浏览器控制台或 Node REPL 中运行本文的参考解法,用五组测试用例验证 findPawnMoves 的全部分支——这本身就是对"输入 → 规则 → 输出"这一编程思维的最小练习。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
927
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
603
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
397
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525