Skip to content

Commit

Permalink
Add minesweeper approaches (#689)
Browse files Browse the repository at this point in the history
* Draft of approaches

* Fixes

* Make uuid unique

* Apply suggestions from code review

* Update exercises/practice/minesweeper/.approaches/introduction.md

---------

Co-authored-by: Ryan Hartlage <[email protected]>
  • Loading branch information
meatball133 and ryanplusplus authored Aug 17, 2024
1 parent 514545a commit e9a5a55
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 0 deletions.
24 changes: 24 additions & 0 deletions exercises/practice/minesweeper/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"introduction": {
"authors": ["meatball133"],
"contributors": []
},
"approaches": [
{
"uuid": "7d266679-f573-42a5-80fa-66a9d15f2364",
"slug": "data-driven",
"title": "Data Driven",
"blurb": "Using a data driven approach to setup the search areas",
"authors": [
"meatball133"
]
},
{
"uuid": "5ac94a13-3e20-4b2c-a7ad-4d1426192e06",
"slug": "translate",
"title": "Translate",
"blurb": "Uses the `tr` method to translate certain parts of a `String`",
"authors": ["meatball133", "ryanplusplus"]
}
]
}
51 changes: 51 additions & 0 deletions exercises/practice/minesweeper/.approaches/data-driven/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Data Driven

```crystal
class Minesweeper
  OFFSETS = [
{-1, -1}, {0, -1}, {1, -1},
{-1,  0},          {1,  0},
{-1,  1}, {0,  1}, {1,  1},
]
  def initialize(@board : Array(String) )
  end
  def annotate()
    nrows = @board.size()
   
    @board.map_with_index do |row, y|
      ncols = row.size()
      row.chars.map_with_index do |cell, x|
        if cell == '*'
          next '*'
        end
        sum = OFFSETS.count do |(dx, dy)|
          new_x = x + dx
          new_y = y + dy
new_x >= 0 && new_x < ncols && new_y >= 0 && new_y < nrows && @board[new_y][new_x] == '*'
        end
        sum == 0 ? ' ' : sum.to_s
      end.join
    end
  end
end
```

Using a data-driven approach to create logic to solve Minesweeper makes the solution concise and flexible.
If the rules change, it is as easy as to change them.
The solution starts by creating an `Array` with the rules which are tuples with the x and y offset.
Defining an `initialize` method takes an `Array(String)` as an argument and assigns it to an instance variable `@board`.

The `annotate` method starts by defining the number of rows on the board.
Then it maps over the `@board` array with the index `y` for each row.
Then, it converts each row into an array of `Char` and maps over it with the index `x` for each cell.

If a cell is a mine, it will be skipped and the mine will be returned.
Otherwise, it will count the number of mines around the cell.
It will do this by having the rules defined beforehand and then calculating the new x and y positions for each rule.
It will then check if the new x and y positions are within the board's bounds and if the cell is mine.
If it is, the count will be increased.

Finally, it will check if the sum is 0. If it is, it will return a space; otherwise, it will return the sum as a string.
Then, it will join the row and return to the board.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
OFFSETS = [
{-1, -1}, {0, -1}, {1, -1},
{-1, 0}, {1, 0},
{-1, 1}, {0, 1}, {1, 1},
]
81 changes: 81 additions & 0 deletions exercises/practice/minesweeper/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Introduction

There are various ways to solve **minesweeper**.

## Approach: Using translation

Using the `tr` method to translate the result of the `annotated_space` method.
This solution is very concise since it doesn't need any conditionals.
It does this by using looping and the `tr` method.

```crystal
class Minesweeper
  def initialize(@board : Array(String))
  end
  def annotate
    @board.map_with_index do |row, y|
      row.chars.map_with_index do |cell, x|
        cell.to_s.tr(" ", annotated_space(x, y))
      end.join
    end
  end
  private def annotated_space(x, y)
([y - 1, 0].max..[y + 1, @board.size - 1].min).map do |y|
([x - 1, 0].max..[x + 1, @board[0].size - 1].min).count do |x|
        @board[y][x] == '*'
      end
    end.sum.to_s.tr("0", " ")
  end
end
```

For more information, see the [translate approach][approach-translate], or if you want to hear Erik Schierboom's thoughts on this approach, see the [video][video-translate].

## Approach: Using a data-driven approach

