HarmonyOS NEXT Development Case: Reversi (Othello)

[Introduction] Reversi, also known as Othello or Anti reversi, is a popular strategy game in Western countries and Japan. Players take turns placing disks to flip their opponent's pieces, with the winner determined by who has more disks on the board at game end. While its rules are simple enough to learn in minutes, the game offers deep strategic complexity that can take a lifetime to master. [Environment Setup] OS: Windows 10 IDE: DevEco Studio NEXT Beta1 Build Version: 5.0.3.806 SDK Version: API 12 Test Device: Mate 60 Pro Languages: ArkTS, ArkUI [Implemented Features] Board Initialization: Creates an 8×8 grid with four initial disks placed according to standard Reversi rules. Disk Display & Animation: ChessCell class manages disk states (black/white) with flip animations. Valid Move Detection: findReversible method checks valid moves across 8 directions. Player Switching: currentPlayerIsBlackChanged handles turn transitions and game-over checks. AI Strategy: Single-player mode features random valid move selection. Game Termination: Ends when neither player can move, displaying the winner. [Core Algorithms] Valid Move Detection Algorithm The findReversible method checks all 8 directions for flippable disks: findReversible(row: number, col: number, color: number): ChessCell[] { let reversibleTiles: ChessCell[] = []; const directions = [ /* 8 directions */ ]; for (const direction of directions) { let foundOpposite = false; let x = row, y = col; do { x += direction[0]; y += direction[1]; // Boundary checks const cell = this.chessBoard[x][y]; if (cell.frontVisibility === color && foundOpposite) { // Collect reversible tiles } else { foundOpposite = true; } } while (valid); } return reversibleTiles; } AI Random Move Strategy aiPlaceRandom() { let validMoves: [number, number][] = []; // Collect valid moves if (validMoves.length > 0) { const randomMove = validMoves[Math.floor(Math.random() * validMoves.length)]; this.placeChessPiece(randomMove[0], randomMove[1], chessCell); } } Disk Flip Animation The ChessCell class handles animations through rotation and opacity changes: flip(time: number) { if (this.frontVisibility === 1) { this.showWhite(time); } else if (this.frontVisibility === 2) { this.showBlack(time); } } [Full Code] import { promptAction } from '@kit.ArkUI'; // Import dialog box utility @ObservedV2 class ChessCell { // Chessboard cell class @Trace frontVisibility: number = 0; // Stone state: 0=empty, 1=black, 2=white @Trace rotationAngle: number = 0; // Card rotation angle @Trace opacity: number = 1; // Opacity isAnimationRunning: boolean = false; // Animation running flag flip(time: number) { // Flip stone method if (this.frontVisibility === 1) { // Current is black, flip to white this.showWhite(time); } else if (this.frontVisibility === 2) { // Current is white, flip to black this.showBlack(time); } } showBlack(animationTime: number, callback?: () => void) { // Show black stone if (this.isAnimationRunning) { // Animation already running return; } this.isAnimationRunning = true; if (animationTime == 0) { // No animation needed this.rotationAngle = 0; this.frontVisibility = 1; this.isAnimationRunning = false; if (callback) { callback(); } } animateToImmediately({ duration: animationTime, // Animation duration iterations: 1, // Iteration count curve: Curve.Linear, // Animation curve type onFinish: () => { animateToImmediately({ duration: animationTime, iterations: 1, curve: Curve.Linear, onFinish: () => { this.isAnimationRunning = false; if (callback) { callback(); } } }, () => { this.frontVisibility = 1; // Show black this.rotationAngle = 0; }); } }, () => { this.rotationAngle = 90; // Rotate to 90 degrees }); } showWhite(animationTime: number, callback?: () => void) { // Show white stone if (this.isAnimationRunning) { return; } this.isAnimationRunning = true; if (animationTime == 0) { this.rotationAngle = 180; this.frontVisibility = 2; this.isAnimationRunning = false; if (callback) { callback(); } } animateToImmediately({ duration: animationTime, iterations: 1, curve: Curve.Linear, onFinish: () => { animateToImmediately({ duration: animationTime, iterations: 1, curve: Curve.Linear, onFinish: () => { this.isAnimationRunning = false; if (callback) { callback(); } } }, () => { this.frontVisibility = 2; // Show white this.ro

May 11, 2025 - 00:32
 0
HarmonyOS NEXT Development Case: Reversi (Othello)

Image description

