Développement HarmonyOS : Création d'une application de démineur simple

Nous allons développer une application de démineur avec les foncsionnalités suivantes :

• Une grille de jeu générée dynamiquement en 10x10. • Placement aléatoire de 10 mines. • Calcul et affichage du nombre de mines autour de chaque case. • Les utilisateurs peuvent révéler des cases en cliquant dessus ou marquer des mines en maintenant appuyé. • La partie se termine si une mine est révélée ; la victoire est atteinte lorsque toutes les cases non minées sont révélées.

import { alertAction } from '@tools.HarmonyUI';

@Entry
@Component
struct MineSweeper {
  // État de la grille de jeu
  @State private gridData: Tile[][] = [];
  // Nombre total de mines
  @State private totalMines: number = 10;
  // Ensembles des cases révélées et marquées
  @State private uncoveredTiles: Set<string> = new Set();
  @State private flaggedTiles: Set<string> = new Set();
  // Taille et marge des cases
  @State private tileSize: number = 60;
  @State private tilePadding: number = 2;
  // Indicateurs de fin de partie
  @State private gameEnded: boolean = false;

  // Initialisation lors de l'apparition du composant
  aboutToAppear(): void {
    this.resetGame();
  }

  // Réinitialiser le jeu
  private resetGame() {
    this.gameEnded = false;
    this.uncoveredTiles.clear();
    this.flaggedTiles.clear();
    this.createGrid();
  }

  // Générer la grille
  private createGrid() {
    this.gridData = Array.from({ length: 10 }, (_, row) =>
      Array.from({ length: 10 }, (_, col) => new Tile(row, col))
    );
    this.setRandomMines();
    this.computeAdjacentMines();
  }

  // Placer les mines aléatoirement
  private setRandomMines() {
    let placedMines = 0;
    while (placedMines < this.totalMines) {
      const x = Math.floor(Math.random() * 10);
      const y = Math.floor(Math.random() * 10);
      if (!this.gridData[x][y].isMine) {
        this.gridData[x][y].isMine = true;
        placedMines++;
      }
    }
  }

  // Calculer les mines adjacentes pour chaque case
  private computeAdjacentMines() {
    for (let i = 0; i < 10; i++) {
      for (let j = 0; j < 10; j++) {
        if (!this.gridData[i][j].isMine) {
          this.gridData[i][j].adjacentMines = this.countNearbyMines(i, j);
        }
      }
    }
  }

