From 9318a07fa0098ecc2c50031d95ee0257f61d91bb Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Wed, 7 Oct 2026 23:59:02 +0800 Subject: [PATCH] Add random_color and the color palette random_color returns an immutable (red, green, blue) tuple of integers from 0 to 255 and random_palette_color picks a named color. Both take an optional random.Random so that tests can seed them. Task: MIL-003#1 Task: MIL-003#2 --- src/turtle_challenges/__init__.py | 3 ++ src/turtle_challenges/colors.py | 44 +++++++++++++++++++++++++ src/turtle_challenges/constants.py | 18 ++++++++++ tests/test_colors.py | 53 ++++++++++++++++++++++++++++++ tests/test_constants.py | 9 +++++ 5 files changed, 127 insertions(+) create mode 100644 src/turtle_challenges/colors.py create mode 100644 tests/test_colors.py diff --git a/src/turtle_challenges/__init__.py b/src/turtle_challenges/__init__.py index af20d82..92a4f74 100644 --- a/src/turtle_challenges/__init__.py +++ b/src/turtle_challenges/__init__.py @@ -8,10 +8,13 @@ The challenges so far are @ref turtle_challenges.square.draw_square and first argument, so it can be tested without a window. """ +from turtle_challenges.colors import random_color, random_palette_color from turtle_challenges.dashed_line import draw_dashed_line from turtle_challenges.square import draw_square __all__ = [ "draw_dashed_line", "draw_square", + "random_color", + "random_palette_color", ] diff --git a/src/turtle_challenges/colors.py b/src/turtle_challenges/colors.py new file mode 100644 index 0000000..40f313d --- /dev/null +++ b/src/turtle_challenges/colors.py @@ -0,0 +1,44 @@ +"""! +@file colors.py +@brief Random colors for the challenges. + +A color is either a name from the palette (challenge 3) or an RGB tuple +(challenges 4 and 5). A tuple is the right type for an RGB value: it is +immutable, so a color cannot change after it is made. +""" + +import random + +from turtle_challenges.constants import ( + COLOR_CHANNEL_MAX, + COLOR_CHANNEL_MIN, + COLOR_PALETTE, +) + +## Generator used when a caller passes none; tests pass a seeded one instead. +_DEFAULT_RANDOM = random.Random() + + +def random_color(rng: random.Random | None = None) -> tuple[int, int, int]: + """! + @brief Pick a random RGB color. + + @param rng Random generator to draw from; a shared one is used if omitted. + @return A tuple of red, green and blue, each from 0 to 255. + """ + generator = rng if rng is not None else _DEFAULT_RANDOM + red = generator.randint(COLOR_CHANNEL_MIN, COLOR_CHANNEL_MAX) + green = generator.randint(COLOR_CHANNEL_MIN, COLOR_CHANNEL_MAX) + blue = generator.randint(COLOR_CHANNEL_MIN, COLOR_CHANNEL_MAX) + return (red, green, blue) + + +def random_palette_color(rng: random.Random | None = None) -> str: + """! + @brief Pick a random color name from the palette. + + @param rng Random generator to draw from; a shared one is used if omitted. + @return One of the names in `COLOR_PALETTE`. + """ + generator = rng if rng is not None else _DEFAULT_RANDOM + return generator.choice(COLOR_PALETTE) diff --git a/src/turtle_challenges/constants.py b/src/turtle_challenges/constants.py index 0c5ca52..8820e51 100644 --- a/src/turtle_challenges/constants.py +++ b/src/turtle_challenges/constants.py @@ -14,6 +14,12 @@ WINDOW_TITLE = "Turtle Challenges" ## colors can be given as RGB tuples from 0 to 255. COLOR_MODE = 255 +## Smallest value of one color channel. +COLOR_CHANNEL_MIN = 0 + +## Largest value of one color channel. +COLOR_CHANNEL_MAX = COLOR_MODE + ## Degrees in a right angle. RIGHT_ANGLE_DEGREES = 90 @@ -32,6 +38,18 @@ DASH_LENGTH = 10 ## Challenge 2: length of the gap after each dash, in turtle units. GAP_LENGTH = 10 +## Challenge 3: named colors a polygon is drawn in. +COLOR_PALETTE: tuple[str, ...] = ( + "CornflowerBlue", + "DarkOrchid", + "IndianRed", + "DeepSkyBlue", + "LightSeaGreen", + "wheat", + "SlateGray", + "SeaGreen", +) + ## Command line name of challenge 1. CHALLENGE_SQUARE = "square" diff --git a/tests/test_colors.py b/tests/test_colors.py new file mode 100644 index 0000000..af1be90 --- /dev/null +++ b/tests/test_colors.py @@ -0,0 +1,53 @@ +import random + +from turtle_challenges.colors import random_color, random_palette_color +from turtle_challenges.constants import ( + COLOR_CHANNEL_MAX, + COLOR_CHANNEL_MIN, + COLOR_PALETTE, +) + + +def test_random_color_is_a_tuple_of_three_integers() -> None: + color = random_color(random.Random(1)) + + assert isinstance(color, tuple) + assert len(color) == 3 + assert all(isinstance(channel, int) for channel in color) + + +def test_random_color_channels_stay_in_range() -> None: + rng = random.Random(2) + + for _ in range(500): + assert all( + COLOR_CHANNEL_MIN <= channel <= COLOR_CHANNEL_MAX + for channel in random_color(rng) + ) + + +def test_random_color_is_repeatable_with_the_same_seed() -> None: + first = [random_color(random.Random(7)) for _ in range(3)] + second = [random_color(random.Random(7)) for _ in range(3)] + + assert first == second + + +def test_random_color_varies_between_calls() -> None: + rng = random.Random(3) + + assert len({random_color(rng) for _ in range(20)}) > 1 + + +def test_random_color_works_without_a_generator() -> None: + assert len(random_color()) == 3 + + +def test_random_palette_color_comes_from_the_palette() -> None: + rng = random.Random(4) + + assert {random_palette_color(rng) for _ in range(100)} <= set(COLOR_PALETTE) + + +def test_random_palette_color_works_without_a_generator() -> None: + assert random_palette_color() in COLOR_PALETTE diff --git a/tests/test_constants.py b/tests/test_constants.py index ac3dd94..20e82d6 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -1,5 +1,14 @@ from turtle_challenges import constants +def test_color_mode_matches_the_largest_channel_value() -> None: + assert constants.COLOR_MODE == constants.COLOR_CHANNEL_MAX == 255 + + def test_challenge_names_are_unique() -> None: assert len(set(constants.CHALLENGE_NAMES)) == len(constants.CHALLENGE_NAMES) + + +def test_palette_has_only_non_empty_names() -> None: + assert constants.COLOR_PALETTE + assert all(name for name in constants.COLOR_PALETTE)