PS0 · Turtle Graphics

海龟绘图:工具链入门

熟悉 TypeScript、VS Code、Mocha 与 Git 工具链,通过海龟绘图练习编写安全免于缺陷易于理解的代码。

基本信息

PS0TypeScriptMochaGit

官方讲义:web.mit.edu/6.102/ps0 ↗

TypeDoc:API 文档 ↗

作业要求

  1. 熟悉 TypeScript/Mocha/Git 工作流
  2. 实现 drawSquare
  3. 实现 chordLengthdrawApproximateCircle
  4. 实现 distancefindPath
  5. 创作 drawPersonalArt
  6. 审查代码中的 magic number 与可读性
官方作业要求 findPath 返回交替的转角/移动值,并以最终转角恢复原始朝向。阅读官方 TypeDoc 后再写测试;此文件不复制完整规格。

规格说明

Turtle 接口

Turtle 支持 color(PenColor)forward(number)turn(number)getSVG()。朝向 0 指向上方;正转为顺时针。

需实现的函数

  1. drawSquare(turtle, sideLength): void — 绘制指定边长的正方形
  2. chordLength(radius, angle): number — 半径 > 0,0 ≤ angle < π,返回弦长
  3. drawApproximateCircle(turtle, radius, numSides): void — 用正多边形近似圆,至少 10 边,绘制后恢复位置和朝向
  4. distance(p1, p2): number — 两点间欧氏距离
  5. findPath(points): number[] — 返回 2N+1 个转角/移动交替值
  6. 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);
  });
});