Flutter for OpenHarmony游戏集合App实战之数字拼图打乱排列

通过网盘分享的文件:game_flutter_openharmony.zip

链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip

前言

数字拼图(15 Puzzle)是一个经典的滑块游戏,把打乱的数字按顺序排好。

打乱是游戏的起点,但不能随便打乱------有些排列是无解的。这篇来聊聊怎么正确地打乱拼图。

数据结构

dart 复制代码
static const int size = 4;
late List<int> tiles;

这里定义了两个关键变量:

  • size是一个编译时常量,值为4,表示拼图是4x4的网格
  • tiles是一个延迟初始化的整数列表,用来存储16个格子的值

用一维数组而不是二维数组来存储,是因为一维数组操作更简单,而且GridView本身就是用线性索引的。

0表示空格,1-15是数字。空格是可以移动的位置,数字块需要滑动到空格位置。

初始化

dart 复制代码
tiles = List.generate(size * size, (i) => i);

List.generate是Dart创建列表的便捷方法:

  • 第一个参数size * size是列表长度,4x4=16
  • 第二个参数是一个函数,接收索引i,返回该位置的值

这行代码生成[0, 1, 2, ..., 15],0在第一个位置。初始状态下,0(空格)在左上角。

打乱方法

dart 复制代码
void _shuffle() {
  final random = Random();
  for (int i = 0; i < 100; i++) {
    int empty = tiles.indexOf(0);
    List<int> neighbors = [];
    if (empty % size > 0) neighbors.add(empty - 1);
    if (empty % size < size - 1) neighbors.add(empty + 1);
    if (empty >= size) neighbors.add(empty - size);
    if (empty < size * (size - 1)) neighbors.add(empty + size);
    int swap = neighbors[random.nextInt(neighbors.length)];
    tiles[empty] = tiles[swap];
    tiles[swap] = 0;
  }
}

这个方法是打乱拼图的核心。让我逐段解释:

创建随机数生成器

dart 复制代码
final random = Random();

Random()创建一个随机数生成器实例。final表示这个变量只赋值一次,但Random对象本身是可变的(可以生成不同的随机数)。

模拟移动循环

