While working on the Flower Field exercise (a Minesweeper-like problem where we annotate a garden with adjacent flower counts), I noticed a subtle issue in how some implementations might handle horizontal neighbor counting, and it seems the current test suite doesn’t catch it.
The Issue
In the exercise, we need to count only immediately adjacent flowers (up to 8 neighbors: horizontal, vertical, diagonal) for each empty space. However, if a solution mistakenly scans and counts contiguous flowers in the horizontal directions (left/right) instead of just the immediate ones, it will overcount in cases with multiple adjacent flowers in a row.
For example:
Input: ["** "] (a row with two flowers followed by a space)
Expected output: ["**1"] (the space is only adjacent to the immediate left flower; the further one is not directly adjacent)
Buggy output: ["**2"] (if the code scans left and counts both flowers)
This passes all the existing tests because none of them include contiguous horizontal flowers next to a space (e.g., the “cross” and “large garden” tests space things out). But it’s a correctness gap that could mislead learners if their code has this flaw, as it doesn’t enforce checking only immediate neighbors—a key part of the adjacency rules.
To demonstrate, I’ve posted a minimal buggy implementation below (in reply #4) that passes the current tests but would fail with the proposed additions. It’s written in Python for illustration, but the logic flaw (scanning contiguous runs instead of single neighbors) could occur in any language.
Proposed Test Addition
To catch this, I’d like to suggest adding simple test cases to the canonical data:
Overcounting horizontal long chain: Input ["*** "], Expected ["***1"]
These would ensure solutions check only immediate neighbors, aligning with the problem specs, without significantly impacting existing correct solutions or adding complexity.
Impact and Justification
Benefit: Improves the exercise’s ability to detect incorrect adjacency logic, helping learners (across all tracks) build more accurate implementations. It’s a small addition that reinforces the core rules without changing what the exercise teaches.
Cross-Track Considerations: As a shared exercise with canonical data, this would apply universally—no forking needed. I’ve considered why the current tests might not include this (perhaps focusing on common patterns), but adding these edges would strengthen it per “Chesterton’s Fence.”
Drawbacks: Minimal; might invalidate a few buggy solutions, but that’s the point of better tests.
Why I’m Posting Here
Following the contribution guidelines, I’d like to get feedback and consensus from maintainers before proposing a PR to the problem-specifications repo. Does this make sense? Any suggestions on the test cases or process?
Thanks for the awesome platform—it’s been super helpful for my Python track journey!
If you can show us a solution that should not pass given the instructions but does pass because a test or tests are missing, then this is a good candidate to propose a change for.
Because Flower Field is indeed a shared exercise with canonical data, the change would not just be for Python but for all tracks that implement this exercise.
If you follow the link Isaac shared, and amend your post to be descriptive and agnostic (do not worry, you can use Python to proof your point, feel free to write in Python what’s currently failing) the process is straightforward:
You change your message
We have current track maintainers inspect and discuss your proposed change
If there is some sort of consensus with at least 3 maintainers wanting to go ahead, you can open a PR on the problem-spec repository
Once merged, an automated system (mostly) will propagate the change to all the tracks that use it
Per track (including Python) the sync can be worked on
def annotate(garden: list) -> list:
"""
Annotate a garden with counts of adjacent flowers.
Expects a rectangular list of strings containing only spaces and ``*``.
Validation errors raise a :class:`ValueError`.
:param list garden: A list of equal-length strings representing the garden.
``*`` marks a flower; space marks empty.
:returns: An annotated garden of the same shape. Empty squares are
replaced by digits (``"1"``–``"8"``) when adjacent to flowers;
squares with zero adjacent flowers remain spaces. Flowers
(``*``) are preserved.
:rtype: list
:raises ValueError: If the garden is non-rectangular or contains
invalid characters.
"""
# empty list
if not garden:
return []
# when the board receives malformed input
if not _is_garden_valid(garden):
raise ValueError("The board is invalid with current input.")
for i_row, row in enumerate(garden):
for i_col, char in enumerate(row):
if char == " ":
flower_count: int = 0
flower_count += _calc_flower_top(i_row, i_col, garden)
flower_count += _calc_flower_bottom(i_row, i_col, garden)
flower_count += _calc_flower_left(i_row, i_col, garden)
flower_count += _calc_flower_right(i_row, i_col, garden)
if flower_count != 0:
garden[i_row] = (
garden[i_row][:i_col]
+ str(flower_count)
+ garden[i_row][i_col + 1 :]
)
return garden
def _calc_flower_left(i_row: int, i_col: int, garden: list) -> int:
"""
Count contiguous flowers to the left of the current position.
Scans leftward from ``(i_row, i_col - 1)`` until a non-flower character is
found or the row boundary is reached.
:param int i_row: Current row index.
:param int i_col: Current column index.
:param list garden: The garden as a list of strings.
:returns: Number of adjacent ``*`` cells to the left.
:rtype: int
"""
flower_count: int = 0
if i_col - 1 >= 0:
for char in garden[i_row][:i_col][::-1]:
if char == "*":
flower_count += 1
else:
break
return flower_count
def _calc_flower_right(i_row: int, i_col: int, garden: list) -> int:
"""
Count contiguous flowers to the right of the current position.
Scans rightward from ``(i_row, i_col + 1)`` until a non-flower character is
found or the row boundary is reached.
:param int i_row: Current row index.
:param int i_col: Current column index.
:param list garden: The garden as a list of strings.
:returns: Number of adjacent ``*`` cells to the right.
:rtype: int
"""
flower_count: int = 0
if i_col + 1 < len(garden[i_row]):
for char in garden[i_row][i_col + 1 :]:
if char == "*":
flower_count += 1
else:
break
return flower_count
def _calc_flower_top(i_row: int, i_col: int, garden: list) -> int:
"""
Count flowers in the three cells directly above the current position.
Checks the top-left, top, and top-right neighbors when the row above
exists and contains any flowers.
:param int i_row: Current row index.
:param int i_col: Current column index.
:param list garden: The garden as a list of strings.
:returns: Number of ``*`` cells among the three upper neighbors.
:rtype: int
"""
flower_count: int = 0
if i_row - 1 >= 0 and "*" in garden[i_row - 1]:
# top-left
if i_col > 0 and garden[i_row - 1][i_col - 1] == "*":
flower_count += 1
# top
if garden[i_row - 1][i_col] == "*":
flower_count += 1
# top-right
if (
i_col + 1 < len(garden[i_row])
and garden[i_row - 1][i_col + 1] == "*"
):
flower_count += 1
return flower_count
def _calc_flower_bottom(i_row: int, i_col: int, garden: list) -> int:
"""
Count flowers in the three cells directly below the current position.
Checks the bottom-left, bottom, and bottom-right neighbors when the row
below exists and contains any flowers.
:param int i_row: Current row index.
:param int i_col: Current column index.
:param list garden: The garden as a list of strings.
:returns: Number of ``*`` cells among the three lower neighbors.
:rtype: int
"""
flower_count: int = 0
if i_row + 1 < len(garden) and "*" in garden[i_row + 1]:
# bottom-left
if i_col > 0 and garden[i_row + 1][i_col - 1] == "*":
flower_count += 1
# bottom
if garden[i_row + 1][i_col] == "*":
flower_count += 1
# bottom-right
if (
i_col + 1 < len(garden[i_row])
and garden[i_row + 1][i_col + 1] == "*"
):
flower_count += 1
return flower_count
def _is_garden_valid(garden: list) -> bool:
"""
Check whether the garden input is a valid rectangular board.
A garden is considered valid when all rows have the same length and only
contain spaces or ``*`` characters.
:param list garden: Candidate garden as a list of strings.
:returns: ``True`` if the input is rectangular and uses only valid
characters; otherwise ``False``.
:rtype: bool
"""
garden_length: int = len(garden[0])
# when the board receives malformed input
for row in garden:
# garden is not a rectangle due to inconsistent row length
if len(row) != garden_length:
return False
# contains invalid chars inside row
valid_chars: bool = all(char in " *" for char in row)
if not valid_chars:
return False
return True
Thanks for the guidance, @IsaacG and @SleeplessByte! I’ve edited the original post to make it language-agnostic, added more on the benefits/impact per the docs, and referenced the buggy example I posted. Looking forward to any feedback from maintainers on proceeding to a PR.
I’m not sure how likely it is for students to stumble across that bug, but I’m not opposed to an additional test to cover this.
The tests are meant as guide rails and not exhaustive tests. I don’t think this edge case call for more than one test, eg "** " => "**1". However, I think the original game was set up so that every * was adjacent to an open spot so I think the above case is … ambiguous/invalid and would propose this instead: " ** " => "1**1" (which happens to also capture both directions in one test).
My standpoint isn’t “we should not have exhaustive tests” but “we should not test for out-of-description inputs” which is where most of the discussion spends time when it comes to exhaustiveness.
If there is a bug that is only visible for a vertical layout and not the horizontal one and the current tests do not catch and matches the expected implementation algorithm (based on the description) then we should add that test, but I don’t think it does at the moment, so the conclusion is the same as Isaac’s.
(The above is my opinion and stance, there is no official Exercism policy on this)
Thanks @IsaacG, @SleeplessByte, and @mk-mxp for the valuable feedback and support—it’s awesome to see this gaining traction! I agree on keeping tests as guide rails and not exhaustive; IsaacG’s suggested test [" ** "] => ["1**1"] is perfect for catching the horizontal overcounting in both directions without ambiguity. As for vertical, per my checks on the buggy example (and aligning with SleeplessByte’s point on out-of-description inputs), it doesn’t trigger the flaw there—current tests like the vertical line already handle it well, so no addition needed.
With three maintainers weighing in positively, does this qualify as the consensus needed to move forward (per the process outlined)? If so, I’d love permission to open a PR on the problem, happy to draft it and link back here for review.
Appreciate your guidance—excited to contribute to making Flower Field even stronger for the Python track and beyond!