Flower Field in Crystal: Complete Solution & Deep Dive Guide

A dense cluster of small pink buds against black.

Mastering 2D Arrays in Crystal: The Complete Guide to the Flower Field Algorithm

Solving the Flower Field problem in Crystal involves iterating through a 2D array representing the garden. For each empty square, you must check its eight adjacent neighbors (horizontally, vertically, and diagonally), count the number of flowers ('*'), and replace the empty square with this count if it's greater than zero.

Have you ever stared at a grid-based programming problem, like a chessboard or a game map, and felt a wave of uncertainty? That moment when you realize the solution for one cell depends entirely on its surroundings is a common hurdle for many developers. Manipulating 2D arrays, especially when you have to manage boundaries and check adjacent cells, can feel like navigating a minefield of off-by-one errors and complex nested loops.

This feeling is completely normal. The logic for handling corners, edges, and central cells differently can be tricky to implement cleanly. But what if you could turn this challenge into a strength? This guide is designed to do exactly that. We will deconstruct the "Flower Field" problem from the exclusive kodikra.com curriculum, providing a clear, step-by-step path to an elegant solution in Crystal. You'll not only solve the problem but also master the fundamental techniques of 2D array traversal and neighbor analysis—skills that are directly applicable to game development, image processing, and countless other algorithmic challenges.


What is the Flower Field Problem?

At its core, the Flower Field problem is an exercise in data transformation on a two-dimensional grid. You are given a rectangular board, represented as an array of strings or a 2D array of characters. This board represents a garden where some squares contain a flower ('*') and others are empty (' ').

Your task is to process this board and produce a new version where every empty square has been updated. For each empty square, you must count how many flowers are in its eight adjacent squares—horizontally, vertically, and diagonally. If the count is zero, the square remains empty. If the count is greater than zero, the empty square is replaced by a character representing that number (e.g., '3').

An Example Transformation

To make it concrete, consider the following input board:


+-----+
| * * |
|  *  |
|  *  |
|* * *|
+-----+

The goal is to analyze each empty space. For instance, the empty space in the second row, first column is adjacent to three flowers. The empty space in the third row, second column is adjacent to five flowers. After processing all empty squares, the final output board should look like this:


+-----+
|*3*2 |
|35*3 |
|35*4 |
|*3*3*|
+-----+

This problem forces you to think systematically about iteration, boundary conditions, and state management. You cannot simply modify the board as you go; you need a clear strategy to ensure the original state is used for all calculations.


Why This Algorithm is a Cornerstone of Computational Thinking

The Flower Field problem might seem simple, but the underlying pattern is incredibly powerful and appears in many complex domains. Mastering this logic is like learning a fundamental musical scale—it's a building block for more intricate compositions.

  • Game Development: The classic game Minesweeper operates on this exact principle. Clicking a square reveals a number that represents the count of adjacent mines. The logic is identical.
  • Image Processing: In computer vision, operations like blurring, sharpening, and edge detection are performed using "convolution kernels." A kernel is a small matrix that slides over an image, and the value of each pixel is recalculated based on the values of its neighbors, weighted by the kernel.
  • Cellular Automata: Famous simulations like Conway's Game of Life use neighbor-counting rules to determine whether a cell will live, die, or be born in the next generation. This forms the basis for modeling complex systems in biology, physics, and social sciences.
  • Pathfinding Algorithms: In algorithms like A* or Dijkstra's, a key step is to explore the "neighbors" of a current node in a grid to find the shortest path. Understanding how to access adjacent cells efficiently is crucial.

By solving this problem, you are not just completing a module from the kodikra Crystal 4 learning path; you are internalizing a computational pattern that is universally applicable.


How to Architect the Solution in Crystal

A robust solution requires a clear, step-by-step plan. Rushing into code without a strategy is a recipe for bugs, especially with grid problems. Let's outline the most reliable approach: creating a new result board.

Why not modify the original board in place? Imagine you calculate a '3' for an empty cell. In the next iteration, when you analyze a neighbor of that cell, your logic might mistakenly see the '3' as something other than an empty space, corrupting the flower count. Reading from an immutable original and writing to a new result board completely avoids this class of errors.

The Algorithmic Blueprint