dart 复制代码
for (int i = 0; i < 100; i++) {

循环100次,每次模拟一次合法的滑动操作。

不是随机打乱数组,而是模拟100次合法移动。这样保证打乱后的状态一定有解------因为是从有解状态(初始状态)通过合法移动得到的。

找到空格

dart 复制代码
int empty = tiles.indexOf(0);

indexOf方法返回元素在列表中的索引。这里找到0(空格)的位置。

因为每次移动后空格位置会变,所以每次循环都要重新找。

找相邻格子

dart 复制代码
List<int> neighbors = [];
if (empty % size > 0) neighbors.add(empty - 1);        // 左
if (empty % size < size - 1) neighbors.add(empty + 1); // 右
if (empty >= size) neighbors.add(empty - size);        // 上
if (empty < size * (size - 1)) neighbors.add(empty + size); // 下

这段代码找出空格的相邻格子(可以移动到空格的格子)。

边界检查的逻辑:

  • 左边界 : empty % size > 0

    • empty % size得到空格在当前行的列号(0-3)
    • 如果列号大于0,说明不在最左列,左边有格子
    • 左边格子的索引是empty - 1
  • 右边界 : empty % size < size - 1

    • 如果列号小于3(size-1),说明不在最右列,右边有格子
    • 右边格子的索引是empty + 1
  • 上边界 : empty >= size

    • 如果索引大于等于4(size),说明不在第一行,上面有格子
    • 上面格子的索引是empty - size(往上一行就是减4)
  • 下边界 : empty < size * (size - 1)

    • 如果索引小于12(4x3),说明不在最后一行,下面有格子
    • 下面格子的索引是empty + size

💡 这个边界检查我调试了好几次 。一开始下边界写成empty < size * size - 1,结果最后一行中间的格子也不能往下移了。正确的应该是size * (size - 1),也就是12,最后一行的起始索引。

随机选择并交换

dart 复制代码
int swap = neighbors[random.nextInt(neighbors.length)];
tiles[empty] = tiles[swap];
tiles[swap] = 0;

这三行完成实际的交换:

  1. random.nextInt(neighbors.length)生成0到neighbors.length-1的随机整数
  2. neighbors[swap]取出随机选中的相邻格子索引
  3. 把相邻格子的值移到空格位置
  4. 把相邻格子设为0(变成新的空格)

为什么是100次

100次移动足够打乱,但不会太慢。

我试过不同的次数:

  • 20次:打乱不够,经常几步就能还原
  • 50次:还行,但有时候太简单
  • 100次:比较合适,难度适中
  • 200次:没必要,100次已经够乱了

100是个经验值,你也可以根据需要调整。

为什么不能随机打乱

如果直接用tiles.shuffle()随机打乱,有50%的概率生成无解的排列。

dart 复制代码
// 错误的做法!
tiles.shuffle();  // 可能生成无解的排列

数字拼图有个数学性质:只有"逆序数"为偶数的排列才有解。

什么是逆序数

逆序数是指:对于每对数字(i, j),如果i在j前面但i > j,就是一个逆序。

举个例子,排列[2, 1, 3]:

  • 2在1前面,2 > 1,这是一个逆序
  • 2在3前面,2 < 3,不是逆序
  • 1在3前面,1 < 3,不是逆序
  • 逆序数 = 1(奇数)

排列[2, 3, 1]:

  • 2在3前面,2 < 3,不是逆序
  • 2在1前面,2 > 1,这是一个逆序
  • 3在1前面,3 > 1,这是一个逆序
  • 逆序数 = 2(偶数)

为什么逆序数决定有解性

这涉及到群论的知识。简单说,每次合法移动(滑动一个数字块)会改变逆序数的奇偶性。

初始状态[1,2,3,...,15,0]的逆序数是0(偶数)。如果目标状态的逆序数是奇数,就永远无法通过合法移动达到。

计算逆序数的代码:

dart 复制代码
int countInversions(List<int> arr) {
  int inversions = 0;
  for (int i = 0; i < arr.length; i++) {
    for (int j = i + 1; j < arr.length; j++) {
      if (arr[i] != 0 && arr[j] != 0 && arr[i] > arr[j]) {
        inversions++;
      }
    }
  }
  return inversions;
}

但这个计算比较麻烦,不如直接用模拟移动的方法,简单又保证有解。

拼图渲染

dart 复制代码
itemBuilder: (_, i) {
  int value = tiles[i];
  if (value == 0) return Container(decoration: BoxDecoration(color: Colors.brown[200], borderRadius: BorderRadius.circular(4)));
  return GestureDetector(
    onTap: () => _tap(i),
    child: Container(
      decoration: BoxDecoration(
        color: Colors.blue[400],
        borderRadius: BorderRadius.circular(4),
        boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 2, offset: const Offset(1, 1))],
      ),
      child: Center(child: Text('$value', style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white))),
    ),
  );
},

这是GridView.builder的itemBuilder回调,负责构建每个格子的Widget。

获取格子值

dart 复制代码
int value = tiles[i];

i是GridView的线性索引(0-15),从tiles数组取出对应的值。

空格渲染

dart 复制代码
if (value == 0) return Container(decoration: BoxDecoration(color: Colors.brown[200], borderRadius: BorderRadius.circular(4)));

空格用浅棕色Container,和背景接近但能区分。

  • Colors.brown[200]是Material Design的棕色色板中较浅的一个
  • borderRadius: BorderRadius.circular(4)给四个角加4像素的圆角

空格不需要GestureDetector,因为点击空格没有意义。

数字块渲染

dart 复制代码
return GestureDetector(
  onTap: () => _tap(i),
  child: Container(
    decoration: BoxDecoration(
      color: Colors.blue[400],
      borderRadius: BorderRadius.circular(4),
      boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 2, offset: const Offset(1, 1))],
    ),
    child: Center(child: Text('$value', style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white))),
  ),
);

数字块的结构:

  • GestureDetector: 包裹整个块,检测点击事件
  • onTap : 点击时调用_tap(i),传入当前索引
  • Container: 数字块的容器
  • BoxDecoration: 装饰,包括颜色、圆角、阴影
  • Center + Text: 居中显示数字

