From a34bf6bbc26da5b3790c9e51b3af1dc188695905 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 7 Oct 2026 23:58:52 +0800 Subject: [PATCH] Add draw_square and draw_dashed_line with tests Challenge 1 draws four sides and turns right 90 degrees after each. Challenge 2 puts the pen down for a dash and up for a gap and leaves it down at the end. Lengths and counts come from constants.py; invalid values raise ValueError. Task: MIL-002#3 Task: MIL-002#4 Task: MIL-002#5 --- src/turtle_challenges/__init__.py | 12 ++++- src/turtle_challenges/constants.py | 18 +++++++ src/turtle_challenges/dashed_line.py | 40 +++++++++++++++ src/turtle_challenges/square.py | 26 ++++++++++ tests/test_dashed_line.py | 76 ++++++++++++++++++++++++++++ tests/test_square.py | 46 +++++++++++++++++ 6 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 src/turtle_challenges/dashed_line.py create mode 100644 src/turtle_challenges/square.py create mode 100644 tests/test_dashed_line.py create mode 100644 tests/test_square.py diff --git a/src/turtle_challenges/__init__.py b/src/turtle_challenges/__init__.py index fda5d58..af20d82 100644 --- a/src/turtle_challenges/__init__.py +++ b/src/turtle_challenges/__init__.py @@ -3,5 +3,15 @@ @brief Solutions to the turtle graphics challenges of day 18 of Udemy's 100 Days of Code. -The challenges are added one milestone at a time; see the README. +The challenges so far are @ref turtle_challenges.square.draw_square and +@ref turtle_challenges.dashed_line.draw_dashed_line. Each takes the turtle as its +first argument, so it can be tested without a window. """ + +from turtle_challenges.dashed_line import draw_dashed_line +from turtle_challenges.square import draw_square + +__all__ = [ + "draw_dashed_line", + "draw_square", +] diff --git a/src/turtle_challenges/constants.py b/src/turtle_challenges/constants.py index cb2fb0a..84f1140 100644 --- a/src/turtle_challenges/constants.py +++ b/src/turtle_challenges/constants.py @@ -13,3 +13,21 @@ WINDOW_TITLE = "Turtle Challenges" ## Largest value of one color channel; also the turtle color mode, so that ## colors can be given as RGB tuples from 0 to 255. COLOR_MODE = 255 + +## Degrees in a right angle. +RIGHT_ANGLE_DEGREES = 90 + +## Challenge 1: number of sides of a square. +SQUARE_SIDES = 4 + +## Challenge 1: length of one side of the square, in turtle units. +SQUARE_SIDE_LENGTH = 100 + +## Challenge 2: number of dashes in the dashed line. +DASH_COUNT = 50 + +## Challenge 2: length of one dash, in turtle units. +DASH_LENGTH = 10 + +## Challenge 2: length of the gap after each dash, in turtle units. +GAP_LENGTH = 10 diff --git a/src/turtle_challenges/dashed_line.py b/src/turtle_challenges/dashed_line.py new file mode 100644 index 0000000..f38ae5d --- /dev/null +++ b/src/turtle_challenges/dashed_line.py @@ -0,0 +1,40 @@ +"""! +@file dashed_line.py +@brief Turtle Challenge 2: draw a dashed line. +""" + +from turtle_challenges.constants import DASH_COUNT, DASH_LENGTH, GAP_LENGTH +from turtle_challenges.pen import Pen + + +def draw_dashed_line( + pen: Pen, + dash_count: int = DASH_COUNT, + dash_length: float = DASH_LENGTH, + gap_length: float = GAP_LENGTH, +) -> None: + """! + @brief Draw a line of dashes: pen down for a dash, pen up for a gap. + + The pen is down again when the function returns, so that the caller can + keep drawing. + + @param pen The turtle that draws. + @param dash_count How many dashes to draw. + @param dash_length Length of one dash, in turtle units. + @param gap_length Length of the gap after each dash, in turtle units. + @exception ValueError If @p dash_count is negative, or a length is not + positive. + """ + if dash_count < 0: + raise ValueError(f"dash_count must not be negative, got {dash_count}") + if dash_length <= 0 or gap_length <= 0: + raise ValueError( + f"lengths must be positive, got dash {dash_length} and gap {gap_length}" + ) + for _ in range(dash_count): + pen.pendown() + pen.forward(dash_length) + pen.penup() + pen.forward(gap_length) + pen.pendown() diff --git a/src/turtle_challenges/square.py b/src/turtle_challenges/square.py new file mode 100644 index 0000000..be0e409 --- /dev/null +++ b/src/turtle_challenges/square.py @@ -0,0 +1,26 @@ +"""! +@file square.py +@brief Turtle Challenge 1: draw a square. +""" + +from turtle_challenges.constants import ( + RIGHT_ANGLE_DEGREES, + SQUARE_SIDE_LENGTH, + SQUARE_SIDES, +) +from turtle_challenges.pen import Pen + + +def draw_square(pen: Pen, side_length: float = SQUARE_SIDE_LENGTH) -> None: + """! + @brief Draw a square: move forward, turn right, four times. + + @param pen The turtle that draws. + @param side_length Length of each side, in turtle units. + @exception ValueError If @p side_length is not positive. + """ + if side_length <= 0: + raise ValueError(f"side_length must be positive, got {side_length}") + for _ in range(SQUARE_SIDES): + pen.forward(side_length) + pen.right(RIGHT_ANGLE_DEGREES) diff --git a/tests/test_dashed_line.py b/tests/test_dashed_line.py new file mode 100644 index 0000000..fec1c7c --- /dev/null +++ b/tests/test_dashed_line.py @@ -0,0 +1,76 @@ +import pytest + +from tests.fakes import FakePen +from turtle_challenges.constants import DASH_COUNT, DASH_LENGTH, GAP_LENGTH +from turtle_challenges.dashed_line import draw_dashed_line + + +def test_draw_dashed_line_alternates_dash_and_gap() -> None: + pen = FakePen() + + draw_dashed_line(pen, dash_count=2) + + assert ( + pen.calls[:-1] + == [ + ("pendown", ()), + ("forward", (DASH_LENGTH,)), + ("penup", ()), + ("forward", (GAP_LENGTH,)), + ] + * 2 + ) + + +def test_draw_dashed_line_draws_the_default_number_of_dashes() -> None: + pen = FakePen() + + draw_dashed_line(pen) + + assert pen.names().count("pendown") == DASH_COUNT + 1 + assert pen.names().count("forward") == 2 * DASH_COUNT + + +def test_draw_dashed_line_leaves_the_pen_down() -> None: + pen = FakePen() + + draw_dashed_line(pen, dash_count=3) + + assert pen.calls[-1] == ("pendown", ()) + + +def test_draw_dashed_line_uses_the_given_lengths() -> None: + pen = FakePen() + + draw_dashed_line(pen, dash_count=1, dash_length=4, gap_length=7) + + assert pen.args_of("forward") == [(4,), (7,)] + + +def test_draw_dashed_line_with_no_dashes_only_puts_the_pen_down() -> None: + pen = FakePen() + + draw_dashed_line(pen, dash_count=0) + + assert pen.names() == ["pendown"] + + +def test_draw_dashed_line_rejects_a_negative_count() -> None: + pen = FakePen() + + with pytest.raises(ValueError, match="dash_count"): + draw_dashed_line(pen, dash_count=-1) + + assert pen.calls == [] + + +@pytest.mark.parametrize(("dash_length", "gap_length"), [(0, 10), (10, 0), (-1, 10)]) +def test_draw_dashed_line_rejects_a_length_that_is_not_positive( + dash_length: float, gap_length: float +) -> None: + pen = FakePen() + + with pytest.raises(ValueError, match="lengths"): + draw_dashed_line(pen, dash_length=dash_length, gap_length=gap_length) + + assert pen.calls == [] diff --git a/tests/test_square.py b/tests/test_square.py new file mode 100644 index 0000000..e0021df --- /dev/null +++ b/tests/test_square.py @@ -0,0 +1,46 @@ +import pytest + +from tests.fakes import FakePen +from turtle_challenges.constants import RIGHT_ANGLE_DEGREES, SQUARE_SIDE_LENGTH +from turtle_challenges.square import draw_square + + +def test_draw_square_moves_forward_and_turns_right_four_times() -> None: + pen = FakePen() + + draw_square(pen) + + assert ( + pen.calls + == [ + ("forward", (SQUARE_SIDE_LENGTH,)), + ("right", (RIGHT_ANGLE_DEGREES,)), + ] + * 4 + ) + + +def test_draw_square_uses_the_given_side_length() -> None: + pen = FakePen() + + draw_square(pen, side_length=40) + + assert pen.args_of("forward") == [(40,)] * 4 + + +def test_draw_square_returns_the_turtle_to_its_start_heading() -> None: + pen = FakePen() + + draw_square(pen) + + assert pen.heading() == 0 + + +@pytest.mark.parametrize("side_length", [0, -5]) +def test_draw_square_rejects_a_side_that_is_not_positive(side_length: float) -> None: + pen = FakePen() + + with pytest.raises(ValueError, match="side_length"): + draw_square(pen, side_length=side_length) + + assert pen.calls == []