Here is a high-level flowchart of our logic. We will iterate through every single cell of the input board, decide what the corresponding cell in our new output board should be, and build the result step-by-step.

 ● Start with Input Board

     │
     ▼
   ┌───────────────────────────┐
   │ Create an empty Result Board │
   │ (same dimensions as input)  │
   └────────────┬──────────────┘
                │
                ▼
  ┌───────────────────────────┐
  │ Loop through each Row (y) │
  └────────────┬──────────────┘
               │
               ▼
        ┌──────────────────────────┐
        │ Loop through each Col (x)  │
        └───────────┬──────────────┘
                    │
                    ▼
           ◆ Is input[y][x] a Flower ('*')?
          ╱                            ╲
        Yes                           No
         │                             │
         ▼                             ▼
┌──────────────────┐      ┌───────────────────────────────┐
│ Copy '*' to      │      │ Count adjacent flowers around │
│ result[y][x]     │      │ (x, y) in the *Input Board*   │
└──────────────────┘      └───────────────┬───────────────┘
                                          │
                                          ▼
                                     ◆ Count > 0?
                                    ╱            ╲
                                  Yes             No
                                   │               │
                                   ▼               ▼
                          ┌────────────────┐  ┌───────────────┐
                          │ Convert count  │  │ Copy ' ' to   │
                          │ to Char & add  │  │ result[y][x]  │
                          │ to result[y][x]│  └───────────────┘
                          └────────────────┘
                                   │
         └─────────────┬───────────┘
                       │
                       ▼
    ┌───────────────────────────────────┐
    │ After loops, join rows of Result  │
    │ Board into an Array of Strings    │
    └─────────────────┬─────────────────┘
                      │
                      ▼
                  ● Return Result

This strategic approach ensures that every calculation is based on the pristine, original state of the garden, guaranteeing correctness.


Where the Logic Meets Code: A Crystal Implementation

Now, let's translate our blueprint into clean, idiomatic Crystal code. We will define a `FlowerField` class with a single public method, `transform`, which takes the board as input.

This implementation emphasizes clarity and correctness. It uses a helper method, count_adjacent_flowers, to encapsulate the neighbor-checking logic, keeping the main loop clean and readable.

The Complete Crystal Solution


# Solution from the kodikra.com Crystal learning path
class FlowerField
  # Defines the 8 relative positions of a cell's neighbors
  # [dy, dx] format: [row_offset, column_offset]
  NEIGHBOR_DELTAS = [
    [-1, -1], [-1, 0], [-1, 1], # Top-left, Top, Top-right
    [ 0, -1],          [ 0, 1], # Left, Right
    [ 1, -1], [ 1, 0], [ 1, 1]  # Bottom-left, Bottom, Bottom-right
  ]

  # Main public method to transform the board.
  # Input: an array of strings representing the board.
  # Output: an array of strings for the transformed board.
  def self.transform(input_board : Array(String)) : Array(String)
    # Return early for empty boards to avoid errors
    return [] of String if input_board.empty? || input_board[0].empty?

    # Get dimensions for boundary checks
    height = input_board.size
    width = input_board[0].size

    # Create a new board (as a 2D array of Chars) to store the results.
    # This avoids mutating the input while iterating over it.
    result_board = Array.new(height) { Array.new(width, ' ') }

    # Iterate over each cell of the input board using its coordinates (y, x)
    height.times do |y|
      width.times do |x|
        cell = input_board[y][x]

        if cell == '*'
          # If the cell is a flower, copy it directly to the result
          result_board[y][x] = '*'
        else
          # If the cell is empty, count its adjacent flowers
          count = count_adjacent_flowers(input_board, y, x, height, width)

          if count > 0
            # If there are adjacent flowers, convert the count to a Char
            # and place it in the result board.
            result_board[y][x] = count.to_s[0]
          else
            # Otherwise, the cell remains an empty space
            result_board[y][x] = ' '
          end
        end
      end
    end

    # Convert the 2D array of Chars back into an array of Strings for the final output
    result_board.map(&.join)
  end

  # Helper method to count flowers in the 8 adjacent cells.
  private def self.count_adjacent_flowers(board, y, x, height, width)
    count = 0

    # Iterate through each of the 8 neighbor coordinate deltas
    NEIGHBOR_DELTAS.each do |dy, dx|
      # Calculate the absolute coordinates of the neighbor
      ny, nx = y + dy, x + dx

      # Boundary Check: Ensure the neighbor is within the board's limits
      if ny >= 0 && ny < height && nx >= 0 && nx < width
        # If the neighbor is a flower, increment the count
        count += 1 if board[ny][nx] == '*'
      end
    end

    count
  end
end

Detailed Code Walkthrough

  1. Class and Constant: We define a class FlowerField to encapsulate the logic. The NEIGHBOR_DELTAS constant is a crucial piece of this solution. It elegantly stores the eight relative coordinate pairs for any given cell's neighbors. This makes the counting logic clean and avoids a messy series of `if` statements.
  2. transform Method Signature: The method is defined as a class method (self.transform) so we can call it directly via FlowerField.transform(...) without needing to instantiate the class. It has explicit type annotations (: Array(String)), which is a best practice in Crystal for clarity and compile-time safety.
  3. Edge Case Handling: The line return [] of String if input_board.empty? || input_board[0].empty? is a defensive guard clause. It prevents potential IndexOutOfBounds errors if an empty or malformed board is passed in.
  4. Board Dimensions: We cache the height and width of the board. This is more efficient than repeatedly calling .size inside the loops and makes the boundary-checking logic more readable.
  5. Result Board Initialization: result_board = Array.new(height) { Array.new(width, ' ') } creates a new 2D array of the same dimensions, pre-filled with empty spaces. This is our "canvas" where we will build the final output.
  6. Nested Iteration: height.times do |y| and width.times do |x| is a standard and efficient way to iterate over every coordinate pair in a 2D grid in Crystal.
  7. Cell Logic:
    • If cell == '*', we simply copy the flower to the same position in our result_board.
    • If the cell is empty, we call our helper method count_adjacent_flowers. This delegation keeps the main method focused on iteration and delegation, improving readability.
    • Based on the returned count, we update the result_board. The expression count.to_s[0] is a concise Crystal idiom to convert a single-digit integer into its character representation.
  8. count_adjacent_flowers Helper: This private method is the heart of the neighbor analysis.
    • It iterates over our NEIGHBOR_DELTAS constant.
    • For each delta [dy, dx], it calculates the neighbor's coordinates ny, nx.
    • The if ny >= 0 && ... block is the critical boundary check. It ensures we don't try to access an index outside the array, which would cause a runtime error. This single check correctly handles corners, edges, and center cells without any special logic.
    • If a valid neighbor is a flower, the count is incremented.
  9. Final Transformation: The last line, result_board.map(&.join), is a beautiful piece of functional Crystal. It iterates over each row (which is an Array(Char)) in our result board and calls the join method on it, converting it into a String. The result is a new Array(String), matching the required output format.

When to Consider Alternative Approaches

While the "create a new board" approach is the most robust and recommended, it's valuable to understand other strategies and their trade-offs. This knowledge deepens your problem-solving toolkit.

The In-Place Modification Strategy (and its Dangers)

One might be tempted to save memory by modifying the input board directly. This is a common but perilous path for this specific problem. Let's analyze why.

    ● Cell (y, x) is empty

        │
        ▼
   ┌─────────────────────────┐
   │ Count its neighbors     │
   │ in the *current* board  │
   └───────────┬─────────────┘
               │
               ▼
     ┌──────────────────┐
     │ Found 3 flowers. │
     │ Update board[y][x] = '3'
     └──────────┬────────┘
                │
                ▼
      ... Later in the loop ...
                │
                ▼
    ● Cell (y, x+1) is empty

        │
        ▼
   ┌───────────────────────────┐
   │ Count its neighbors,      │
   │ including (y, x)          │
   └───────────┬───────────────┘
               │
               ▼
      ◆ Is board[y][x] a flower?
     ╱                          ╲
   No, it's '3'                ...
    │
    ▼
  ┌─────────────────────────────────┐
  │ Logic fails. The count for      │
  │ (y, x+1) is now incorrect because │
  │ it didn't see the original state.│
  └─────────────────────────────────┘

This illustrates the core issue: you create a race condition where the result of one calculation incorrectly influences the next. It's only safe if you can guarantee that the new values you write (`'1'`, `'2'`, etc.) will never be misinterpreted by your counting logic. A two-pass approach could work (first pass calculates counts and stores them in a separate structure, second pass updates the board), but that essentially mimics the "new board" strategy with more complexity.

Pros and Cons: New Board vs. In-Place

Aspect New Result Board (Recommended) In-Place Modification
Correctness Guaranteed. Calculations are always based on the immutable original state. High Risk. Prone to data corruption and incorrect counts unless handled with extreme care (e.g., two-pass).
Readability High. The logic is straightforward: read from A, write to B. Easy to follow. Low. The code becomes more complex as you have to manage state changes within the same data structure.
Memory Usage O(R*C). Requires memory for a second board of the same size as the input. O(1) extra space. This is its only significant advantage, but it's rarely worth the trade-off.
Debugging Easier. You can inspect both the original and the result board at any point. Harder. The state is constantly changing, making it difficult to trace where a calculation went wrong.

For almost all scenarios, especially in a language like Crystal where performance is high and memory is not usually the primary constraint for a problem of this scale, the clarity and safety of the "new result board" method make it the superior choice.


Frequently Asked Questions (FAQ)

Q1: Why is it better to create a new board instead of modifying the original one in place?

Modifying the board in place creates data integrity issues. When you calculate the flower count for a cell, you rely on the original state of its neighbors. If you've already replaced a neighboring empty cell with a number (e.g., '2'), your counting logic for the current cell will be incorrect because it might not recognize '2' as a non-flower, leading to inaccurate results. Using a separate result board ensures all calculations are based on the pristine input data.

Q2: How does the provided solution handle the corners and edges of the board?

The solution handles all boundary cases (corners and edges) elegantly with a single boundary check. The NEIGHBOR_DELTAS constant provides 8 potential neighbor positions. Before accessing the board at a neighbor's calculated coordinate (ny, nx), the code checks if ny is between 0 and height-1 and if nx is between 0 and width-1. If the coordinate is outside these bounds, it's simply ignored. This prevents IndexOutOfBounds errors and works universally for any cell, regardless of its position.

Q3: What is the time and space complexity of this algorithm?

Let R be the number of rows and C be the number of columns on the board.

  • Time Complexity: O(R * C). We must visit every cell on the board once. For each cell, we perform a constant number of operations (checking 8 neighbors). Therefore, the total time is directly proportional to the number of cells.
  • Space Complexity: O(R * C). The dominant factor in memory usage is the result_board, which has the same dimensions as the input board.
Q4: Can this neighbor-counting logic be applied to other problems?

Absolutely. This is a fundamental pattern in computer science. It's the core logic behind the game Minesweeper, cellular automata like Conway's Game of Life, and many image processing filters (like blur or edge detection) that calculate a pixel's new value based on its neighbors.

Q5: Why does the solution use `Array(Array(Char))` internally before converting back to `Array(String)`?

While the input and output are arrays of strings, working with an Array(Array(Char)) is often more convenient and efficient for grid manipulation. Accessing or modifying a single character at board[y][x] is a direct operation. If we worked with strings, modifying a character would require creating a new string for that entire row, which is less efficient. We convert back to the required Array(String) format only at the very end.

Q6: What would happen if the input board contained invalid characters?

The current solution is designed for the specified problem constraints (only '*' and ' '). If the board contained other characters, they would be treated like empty spaces (' ') because the logic only explicitly checks for flowers (if cell == '*'). A more robust, production-grade solution might include error handling to raise an exception if unexpected characters are found.

Q7: Is there a more "functional" way to write this in Crystal?

Yes, you could structure it more functionally, though it can sometimes reduce clarity for 2D grid problems. A functional approach might involve mapping over the rows and their indices, and then within that, mapping over the characters and their indices to build the new row. The core neighbor-counting logic would remain similar. For this specific problem, the explicit nested loops (.times do) are highly readable and idiomatic in Crystal, striking a good balance between imperative style and clarity.


Conclusion: From Grid Problem to General Pattern

We have successfully navigated the Flower Field problem, transforming a seemingly complex grid manipulation task into a clean, understandable, and robust Crystal solution. By prioritizing a sound strategy—reading from an immutable source and writing to a new destination—we eliminated an entire class of potential bugs and produced code that is both correct and easy to reason about.

The key takeaways from this exercise extend far beyond this single module. You've practiced essential skills in 2D array iteration, implemented foolproof boundary checks, and seen the power of helper methods and constants to create clean, self-documenting code. This neighbor-analysis pattern is a fundamental tool you will use repeatedly in more advanced algorithms and applications.

As you continue your journey through the kodikra Crystal learning path, remember the principles applied here. A solid plan, a focus on data immutability, and clear, modular code will empower you to solve even the most daunting challenges. Ready to keep growing? Explore the complete Crystal curriculum on kodikra.com to further sharpen your skills.

Disclaimer: This solution was developed and verified using Crystal version 1.12.x. The core algorithmic logic is stable, but minor syntax details may evolve in future releases of the language.


Published by Kodikra — Your trusted Crystal learning resource.