From 0f58b6308903d39b53e109aa2b669a9574c11b83 Mon Sep 17 00:00:00 2001 From: colinleach Date: Fri, 29 Mar 2024 18:08:49 -0700 Subject: [PATCH 1/2] [Minesweeper] draft approaches --- .../minesweeper/.approaches/config.json | 7 + .../minesweeper/.approaches/introduction.md | 191 ++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 exercises/practice/minesweeper/.approaches/config.json create mode 100644 exercises/practice/minesweeper/.approaches/introduction.md diff --git a/exercises/practice/minesweeper/.approaches/config.json b/exercises/practice/minesweeper/.approaches/config.json new file mode 100644 index 0000000000..21b3a12720 --- /dev/null +++ b/exercises/practice/minesweeper/.approaches/config.json @@ -0,0 +1,7 @@ +{ + "introduction": { + "authors": ["colinleach", + "BethanyG"], + "contributors": [] + } +} diff --git a/exercises/practice/minesweeper/.approaches/introduction.md b/exercises/practice/minesweeper/.approaches/introduction.md new file mode 100644 index 0000000000..74663bba6b --- /dev/null +++ b/exercises/practice/minesweeper/.approaches/introduction.md @@ -0,0 +1,191 @@ +# Introduction + +This exercise tests iteration, logic and error handling. + +## General considerations + +It is possible to break the exercise down into a series of sub-tasks, with plenty of scope to mix and match approaches within these. + +- Is the board valid? +- Is the current square a mine? +- what are the valid neighboring cells, and how many of them are mines? + +Core Python does not support N-dimensional arrays, though these are at the heart of many third-party packes such as NumPy. + +Thus, the input board and the final result are implemented as lists of strings, though intermediate processing is likely to use lists of lists plus a final `''.join()` in the `return` statement. + +Helpfully, Python can iterate over strings exactly like lists. + +There is some ambiguity about rows *vs.* columns in this representation, so the sample code may not be entirely consistent in naming. +It makes no difference to the results. + +## Valid boards + +The board must be rectangular: essentially, all columns must be the same length as the first column. + +Perhaps surprisingly, the row and column lengths can be zero, so an apparently non-existent board is valid. + +```python + rows = len(minefield) + if rows > 0: + cols = len(minefield[0]) + else: + return [] + if any([len(row) != cols for row in minefield]): + raise ValueError('The board is invalid with current input.') +``` + +Additionally, the only valid entries are a space `' '` or an asterisk `'*'`. All other characters should raise an error. + +Some solutions use regular expressions for this test, but something like `minefield[row][col] not in [' ', '*']` is probably simpler. + +Depending how the code is structured, it may be possible to combine these tests. + +More commonly, the board dimensions are checked at the beginning, then invalid characters are detected while iterating through the board. + +## Processing squares + +Squares containg a mine are easy: just copy `'*'` to the corresponding square in the result. + +For empty squares, the challenge is to count how many mines are in the adjacent squares. + +*How many squares are adjacent?* In the middle of a reasonably large board there will be 8, but this is reduced for squares at the edges or corners. + +### Nested `if..elif` statements + +This can be made to work, but quickly becomes very verbose. + +### Explicit coordinates + +```python + def count_adjacent(r, c): + adj_squares = ( + (r-1, c-1), (r-1, c), (r-1, c+1), + (r, c-1), (r, c+1), + (r+1, c-1), (r+1, c), (r+1, c+1), + ) + + # which are on the board? + neighbors = [minefield[r][c] for r, c in adj_squares + if 0 <= r < rows and 0 <= c < cols] + # how many contain mines? + return len([adj for adj in neighbors if adj == '*']) +``` + +Slightly better, this lists all the possibilities then filters out any that fall outside the board. + +Note that we only want a count of nearby mines, their precise location is irrelevant. + +### Use a comprehension or generator + +A key insight is that we can work on a 3x3 block of cells, because we already ensured that the central cell does *not* contain a mine that would affect our count. + +```python + squares = ((row + row_diff, col + col_diff) + for row_diff in (-1, 0, 1) + for col_diff in (-1, 0, 1)) +``` + +We can then filter and count as in the previous code. + +### Use complex numbers + +A particularly elegant solution is to treat the board as a portion of the complex plane. + +In Python, [complex numbers][complex-numbers] are a standard numeric type, alongside integers and floats. + +*This is less widely known than it deserves to be.* + +```python +def neighbors(cell: complex) -> Generator[complex, None, None]: + """Yield all eight neighboring cells.""" + for x in (-1, 0, 1): + for y in (-1, 0, 1): + if offset := x + y * 1j: + yield cell + offset +``` + +The constructor for a complex number is `complex(x, y)` or (as here) `x + y * 1j`, where `x` and `y` are the real and imaginary parts, respectively. + +There are two properties of complex numbers that help us in this case: + +- The real and imaginary parts act independently under addition. +- The value `complex(0, 0)` is the complex zero, which like integer zero is treated as False in Python conditionals. + +A tuple of integers would not work as a substitute, because `+` behaves as the concatenation operator for tuples: + +```python +>>> complex(1, 2) + complex(3, 4) +(4+6j) +>>> (1, 2) + (3, 4) +(1, 2, 3, 4) +``` + +Note also the use of the ["walrus" operator][walrus-operator] `:=` in the definition of `offset` above. + +This relatively recent addition to Python simplifies variable assignment within the limited scope of an if statement or a comprehension. + +***Bethany - any chance we could finish and merge PR #3585 so that I can reference it here?*** + +## Putting it all together + +This example is an object-oriented approach using complex numbers, included because it is a particularly clear illustration of the various topics discussed above: + +```python +"""Minesweeper.""" + +from typing import Generator + + +def neighbors(cell: complex) -> Generator[complex, None, None]: + """Yield all eight neighboring cells.""" + for x in (-1, 0, 1): + for y in (-1, 0, 1): + if offset := x + y * 1j: + yield cell + offset + + +class Minefield: + """Minefield helper.""" + + def __init__(self, data: list[str]): + """Initialize.""" + self.height = len(data) + self.width = len(data[0]) if data else 0 + + if not all(len(row) == self.width for row in data): + raise ValueError("The board is invalid with current input.") + + self.data = {} + for y, line in enumerate(data): + for x, val in enumerate(line): + self.data[x + y * 1j] = val + if not all(v in (" ", "*") for v in self.data.values()): + raise ValueError("The board is invalid with current input.") + + def val(self, x: int, y: int) -> str: + """Return the value for one square.""" + cur = x + y * 1j + if self.data[cur] == "*": + return "*" + count = sum(self.data.get(neighbor, "") == "*" for neighbor in neighbors(cur)) + return str(count) if count else " " + + def convert(self) -> list[str]: + """Convert the minefield.""" + return [ + "".join(self.val(x, y) for x in range(self.width)) + for y in range(self.height) + ] + + +def annotate(minefield: list[str]) -> list[str]: + """Annotate a minefield.""" + return Minefield(minefield).convert() +``` + +The import is only needed for type annotation, so can be considered optional. + +All validation is done in the object constructor. + +[complex-numbers]: https://exercism.org/tracks/python/concepts/complex-numbers From 43f9667073295ef3f554f05fe45d707591876123 Mon Sep 17 00:00:00 2001 From: colinleach Date: Sat, 30 Mar 2024 10:23:39 -0700 Subject: [PATCH 2/2] minor edits for clarity --- .../minesweeper/.approaches/introduction.md | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/exercises/practice/minesweeper/.approaches/introduction.md b/exercises/practice/minesweeper/.approaches/introduction.md index 74663bba6b..8b46d273a0 100644 --- a/exercises/practice/minesweeper/.approaches/introduction.md +++ b/exercises/practice/minesweeper/.approaches/introduction.md @@ -1,3 +1,4 @@ + # Introduction This exercise tests iteration, logic and error handling. @@ -10,20 +11,17 @@ It is possible to break the exercise down into a series of sub-tasks, with plent - Is the current square a mine? - what are the valid neighboring cells, and how many of them are mines? -Core Python does not support N-dimensional arrays, though these are at the heart of many third-party packes such as NumPy. +Core Python does not support matrices, or N-dimensional arrays more generally, though these are at the heart of many third-party packes such as NumPy. -Thus, the input board and the final result are implemented as lists of strings, though intermediate processing is likely to use lists of lists plus a final `''.join()` in the `return` statement. +Thus, the input board and the final result are implemented as lists of strings, though intermediate processing is likely to use lists of lists plus a final `''.join()` for each row in the `return` statement. Helpfully, Python can iterate over strings exactly like lists. -There is some ambiguity about rows *vs.* columns in this representation, so the sample code may not be entirely consistent in naming. -It makes no difference to the results. - ## Valid boards -The board must be rectangular: essentially, all columns must be the same length as the first column. +The board must be rectangular: essentially, all rows must be the same length as the first row. -Perhaps surprisingly, the row and column lengths can be zero, so an apparently non-existent board is valid. +Perhaps surprisingly, the row and column lengths can be zero, so an apparently non-existent board is valid and needs special handling. ```python rows = len(minefield) @@ -37,9 +35,14 @@ Perhaps surprisingly, the row and column lengths can be zero, so an apparently n Additionally, the only valid entries are a space `' '` or an asterisk `'*'`. All other characters should raise an error. -Some solutions use regular expressions for this test, but something like `minefield[row][col] not in [' ', '*']` is probably simpler. +Some solutions use regular expressions for this test, but there are simpler options: + +```python + if minefield[row][col] not in (' ', '*'): + # raise error +``` -Depending how the code is structured, it may be possible to combine these tests. +Depending how the code is structured, it may be possible to combine the tests. More commonly, the board dimensions are checked at the beginning, then invalid characters are detected while iterating through the board. @@ -51,11 +54,11 @@ For empty squares, the challenge is to count how many mines are in the adjacent *How many squares are adjacent?* In the middle of a reasonably large board there will be 8, but this is reduced for squares at the edges or corners. -### Nested `if..elif` statements +### 1. Nested `if..elif` statements This can be made to work, but quickly becomes very verbose. -### Explicit coordinates +### 2. Explicit coordinates ```python def count_adjacent(r, c): @@ -76,7 +79,7 @@ Slightly better, this lists all the possibilities then filters out any that fall Note that we only want a count of nearby mines, their precise location is irrelevant. -### Use a comprehension or generator +### 3. Use a comprehension or generator A key insight is that we can work on a 3x3 block of cells, because we already ensured that the central cell does *not* contain a mine that would affect our count. @@ -88,7 +91,7 @@ A key insight is that we can work on a 3x3 block of cells, because we already en We can then filter and count as in the previous code. -### Use complex numbers +### 4. Use complex numbers A particularly elegant solution is to treat the board as a portion of the complex plane. @@ -129,7 +132,7 @@ This relatively recent addition to Python simplifies variable assignment within ## Putting it all together -This example is an object-oriented approach using complex numbers, included because it is a particularly clear illustration of the various topics discussed above: +The example below is an object-oriented approach using complex numbers, included because it is a particularly clear illustration of the various topics discussed above: ```python """Minesweeper.""" @@ -189,3 +192,4 @@ The import is only needed for type annotation, so can be considered optional. All validation is done in the object constructor. [complex-numbers]: https://exercism.org/tracks/python/concepts/complex-numbers +[walrus-operator]: