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();
}
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();
}
}
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());
}
}
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;
}
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;
}
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);
}
}
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)
}
Conclusion
This implementation demonstrates several key HarmonyOS NEXT features:
- Declarative UI construction with ArkUI components
- Reactive state management using @State decorators
- Gesture recognition with built-in swipe detection
- Animation integration with transition effects
- 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)