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

[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.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);
}
}