Minesweeper in Abap: Complete Solution & Deep Dive Guide
Mastering Abap Minesweeper: A Complete Walkthrough for Grid Transformation
This comprehensive guide provides a complete solution to the Minesweeper annotation problem using modern Abap. You will learn to process a 2D grid represented by an internal table, iterate through its cells, count adjacent mines for empty squares, and construct the final annotated board, mastering essential algorithmic thinking.
Have you ever stared at a complex data grid in an SAP report and felt a wave of uncertainty? The challenge of transforming raw, grid-based data into meaningful, calculated information is a daily reality for Abap developers. It’s a task that requires precision, careful handling of boundaries, and a solid algorithmic foundation. Many developers find themselves tangled in nested loops and off-by-one errors, turning a seemingly simple task into a frustrating ordeal.
This is where classic programming challenges, like the Minesweeper annotation problem from the kodikra learning path, become invaluable. It’s not just about building a game; it’s a perfect, self-contained exercise for honing the exact skills you need for real-world Abap development. In this guide, we will dissect this problem from the ground up. We’ll build a robust, elegant, and efficient solution in Abap, transforming you from a hesitant grid processor into a confident data manipulator.
What is the Minesweeper Annotation Problem?
At its core, the Minesweeper annotation problem is a data transformation task. You are given an input, which is a rectangular board or grid representing a Minesweeper game after all the mines have been placed. This board contains only two types of squares:
- A mine, represented by an asterisk (
*). - An empty square, represented by a space (
' ').
Your objective is to create an output board of the same dimensions. However, in this new board, every empty square must be updated to show the total number of mines that are adjacent to it. "Adjacent" includes all eight surrounding squares: horizontally, vertically, and diagonally. If an empty square has zero adjacent mines, it should remain a space.
An Illustrative Example
To make this concrete, consider the following 4x5 input board:
+-----+
| * * |
| * |
| * |
| |
+-----+
The goal is to process this board and produce the following annotated output:
+-----+
|1*3*1|
|13*31|
| 2*2 |
| 111 |
+-----+
As you can see, the mines (*) remain untouched. The empty spaces have been replaced by digits representing the count of their neighboring mines. This seemingly simple transformation requires a systematic approach to avoid errors, especially at the edges and corners of the board.
Why This Challenge is a Perfect Fit for Abap Developers
While it might seem like a simple game, this problem from the kodikra Abap curriculum is a powerful exercise that directly maps to common tasks in SAP development. It’s a microcosm of the challenges you face when working with complex data structures, especially in reporting and data migration.
The primary data structure in Abap is the internal table. This problem forces you to think about how to represent a 2D grid using a table of strings (or a table of structures) and how to efficiently iterate over it. The logic for checking adjacent cells mirrors the logic you might use to calculate subtotals or perform cross-row validations in an ALV grid report.
Furthermore, the most critical part of the solution—boundary checking—is a skill that separates junior from senior developers. Ensuring your code doesn't try to access a row or column that doesn't exist (e.g., row -1) is fundamental to writing stable, bug-free programs. Mastering this in a controlled environment like the Minesweeper problem builds the muscle memory needed to prevent short dumps (CX_SY_ITAB_LINE_NOT_FOUND) in production code.
How to Design the Abap Solution: The Core Algorithm
Before writing a single line of code, a solid plan is essential. Our strategy will be to iterate through every single cell of the input board, decide what its corresponding cell in the output board should be, and build the output board step-by-step.
Data Representation in Abap
The most straightforward way to represent the board is using an internal table where each row of the table is a string representing a row of the board. This is both memory-efficient and easy to work with in Abap.
TYPES: ty_board TYPE TABLE OF string WITH EMPTY KEY.
This defines a table type ty_board that we can use for both our input and output boards.
The Algorithmic Flow
Our algorithm can be visualized as a systematic scan of the grid. Here is the high-level logic:
● Start with Input Board
│
▼
┌───────────────────────────┐
│ Initialize Empty Output Board │
│ with same dimensions │
└────────────┬──────────────┘
│
▼
┌─── Loop through each Row (r) ───┐
│ │
▼ ▼
┌─── Loop through each Column (c) ───┐
│ │
▼ ▼
◆ Is cell (r, c) a mine ('*')?
╱ ╲
Yes No
│ │
▼ ▼
┌──────────────┐ ┌───────────────────────────┐
│ Copy '*' to │ │ Count adjacent mines for │
│ Output Board │ │ cell (r, c) │
└──────────────┘ └────────────┬──────────────┘
│
▼
◆ Count > 0?
╱ ╲
Yes No
│ │
▼ ▼
┌───────────┐ ┌───────────┐
│ Convert │ │ Place ' ' │
│ count to │ │ in Output │
│ string │ │ Board │
└───────────┘ └───────────┘
│ │
└──────┬───────┘
│
▼
┌─────────────┐
│ Update Output │
│ Board at │
│ (r, c) │
└─────────────┘
│ │
└─────── End Column Loop ───────────┘
│ │
└──────── End Row Loop ────────────┘
│
▼
● Return Final Output Board
This flow ensures that every cell is evaluated independently. The most complex part is the "Count adjacent mines" step, which requires its own sub-logic for checking neighbors while respecting the board's boundaries.
Where to Implement the Logic: The Complete Abap Code
The best practice for encapsulating logic in modern Abap is to use a class. We will create a class zcl_minesweeper with a public method annotate that performs the transformation.
The `zcl_minesweeper` Class Definition
CLASS zcl_minesweeper DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .
PUBLIC SECTION.
TYPES:
ty_board TYPE TABLE OF string WITH EMPTY KEY.
METHODS annotate
IMPORTING
i_board TYPE ty_board
RETURNING
VALUE(r_board) TYPE ty_board.
PRIVATE SECTION.
METHODS count_adjacent_mines
IMPORTING
i_board TYPE ty_board
i_row_idx TYPE i
i_col_idx TYPE i
RETURNING
VALUE(r_count) TYPE i.
ENDCLASS.
The `zcl_minesweeper` Class Implementation
CLASS zcl_minesweeper IMPLEMENTATION.
METHOD annotate.
" If the input board is empty, return an empty board immediately.
IF i_board IS INITIAL.
RETURN.
ENDIF.
" Determine the dimensions of the board.
DATA(lv_rows) = lines( i_board ).
DATA(lv_cols) = strlen( i_board[ 1 ] ).
" Create a copy of the board to serve as our output board.
r_board = i_board.
" Loop through every row of the board.
DO lv_rows TIMES.
DATA(lv_current_row_idx) = sy-index.
DATA(lv_current_row_str) = r_board[ lv_current_row_idx ].
" Loop through every column in the current row.
DO lv_cols TIMES.
DATA(lv_current_col_idx) = sy-index.
DATA(lv_offset) = lv_current_col_idx - 1.
" Check the character at the current position (r, c).
IF lv_current_row_str+lv_offset(1) = '*'.
" If it's a mine, we do nothing and continue to the next cell.
CONTINUE.
ELSE.
" If it's an empty square, we need to count its adjacent mines.
DATA(lv_mine_count) = count_adjacent_mines(
i_board = i_board
i_row_idx = lv_current_row_idx
i_col_idx = lv_current_col_idx
).
" If the count is greater than zero, update the output board.
IF lv_mine_count > 0.
lv_current_row_str+lv_offset(1) = |{ lv_mine_count }|.
ENDIF.
ENDIF.
ENDDO.
" Update the row in our result board.
r_board[ lv_current_row_idx ] = lv_current_row_str.
ENDDO.
ENDMETHOD.
METHOD count_adjacent_mines.
" This private helper method counts mines around a specific cell.
DATA(lv_rows) = lines( i_board ).
DATA(lv_cols) = strlen( i_board[ 1 ] ).
r_count = 0.
" Iterate through the 3x3 grid centered on the given cell (i_row_idx, i_col_idx).
" The offsets will range from -1 to +1 for both rows and columns.
DO 3 TIMES.
DATA(lv_row_offset) = sy-index - 2. " Results in -1, 0, 1
DATA(lv_check_row) = i_row_idx + lv_row_offset.
DO 3 TIMES.
DATA(lv_col_offset) = sy-index - 2. " Results in -1, 0, 1
DATA(lv_check_col) = i_col_idx + lv_col_offset.
" Skip the center cell itself (offset 0,0).
IF lv_row_offset = 0 AND lv_col_offset = 0.
CONTINUE.
ENDIF.
" *** CRITICAL BOUNDARY CHECKS ***
" Check if the neighbor cell is within the board's bounds.
IF lv_check_row BETWEEN 1 AND lv_rows AND
lv_check_col BETWEEN 1 AND lv_cols.
" If it is, get the character at that position.
DATA(lv_neighbor_char) = i_board[ lv_check_row ]+lv_check_col-1(1).
" If the neighbor is a mine, increment our counter.
IF lv_neighbor_char = '*'.
r_count = r_count + 1.
ENDIF.
ENDIF.
ENDDO.
ENDDO.
ENDMETHOD.
ENDCLASS.
A Step-by-Step Code Walkthrough
Understanding the code is as important as having it. Let's break down the logic of our zcl_minesweeper class piece by piece.
The `annotate` Method: The Orchestra Conductor
This is the main public method that orchestrates the entire process. Its job is to manage the high-level loops and delegate the complex counting logic to a helper method.
- Input Validation: The first line,
IF i_board IS INITIAL., is a simple but crucial guard clause. It handles the edge case of an empty input and prevents potential errors, returning immediately. - Dimension Discovery: We use
lines( i_board )to get the number of rows andstrlen( i_board[ 1 ] )to get the number of columns from the first row. We assume the board is a valid rectangle. - Output Board Initialization:
r_board = i_board.is a key decision. We create a copy of the input board. This is safer than modifying the board "in-place" because our counting logic needs a clean, original reference of where the mines are. - The Main Loops: The nested
DO ... TIMESloops are the heart of the traversal. The outer loop iterates through row indices (from 1 tolv_rows), and the inner loop iterates through column indices (from 1 tolv_cols).sy-indexprovides the current loop counter. - Cell Evaluation: Inside the loops, we check the character at the current coordinate. If it's a mine (
*), we simplyCONTINUE, leaving it as is in our output board. - Delegation: If the cell is empty, we call our private helper method:
count_adjacent_mines(...). This is a good design principle; the main method doesn't need to know the messy details of neighbor checking. - Updating the Board: Based on the returned
lv_mine_count, we update the character in our local row variable,lv_current_row_str. If the count is greater than 0, we convert it to a character and place it. The syntaxlv_current_row_str+lv_offset(1) = ...is Abap's way of modifying a single character within a string (string slicing). - Finalizing the Row: After the inner loop finishes,
r_board[ lv_current_row_idx ] = lv_current_row_str;commits the modified string back into our result table.
The `count_adjacent_mines` Method: The Specialist
This private method has one specific, critical job: given a coordinate, count the mines surrounding it. This is where the real algorithmic detail lies.
The logic for checking the 8 neighbors of a cell (r, c) can be visualized as follows:
(r-1, c-1) (r-1, c) (r-1, c+1)
╲ │ ╱
╲ │ ╱
(r, c-1) ─── (r, c) ─── (r, c+1)
╱ │ ╲
╱ │ ╲
(r+1, c-1) (r+1, c) (r+1, c+1)
● Target Cell (r, c)
│
├─ Check Offset (-1, -1) → (r-1, c-1)
├─ Check Offset (-1, 0) → (r-1, c)
├─ Check Offset (-1, +1) → (r-1, c+1)
├─ Check Offset ( 0, -1) → (r, c-1)
├─ SKIP Offset ( 0, 0) → The cell itself
├─ Check Offset ( 0, +1) → (r, c+1)
├─ Check Offset (+1, -1) → (r+1, c-1)
├─ Check Offset (+1, 0) → (r+1, c)
└─ Check Offset (+1, +1) → (r+1, c+1)
│
▼
● Return Total Count
- Nested Loops for Offsets: We use two nested
DO 3 TIMESloops. By subtracting 2 fromsy-index, we generate offsets of -1, 0, and 1. This elegantly covers all 9 cells in a 3x3 grid around our target cell. - Skipping the Center: The condition
IF lv_row_offset = 0 AND lv_col_offset = 0.is vital. It ensures we don't count the cell itself. - Boundary Checks: This is the most important part of the method.
IF lv_check_row BETWEEN 1 AND lv_rows AND lv_check_col BETWEEN 1 AND lv_cols. Before we attempt to access any character, we first verify that the coordinate we are about to check is actually on the board. This single check prevents a whole class of runtime errors. - Character Inspection & Counting: Only after the boundary check passes do we access the character using
i_board[ lv_check_row ]+lv_check_col-1(1). If it's a mine, we increment ourr_count. - Return Value: Finally, the method returns the total accumulated count.
Alternative Approaches and Performance Considerations
The solution presented is clear, readable, and correct. However, for very large boards or performance-critical applications, it's worth considering other strategies.
Pros and Cons of the "Iterate and Count" Approach
| Pros | Cons |
|---|---|
| Highly Readable: The logic directly follows the problem statement, making it easy to understand and maintain. | Redundant Checks: For each empty cell, we perform up to 8 reads. In a dense grid, many cells are checked multiple times by their neighbors. |
| Simple State Management: We only need to focus on one cell at a time. The overall state is managed implicitly by the loops. | Potentially Slower on Sparse Boards: The work done is proportional to the total number of cells (Rows x Columns), regardless of how many mines there are. |
Alternative: The "Mine-Centric" Approach
An alternative algorithm flips the logic on its head. Instead of iterating through empty cells and looking for mines, we can iterate through the mines and "notify" their empty neighbors.
The Algorithm:
- Create an integer matrix (a 2D array or internal table of integer structures) of the same dimensions as the board, initialized to all zeros. This will be our "count map".
- Iterate through the input board once.
- When you find a mine at
(r, c):- Iterate through its 8 neighbors.
- For each valid neighbor (after a boundary check), increment the value in your count map at that neighbor's coordinate.
- Finally, create the output board. Iterate through the input board and the count map simultaneously. If the input cell is a mine, place a
*. Otherwise, place the number from the count map (or a space if it's 0).
This approach can be more performant on boards that are very large but have very few mines, as the expensive neighbor-update logic only runs for each mine, not for every empty cell.
Frequently Asked Questions (FAQ)
How do I handle invalid input boards, like jagged arrays?
Our current solution assumes a rectangular board. In a production scenario, you should add validation at the beginning of the annotate method. You could loop through the input table and check if strlen() is the same for every row. If not, you could raise an exception or return an empty board to signal an error.
What is the most common error when solving this problem?
By far, the most common errors are "off-by-one" errors and failed boundary checks. Developers often forget that Abap table indices are 1-based while string offsets are 0-based, leading to confusion. Forgetting to check if a neighbor's coordinate is less than 1 or greater than the board dimension will inevitably lead to a runtime error or short dump.
Can this logic be optimized for extremely large boards?
Yes. For massive boards that might not fit in memory, you would need to process the board in chunks or streams. However, for typical SAP scenarios, the provided solution is perfectly adequate. The "Mine-Centric" approach discussed earlier is a good algorithmic optimization for boards with sparse mines.
Why use an internal table of strings instead of a true 2D array?
While some languages have native multi-dimensional arrays, Abap's primary tool for handling collections of data is the internal table. An internal table of strings (TABLE OF string) is the most idiomatic and efficient way to represent this kind of grid in Abap. It leverages highly optimized memory management and provides easy-to-use statements like LOOP AT and lines().
How does this problem relate to ABAP ALV grid programming?
The logical patterns are very similar. When you loop through an ALV grid's data table, you often need to perform calculations based on values in the previous or next row (e.g., running totals). The boundary checks (AT FIRST, AT LAST) in ALV are analogous to our manual checks for the board's edges. This exercise builds the fundamental skills for any kind of row-by-row data processing.
Is it better to modify the board in-place or create a new one?
Creating a new board (or a copy, as we did) is almost always the safer and cleaner approach. Modifying the board in-place can lead to bugs. For example, if you replace a space with a '1', a neighboring cell's counting logic might later misinterpret that '1' as a non-empty, non-mine cell, leading to incorrect behavior. Always work with an immutable original source when performing transformations.
What are the future trends in Abap development relevant to this?
Modern Abap is moving towards cleaner, more expressive syntax and a focus on testability and encapsulation, as seen in our class-based approach. The future lies with the ABAP RESTful Application Programming Model (RAP) for building Fiori apps and services on S/4HANA. While RAP focuses on business objects, the underlying Abap logic for data manipulation, validation, and transformation, as practiced in this exercise, remains a core and essential skill for any developer.
Conclusion: Beyond the Game
We have successfully navigated the Minesweeper annotation challenge, building a robust and well-structured Abap solution from scratch. More importantly, we've explored the fundamental concepts that underpin it: grid traversal, meticulous boundary checking, algorithmic design, and the power of encapsulation using Abap Objects.
The skills honed here—manipulating internal tables, writing clean loops, and thinking defensively about edge cases—are not just for solving puzzles. They are the building blocks of high-quality, professional Abap programs. The next time you face a complex data transformation in an ALV report, a data migration, or an interface, you can draw on the precise, systematic thinking you practiced here.
Disclaimer: The code in this article is written for modern Abap syntax (version 7.40 and higher). The concepts are universal, but specific syntax may need adaptation for older SAP systems.
Ready to tackle the next challenge? Explore the complete Abap 5 learning roadmap to continue building your skills, or dive deeper into the language with our comprehensive Abap language guide.
Published by Kodikra — Your trusted Abap learning resource.
Post a Comment