DEV Community

Cover image for HarmonyOS NEXT Development Case: Minesweeper
zhongcx
zhongcx

Posted on

HarmonyOS NEXT Development Case: Minesweeper

Image description

The following code demonstrates how to implement a classic Minesweeper game using HarmonyOS NEXT's ArkUI framework. This implementation includes core game logic, state management, and interactive UI components.

Core Code Implementation

import { promptAction } from '@kit.ArkUI'; // Import dialog display utility

// Define Cell class
@ObservedV2 // Enable automatic state tracking
class Cell {
  row: number; // Cell's row index
  column: number; // Cell's column index
  hasMine: boolean = false; // Mine presence flag
  neighborMines: number = 0; // Number of adjacent mines
  @Trace isFlag: boolean = false; // Flag marking state
  @Trace value: string; // Display value (number or 'Mine')

  constructor(row: number, column: number) {
    this.row = row;
    this.column = column;
    this.value = ''; // Initialize empty value
  }
}

// Main game component
@Entry // Entry component declaration
@Component // Component declaration
struct MineSweeper {
  @State private gameBoard: Cell[][] = []; // Game board data
  @State private mineCount: number = 10; // Total mines
  @State private revealedCells: Set<string> = new Set(); // Revealed cells
  @State private flaggedCells: Set<string> = new Set(); // Flagged cells
  @State private cellSize: number = 60; // Cell dimension
  @State private cellMargin: number = 2; // Cell spacing
  private startTime: number = Date.now(); // Game start timestamp
  @State private isGameOver: boolean = false; // Game over flag

  // Lifecycle method
  aboutToAppear(): void {
    this.initializeGame();
  }

  private initializeGame() {
    this.isGameOver = false;
    this.startTime = Date.now();
    this.revealedCells.clear();
    this.flaggedCells.clear();
    this.generateBoard();
  }

  private generateBoard() {
    this.gameBoard = [];
    for (let i = 0; i < 10; i++) {
      this.gameBoard.push([]);
      for (let j = 0; j < 10; j++) {
        this.gameBoard[i].push(new Cell(i, j));
      }
    }
    this.placeMines();
    this.calculateNumbers();
  }

  private placeMines() {
    let placed = 0;
    while (placed < this.mineCount) {
      let x = Math.floor(Math.random() * 10);
      let y = Math.floor(Math.random() * 10);
      if (!this.gameBoard[x][y].hasMine) {
        this.gameBoard[x][y].hasMine = true;
        placed++;
      }
    }
  }

  private calculateNumbers() {
    for (let i = 0; i < 10; i++) {
      for (let j = 0; j < 10; j++) {
        if (!this.gameBoard[i][j].hasMine) {
          this.gameBoard[i][j].neighborMines = this.countNeighborMines(i, j);
          this.gameBoard[i][j].value = this.gameBoard[i][j].neighborMines.toString();
        } else {
          this.gameBoard[i][j].value = ''; // Chinese character for mine
        }
      }
    }
  }

  private countNeighborMines(row: number, col: number): number {
    let count = 0;
    for (let dx = -1; dx <= 1; dx++) {
      for (let dy = -1; dy <= 1; dy++) {
        if (dx === 0 && dy === 0) continue;
        let newRow = row + dx, newCol = col + dy;
        if (newRow >= 0 && newRow < 10 && newCol >= 0 && newCol < 10 && 
            this.gameBoard[newRow][newCol].hasMine) {
          count++;
        }
      }
    }
    return count;
  }

  private revealCell(row: number, col: number) {
    if (this.isGameOver || this.revealedCells.has(`${row},${col}`)) return;

    const key = `${row},${col}`;
    this.revealedCells.add(key);

    if (this.gameBoard[row][col].hasMine) {
      this.showGameOverDialog();
    } else {
      if (this.gameBoard[row][col].neighborMines === 0) {
        for (let dx = -1; dx <= 1; dx++) {
          for (let dy = -1; dy <= 1; dy++) {
            if (dx === 0 && dy === 0) continue;
            let newRow = row + dx, newCol = col + dy;
            if (newRow >= 0 && newRow < 10 && newCol >= 0 && newCol < 10) {
              this.revealCell(newRow, newCol);
            }
          }
        }
      }
    }
    if (this.isVictory()) this.showVictoryDialog();
  }