Using rules to create logic to check for mines.
This solution is concise and flexible. If the rules change, it is as easy as changing the rules.

```crystal
class Minesweeper
  OFFSETS = [
{-1, -1}, {0, -1}, {1, -1},
{-1,  0},          {1,  0},
{-1,  1}, {0,  1}, {1,  1},
]
  def initialize(@board : Array(String) )
  end
  def annotate()
    nrows = @board.size()
   
    @board.map_with_index do |row, y|
      ncols = row.size()
      row.chars.map_with_index do |cell, x|
        if cell == '*'
          next '*'
        end
        sum = 0
        OFFSETS.each do |(dx, dy)|
          new_x = x + dx
          new_y = y + dy
          if new_x < 0 || new_x > ncols - 1 || new_y < 0 || new_y > nrows - 1 || @board[new_y][new_x] == ' '
            next
          end
          sum += 1
        end
        sum == 0 ? ' ' : sum.to_s
      end.join
    end
  end
end
```

For more information, check the [data-driven approach][approach-data-driven].

[approach-data-driven]: https://exercism.org/tracks/crystal/exercises/minesweeper/approaches/data-driven
[approach-translate]: https://exercism.org/tracks/crystal/exercises/minesweeper/approaches/translate
[video-translate]: https://youtu.be/dLT2h2hODhs?t=951
52 changes: 52 additions & 0 deletions exercises/practice/minesweeper/.approaches/translate/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Iteration and `tr` method

```crystal
class Minesweeper
  def initialize(@board : Array(String))
  end
  def annotate
    @board.map_with_index do |row, y|
      row.chars.map_with_index do |cell, x|
        cell.to_s.tr(" ", annotated_space(x, y))
      end.join
    end
  end
  private def annotated_space(x, y)
([y - 1, 0].max..[y + 1, @board.size - 1].min).sum do |y|
([x - 1, 0].max..[x + 1, @board[0].size - 1].min).count do |x|
        @board[y][x] == '*'
      end
    end.to_s.tr("0", " ")
  end
end
```

What makes this approach interesting is the use of the [`tr` method][tr-method], which allows us to have a concise solution that doesn't use conditionals.
This approach starts with defining a `initialize` method that takes an `Array(String)` as an argument and assigns it to an instance variable `@board`.
The `annotate` method starts with mapping over the `@board` array with the index `y` for each row.
Then, it converts each row into an array of characters and maps over it with the index `x` for each cell.
It converts each cell from a `Char` to a `String` and then uses the method `tr`.

The `tr` method allows you to replace a set of characters with another set of characters in a string.
This is useful since it allows us to replace only the space character with the result of the `annotated_space` method.

The `annotated_space` method takes the `x` and `y` coordinates of the cell and calculates the number of mines around it.
It then creates a range representing the area's height to check for mines.
Since the index is 0, taking minus 1 would make the value -1, which would be valid in Crystal but would represent the last element.
We don't want this behavior, so we use the [`max`][max-method] method to ensure that the value is at least 0.
The same is done for the [`min`][min-method] value, but here we want to ensure that the value is, at most, the last index.

Then, we use the [`sum`][sum-method] method to sum the total number of mines returned by the inner loop.
The inner loop creates a range representing the area's width to check for mines.
As before, we use the `max` and `min` methods to ensure that the values are within the board's bounds.
Then, we use the [`count`][count-method] method to count how many times the inner condition is true, which indicates whether the cell is a mine or not.

Finally, we convert the sum to a string and use the `tr` method to replace all the zeros with spaces.

[tr-method]: https://crystal-lang.org/api/String.html#tr%28from%3AString%2Cto%3AString%29%3AString-instance-method
[sum-method]: https://crystal-lang.org/api/Enumerable.html#sum-instance-method
[count-method]: https://crystal-lang.org/api/Enumerable.html#count%28%26%3AT-%3E%29%3AInt32-instance-method
[max-method]: https://crystal-lang.org/api/Enumerable.html#max%3AT-instance-method
[min-method]: https://crystal-lang.org/api/Enumerable.html#min%3AT-instance-method
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
def annotate
@board.map_with_index do |row, y|
row.chars.map_with_index do |cell, x|
cell.to_s.tr(" ", annotated_space(x, y))
end.join
end
end

0 comments on commit e9a5a55

Please sign in to comment.