阴影的参数:

  • color: Colors.black26:26%透明度的黑色
  • blurRadius: 2:模糊半径2像素
  • offset: const Offset(1, 1):向右下偏移1像素

棋盘背景

dart 复制代码
Container(
  margin: const EdgeInsets.all(16),
  padding: const EdgeInsets.all(4),
  decoration: BoxDecoration(color: Colors.brown[300], borderRadius: BorderRadius.circular(8)),

棋盘容器的设置:

  • margin: 外边距16像素,让棋盘不贴着屏幕边缘
  • padding: 内边距4像素,让数字块不贴着棋盘边缘
  • color: 棕色背景,模拟木质拼图板
  • borderRadius: 8像素圆角,比数字块的圆角大一点

padding配合GridView的间距,形成网格线效果。

GridView间距

dart 复制代码
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: size, mainAxisSpacing: 4, crossAxisSpacing: 4),

GridView的布局配置:

  • crossAxisCount: size: 每行4个格子
  • mainAxisSpacing: 4: 主轴(垂直)方向间距4像素
  • crossAxisSpacing: 4: 交叉轴(水平)方向间距4像素

4像素间距露出棕色背景,形成网格效果。

胜利条件

dart 复制代码
bool _checkWin() {
  for (int i = 0; i < tiles.length - 1; i++) {
    if (tiles[i] != i + 1) return false;
  }
  return tiles.last == 0;
}

这个方法检查拼图是否完成:

检查前15个位置

dart 复制代码
for (int i = 0; i < tiles.length - 1; i++) {
  if (tiles[i] != i + 1) return false;
}

遍历索引0-14,检查每个位置的值是否等于索引+1。

  • 索引0应该是1
  • 索引1应该是2
  • ...
  • 索引14应该是15

任何一个不对就返回false。

检查最后一个位置

dart 复制代码
return tiles.last == 0;

最后一个位置(索引15)应该是0(空格)。

正确顺序是[1, 2, 3, ..., 15, 0]。

完整的游戏初始化

dart 复制代码
void _initGame() {
  tiles = List.generate(size * size, (i) => i);
  moves = 0;
  _shuffle();
}

初始化游戏的步骤:

  1. 生成有序数组[0, 1, 2, ..., 15]
  2. 重置步数为0
  3. 调用_shuffle打乱

注意_shuffle是在有序数组基础上打乱的,这保证了有解。

小结

这篇讲了数字拼图的打乱排列,核心知识点:

  • 一维数组: 用16个元素表示4x4拼图,比二维数组操作更简单
  • 模拟移动打乱: 从有解状态出发,通过合法移动打乱,保证结果有解
  • 边界检查: 用取余和比较判断相邻格子,注意边界条件
  • 100次移动: 经验值,足够打乱又不会太慢
  • 逆序数: 随机打乱可能无解的数学原因
  • 空格显示: 浅棕色,和背景区分
  • 数字块设计: 蓝色背景 + 白色数字 + 圆角 + 阴影

打乱是数字拼图的关键,用模拟移动的方法既简单又可靠。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
2501_944424122 小时前
Flutter for OpenHarmony游戏集合App实战之记忆翻牌表情图案
开发语言·javascript·flutter·游戏·harmonyos
爱吃大芒果2 小时前
Flutter for OpenHarmony前置知识:Dart 语法核心知识点总结(上)
开发语言·flutter·dart
运维行者_2 小时前
OpManager 对接 ERP 避坑指南,网络自动化提升数据同步效率
运维·服务器·开发语言·网络·microsoft·网络安全·php
文 丰2 小时前
【Android Studio】gradle下载慢解决方案(替换配置-非手工下载安装包)
android·ide·android studio
爱编程的小庄2 小时前
Rust初识
开发语言·rust
23124_802 小时前
热身签到-ctfshow
开发语言·python
小白学大数据2 小时前
移动端Temu App数据抓包与商品爬取方案
开发语言·爬虫·python
吃吃喝喝小朋友2 小时前
JavaScript文件的操作方法
开发语言·javascript·ecmascript
2301_797312262 小时前
学习Java42天
java·开发语言·学习