[Introduction]

Reversi, also known as Othello or Anti reversi, is a popular strategy game in Western countries and Japan. Players take turns placing disks to flip their opponent's pieces, with the winner determined by who has more disks on the board at game end. While its rules are simple enough to learn in minutes, the game offers deep strategic complexity that can take a lifetime to master.

[Environment Setup]

OS: Windows 10
IDE: DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
SDK Version: API 12
Test Device: Mate 60 Pro
Languages: ArkTS, ArkUI
[Implemented Features]

Board Initialization: Creates an 8×8 grid with four initial disks placed according to standard Reversi rules.
Disk Display & Animation: ChessCell class manages disk states (black/white) with flip animations.
Valid Move Detection: findReversible method checks valid moves across 8 directions.
Player Switching: currentPlayerIsBlackChanged handles turn transitions and game-over checks.
AI Strategy: Single-player mode features random valid move selection.
Game Termination: Ends when neither player can move, displaying the winner.
[Core Algorithms]

  1. Valid Move Detection Algorithm

The findReversible method checks all 8 directions for flippable disks:

findReversible(row: number, col: number, color: number): ChessCell[] {
  let reversibleTiles: ChessCell[] = [];
  const directions = [ /* 8 directions */ ];

  for (const direction of directions) {
    let foundOpposite = false;
    let x = row, y = col;
    do {
      x += direction[0];
      y += direction[1];
      // Boundary checks
      const cell = this.chessBoard[x][y];
      if (cell.frontVisibility === color && foundOpposite) {
        // Collect reversible tiles
      } else {
        foundOpposite = true;
      }
    } while (valid);
  }
  return reversibleTiles;
}

  1. AI Random Move Strategy
aiPlaceRandom() {
  let validMoves: [number, number][] = [];
  // Collect valid moves
  if (validMoves.length > 0) {
    const randomMove = validMoves[Math.floor(Math.random() * validMoves.length)];
    this.placeChessPiece(randomMove[0], randomMove[1], chessCell);
  }
}

  1. Disk Flip Animation The ChessCell class handles animations through rotation and opacity changes:
flip(time: number) {
  if (this.frontVisibility === 1) {
    this.showWhite(time);
  } else if (this.frontVisibility === 2) {
    this.showBlack(time);
  }
}

[Full Code]

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

@ObservedV2
class ChessCell { // Chessboard cell class
  @Trace frontVisibility: number = 0; // Stone state: 0=empty, 1=black, 2=white
  @Trace rotationAngle: number = 0; // Card rotation angle
  @Trace opacity: number = 1; // Opacity
  isAnimationRunning: boolean = false; // Animation running flag

  flip(time: number) { // Flip stone method
    if (this.frontVisibility === 1) { // Current is black, flip to white
      this.showWhite(time);
    } else if (this.frontVisibility === 2) { // Current is white, flip to black
      this.showBlack(time);
    }
  }

  showBlack(animationTime: number, callback?: () => void) { // Show black stone
    if (this.isAnimationRunning) { // Animation already running
      return;
    }
    this.isAnimationRunning = true;
    if (animationTime == 0) { // No animation needed
      this.rotationAngle = 0;
      this.frontVisibility = 1;
      this.isAnimationRunning = false;
      if (callback) {
        callback();
      }
    }

    animateToImmediately({
      duration: animationTime, // Animation duration
      iterations: 1, // Iteration count
      curve: Curve.Linear, // Animation curve type
      onFinish: () => {
        animateToImmediately({
          duration: animationTime,
          iterations: 1,
          curve: Curve.Linear,
          onFinish: () => {
            this.isAnimationRunning = false;
            if (callback) {
              callback();
            }
          }
        }, () => {
          this.frontVisibility = 1; // Show black
          this.rotationAngle = 0;
        });
      }
    }, () => {
      this.rotationAngle = 90; // Rotate to 90 degrees
    });
  }

  showWhite(animationTime: number, callback?: () => void) { // Show white stone
    if (this.isAnimationRunning) {
      return;
    }
    this.isAnimationRunning = true;
    if (animationTime == 0) {
      this.rotationAngle = 180;
      this.frontVisibility = 2;
      this.isAnimationRunning = false;
      if (callback) {
        callback();
      }
    }

    animateToImmediately({
      duration: animationTime,
      iterations: 1,
      curve: Curve.Linear,
      onFinish: () => {
        animateToImmediately({
          duration: animationTime,
          iterations: 1,
          curve: Curve.Linear,
          onFinish: () => {
            this.isAnimationRunning = false;
            if (callback) {
              callback();
            }
          }
        }, () => {
          this.frontVisibility = 2; // Show white
          this.rotationAngle = 180;
        });
      }
    }, () => {
      this.rotationAngle = 90;
    });
  }

