Build a Turtle class that tracks a position and a heading as you issue movement and turn commands, the way turtle graphics work in Logo or Python's turtle module. The turtle lives on a 2D plane. You can turn it left or right, send it forward or backward, and ask where it is and which way it's pointing. The whole job is keeping x, y, and the heading consistent as those commands accumulate.
The convention is fixed and you must follow it exactly. The turtle starts at { x: 0, y: 0 } facing east, with heading 0. Angles are in degrees, measured counterclockwise — the standard math convention — so left increases the heading and right decreases it.
class Turtle {
// No arguments. Starts at (0, 0) facing east (heading 0).
constructor();
forward(distance: number): void; // move `distance` along the current heading
backward(distance: number): void; // move `distance` opposite the heading
left(deg: number): void; // turn counterclockwise: heading += deg
right(deg: number): void; // turn clockwise: heading -= deg
position(): { x: number; y: number }; // current location
heading(): number; // current angle, normalized to [0, 360)
}
Internally, a heading of theta degrees means forward(d) adds d * cos(theta) to x and d * sin(theta) to y (with theta first converted to radians). So heading 0 points along +x (east), 90 along +y (north), 180 along -x (west), and 270 along -y (south).
// Turn north, walk 5: x stays 0, y becomes 5.
const t = new Turtle();
t.left(90);
t.forward(5);
t.position(); // → { x: 0, y: 5 }
t.heading(); // → 90
// A closed square returns to the origin and the original heading.
const t = new Turtle();
for (let i = 0; i < 4; i++) {
t.forward(10);
t.left(90);
}
t.position(); // → { x: 0, y: 0 }
t.heading(); // → 0 (four left(90) turns sum to 360, which normalizes to 0)
[0, 360). heading() always returns an angle in that half-open range. left(370) reads as 10; right(90) from 0 reads as 270; a negative turn like left(-90) is the same as right(90) and reads as 270.right is the mirror of left. right(deg) is exactly left(-deg) — turning right by 90 is turning left by −90. Implement one in terms of the other if you like, but test both.backward is forward reversed. backward(d) moves the turtle d units opposite its heading and leaves the heading itself unchanged. It is forward(-d).cos(90°) is not exactly 0 in floating point — it's about 6e-17. After a 90-degree turn, a naive forward leaves dust like 4.3e-16 in the coordinate that should read 0. Decide how to handle that (the solution snaps near-integers); axis-aligned moves should land cleanly on whole numbers.forward(0) is a no-op. Moving zero distance changes neither position nor heading.You'll keep three numbers in sync — an x, a y, and a heading angle — and update them every time the turtle is told to move or turn.
Picture a small robot sitting on graph paper holding a pen. You can spin it in place (left, right) or send it walking in a straight line (forward, backward). It never moves sideways — it only ever goes the way it's currently pointing. Your class is the robot's memory: where it is right now, and which way its nose is aimed. Every command nudges those two facts. Turning changes only the angle; walking changes only the position. The hard part is not the bookkeeping — it's pinning down the convention (which way is 0, does left add or subtract) and then translating "walk 5 units at this angle" into a change in x and y with trigonometry.
Lay the turtle on standard math axes: +x points east (right), +y points north (up). The heading is an angle measured counterclockwise from east, in degrees. At the start the turtle faces east, so its heading is 0. left(deg) rotates counterclockwise, so it adds to the angle; right(deg) rotates clockwise, so it subtracts. That single decision — counterclockwise is positive — is the spine of the whole problem. Get it backwards and every test that checks a direction will fail.
To turn "walk distance at angle theta" into a position change, drop a right triangle. The move is the hypotenuse; its horizontal leg is distance * cos(theta) and its vertical leg is distance * sin(theta). Those legs are exactly how much x and y change. One catch: Math.cos and Math.sin expect radians, not degrees, so you convert first with theta * Math.PI / 180.
The structure comes easily — store x, y, angle; add to the angle on a turn; push x and y on a move:
class Turtle {
constructor() {
this.x = 0;
this.y = 0;
this.angle = 0;
}
left(deg) { this.angle += deg; }
right(deg) { this.angle -= deg; }
forward(distance) {
const theta = this.angle * Math.PI / 180;
this.x += distance * Math.cos(theta);
this.y += distance * Math.sin(theta);
}
backward(distance) { this.forward(-distance); }
position() { return { x: this.x, y: this.y }; }
heading() { return this.angle; }
}
This is almost right, and for non-axis-aligned moves it's exactly right. But it has two visible problems. First, heading() returns the raw angle, so after left(370) it reports 370 instead of 10, and after right(90) it reports -90 instead of 270 — the spec wants the answer normalized into [0, 360). Second, the trig leaves floating-point dust: cos(90°) is not 0 but 6.12e-17, so after left(90); forward(7) the turtle reports x: 4.29e-16 instead of a clean x: 0. A test asserting position().x is exactly 0 fails on a value that's almost zero.
Two additions fix both gaps: a snap helper that cleans up near-integers, and a normalizing heading().
const DEG_TO_RAD = Math.PI / 180;
// Snap a value that is within a hair of an integer back ONTO that integer.
// cos(90 deg) and sin(180 deg) are tiny non-zero floats (~6e-17), so without
// this an axis-aligned move leaves dust like 4.3e-16 in the "zero" coordinate.
function snap(n) {
const rounded = Math.round(n);
return Math.abs(n - rounded) < 1e-9 ? rounded : n;
}
class Turtle {
constructor() {
this.x = 0;
this.y = 0;
this.angle = 0; // degrees, counterclockwise from east
}
forward(distance) {
const theta = this.angle * DEG_TO_RAD;
this.x = snap(this.x + distance * Math.cos(theta));
this.y = snap(this.y + distance * Math.sin(theta));
}
backward(distance) {
this.forward(-distance);
}
left(deg) {
this.angle += deg;
}
right(deg) {
this.angle -= deg;
}
position() {
return { x: this.x, y: this.y };
}
heading() {
// Normalize into [0, 360). The double modulo handles negative angles:
// (-90 % 360) is -90, so adding 360 and taking % 360 again lands on 270.
return ((this.angle % 360) + 360) % 360;
}
}
module.exports = { Turtle };
The snap helper rounds to the nearest integer and, if the value is within 1e-9 of it, returns the clean integer; otherwise it returns the value untouched, so a genuine 4.5 is left alone while 4.29e-16 collapses to 0. The threshold 1e-9 is comfortably larger than the ~1e-16 trig dust but far smaller than any meaningful coordinate, so it never corrupts a real result. The heading() normalization uses a double modulo: angle % 360 lands in (-360, 360), then + 360 shoves any negative into positive territory, and the final % 360 brings a value that's now in [360, 720) back down — the net effect maps any input into [0, 360). Note that backward, left, and right are each one line by delegating: backward(d) calls forward(-d), so the trig lives in exactly one place.
Trace the closed square: (forward(10), left(90)) repeated four times. The turtle starts at (0, 0) with angle = 0.
start (0, 0) angle 0 (facing east)
forward(10) θ = 0·π/180 = 0
x = snap(0 + 10·cos 0) = snap(10) = 10
y = snap(0 + 10·sin 0) = snap(0) = 0 → (10, 0)
left(90) angle = 90
forward(10) θ = 90·π/180 = π/2
x = snap(10 + 10·cos 90°) = snap(10 + 6e-16) = 10
y = snap(0 + 10·sin 90°) = snap(10) = 10 → (10, 10)
left(90) angle = 180
forward(10) x = snap(10 + 10·cos 180°) = snap(10 - 10) = 0
y = snap(10 + 10·sin 180°) = snap(10 + 1e-15) = 10 → (0, 10)
left(90) angle = 270
forward(10) x = snap(0 + 10·cos 270°) = snap(-2e-15) = 0
y = snap(10 + 10·sin 270°) = snap(10 - 10) = 0 → (0, 0)
left(90) angle = 360
position() → { x: 0, y: 0 }
heading() → ((360 % 360) + 360) % 360 = 0
Two things to notice. Every forward move that should land on an axis produced a tiny non-zero float (6e-16, 1e-15) that snap flattened — without it, the final position would be a smear like { x: -2.4e-15, y: 1.5e-15 } instead of { x: 0, y: 0 }. And the heading walked 0 → 90 → 180 → 270 → 360, and 360 normalized straight back to 0, so the turtle ends facing exactly the way it started.
left adds to the angle and right subtracts. If you flip them (left subtracts), left(90); forward(5) walks the turtle to y = -5 (south) instead of +5 (north), and every direction test fails. When in doubt, anchor on one case: left(90) must face north.Math.cos. Math.cos and Math.sin take radians. Passing the raw degree value computes cos(90) (≈ -0.448, the cosine of 90 radians) instead of cos(90°) (= 0). Always convert: theta * Math.PI / 180. The symptom is positions that look random rather than wrong-by-a-sign.heading(). After right(90) the internal angle is -90; after left(370) it's 370. The spec wants [0, 360), so heading() must normalize. ((angle % 360) + 360) % 360 handles both the negative and the over-360 case in one expression — a single angle % 360 leaves -90 as -90.cos(90°) === 0. It isn't — it's 6.12e-17. Comparing a post-turn coordinate to 0 with ===, or asserting an exact integer, fails on the dust. Either snap near-integers (as here) or compare with a tolerance (toBeCloseTo) on axis-aligned moves. Don't Math.round the coordinates outright — that would destroy a legitimate 4.5.backward flip the heading. backward(d) should move opposite the heading but leave the heading unchanged — it's forward(-d), not "turn 180 then forward." If you mutate the angle inside backward, the next forward walks the wrong way. Keep turning and walking separate.position(). Returning the internal state object (return this._pos) lets a caller mutate your turtle from outside, and a later forward would build on the tampered value. Return a fresh { x, y } literal each call, as the solution does.penUp() / penDown() and have forward push line segments into a paths array while the pen is down — now the turtle draws, and you can render the result as SVG or canvas.setHeading and goto. Absolute commands complement the relative ones: setHeading(deg) snaps the angle to a fixed bearing (normalized the same way), and goto(x, y) jumps to a coordinate without walking. With goto you can also derive distanceTo(x, y) and angleTo(x, y) using Math.hypot and Math.atan2.{ type: 'forward', distance: 10 }) and you can replay, undo, or serialize a drawing. Undo pops the last command and recomputes from the start, or stores a position/heading snapshot per step for an O(1) rollback.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Build a Turtle class that tracks a position and a heading as you issue movement and turn commands, the way turtle graphics work in Logo or Python's turtle module. The turtle lives on a 2D plane. You can turn it left or right, send it forward or backward, and ask where it is and which way it's pointing. The whole job is keeping x, y, and the heading consistent as those commands accumulate.
The convention is fixed and you must follow it exactly. The turtle starts at { x: 0, y: 0 } facing east, with heading 0. Angles are in degrees, measured counterclockwise — the standard math convention — so left increases the heading and right decreases it.
class Turtle {
// No arguments. Starts at (0, 0) facing east (heading 0).
constructor();
forward(distance: number): void; // move `distance` along the current heading
backward(distance: number): void; // move `distance` opposite the heading
left(deg: number): void; // turn counterclockwise: heading += deg
right(deg: number): void; // turn clockwise: heading -= deg
position(): { x: number; y: number }; // current location
heading(): number; // current angle, normalized to [0, 360)
}
Internally, a heading of theta degrees means forward(d) adds d * cos(theta) to x and d * sin(theta) to y (with theta first converted to radians). So heading 0 points along +x (east), 90 along +y (north), 180 along -x (west), and 270 along -y (south).
// Turn north, walk 5: x stays 0, y becomes 5.
const t = new Turtle();
t.left(90);
t.forward(5);
t.position(); // → { x: 0, y: 5 }
t.heading(); // → 90
// A closed square returns to the origin and the original heading.
const t = new Turtle();
for (let i = 0; i < 4; i++) {
t.forward(10);
t.left(90);
}
t.position(); // → { x: 0, y: 0 }
t.heading(); // → 0 (four left(90) turns sum to 360, which normalizes to 0)
[0, 360). heading() always returns an angle in that half-open range. left(370) reads as 10; right(90) from 0 reads as 270; a negative turn like left(-90) is the same as right(90) and reads as 270.right is the mirror of left. right(deg) is exactly left(-deg) — turning right by 90 is turning left by −90. Implement one in terms of the other if you like, but test both.backward is forward reversed. backward(d) moves the turtle d units opposite its heading and leaves the heading itself unchanged. It is forward(-d).cos(90°) is not exactly 0 in floating point — it's about 6e-17. After a 90-degree turn, a naive forward leaves dust like 4.3e-16 in the coordinate that should read 0. Decide how to handle that (the solution snaps near-integers); axis-aligned moves should land cleanly on whole numbers.forward(0) is a no-op. Moving zero distance changes neither position nor heading.You'll keep three numbers in sync — an x, a y, and a heading angle — and update them every time the turtle is told to move or turn.
Picture a small robot sitting on graph paper holding a pen. You can spin it in place (left, right) or send it walking in a straight line (forward, backward). It never moves sideways — it only ever goes the way it's currently pointing. Your class is the robot's memory: where it is right now, and which way its nose is aimed. Every command nudges those two facts. Turning changes only the angle; walking changes only the position. The hard part is not the bookkeeping — it's pinning down the convention (which way is 0, does left add or subtract) and then translating "walk 5 units at this angle" into a change in x and y with trigonometry.
Lay the turtle on standard math axes: +x points east (right), +y points north (up). The heading is an angle measured counterclockwise from east, in degrees. At the start the turtle faces east, so its heading is 0. left(deg) rotates counterclockwise, so it adds to the angle; right(deg) rotates clockwise, so it subtracts. That single decision — counterclockwise is positive — is the spine of the whole problem. Get it backwards and every test that checks a direction will fail.
To turn "walk distance at angle theta" into a position change, drop a right triangle. The move is the hypotenuse; its horizontal leg is distance * cos(theta) and its vertical leg is distance * sin(theta). Those legs are exactly how much x and y change. One catch: Math.cos and Math.sin expect radians, not degrees, so you convert first with theta * Math.PI / 180.
The structure comes easily — store x, y, angle; add to the angle on a turn; push x and y on a move:
class Turtle {
constructor() {
this.x = 0;
this.y = 0;
this.angle = 0;
}
left(deg) { this.angle += deg; }
right(deg) { this.angle -= deg; }
forward(distance) {
const theta = this.angle * Math.PI / 180;
this.x += distance * Math.cos(theta);
this.y += distance * Math.sin(theta);
}
backward(distance) { this.forward(-distance); }
position() { return { x: this.x, y: this.y }; }
heading() { return this.angle; }
}
This is almost right, and for non-axis-aligned moves it's exactly right. But it has two visible problems. First, heading() returns the raw angle, so after left(370) it reports 370 instead of 10, and after right(90) it reports -90 instead of 270 — the spec wants the answer normalized into [0, 360). Second, the trig leaves floating-point dust: cos(90°) is not 0 but 6.12e-17, so after left(90); forward(7) the turtle reports x: 4.29e-16 instead of a clean x: 0. A test asserting position().x is exactly 0 fails on a value that's almost zero.
Two additions fix both gaps: a snap helper that cleans up near-integers, and a normalizing heading().
const DEG_TO_RAD = Math.PI / 180;
// Snap a value that is within a hair of an integer back ONTO that integer.
// cos(90 deg) and sin(180 deg) are tiny non-zero floats (~6e-17), so without
// this an axis-aligned move leaves dust like 4.3e-16 in the "zero" coordinate.
function snap(n) {
const rounded = Math.round(n);
return Math.abs(n - rounded) < 1e-9 ? rounded : n;
}
class Turtle {
constructor() {
this.x = 0;
this.y = 0;
this.angle = 0; // degrees, counterclockwise from east
}
forward(distance) {
const theta = this.angle * DEG_TO_RAD;
this.x = snap(this.x + distance * Math.cos(theta));
this.y = snap(this.y + distance * Math.sin(theta));
}
backward(distance) {
this.forward(-distance);
}
left(deg) {
this.angle += deg;
}
right(deg) {
this.angle -= deg;
}
position() {
return { x: this.x, y: this.y };
}
heading() {
// Normalize into [0, 360). The double modulo handles negative angles:
// (-90 % 360) is -90, so adding 360 and taking % 360 again lands on 270.
return ((this.angle % 360) + 360) % 360;
}
}
module.exports = { Turtle };
The snap helper rounds to the nearest integer and, if the value is within 1e-9 of it, returns the clean integer; otherwise it returns the value untouched, so a genuine 4.5 is left alone while 4.29e-16 collapses to 0. The threshold 1e-9 is comfortably larger than the ~1e-16 trig dust but far smaller than any meaningful coordinate, so it never corrupts a real result. The heading() normalization uses a double modulo: angle % 360 lands in (-360, 360), then + 360 shoves any negative into positive territory, and the final % 360 brings a value that's now in [360, 720) back down — the net effect maps any input into [0, 360). Note that backward, left, and right are each one line by delegating: backward(d) calls forward(-d), so the trig lives in exactly one place.
Trace the closed square: (forward(10), left(90)) repeated four times. The turtle starts at (0, 0) with angle = 0.
start (0, 0) angle 0 (facing east)
forward(10) θ = 0·π/180 = 0
x = snap(0 + 10·cos 0) = snap(10) = 10
y = snap(0 + 10·sin 0) = snap(0) = 0 → (10, 0)
left(90) angle = 90
forward(10) θ = 90·π/180 = π/2
x = snap(10 + 10·cos 90°) = snap(10 + 6e-16) = 10
y = snap(0 + 10·sin 90°) = snap(10) = 10 → (10, 10)
left(90) angle = 180
forward(10) x = snap(10 + 10·cos 180°) = snap(10 - 10) = 0
y = snap(10 + 10·sin 180°) = snap(10 + 1e-15) = 10 → (0, 10)
left(90) angle = 270
forward(10) x = snap(0 + 10·cos 270°) = snap(-2e-15) = 0
y = snap(10 + 10·sin 270°) = snap(10 - 10) = 0 → (0, 0)
left(90) angle = 360
position() → { x: 0, y: 0 }
heading() → ((360 % 360) + 360) % 360 = 0
Two things to notice. Every forward move that should land on an axis produced a tiny non-zero float (6e-16, 1e-15) that snap flattened — without it, the final position would be a smear like { x: -2.4e-15, y: 1.5e-15 } instead of { x: 0, y: 0 }. And the heading walked 0 → 90 → 180 → 270 → 360, and 360 normalized straight back to 0, so the turtle ends facing exactly the way it started.
left adds to the angle and right subtracts. If you flip them (left subtracts), left(90); forward(5) walks the turtle to y = -5 (south) instead of +5 (north), and every direction test fails. When in doubt, anchor on one case: left(90) must face north.Math.cos. Math.cos and Math.sin take radians. Passing the raw degree value computes cos(90) (≈ -0.448, the cosine of 90 radians) instead of cos(90°) (= 0). Always convert: theta * Math.PI / 180. The symptom is positions that look random rather than wrong-by-a-sign.heading(). After right(90) the internal angle is -90; after left(370) it's 370. The spec wants [0, 360), so heading() must normalize. ((angle % 360) + 360) % 360 handles both the negative and the over-360 case in one expression — a single angle % 360 leaves -90 as -90.cos(90°) === 0. It isn't — it's 6.12e-17. Comparing a post-turn coordinate to 0 with ===, or asserting an exact integer, fails on the dust. Either snap near-integers (as here) or compare with a tolerance (toBeCloseTo) on axis-aligned moves. Don't Math.round the coordinates outright — that would destroy a legitimate 4.5.backward flip the heading. backward(d) should move opposite the heading but leave the heading unchanged — it's forward(-d), not "turn 180 then forward." If you mutate the angle inside backward, the next forward walks the wrong way. Keep turning and walking separate.position(). Returning the internal state object (return this._pos) lets a caller mutate your turtle from outside, and a later forward would build on the tampered value. Return a fresh { x, y } literal each call, as the solution does.penUp() / penDown() and have forward push line segments into a paths array while the pen is down — now the turtle draws, and you can render the result as SVG or canvas.setHeading and goto. Absolute commands complement the relative ones: setHeading(deg) snaps the angle to a fixed bearing (normalized the same way), and goto(x, y) jumps to a coordinate without walking. With goto you can also derive distanceTo(x, y) and angleTo(x, y) using Math.hypot and Math.atan2.{ type: 'forward', distance: 10 }) and you can replay, undo, or serialize a drawing. Undo pops the last command and recomputes from the start, or stores a position/heading snapshot per step for an O(1) rollback.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.