鸿蒙开发案例:实现数字华容道游戏

 

数字华容道游戏是一种经典的益智游戏,由8个编号为1至8的方块和一个空白格组成。玩家通过滑动方块将其按照正确的顺序排列。

【支持的功能】

1. 点击操作
游戏支持通过点击数字方块来移动它们。当玩家点击一个可移动的方块时,该方块会自动滑向相邻的空白格。
2. 滑动操作
除了点击操作之外,游戏还支持滑动手势来移动方块。玩家可以通过在屏幕上滑动来指示移动方向,游戏会检测滑动的方向并相应地移动方块。
3. 动画效果
方块在移动时会有流畅的动画效果,这使得游戏的操作体验更加自然和直观。动画效果可以通过shouldAnimate状态开关来控制开启或关闭。
4. 游戏胜利提示
当玩家成功将所有方块按照正确的顺序排列时,游戏会弹出一个对话框提示玩家胜利,并显示所用的时间。玩家可以选择重新开始游戏。
5. 重新开始按钮
游戏提供了一个“重新开始”按钮,玩家可以在任何时候点击此按钮来重新开始游戏。重新开始会打乱游戏面板,使游戏回到初始状态。

【注意事项】

1. 确保游戏始终可解
在设计游戏时,一个重要的注意事项是确保游戏面板被打乱后仍可解。如果随机打乱游戏面板,可能会导致某些情况下游戏无解。因此,我们在打乱游戏面板时,采用了模拟合法滑动的方式,即通过一系列合法的滑动操作来打乱面板,而不是简单地随机交换方块位置。这样可以确保每次游戏都有解。
2. 操作体验优化
为了提供更好的操作体验,游戏设计时充分考虑了用户的交互习惯。点击和滑动两种操作方式的结合使得游戏更加灵活和方便。此外,动画效果的加入让方块的移动更加流畅自然,增强了游戏的趣味性和互动性。

【完整代码】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct NumberPuzzle {
  @State gameBoard: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 0]; // 游戏面板数据,0表示空白格
  @State selectedTile: number = -1; // 当前选中的方块
  @State isGameOver: boolean = false; // 游戏是否结束
  @State cellSize: number = 100; // 单元格大小
  @State cellMargin: number = 5; // 单元格边距
  @State startTime: number = 0; // 游戏开始时间
  @State screenStartX: number = 0; // 触摸开始时的屏幕X坐标
  @State screenStartY: number = 0; // 触摸开始时的屏幕Y坐标
  @State lastScreenX: number = 0; // 触摸结束时的屏幕X坐标
  @State lastScreenY: number = 0; // 触摸结束时的屏幕Y坐标
  @State shouldAnimate: boolean = true; // 控制动画是否开启
 
  aboutToAppear(): void {
    this.shuffleGameBoard(); // 在组件出现之前打乱游戏面板
  }
 
  // 检查指定位置的方块是否可以移动到空白处
  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) ||
        (tileCol === blankCol && Math.abs(tileRow - blankRow) === 1)
    );
  }
 
  // 移动方块
  private moveTile(tileIndex: number) {
    if (this.canMove(tileIndex)) {
      const blankIndex = this.gameBoard.indexOf(0);
      let temp = this.gameBoard[tileIndex];
      this.gameBoard[tileIndex] = this.gameBoard[blankIndex];
      this.gameBoard[blankIndex] = temp;
      this.selectedTile = -1;
      this.checkForWin(); // 检查是否获胜
    }
  }
 
  // 检查是否获胜
  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: '游戏胜利!',
        message: '恭喜你,用时:' + ((Date.now() - this.startTime) / 1000).toFixed(3) + '秒',
        buttons: [{ text: '重新开始', color: '#ffa500' }]
      }).then(() => {
        this.shuffleGameBoard(); // 重新开始游戏
      });
    }
  }
 
  // 打乱游戏面板
  private shuffleGameBoard() {
    this.startTime = Date.now();
    let tempBoard = [...this.gameBoard];
    let moves = 0;
    const maxMoves = 10000; // 最大移动次数
    this.shouldAnimate = false; // 关闭动画
 
    // 寻找空白格的位置
    const findBlankIndex = () => tempBoard.indexOf(0);
 
    // 合法的移动方向
    const validDirections = (index: number) => {
      let valid: string[] = [];
      if (index % 3 > 0) {
        valid.push('left');
      }
      if (index % 3 < 2) {
        valid.push('right');
      }
      if (index >= 3) {
        valid.push('up');
      }
      if (index <= 5) {
        valid.push('down');
      }
      return valid;
    };
 
    // 移动空白格
    const moveBlank = (direction: string, index: number) => {
      let newIndex = index;
      switch (direction) {
        case 'up':
          newIndex -= 3;
          break;
        case 'down':
          newIndex += 3;
          break;
        case 'left':
          newIndex -= 1;
          break;
        case 'right':
          newIndex += 1;
          break;
      }
      let temp = tempBoard[newIndex];
      tempBoard[newIndex] = tempBoard[index];
      tempBoard[index] = temp;
    };
 
    // 模拟手势移动
    while (moves < maxMoves) {
      const blankIndex = findBlankIndex();
      const possibleDirections = validDirections(blankIndex);
      if (possibleDirections.length > 0) {
        const direction = possibleDirections[Math.floor(Math.random() * possibleDirections.length)];
        moveBlank(direction, blankIndex);
        moves++;
      } else {
        break;
      }
    }
 
    this.gameBoard = tempBoard;
    this.selectedTile = -1;
    this.isGameOver = false;
    this.shouldAnimate = true; // 重新开启动画
  }
 
  // 更新动画
  private updateAnim(index: number) {
    if (!this.shouldAnimate) return undefined;
    if (this.canMove(index)) {
      const blankIndex = this.gameBoard.indexOf(0);
      const diff = Math.abs(index - blankIndex);
      if (diff === 1) { // 左右移动
        return TransitionEffect.translate({
          x: `${(diff === 1 ? (index > blankIndex ? -1 : 1) : 0) * (this.cellSize + this.cellMargin * 2)}lpx`
        }).animation({ duration: 100 });
      } else if (diff === 3) { // 上下移动
        return TransitionEffect.translate({
          y: `${(diff === 3 ? (index > blankIndex ? -1 : 1) : 0) * (this.cellSize + this.cellMargin * 2)}lpx`
        }).animation({ duration: 100 });
      }
    }
    return undefined;
  }
 
  build() {
    Column({ space: 10 }) { // 主容器
      // 游戏面板容器
      Flex({ wrap: FlexWrap.Wrap, direction: FlexDirection.Row }) {
        ForEach(this.gameBoard, (item: number, index: number) => {
          Text(`${item}`) // 显示数字文本
            .width(`${this.cellSize}lpx`) // 设置宽度
            .height(`${this.cellSize}lpx`) // 设置高度
            .margin(`${this.cellMargin}lpx`) // 设置外边距
            .fontSize(`${this.cellSize / 2}lpx`) // 设置字体大小
            .textAlign(TextAlign.Center) // 文本居中
            .backgroundColor(this.gameBoard[index] === 0 ? Color.White : Color.Orange) // 背景颜色
            .fontColor(Color.White) // 字体颜色
            .borderRadius(5) // 圆角
            .visibility(item == 0 ? Visibility.Hidden : Visibility.Visible) // 隐藏空白格
            .transition(this.updateAnim(index)) // 设置动画过渡
            .onClick(() => {
              if (this.canMove(index)) {
                this.moveTile(index);
              }
            }); // 点击事件
        })
      }
      .width(`${(this.cellSize + this.cellMargin * 2) * 3}lpx`) // 设置容器宽度
 
      // 重新开始按钮
      Button('重新开始')
        .width('50%') // 设置按钮宽度
        .height('10%') // 设置按钮高度
        .onClick(() => {
          this.shuffleGameBoard(); // 重新开始游戏
        });
    }
    .width('100%') // 设置主容器宽度
    .height('100%') // 设置主容器高度
    .onTouch((e) => {
      if (e.type === TouchType.Down && e.touches.length > 0) { // 触摸开始,记录初始位置
        this.screenStartX = e.touches[0].x;
        this.screenStartY = e.touches[0].y;
      } else if (e.type === TouchType.Up && e.changedTouches.length > 0) { // 当手指抬起时,更新最后的位置
        this.lastScreenX = e.changedTouches[0].x;
        this.lastScreenY = e.changedTouches[0].y;
      }
    })
    .gesture(
      SwipeGesture({ direction: SwipeDirection.All }) // 支持方向中 all可以是上下左右
        .onAction((_event: GestureEvent) => {
          const swipeX = this.lastScreenX - this.screenStartX;
          const swipeY = this.lastScreenY - this.screenStartY;
 
          // 判断滑动方向
          let directionText = '';
          if (Math.abs(swipeX) > Math.abs(swipeY)) {
            if (swipeX > 0) {
              directionText = 'Right'; // 向右滑动
            } else {
              directionText = 'Left'; // 向左滑动
            }
          } else {
            if (swipeY > 0) {
              directionText = 'Down'; // 向下滑动
            } else {
              directionText = 'Up'; // 向上滑动
            }
          }
 
          console.info('====滑动方向:', directionText);
          // console.info('====起点x:', this.screenStartX);
          // console.info('====起点y:', this.screenStartY);
          // console.info('====终点x:', this.lastScreenX);
          // console.info('====终点y:', this.lastScreenY);
 
          // 清除开始位置记录,准备下一次滑动判断
          this.screenStartX = 0;
          this.screenStartY = 0;
 
          this.moveOnSwipe(directionText); // 根据方向移动方块
        })
    )
  }
 
  private moveOnSwipe(direction: string) {
    const blankIndex = this.gameBoard.indexOf(0);
    let newIndex = blankIndex;
 
    switch (direction) {
      case 'Up':
        newIndex = blankIndex + 3;
        break;
      case 'Down':
        newIndex = blankIndex - 3;
        break;
      case 'Left':
        newIndex = blankIndex + 1;
        break;
      case 'Right':
        newIndex = blankIndex - 1;
        break;
    }
 
    if (this.isValidMove(newIndex)) {
      this.moveTile(newIndex);
    }
  }
 
  private isValidMove(newIndex: number): boolean {
    // 检查新位置是否越界
    return newIndex >= 0 && newIndex < this.gameBoard.length;
  }
}

  

posted @   zhongcx  阅读(38)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示