Land milestones 002 to 004 on main #34
@@ -3,17 +3,21 @@
|
|||||||
@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 and
|
The challenges so far are @ref turtle_challenges.square.draw_square,
|
||||||
@ref turtle_challenges.dashed_line.draw_dashed_line. Each takes the turtle as its
|
@ref turtle_challenges.dashed_line.draw_dashed_line and
|
||||||
|
@ref turtle_challenges.shapes.draw_shape. 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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from turtle_challenges.colors import random_color, random_palette_color
|
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.shapes import draw_shape, draw_shapes
|
||||||
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_shapes",
|
||||||
"draw_square",
|
"draw_square",
|
||||||
"random_color",
|
"random_color",
|
||||||
"random_palette_color",
|
"random_palette_color",
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ COLOR_CHANNEL_MIN = 0
|
|||||||
## Largest value of one color channel.
|
## Largest value of one color channel.
|
||||||
COLOR_CHANNEL_MAX = COLOR_MODE
|
COLOR_CHANNEL_MAX = COLOR_MODE
|
||||||
|
|
||||||
|
## Degrees in a full turn.
|
||||||
|
FULL_TURN_DEGREES = 360
|
||||||
|
|
||||||
## Degrees in a right angle.
|
## Degrees in a right angle.
|
||||||
RIGHT_ANGLE_DEGREES = 90
|
RIGHT_ANGLE_DEGREES = 90
|
||||||
|
|
||||||
@@ -38,6 +41,15 @@ DASH_LENGTH = 10
|
|||||||
## Challenge 2: length of the gap after each dash, in turtle units.
|
## Challenge 2: length of the gap after each dash, in turtle units.
|
||||||
GAP_LENGTH = 10
|
GAP_LENGTH = 10
|
||||||
|
|
||||||
|
## Challenge 3: fewest sides of a polygon (a triangle).
|
||||||
|
MIN_POLYGON_SIDES = 3
|
||||||
|
|
||||||
|
## Challenge 3: most sides of a polygon drawn in a row (a decagon).
|
||||||
|
MAX_POLYGON_SIDES = 10
|
||||||
|
|
||||||
|
## Challenge 3: length of one side of a polygon, in turtle units.
|
||||||
|
POLYGON_SIDE_LENGTH = 100
|
||||||
|
|
||||||
## Challenge 3: named colors a polygon is drawn in.
|
## Challenge 3: named colors a polygon is drawn in.
|
||||||
COLOR_PALETTE: tuple[str, ...] = (
|
COLOR_PALETTE: tuple[str, ...] = (
|
||||||
"CornflowerBlue",
|
"CornflowerBlue",
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""!
|
||||||
|
@file shapes.py
|
||||||
|
@brief Turtle Challenge 3: draw different shapes, each in a random color.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from turtle_challenges.colors import random_palette_color
|
||||||
|
from turtle_challenges.constants import (
|
||||||
|
FULL_TURN_DEGREES,
|
||||||
|
MAX_POLYGON_SIDES,
|
||||||
|
MIN_POLYGON_SIDES,
|
||||||
|
POLYGON_SIDE_LENGTH,
|
||||||
|
)
|
||||||
|
from turtle_challenges.pen import Pen
|
||||||
|
|
||||||
|
|
||||||
|
def draw_shape(
|
||||||
|
pen: Pen, num_sides: int, side_length: float = POLYGON_SIDE_LENGTH
|
||||||
|
) -> None:
|
||||||
|
"""!
|
||||||
|
@brief Draw a regular polygon.
|
||||||
|
|
||||||
|
The turtle turns 360 divided by the number of sides after each side, so
|
||||||
|
five sides turn 72 degrees and the shape closes after one lap.
|
||||||
|
|
||||||
|
@param pen The turtle that draws.
|
||||||
|
@param num_sides Number of sides; at least 3.
|
||||||
|
@param side_length Length of each side, in turtle units.
|
||||||
|
@exception ValueError If @p num_sides is below 3, or @p side_length is not
|
||||||
|
positive.
|
||||||
|
"""
|
||||||
|
if num_sides < MIN_POLYGON_SIDES:
|
||||||
|
raise ValueError(
|
||||||
|
f"num_sides must be at least {MIN_POLYGON_SIDES}, got {num_sides}"
|
||||||
|
)
|
||||||
|
if side_length <= 0:
|
||||||
|
raise ValueError(f"side_length must be positive, got {side_length}")
|
||||||
|
angle = FULL_TURN_DEGREES / num_sides
|
||||||
|
for _ in range(num_sides):
|
||||||
|
pen.forward(side_length)
|
||||||
|
pen.right(angle)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_shapes(pen: Pen, rng: random.Random | None = None) -> None:
|
||||||
|
"""!
|
||||||
|
@brief Draw a triangle, square, pentagon and so on up to a decagon.
|
||||||
|
|
||||||
|
Each shape gets a random color from the palette.
|
||||||
|
|
||||||
|
@param pen The turtle that draws.
|
||||||
|
@param rng Random generator for the colors; a shared one is used if omitted.
|
||||||
|
"""
|
||||||
|
for num_sides in range(MIN_POLYGON_SIDES, MAX_POLYGON_SIDES + 1):
|
||||||
|
pen.color(random_palette_color(rng))
|
||||||
|
draw_shape(pen, num_sides)
|
||||||
@@ -12,3 +12,8 @@ def test_challenge_names_are_unique() -> None:
|
|||||||
def test_palette_has_only_non_empty_names() -> None:
|
def test_palette_has_only_non_empty_names() -> None:
|
||||||
assert constants.COLOR_PALETTE
|
assert constants.COLOR_PALETTE
|
||||||
assert all(name for name in constants.COLOR_PALETTE)
|
assert all(name for name in constants.COLOR_PALETTE)
|
||||||
|
|
||||||
|
|
||||||
|
def test_polygon_range_runs_from_triangle_to_decagon() -> None:
|
||||||
|
assert constants.MIN_POLYGON_SIDES == 3
|
||||||
|
assert constants.MAX_POLYGON_SIDES == 10
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests.fakes import FakePen
|
||||||
|
from turtle_challenges.constants import (
|
||||||
|
COLOR_PALETTE,
|
||||||
|
FULL_TURN_DEGREES,
|
||||||
|
MAX_POLYGON_SIDES,
|
||||||
|
MIN_POLYGON_SIDES,
|
||||||
|
POLYGON_SIDE_LENGTH,
|
||||||
|
)
|
||||||
|
from turtle_challenges.shapes import draw_shape, draw_shapes
|
||||||
|
|
||||||
|
SIDE_COUNTS = range(MIN_POLYGON_SIDES, MAX_POLYGON_SIDES + 1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("num_sides", SIDE_COUNTS)
|
||||||
|
def test_draw_shape_draws_every_side_at_the_default_length(num_sides: int) -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_shape(pen, num_sides)
|
||||||
|
|
||||||
|
assert pen.args_of("forward") == [(POLYGON_SIDE_LENGTH,)] * num_sides
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("num_sides", SIDE_COUNTS)
|
||||||
|
def test_draw_shape_turns_a_full_lap_in_equal_steps(num_sides: int) -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_shape(pen, num_sides)
|
||||||
|
|
||||||
|
expected_angle = FULL_TURN_DEGREES / num_sides
|
||||||
|
assert pen.args_of("right") == [(expected_angle,)] * num_sides
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_shape_turns_72_degrees_for_a_pentagon() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_shape(pen, 5)
|
||||||
|
|
||||||
|
assert pen.args_of("right")[0] == (72,)
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_shape_uses_the_given_side_length() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_shape(pen, 3, side_length=25)
|
||||||
|
|
||||||
|
assert pen.args_of("forward") == [(25,)] * 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("num_sides", [-1, 0, 1, 2])
|
||||||
|
def test_draw_shape_rejects_fewer_than_three_sides(num_sides: int) -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="num_sides"):
|
||||||
|
draw_shape(pen, num_sides)
|
||||||
|
|
||||||
|
assert pen.calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_shape_rejects_a_side_that_is_not_positive() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="side_length"):
|
||||||
|
draw_shape(pen, 4, side_length=0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_shapes_draws_a_triangle_up_to_a_decagon() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_shapes(pen, random.Random(5))
|
||||||
|
|
||||||
|
assert len(pen.args_of("forward")) == sum(SIDE_COUNTS)
|
||||||
|
assert len(pen.args_of("color")) == len(SIDE_COUNTS)
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_shapes_sets_a_palette_color_before_each_shape() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_shapes(pen, random.Random(6))
|
||||||
|
|
||||||
|
colors = [args[0] for args in pen.args_of("color")]
|
||||||
|
assert all(color in COLOR_PALETTE for color in colors)
|
||||||
|
assert pen.names()[0] == "color"
|
||||||
|
|
||||||
|
|
||||||
|
def test_draw_shapes_works_without_a_generator() -> None:
|
||||||
|
pen = FakePen()
|
||||||
|
|
||||||
|
draw_shapes(pen)
|
||||||
|
|
||||||
|
assert len(pen.args_of("color")) == len(SIDE_COUNTS)
|
||||||
Reference in New Issue
Block a user