DEV Community

HarmonyOS
HarmonyOS

Posted on • Edited on

Building 2048 with ArkTS 3

Read the original article:Building 2048 with ArkTS 3

Let’s build with Arkts

Introduction

Welcome back! We are one step away from our goal. If you haven't seen them, please refer to parts 1 and 2.

Adding a new tile

With every swipe operation, we will add a new tile, a 2 or a 4.

  • First, implement the function.

  • private _addItem(arr: number[]) {
      // get empty indexes
      const emptyIndexes = arr.map((value, index) => value == 0 ? index : -1).filter((val) => val >= 0)
      // select a random index
      const index = emptyIndexes[this._getRandomIndex(emptyIndexes.length - 1)]
    
      const arrCopy = Array.from(arr);
      // put 2 or 4 randomly
      arrCopy[index] = [2, 4][this._getRandomIndex(1)]
      return arrCopy;
    }
    
    Enter fullscreen mode Exit fullscreen mode
    Add new tile
  • There is one thing we should be careful about: we should not add a new tile if the board has not changed. Let’s implement the check function.

  • private _boardChanged(newBoard: number[]) {
      return !this.board.every((val, index) => val == newBoard[index])
    }
    
    
    Enter fullscreen mode Exit fullscreen mode
    Check if the board changed
  • Time to put things together. Modify the swipe functions to add a new tile.

  • // flatten board as list
    let newBoard = board2D.flat()
    
    // add new element if the board changed
    if (this._boardChanged(newBoard)) {
      newBoard = this._addItem(newBoard)
      this.board = newBoard;
    }
    
    Enter fullscreen mode Exit fullscreen mode
    Modify all swipe functions

    The result should look like this. After every swipe, your app should add a 2 or a 4 to a random location.

    Game Status

    So far, we have created a playable game, now we will implement the win-lose logic. The game ends when the player reaches 2048 or when no more moves are possible. Let’s implement the game checker.

  • But first, we will define game status. Create “GameStatus.ets” under the model directory.

  • export enum GameStatus {
      running,
      lose,
      win
    }
    
    Enter fullscreen mode Exit fullscreen mode
    GameStatus.ets
  • Modify the Game to reflect the current game status.

  • status: GameStatus = GameStatus.running // add status parameter
    
    Enter fullscreen mode Exit fullscreen mode
  • Now the checker:

  • private _checkGame() {
      // check if win
      if (this.board.some((val) => val == GameNum._2048)) {
        this.status = GameStatus.win;
      }
    
      // check if the board is full and no move exists
      if (this.board.every((val) => val != GameNum._0) && !this._moveExists()) {
        this.status = GameStatus.lose
      }
      // else game can continue
    }
    
    
    Enter fullscreen mode Exit fullscreen mode
    Check if the game ended

    And the move checker:

    private _moveExists() {
      // reshape flat list as board
      let board2D: number[][] = [];
      for (let i = 0; i < this.board.length; i += 4) {
        board2D.push(this.board.slice(i, i + 4));
      }
    
      // check each row and column
      for (let row = 0; row < board2D.length; row++) {
        for (let col = 0; col < board2D[row].length - 1; col++) {
          if (board2D[row][col] == board2D[row][col + 1]) {
            return true;
          }
    
          // if not last row, compare with next row
          if (row < board2D.length - 1) {
            if (board2D[row][col] == board2D[row + 1][col]) {
              return true
            }
          }
        }
      }
    
      // else
      return false
    }
    
    
    Enter fullscreen mode Exit fullscreen mode
    Check if any move exists

    Now, modify all the swipe functions to check if the game ends.

    // add new element if the board changed
    if (this._boardChanged(newBoard)) {
      newBoard = this._addItem(newBoard)
      this.board = newBoard;
    
      this._checkGame(); // add this line
    }
    
    Enter fullscreen mode Exit fullscreen mode
    Modify all swipe functions

    Adding the UI

    To inform our users when the game status changes, we will use a custom pop-up. We will show the final result and a refresh button to restart the game. Start with the pop-up UI.

    Create “WinLosePopup.ets” under the components directory. You can customize the UI as you wish.

    @Component
    export default struct WinLosePopup {
      @Link showPopup: boolean;
      @Require win: boolean;
    
      build() {
        Column() {
          Text(`YOU ${this.win ? 'WON' : 'LOST'}`)
            .fontColor(this.win ? Color.Green : Color.Red)
    
          Blank().size({ height: 8 })
    
          Button() {
            SymbolGlyph($r('sys.symbol.arrow_clockwise'))
              .fontColor([Color.White])
          }
          .size({
            width: '100%',
            height: 32
          })
          .onClick(() => {
            this.showPopup = false // close the popup
          })
        }
        .width('70%')
        .backgroundColor(Color.White)
        .padding(16)
      }
    }
    
    Enter fullscreen mode Exit fullscreen mode
    WinLosePopup.ets

    We will watch the Game and show the popup when the game status changes to win or lose. Modify “Index.ets”.

  • Add a state parameter and pop-up builder.

  • @State pop: boolean = false;
    
    @Builder
    winLosePopupBuilder() {
      WinLosePopup({
        showPopup: this.pop,
        win: this.game.status == GameStatus.win
      })
    }
    
    Enter fullscreen mode Exit fullscreen mode
    Modify Index.ets to use popup
  • Add “.bindPopup()”.

  • RelativeContainer() {
        // ...
    }
    .bindPopup(this.pop, {
      builder: this.winLosePopupBuilder(),
      autoCancel: false,
    })
    
    Enter fullscreen mode Exit fullscreen mode
    Bind popup
  • Add “@Watch” to show the pop-up automatically.

  • // add @Watch
    @State @Watch('winLose') game: Game = new Game()
    
    // show pop-up if the game has ended
    winLose() {
      this.pop = this.game.status != GameStatus.running
    }
    
    Enter fullscreen mode Exit fullscreen mode
    Watch the state

    Now we have the automatic pop-up. It should open when the game ends.

    Automatic pop-up
  • We can change the page’s background color based on the game status.

  • // add function to change color
    bgColor() {
      switch (this.game.status) {
        case GameStatus.running:
          return $r('app.color.color_background')
        case GameStatus.lose:
          return Color.Red
        case GameStatus.win:
          return Color.Green
      }
    }
    
    // add background color
    RelativeContainer() {
      // ...
    }
    .backgroundColor(this.bgColor())
    
    Enter fullscreen mode Exit fullscreen mode
    Customize background color

    Final Touch: restarting the game

    We will use .bindPopup’s onStateChange attribute to restart the game after the user closes the pop-up.

  • First, implement the refresh function.

  • refresh() {
      this.board = this._init();
      this.status = GameStatus.running
    }
    
    Enter fullscreen mode Exit fullscreen mode
    Restart the game
  • Then add the onStateChange attribute.

  • .bindPopup(this.pop, {
      builder: this.winLosePopupBuilder(),
      autoCancel: false,
      // add this to refresh the game
      onStateChange: (e) => {
        if (!e.isVisible) {
          this.game.refresh()
        }
      },
    })
    
    Enter fullscreen mode Exit fullscreen mode
    Restart the game when popup closes

    Final Result

    Conclusion

    Congratulations, everyone, for not giving up! It is time to play and enjoy. Keep on building new things and learning. See you all in new adventures. :)

    Fortuna Favet Fortibus

    References

    (https://forums.developer.huawei.com/forumPortal/en/topic/0201189245864780049?fid=0102647487706140266)

    Written by Mr.Karaaslan

    Top comments (0)