PS0 · Turtle Graphics
海龟绘图:工具链入门
熟悉 TypeScript、VS Code、Mocha 与 Git 工具链,通过海龟绘图练习编写安全免于缺陷且易于理解的代码。
作业要求
- 熟悉 TypeScript/Mocha/Git 工作流
- 实现
drawSquare - 实现
chordLength和drawApproximateCircle - 实现
distance和findPath - 创作
drawPersonalArt - 审查代码中的 magic number 与可读性
官方作业要求 findPath 返回交替的转角/移动值,并以最终转角恢复原始朝向。阅读官方 TypeDoc 后再写测试;此文件不复制完整规格。
规格说明
Turtle 接口
Turtle 支持 color(PenColor)、forward(number)、turn(number)、getSVG()。朝向 0 指向上方;正转为顺时针。
需实现的函数
drawSquare(turtle, sideLength): void— 绘制指定边长的正方形chordLength(radius, angle): number— 半径 > 0,0 ≤ angle < π,返回弦长drawApproximateCircle(turtle, radius, numSides): void— 用正多边形近似圆,至少 10 边,绘制后恢复位置和朝向distance(p1, p2): number— 两点间欧氏距离findPath(points): number[]— 返回 2N+1 个转角/移动交替值drawPersonalArt(turtle): void— 开放式创作
完成清单
- drawSquare
- chordLength
- drawApproximateCircle
- distance
- findPath
- 个人艺术 + magic-number 审查
源码骨架
turtle.ts — Turtle 接口与 RecordingTurtle
/** Clean-room turtle API matching the public PS0 TypeDoc surface. */
export enum PenColor { Black = "black", Red = "red", Green = "green", Blue = "blue", Purple = "purple", Orange = "orange" }
export interface Turtle {
color(color: PenColor): void;
forward(units: number): void;
getSVG(): string;
turn(degrees: number): void;
}
/** Independently-written test helper; not MIT starter code. */
export class RecordingTurtle implements Turtle {
public readonly commands: Array<{kind:"turn"|"forward"|"color", value:number|string}> = [];
private x = 0; private y = 0; private heading = 0; private pen = PenColor.Black;
private readonly lines: Array<[number,number,number,number,PenColor]> = [];
color(c: PenColor): void { this.pen = c; this.commands.push({kind:"color", value:c}); }
forward(units: number): void {
if (units < 0) throw new Error('forward units must be nonnegative in this helper');
const rad = this.heading * Math.PI / 180;
const nx = this.x + units * Math.sin(rad); const ny = this.y + units * Math.cos(rad);
this.lines.push([this.x,this.y,nx,ny,this.pen]); this.x=nx; this.y=ny;
this.commands.push({kind:"forward", value:units});
}
turn(degrees: number): void { this.heading = (this.heading + degrees) % 360; this.commands.push({kind:"turn", value:degrees}); }
get position(): readonly [number,number] { return [this.x,this.y]; }
get currentHeading(): number { return this.heading; }
getSVG(): string { /* ... SVG rendering ... */ }
}
export interface Point { readonly x: number; readonly y: number; }
turtlesoup.ts — 待实现的函数
import type { Point, Turtle } from './turtle.js';
const todo = (name: string): never => { throw new Error(`TODO: implement ${name}`); };
export function drawSquare(turtle: Turtle, sideLength: number): void { void turtle; void sideLength; todo('drawSquare'); }
export function chordLength(radius: number, angle: number): number { void radius; void angle; return todo('chordLength'); }
export function drawApproximateCircle(turtle: Turtle, radius: number, numSides: number): void { void turtle; void radius; void numSides; todo('drawApproximateCircle'); }
export function distance(p1: Point, p2: Point): number { void p1; void p2; return todo('distance'); }
export function findPath(points: Point[]): number[] { void points; return todo('findPath'); }
export function drawPersonalArt(turtle: Turtle): void { void turtle; todo('drawPersonalArt'); }
冒烟测试 — turtlesoup.smoke.test.ts
import assert from 'node:assert/strict';
import { describe, it } from 'mocha';
import { RecordingTurtle } from '../src/turtle.js';
import { distance, drawSquare } from '../src/turtlesoup.js';
describe('PS0 smoke checks', () => {
it('distance handles a 3-4-5 triangle', () => assert.equal(distance({x:0,y:0},{x:3,y:4}), 5));
it('drawSquare issues four forward moves and returns to its start', () => {
const turtle = new RecordingTurtle(); drawSquare(turtle, 10);
assert.equal(turtle.commands.filter(c => c.kind === 'forward').length, 4);
const [x,y]=turtle.position; assert.ok(Math.abs(x)<1e-9 && Math.abs(y)<1e-9);
});
});