  showWhiteAi(animationTime: number, callback?: () => void) { // Show white stone with AI animation
    if (this.isAnimationRunning) {
      return;
    }
    this.isAnimationRunning = true;
    if (animationTime == 0) {
      this.rotationAngle = 180;
      this.frontVisibility = 2;
      this.isAnimationRunning = false;
      if (callback) {
        callback();
      }
    }
    this.rotationAngle = 180;
    this.frontVisibility = 2;

    animateToImmediately({
      duration: animationTime * 3, // Longer duration
      curve: Curve.EaseOut, // Different curve
      iterations: 3, // Multiple iterations
      onFinish: () => {
        animateToImmediately({
          duration: animationTime,
          iterations: 1,
          curve: Curve.Linear,
          onFinish: () => {
            this.isAnimationRunning = false;
            if (callback) {
              callback();
            }
          }
        }, () => {
          this.opacity = 1; // Full opacity
        });
      }
    }, () => {
      this.opacity = 0.2; // Semi-transparent
    });
  }
}

@ObservedV2
class TileHighlight { // Valid move highlight
  @Trace isValidMove: boolean = false; // Valid move flag
}

@Entry
@Component
struct OthelloGame { // Othello game component
  @State chessBoard: ChessCell[][] = []; // Board array
  @State cellSize: number = 70; // Cell size
  @State cellSpacing: number = 5; // Cell spacing
  @State transitionDuration: number = 200; // Animation duration
  @State @Watch('currentPlayerIsBlackChanged') currentPlayerIsBlack: boolean = true; // Current player
  @State chessBoardSize: number = 8; // 8x8 board
  @State validMoveIndicators: TileHighlight [][] = []; // Valid move indicators
  @State isTwoPlayerMode: boolean = false; // Two-player mode
  @State isAIPlaying:boolean = false; // AI playing flag

  currentPlayerIsBlackChanged() { // Player change handler
    setTimeout(() => {
      const color = this.currentPlayerIsBlack ? 1 : 2;
      let hasMoves = this.hasValidMoves(color);

      if (!hasMoves) {
        let opponentHasMoves = this.hasValidMoves(!this.currentPlayerIsBlack ? 1 : 2);
        if (!opponentHasMoves) {
          const winner = this.determineWinner();
          console.log(winner);
          promptAction.showDialog({
            title: 'Game Over',
            message: `${winner}`,
            buttons: [{ text: 'Restart', color: '#ffa500' }]
          }).then(() => {
            this.initGame();
          });
        } else {
          this.currentPlayerIsBlack = !this.currentPlayerIsBlack;
        }
      } else {
        if (!this.currentPlayerIsBlack) { // AI turn
          if (!this.isTwoPlayerMode) {
            setTimeout(() => {
              this.aiPlaceRandom();
            }, this.transitionDuration + 20);
          }
        }
      }
    }, this.transitionDuration + 20);
  }

  aiPlaceRandom() { // AI random placement
    let validMoves: [number, number][] = [];
    for (let i = 0; i < this.validMoveIndicators.length; i++) {
      for (let j = 0; j < this.validMoveIndicators[i].length; j++) {
        if (this.validMoveIndicators[i][j].isValidMove) {
          validMoves.push([i, j]);
        }
      }
    }

    if (validMoves.length > 0) {
      const randomMove = validMoves[Math.floor(Math.random() * validMoves.length)];
      let chessCell = this.chessBoard[randomMove[0]][randomMove[1]];
      this.placeChessPiece(randomMove[0], randomMove[1], chessCell);
    }
  }

