Add draw_spirograph with tests
Draws int(360 / size_of_gap) circles, each in a random RGB color, turning the heading by the gap after each. A gap that is not positive raises ValueError, and the integer count avoids the TypeError that range raises for a float. Task: MIL-004#2 Task: MIL-004#3
This commit is contained in:
@@ -3,10 +3,11 @@
|
|||||||
@brief Solutions to the turtle graphics challenges of day 18 of Udemy's
|
@brief Solutions to the turtle graphics challenges of day 18 of Udemy's
|
||||||
100 Days of Code.
|
100 Days of Code.
|
||||||
|
|
||||||
The challenges so far are @ref turtle_challenges.square.draw_square,
|
The five challenges are @ref turtle_challenges.square.draw_square,
|
||||||
@ref turtle_challenges.dashed_line.draw_dashed_line,
|
@ref turtle_challenges.dashed_line.draw_dashed_line,
|
||||||
@ref turtle_challenges.shapes.draw_shape and
|
@ref turtle_challenges.shapes.draw_shape,
|
||||||
@ref turtle_challenges.random_walk.random_walk. Each takes the turtle as its
|
@ref turtle_challenges.random_walk.random_walk and
|
||||||
|
@ref turtle_challenges.spirograph.draw_spirograph. Each takes the turtle as its
|
||||||
first argument, so it can be tested without a window.
|
first argument, so it can be tested without a window.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -14,12 +15,14 @@ from turtle_challenges.colors import random_color, random_palette_color
|
|||||||
from turtle_challenges.dashed_line import draw_dashed_line
|
from turtle_challenges.dashed_line import draw_dashed_line
|
||||||
from turtle_challenges.random_walk import random_walk
|
from turtle_challenges.random_walk import random_walk
|
||||||
from turtle_challenges.shapes import draw_shape, draw_shapes
|
from turtle_challenges.shapes import draw_shape, draw_shapes
|
||||||
|
from turtle_challenges.spirograph import draw_spirograph
|
||||||
from turtle_challenges.square import draw_square
|
from turtle_challenges.square import draw_square
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"draw_dashed_line",
|
"draw_dashed_line",
|
||||||
"draw_shape",
|
"draw_shape",
|
||||||
"draw_shapes",
|
"draw_shapes",
|
||||||
|
"draw_spirograph",
|
||||||
"draw_square",
|
"draw_square",
|
||||||
"random_color",
|
"random_color",
|
||||||
"random_palette_color",
|
"random_palette_color",
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ WALK_PEN_SIZE = 10
|
|||||||
## Challenge 4: headings of a step: east, north, west and south, in degrees.
|
## Challenge 4: headings of a step: east, north, west and south, in degrees.
|
||||||
WALK_HEADINGS: tuple[int, ...] = (0, 90, 180, 270)
|
WALK_HEADINGS: tuple[int, ...] = (0, 90, 180, 270)
|
||||||
|
|
||||||
|
## Challenge 5: radius of every circle of the spirograph, in turtle units.
|
||||||
|
SPIROGRAPH_RADIUS = 100
|
||||||
|
|
||||||
|
## Challenge 5: degrees the heading turns after each circle.
|
||||||
|
SPIROGRAPH_GAP_DEGREES = 5
|
||||||
|
|
||||||
## Command line name of challenge 1.
|
## Command line name of challenge 1.
|
||||||
CHALLENGE_SQUARE = "square"
|
CHALLENGE_SQUARE = "square"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""!
|
||||||
|
@file spirograph.py
|
||||||
|
@brief Turtle Challenge 5: draw a spirograph.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from turtle_challenges.colors import random_color
|
||||||
|
from turtle_challenges.constants import (
|
||||||
|
FASTEST_SPEED,
|
||||||
|
FULL_TURN_DEGREES,
|
||||||
|
SPIROGRAPH_GAP_DEGREES,
|
||||||
|
SPIROGRAPH_RADIUS,
|
||||||
|
)
|
||||||
|
from turtle_challenges.pen import Pen
|
||||||
|
|
||||||
|
|
||||||
|
def draw_spirograph(
|
||||||
|
pen: Pen,
|
||||||
|
size_of_gap: float = SPIROGRAPH_GAP_DEGREES,
|
||||||
|
radius: float = SPIROGRAPH_RADIUS,
|
||||||
|
rng: random.Random | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""!
|
||||||
|
@brief Draw overlapping circles, turning a little after each one.
|
||||||
|
|
||||||
|
After every circle the heading grows by @p size_of_gap, so the circles
|
||||||
|
overlap into a spirograph once the heading has made a full turn. The number
|
||||||
|
of circles is 360 divided by the gap, rounded down: `range` takes only an
|
||||||
|
integer, and a float such as 72.0 would raise a `TypeError`.
|
||||||
|
|
||||||
|
@param pen The turtle that draws.
|
||||||
|
@param size_of_gap Degrees between two circles; must be positive.
|
||||||
|
@param radius Radius of every circle, in turtle units.
|
||||||
|
@param rng Random generator for the colors; a shared one is used if
|
||||||
|
omitted.
|
||||||
|
@exception ValueError If @p size_of_gap or @p radius is not positive.
|
||||||
|
"""
|
||||||
|
if size_of_gap <= 0:
|
||||||
|
raise ValueError(f"size_of_gap must be positive, got {size_of_gap}")
|
||||||
|
if radius <= 0:
|
||||||
|
raise ValueError(f"radius must be positive, got {radius}")
|
||||||
|
pen.speed(FASTEST_SPEED)
|
||||||
|
for _ in range(int(FULL_TURN_DEGREES / size_of_gap)):
|
||||||
|
pen.color(random_color(rng))
|
||||||
|
pen.circle(radius)
|
||||||
|
pen.setheading(pen.heading() + size_of_gap)
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.fakes import FakePen
|
||||||
|
from turtle_challenges.constants import (
|
||||||
|
FASTEST_SPEED,
|
||||||
|
FULL_TURN_DEGREES,
|
||||||
|
SPIROGRAPH_GAP_DEGREES,
|
||||||
|
SPIROGRAPH_RADIUS,
|
||||||
|
)
|
||||||
|
from turtle_challenges.spirograph import draw_spirograph
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_spirograph_draws_one_circle_per_gap_in_a_full_turn() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_spirograph(pen, rng=random.Random(1))
|
||||||
|
|
||||||
|
expected = FULL_TURN_DEGREES // SPIROGRAPH_GAP_DEGREES
|
||||||
|
assert pen.args_of("circle") == [(SPIROGRAPH_RADIUS,)] * expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_spirograph_turns_by_the_gap_after_each_circle() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_spirograph(pen, size_of_gap=90, rng=random.Random(2))
|
||||||
|
|
||||||
|
assert pen.args_of("setheading") == [(90,), (180,), (270,), (360,)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_spirograph_sets_a_color_before_each_circle() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_spirograph(pen, size_of_gap=120, rng=random.Random(3))
|
||||||
|
|
||||||
|
assert pen.names()[1:] == ["color", "circle", "setheading"] * 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_spirograph_gives_every_circle_an_rgb_color() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_spirograph(pen, size_of_gap=30, rng=random.Random(4))
|
||||||
|
|
||||||
|
for (color,) in pen.args_of("color"):
|
||||||
|
assert isinstance(color, tuple)
|
||||||
|
assert len(color) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_spirograph_sets_the_fastest_speed_first() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_spirograph(pen, size_of_gap=180)
|
||||||
|
|
||||||
|
assert pen.calls[0] == ("speed", (FASTEST_SPEED,))
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_spirograph_rounds_the_circle_count_down_for_an_uneven_gap() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_spirograph(pen, size_of_gap=7)
|
||||||
|
|
||||||
|
assert len(pen.args_of("circle")) == 51
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_spirograph_accepts_a_float_gap_without_a_type_error() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_spirograph(pen, size_of_gap=2.5)
|
||||||
|
|
||||||
|
assert len(pen.args_of("circle")) == 144
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_spirograph_uses_the_given_radius() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_spirograph(pen, size_of_gap=180, radius=42)
|
||||||
|
|
||||||
|
assert pen.args_of("circle") == [(42,), (42,)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_spirograph_is_repeatable_with_the_same_seed() -> None:
|
||||||
|
first = FakePen()
|
||||||
|
second = FakePen()
|
||||||
|
|
||||||
|
draw_spirograph(first, size_of_gap=45, rng=random.Random(9))
|
||||||
|
draw_spirograph(second, size_of_gap=45, rng=random.Random(9))
|
||||||
|
|
||||||
|
assert first.calls == second.calls
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("size_of_gap", [0, -5])
|
||||||
|
def test_draw_spirograph_rejects_a_gap_that_is_not_positive(
|
||||||
|
size_of_gap: float,
|
||||||
|
) -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="size_of_gap"):
|
||||||
|
draw_spirograph(pen, size_of_gap=size_of_gap)
|
||||||
|
|
||||||
|
assert pen.calls == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("radius", [0, -1])
|
||||||
|
def test_draw_spirograph_rejects_a_radius_that_is_not_positive(radius: float) -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="radius"):
|
||||||
|
draw_spirograph(pen, radius=radius)
|
||||||
|
|
||||||
|
assert pen.calls == []
|
||||||
Reference in New Issue
Block a user