  private showGameOverDialog() {
    this.isGameOver = true;
    promptAction.showDialog({
      title: 'Game Over: You Lost!',
      buttons: [{ text: 'Restart', color: '#ffa500' }]
    }).then(() => this.initializeGame());
  }

  private showVictoryDialog() {
    this.isGameOver = true;
    promptAction.showDialog({
      title: 'Congratulations! You Won!',
      message: `Time: ${((Date.now() - this.startTime) / 1000).toFixed(3)}s`,
      buttons: [{ text: 'Restart', color: '#ffa500' }]
    }).then(() => this.initializeGame());
  }

  private isVictory(): boolean {
    let revealedNonMineCount = 0;
    for (let i = 0; i < this.gameBoard.length; i++) {
      for (let j = 0; j < this.gameBoard[i].length; j++) {
        if (this.revealedCells.has(`${i},${j}`)) {
          revealedNonMineCount++;
        }
      }
    }
    return revealedNonMineCount === 90;
  }

  private isShowValue(cell: Cell): string {
    if (this.isGameOver) {
      return cell.value === '0' ? '' : cell.value;
    } else {
      return this.revealedCells.has(`${cell.row},${cell.column}`) ? 
             (cell.value === '0' ? '' : cell.value) : '';
    }
  }

  build() {
    Column({ space: 10 }) {
      Button('Restart').onClick(() => this.initializeGame())
      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(this.gameBoard, (row: Cell[], rowIndex: number) => {
          ForEach(row, (cell: Cell, colIndex: number) => {
            Stack() {
              Text(this.isShowValue(cell))
                .width(`${this.cellSize}lpx`)
                .height(`${this.cellSize}lpx`)
                .margin(`${this.cellMargin}lpx`)
                .fontSize(`${this.cellSize / 2}lpx`)
                .textAlign(TextAlign.Center)
                .backgroundColor(this.revealedCells.has(`${rowIndex},${colIndex}`) ? 
                  (this.isShowValue(cell) === '' ? Color.Red : Color.White) : Color.Gray)
                .fontColor(!this.revealedCells.has(`${rowIndex},${colIndex}`) || 
                  this.isShowValue(cell) === '' ? Color.White : Color.Black)
                .borderRadius(5)
                .parallelGesture(GestureGroup(GestureMode.Exclusive,
                  TapGesture({ count: 1, fingers: 1 })
                    .onAction(() => this.revealCell(rowIndex, colIndex)),
                  LongPressGesture({ repeat: true })
                    .onAction(() => cell.isFlag = true)
                ));
              Text(`${!this.revealedCells.has(`${rowIndex},${colIndex}`) ? '🚩' : ''}`)
                .width(`${this.cellSize}lpx`)
                .height(`${this.cellSize}lpx`)
                .margin(`${this.cellMargin}lpx`)
                .fontSize(`${this.cellSize / 2}lpx`)
                .textAlign(TextAlign.Center)
                .fontColor(Color.White)
                .visibility(cell.isFlag && !this.isGameOver ? 
                  Visibility.Visible : Visibility.None)
            }
          });
        });
      }
      .width(`${(this.cellSize + this.cellMargin * 2) * 10}lpx`);
    }
    .backgroundColor(Color.Orange)
    .width('100%')
    .height('100%');
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Technical Features

  1. State Management:

    • Uses @ObservedV2 decorator for automatic state tracking
    • Implements @Trace for flag state synchronization
    • Manages game state with @State properties
  2. Game Logic:

    • Recursive cell revealing algorithm
    • Mine placement randomization
    • Victory detection (90 safe cells revealed)
  3. UI Features:

    • Responsive grid layout with Flex container
    • Multi-gesture support (tap & long-press)
    • Dynamic cell styling based on game state
    • Animated flag markers (🚩)
  4. Performance:

    • Efficient cell rendering with ForEach
    • Optimized state updates using ArkUI's reactive system

This implementation demonstrates how to build complex game logic while leveraging HarmonyOS NEXT's declarative UI capabilities and reactive programming model. The code follows modern ArkUI development patterns and can be extended with additional features like difficulty levels or score tracking.

Top comments (0)