  placeChessPiece(i: number, j: number, chessCell: ChessCell) { // Place stone
    let reversibleTiles = this.findReversible(i, j, this.currentPlayerIsBlack ? 1 : 2);
    console.info(`reversibleTiles:${JSON.stringify(reversibleTiles)}`);

    if (reversibleTiles.length > 0) {
      if (this.currentPlayerIsBlack) {
        this.currentPlayerIsBlack = false;
        chessCell.showBlack(0);
        for (let i = 0; i < reversibleTiles.length; i++) {
          reversibleTiles[i].flip(this.transitionDuration);
        }
      } else {
        this.currentPlayerIsBlack = true;
        if (this.isTwoPlayerMode) {
          chessCell.showWhite(0);
          for (let i = 0; i < reversibleTiles.length; i++) {
            reversibleTiles[i].flip(this.transitionDuration);
          }
        } else {
          this.isAIPlaying = true;
          chessCell.showWhiteAi(this.transitionDuration, () => {
            for (let i = 0; i < reversibleTiles.length; i++) {
              reversibleTiles[i].flip(this.transitionDuration);
            }
            this.currentPlayerIsBlackChanged();
            this.isAIPlaying = false;
          });
        }
      }
    }
  }

  hasValidMoves(color: number) { // Check valid moves
    let hasMoves = false;
    for (let row = 0; row < this.chessBoardSize; row++) {
      for (let col = 0; col < this.chessBoardSize; col++) {
        if (this.chessBoard[row][col].frontVisibility === 0 && this.findReversible(row, col, color).length > 0) {
          this.validMoveIndicators[row][col].isValidMove = true;
          hasMoves = true;
        } else {
          this.validMoveIndicators[row][col].isValidMove = false;
        }
      }
    }
    return hasMoves;
  }

  aboutToAppear(): void { // Component lifecycle
    for (let i = 0; i < this.chessBoardSize; i++) {
      this.chessBoard.push([]);
      this.validMoveIndicators.push([]);
      for (let j = 0; j < this.chessBoardSize; j++) {
        this.chessBoard[i].push(new ChessCell());
        this.validMoveIndicators[i].push(new TileHighlight());
      }
    }
    this.initGame();
  }

  initGame() { // Initialize game
    this.currentPlayerIsBlack = true;
    for (let i = 0; i < this.chessBoardSize; i++) {
      for (let j = 0; j < this.chessBoardSize; j++) {
        this.chessBoard[i][j].frontVisibility = 0;
      }
    }
    // Initial board setup
    this.chessBoard[3][3].frontVisibility = 2; // White
    this.chessBoard[3][4].frontVisibility = 1; // Black
    this.chessBoard[4][3].frontVisibility = 1; // Black
    this.chessBoard[4][4].frontVisibility = 2; // White
    this.currentPlayerIsBlackChanged();
  }

  findReversible(row: number, col: number, color: number): ChessCell[] { // Find reversible stones
    let reversibleTiles: ChessCell[] = [];
    const directions = [ // 8 directions
      [-1, -1], // NW
      [-1, 0], // N
      [-1, 1], // NE
      [0, -1], // W
      [0, 1], // E
      [1, -1], // SW
      [1, 0], // S
      [1, 1] // SE
    ];
    for (const direction of directions) {
      let foundOpposite = false;
      let x = row;
      let y = col;
      do {
        x += direction[0];
        y += direction[1];
        if (x < 0 || y < 0 || x >= this.chessBoardSize || y >= this.chessBoardSize) break;
        const cell = this.chessBoard[x][y];
        if (cell.frontVisibility === 0) break;
        if (cell.frontVisibility === color) {
          if (foundOpposite) {
            let tempX: number = x - direction[0];
            let tempY: number = y - direction[1];
            while (tempX !== row || tempY !== col) {
              reversibleTiles.push(this.chessBoard[tempX][tempY]);
              tempX -= direction[0];
              tempY -= direction[1];
            }
          }
          break;
        } else {
          foundOpposite = true;
        }
      } while (true);
    }
    return reversibleTiles;
  }

  determineWinner(): string { // Determine winner
    let blackCount = 0;
    let whiteCount = 0;
    for (let row of this.chessBoard) {
      for (let cell of row) {
        if (cell.frontVisibility === 1) blackCount++;
        if (cell.frontVisibility === 2) whiteCount++;
      }
    }
    if (blackCount > whiteCount) return "Black Wins!";
    if (whiteCount > blackCount) return "White Wins!";
    return "Draw!";
  }

  hasValidMove(color: number): boolean { // Check valid moves
    for (let row = 0; row < this.chessBoardSize; row++) {
      for (let col = 0; col < this.chessBoardSize; col++) {
        if (this.chessBoard[row][col].frontVisibility === 0 && 
            this.findReversible(row, col, color).length > 0) {
          return true;
        }
      }
    }
    return false;
  }

  build() { // UI construction
    Column({ space: 20 }) {
      Row() {
        Row() { // Black player indicator
          Text(``)
            .width(`${this.cellSize}lpx`)
            .height(`${this.cellSize}lpx`)
            .textAlign(TextAlign.Center)
            .backgroundColor(Color.Black)
            .borderRadius('50%')
            .padding(10)
          Text(`Black's Turn`)
            .fontColor(Color.White)
            .padding(10)
        }
        .visibility(this.currentPlayerIsBlack ? Visibility.Visible : Visibility.Hidden)

        Row() { // White player indicator
          Text(`White's Turn`)
            .fontColor(Color.White)
            .padding(10)
          Text(``)
            .width(`${this.cellSize}lpx`)
            .height(`${this.cellSize}lpx`)
            .textAlign(TextAlign.Center)
            .backgroundColor(Color.White)
            .fontColor(Color.White)
            .borderRadius('50%')
            .padding(10)
        }
        .visibility(!this.currentPlayerIsBlack ? Visibility.Visible : Visibility.Hidden)
      }
      .width(`${(this.cellSize + this.cellSpacing * 2) * 8}lpx`)
      .justifyContent(FlexAlign.SpaceBetween)
      .margin({ top: 20 })

      Stack() { // Game board
        Flex({ wrap: FlexWrap.Wrap }) { // Valid moves layer
          ForEach(this.validMoveIndicators, (row: boolean[], _rowIndex: number) => {
            ForEach(row, (item: TileHighlight, _colIndex: number) => {
              Text(`${item.isValidMove ? '+' : ''}`)
                .width(`${this.cellSize}lpx`)
                .height(`${this.cellSize}lpx`)
                .margin(`${this.cellSpacing}lpx`)
                .fontSize(`${this.cellSize / 2}lpx`)
                .fontColor(Color.White)
                .textAlign(TextAlign.Center)
                .backgroundColor(Color.Gray)
                .borderRadius(2);
            });
          });
        }
        .width(`${(this.cellSize + this.cellSpacing * 2) * 8}lpx`)

        Flex({ wrap: FlexWrap.Wrap }) { // Stones layer
          ForEach(this.chessBoard, (row: ChessCell[], rowIndex: number) => {
            ForEach(row, (chessCell: ChessCell, colIndex: number) => {
              Text(``)
                .width(`${this.cellSize}lpx`)
                .height(`${this.cellSize}lpx`)
                .margin(`${this.cellSpacing}lpx`)
                .fontSize(`${this.cellSize / 2}lpx`)
                .textAlign(TextAlign.Center)
                .opacity(chessCell.opacity)
                .backgroundColor(chessCell.frontVisibility != 0 ? 
                  (chessCell.frontVisibility === 1 ? Color.Black : Color.White) : 
                Color.Transparent)
                .borderRadius('50%')
                .rotate({
                  x: 0,
                  y: 1,
                  z: 0,
                  angle: chessCell.rotationAngle,
                  centerX: `${this.cellSize / 2}lpx`,
                  centerY: `${this.cellSize / 2}lpx`,
                })
                .onClick(() => {
                  if (this.isAIPlaying) {
                    console.info(`AI is making move, please wait`);
                    return;
                  }
                  if (chessCell.frontVisibility === 0) {
                    this.placeChessPiece(rowIndex, colIndex, chessCell);
                  }
                });
            });
          });
        }
        .width(`${(this.cellSize + this.cellSpacing * 2) * 8}lpx`)
      }
      .padding(`${this.cellSpacing}lpx`)
      .backgroundColor(Color.Black)

      Row() { // Game mode toggle
        Text(`${this.isTwoPlayerMode ? 'Two Player' : 'Single Player'}`)
          .height(50)
          .padding({ left: 10 })
          .fontSize(16)
          .textAlign(TextAlign.Start)
          .backgroundColor(0xFFFFFF);
        Toggle({ type: ToggleType.Switch, isOn: this.isTwoPlayerMode })
          .margin({ left: 200, right: 10 })
          .onChange((isOn: boolean) => {
            this.isTwoPlayerMode = isOn;
          });
      }
      .backgroundColor(0xFFFFFF)
      .borderRadius(5);

      Button('Restart').onClick(() => {
        this.initGame();
      });
    }
    .height('100%').width('100%')
    .backgroundColor(Color.Orange);
  }
}