  // Compter les mines autour d'une case donnée
  private countNearbyMines(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;
        const newRow = row + dx;
        const newCol = col + dy;
        if (newRow >= 0 && newRow < 10 && newCol >= 0 && newCol < 10 && this.gridData[newRow][newCol].isMine) {
          count++;
        }
      }
    }
    return count;
  }

  // Révéler une case
  private uncoverTile(row: number, col: number) {
    if (this.gameEnded || this.uncoveredTiles.has(`${row},${col}`)) return;

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

    if (this.gridData[row][col].isMine) {
      this.displayLoseMessage();
    } else if (this.gridData[row][col].adjacentMines === 0) {
      for (let dx = -1; dx <= 1; dx++) {
        for (let dy = -1; dy <= 1; dy++) {
          if (dx === 0 && dy === 0) continue;
          const newRow = row + dx;
          const newCol = col + dy;
          if (newRow >= 0 && newRow < 10 && newCol >= 0 && newCol < 10) {
            this.uncoverTile(newRow, newCol);
          }
        }
      }
    }

    if (this.checkWinCondition()) this.displayWinMessage();
  }

  // Afficher un message de défaite
  private displayLoseMessage() {
    this.gameEnded = true;
    alertAction.showDialog({
      title: 'Défaite !',
      buttons: [{ text: 'Recommencer', color: '#4CAF50' }]
    }).then(() => this.resetGame());
  }

  // Afficher un message de victoire
  private displayWinMessage() {
    this.gameEnded = true;
    alertAction.showDialog({
      title: 'Victoire !',
      buttons: [{ text: 'Nouvelle Partie', color: '#FF9800' }]
    }).then(() => this.resetGame());
  }

  // Vérifier si le joueur a gagné
  private checkWinCondition(): boolean {
    const safeTilesCount = Array.from(this.uncoveredTiles).filter(
      (tile) => !this.gridData[parseInt(tile.split(',')[0])][parseInt(tile.split(',')[1])].isMine
    ).length;
    return safeTilesCount === 90;
  }

  build() {
    Column({ space: 10 }) {
      Button('Nouvelle Partie').onClick(() => this.resetGame());

      Flex({ wrap: FlexWrap.Wrap }) {
        ForEach(this.gridData, (row: Tile[], rowIndex: number) => {
          ForEach(row, (tile: Tile, colIndex: number) => {
            Stack() {
              Text(this.getDisplayValue(tile))
                .width(`${this.tileSize}px`)
                .height(`${this.tileSize}px`)
                .margin(`${this.tilePadding}px`)
                .fontSize(`${this.tileSize / 2}px`)
                .textAlign(TextAlign.Center)
                .backgroundColor(this.uncoveredTiles.has(`${rowIndex},${colIndex}`) ? Color.White : Color.Gray)
                .fontColor(this.uncoveredTiles.has(`${rowIndex},${colIndex}`) ? Color.Black : Color.White)
                .borderRadius(5)
                .parallelGesture(GestureGroup(GestureMode.Exclusive,
                  TapGesture({ count: 1, fingers: 1 })
                    .onAction(() => this.uncoverTile(rowIndex, colIndex)),
                  LongPressGesture({ repeat: true })
                    .onAction(() => this.flaggedTiles.add(`${rowIndex},${colIndex}`))
                ));

              Text('Drapeau')
                .visibility(this.flaggedTiles.has(`${rowIndex},${colIndex}`) ? Visibility.Visible : Visibility.Hidden)
                .onClick(() => this.flaggedTiles.delete(`${rowIndex},${colIndex}`));
            }
          });
        });
      }
      .width(`${(this.tileSize + this.tilePadding * 2) * 10}px`);
    }
    .backgroundColor(Color.LightGray)
    .width('100%')
    .height('100%');
  }

  private getDisplayValue(tile: Tile): string {
    return this.uncoveredTiles.has(`${tile.row},${tile.column}`)
      ? tile.isMine ? 'M' : tile.adjacentMines.toString()
      : '';
  }
}

// Classe représentant une tuile
@ObservedV2
class Tile {
  row: number;
  column: number;
  isMine: boolean = false;
  adjacentMines: number = 0;

  constructor(row: number, column: number) {
    this.row = row;
    this.column = column;
  }
}

Étapes de réalisation

Étape 1 : Définir l'état du jeu Créer les variables d'état nécessaires pour représenter la grille, les mines, les cases révélées et marquées.

struct MineSweeper {
  @State private gridData: Tile[][] = [];
  @State private totalMines: number = 10;
  @State private uncoveredTiles: Set<string> = new Set();
  @State private flaggedTiles: Set<string> = new Set();
}

Étape 2 : Initialiser le jeu Nettoyer les données existantes et générer une nouvelle grille avant chaque partie.

aboutToAppear(): void {
  this.resetGame();
}

private resetGame() {
  this.gameEnded = false;
  this.uncoveredTiles.clear();
  this.flaggedTiles.clear();
  this.createGrid();
}

Étape 3 : Générer la grille et placer les mines Construire une grille de 10x10 cases et positionner aléatoirement les mines.

private createGrid() {
  this.gridData = Array.from({ length: 10 }, (_, row) =>
    Array.from({ length: 10 }, (_, col) => new Tile(row, col))
  );
  this.setRandomMines();
  this.computeAdjacentMines();
}

Étape 4 : Gérer les interactions utilisateur Implémenter la logique pour révéler des cases et marquer des mines via des gestes.

private uncoverTile(row: number, col: number) {
  // Logique de révélation...
}

build() {
  // Utiliser Flex et ForEach pour itérer sur la grille...
}

Étape 5 : Afficher les résultats du jeu Afficher un message de victoire ou de défaite selon l'état final du jeu.

private displayLoseMessage() {
  // Message de défaite...
}

private displayWinMessage() {
  // Message de victoire...
}

Étape 6 : Définir la classe Tile Créer une classe pour stocker les informations relatives à chaque case.

@ObservedV2
class Tile {
  // Attributs de la tuile...
}

Étiquettes: HarmonyOS TypeScript MineSweeper

Publié le 20 août à 05h35