DEV Community

Cover image for HarmonyOS NEXT Development Case: Digital Klotski Puzzle
zhongcx
zhongcx

Posted on

HarmonyOS NEXT Development Case: Digital Klotski Puzzle

Image description

Introduction

This article demonstrates how to implement a classic Digital Klotski (Number Sliding) Puzzle game using HarmonyOS NEXT's ArkUI framework. The game features touch/swipe controls, animation effects, and victory detection, showcasing key ArkUI capabilities including state management, component lifecycle, and gesture handling.

Implementation Details

1. Game Initialization

@Entry
@Component
struct NumberPuzzle {
  @State gameBoard: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 0]; // Game board data, 0 represents the blank tile
  @State selectedTile: number = -1; // Currently selected tile index
  @State isGameOver: boolean = false; // Game completion status
  @State cellSize: number = 100; // Tile size in logical pixels
  @State cellMargin: number = 5; // Tile margin size
  @State startTime: number = 0; // Game start timestamp
  // ...other state variables

  // Shuffle tiles before component appears
  aboutToAppear(): void {
    this.shuffleGameBoard();
  }
Enter fullscreen mode Exit fullscreen mode

2. Tile Movement Logic

// Check if tile can move to blank space
private canMove(tileIndex: number): boolean {
  const blankIndex = this.gameBoard.indexOf(0);
  const blankRow = Math.floor(blankIndex / 3);
  const blankCol = blankIndex % 3;
  const tileRow = Math.floor(tileIndex / 3);
  const tileCol = tileIndex % 3;

  return (
    (tileRow === blankRow && Math.abs(tileCol - blankCol) === 1) || // Same row
    (tileCol === blankCol && Math.abs(tileRow - blankRow) === 1) // Same column
  );
}

// Perform tile movement
private moveTile(tileIndex: number) {
  if (this.canMove(tileIndex)) {
    const blankIndex = this.gameBoard.indexOf(0);
    [this.gameBoard[tileIndex], this.gameBoard[blankIndex]] = 
      [this.gameBoard[blankIndex], this.gameBoard[tileIndex]];
    this.checkForWin();
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Victory Detection

private checkForWin() {
  const winState = [1, 2, 3, 4, 5, 6, 7, 8, 0];
  this.isGameOver = this.gameBoard.join(',') === winState.join(',');
  if (this.isGameOver) {
    promptAction.showDialog({
      title: 'Victory!',
      message: 'Completion Time: ' + 
        ((Date.now() - this.startTime) / 1000).toFixed(3) + 's',
      buttons: [{ text: 'Restart', color: '#ffa500' }]
    }).then(() => this.shuffleGameBoard());
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Board Shuffling Algorithm

private shuffleGameBoard() {
  this.startTime = Date.now();
  let tempBoard = [...this.gameBoard];
  let blankIndex = tempBoard.indexOf(0);

  // Valid movement directions checker
  const validDirections = (index: number) => {
    const directions = [];
    if (index % 3 > 0) directions.push('left');
    if (index % 3 < 2) directions.push('right');
    if (index >= 3) directions.push('up');
    if (index <= 5) directions.push('down');
    return directions;
  };

  // Make 1000 random valid moves to ensure solvable puzzle
  for (let i = 0; i < 1000; i++) {
    const directions = validDirections(blankIndex);
    const dir = directions[Math.floor(Math.random() * directions.length)];
    const newIndex = this.calculateNewIndex(blankIndex, dir);
    [tempBoard[blankIndex], tempBoard[newIndex]] = 
      [tempBoard[newIndex], tempBoard[blankIndex]];
    blankIndex = newIndex;
  }

  this.gameBoard = tempBoard;
  this.isGameOver = false;
}
Enter fullscreen mode Exit fullscreen mode

5. Animation Effects

private updateAnim(index: number) {
  if (!this.shouldAnimate) return undefined;

  const blankIndex = this.gameBoard.indexOf(0);
  const diff = Math.abs(index - blankIndex);

  if (diff === 1) { // Horizontal movement
    return TransitionEffect.translate({
      x: `${(index > blankIndex ? -1 : 1) * 
        (this.cellSize + this.cellMargin * 2)}lpx`
    }).animation({ duration: 100 });
  }

  if (diff === 3) { // Vertical movement
    return TransitionEffect.translate({
      y: `${(index > blankIndex ? -1 : 1) * 
        (this.cellSize + this.cellMargin * 2)}lpx`
    }).animation({ duration: 100 });
  }

  return undefined;
}
Enter fullscreen mode Exit fullscreen mode

6. User Interaction

// Touch gesture handling
.gesture(
  SwipeGesture({ direction: SwipeDirection.All })
    .onAction((_event: GestureEvent) => {
      const swipeX = this.lastScreenX - this.screenStartX;
      const swipeY = this.lastScreenY - this.screenStartY;

      // Determine primary swipe direction
      let direction = Math.abs(swipeX) > Math.abs(swipeY) 
        ? (swipeX > 0 ? 'Right' : 'Left')
        : (swipeY > 0 ? 'Down' : 'Up');

      this.moveOnSwipe(direction);
    })
)

// Swipe-based movement
private moveOnSwipe(direction: string) {
  const blankIndex = this.gameBoard.indexOf(0);
  let targetIndex = blankIndex;

  switch(direction) {
    case 'Up': targetIndex += 3; break;
    case 'Down': targetIndex -= 3; break;
    case 'Left': targetIndex += 1; break;
    case 'Right': targetIndex -= 1; break;
  }

  if (targetIndex >= 0 && targetIndex < 9) {
    this.moveTile(targetIndex);
  }
}
Enter fullscreen mode Exit fullscreen mode

Complete Component Structure

build() {
  Column({ space: 10 }) {
    // Game board
    Flex({ wrap: FlexWrap.Wrap }) {
      ForEach(this.gameBoard, (item, index) => 
        Text(`${item}`)
          .size(`${this.cellSize}lpx`)
          .margin(this.cellMargin)
          .fontSize(this.cellSize/2)
          .backgroundColor(item === 0 ? Color.White : Color.Orange)
          .visibility(item === 0 ? Visibility.Hidden : Visibility.Visible)
          .transition(this.updateAnim(index))
          .onClick(() => this.canMove(index) && this.moveTile(index))
    }
    .width((this.cellSize + this.cellMargin * 2) * 3)

    // Restart button
    Button('Restart')
      .width('50%')
      .onClick(() => this.shuffleGameBoard())
  }
  .size('100%')
  .onTouch(this.handleTouchEvents)
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This implementation demonstrates several key HarmonyOS NEXT features:

  1. Declarative UI construction with ArkUI components
  2. Reactive state management using @State decorators
  3. Gesture recognition with built-in swipe detection
  4. Animation integration with transition effects
  5. Component lifecycle management (aboutToAppear)

The game can be further enhanced by:

  • Adding move counter
  • Implementing difficulty levels
  • Adding sound effects
  • Supporting different grid sizes (4x4, 5x5)
  • Adding persistent score tracking

This case study provides a practical example of building interactive games using HarmonyOS NEXT's modern UI framework.

Top comments (0)