// board.dart import 'dart:async'; import 'dart:math'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:image_puzzle/config/device.dart'; import 'package:image_puzzle/play/piece.dart'; import 'package:logging/logging.dart'; import 'package:vector_math/vector_math.dart' as vmath; final Logger _log = Logger('board.dart'); enum BoardStatus { loading, playing, success } class Board { // 原图 final ui.Image image; // 所有拼图碎片 final List pieces = []; /// 拼图行数(3/4/5,对应9/16/25宫格) final int rows; /// 拼图列数(3/4/5) final int cols; /// 整个拼图在屏幕上的目标区域(最终完整显示的位置和大小) final Rect targetRect; // 碎片的逻辑宽高 double get pieceLogicalWidth => targetRect.width / cols; double get pieceLogicalHeight => targetRect.height / rows; // 设备信息 final Device device; // 静态背景绘图 Picture (只绘制一次) ui.Picture? _backgroundPicture; ui.Picture? get backgroundPicture => _backgroundPicture; ValueNotifier boardNotifier = ValueNotifier(1); // 用户是否真正可以开始动手开玩(所有加载初始化完成,并且进入的插屏广告播放完成) final Completer startPlayCompleter = Completer(); BoardStatus _status = BoardStatus.loading; BoardStatus get status => _status; // 备份的 groups (PieceGroup 对象的列表) List backupGroups = []; void start() { _status = BoardStatus.playing; invalidate(); } void success() { _status = BoardStatus.success; invalidate(); } void invalidate() { boardNotifier.value++; } // 是否全部完成 bool get isAllDone => pieces.every((p) => p.isOK); final TickerProviderStateMixin ticker; Board(this.ticker, this.image, this.cols, this.rows, this.targetRect, this.device, {Map? json}) { _recordBackground(); // 录制静态背景,提升性能 _initPieces(); rebuildAllGroups(); backupAllGroups(); } /// 初始化碎片 _initPieces() { final double imagePixelWidth = image.width.toDouble(); // 图片像素宽度 final double imagePixelHeight = image.height.toDouble(); // 图片像素高度 final double pieceLogicalWidth = targetRect.width / cols; // 绘图区域逻辑宽度 / 列数 = 每个piece的逻辑宽度 final double pieceLogicalHeight = targetRect.height / rows; // 绘图区域逻辑高度 / 行数 = 每个piece的逻辑高度 final double scaleX = imagePixelWidth / targetRect.width; // 像素宽度与逻辑宽度的scale比例 final double scaleY = imagePixelHeight / targetRect.height; // 像素高度与逻辑高度的scale比例 final double piecePixelWidth = pieceLogicalWidth * scaleX; // 每个piece的像素宽度 final double piecePixelHeight = pieceLogicalHeight * scaleY; // 每个piece的像素高度 final List<({int col, int row})> slotCoords = []; for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { slotCoords.add((col: c, row: r)); } } slotCoords.shuffle(Random()); int index = 0; for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { final correctX = targetRect.left + c * pieceLogicalWidth; // piece的正确位置坐标 x final correctY = targetRect.top + r * pieceLogicalHeight; // piece的正确位置坐标 y final correctOffset = Offset(correctX, correctY); final initialSlot = slotCoords[index]; final initialX = targetRect.left + initialSlot.col * pieceLogicalWidth; // 初始位置坐标 final initialY = targetRect.top + initialSlot.row * pieceLogicalHeight; final initialTransform = vmath.Matrix4.translationValues(initialX, initialY, 0.0); // 记录初始位置坐标transform matrix final sourceRect = Rect.fromLTWH(c * piecePixelWidth, r * piecePixelHeight, piecePixelWidth, piecePixelHeight); // 对应图片的像素矩形 pieces.add( Piece( board: this, index: index, row: r, col: c, rows: rows, cols: cols, correctOffset: correctOffset, curCol: initialSlot.col, curRow: initialSlot.row, sourceRect: sourceRect, transform: initialTransform, borders: [true, true, true, true], ), ); index++; } } } // 根据坐标查找指定位置的碎片 Piece? findPieceAt(Offset localPos) { for (var piece in pieces.reversed) { // 从上层开始找 // 计算碎片在画布上的绝对位置board final transform = piece.transform; final posX = transform.storage[12]; final posY = transform.storage[13]; final pieceRect = Rect.fromLTWH(posX, posY, pieceLogicalWidth, pieceLogicalHeight); if (pieceRect.contains(localPos)) { return piece; } } return null; } // 查找某个坐标上的碎片,排除某个piece Piece? findPieceAtExclude(Offset localPos, Piece excludePiece) { for (var piece in pieces.reversed) { // 从上层开始找 if (piece == excludePiece) continue; // 计算碎片在画布上的绝对位置 final transform = piece.transform; final posX = transform.storage[12]; final posY = transform.storage[13]; final pieceRect = Rect.fromLTWH(posX, posY, pieceLogicalWidth, pieceLogicalHeight); if (pieceRect.contains(localPos)) { return piece; } } return null; } vmath.Matrix4 getTransformByCoordinate(int row, int col) { final x = targetRect.left + col * pieceLogicalWidth; final y = targetRect.top + row * pieceLogicalHeight; final transform = vmath.Matrix4.translationValues(x, y, 0.0); return transform; } // 根据坐标获取该位置上的piece Piece? getPieceByCoordinate(int row, int col) { return pieces.firstWhereOrNull((p) => p.curRow == row && p.curCol == col); } // 根据坐标获取该位置上的piece Piece? getPieceByIndex(int index) { return pieces.firstWhereOrNull((p) => p.index == index); } // 初始化检查合并分组 (改进版:固定点迭代合并) void rebuildAllGroups() { _log.info('rebuildAllGroups'); // 1. 清除所有旧组 和 原path for (var p in pieces) { p.group = null; p.path = null; p.outLinePath = null; p.innerLinePath = null; } bool mergedInPass; // 2. 迭代合并,直到一轮循环中没有发生任何合并 do { mergedInPass = false; // 3. 遍历所有碎片对 (i, j) for (int i = 0; i < pieces.length; i++) { for (int j = i + 1; j < pieces.length; j++) { final piece = pieces[i]; final otherPiece = pieces[j]; // 4. 检查是否可以合并,并且它们不属于同一个组 if (piece.canMerge(otherPiece)) { if (!piece.isSameGroup(otherPiece)) { // 5. 合并组 // piece.groupWith(otherPiece) 会创建一个新的 PieceGroup, // 并将 piece 和 otherPiece (以及它们可能已有的组员) 的 group 引用全部指向新组。 piece.groupWith(otherPiece); mergedInPass = true; } } } } // 如果 mergedInPass 为 true,说明本轮循环发生了合并,需要重新开始下一轮遍历 // 因为新的合并可能促使其他碎片或碎片组也得以连接 } while (mergedInPass); } // 备份所有group, 方便进行比较, 发现新合成的group,以便呈现动画特效 void backupAllGroups() { backupGroups.clear(); for (var p in pieces) { if (p.group != null && !backupGroups.contains(p.group)) { backupGroups.add(p.group!); } } _log.info('backupAllGroups: ${backupGroups.length}'); } // 当前group与之前备份的group进行比较,返回新合并的group列表,这些group需要展示动画特效 List compareAllGroups() { _log.info('compareAllGroups'); List newGroups = []; List currentGroups = []; for (var p in pieces) { if (p.group != null && !currentGroups.contains(p.group)) { currentGroups.add(p.group!); } } if (backupGroups.isEmpty) { // 之前都不存在群组,那么当前新合成的所有group都是新group newGroups.addAll(currentGroups); } else { for (var g in currentGroups) { bool alreadyExists = false; for (var bakgroup in backupGroups) { if (bakgroup.containsGroup(g)) { alreadyExists = true; break; } } if (!alreadyExists) { newGroups.add(g); } } } if (newGroups.isNotEmpty) { for (var g in newGroups) { _log.info('发现新群组:'); g.print(); } } return newGroups; } // 检查游戏是否全部完成 bool checkWinCondition() { return pieces.every((p) => p.isOK); } /// 退出释放资源 dispose() { _backgroundPicture?.dispose(); _backgroundPicture = null; image.dispose(); boardNotifier.dispose(); } // 录制背景,避免每次都重复绘制,提升性能 void _recordBackground() { // 确保在录制前释放旧的 Picture 资源 _backgroundPicture?.dispose(); _backgroundPicture = null; final recorder = ui.PictureRecorder(); // 录制器的边界设置为整个屏幕尺寸,因为我们在这里绘制的是 full screen background。 final recordBounds = Rect.fromLTWH(0, 0, device.screenSize.width, device.screenSize.height); final canvas = Canvas(recorder, recordBounds); // --- 静态绘制配置 --- const double cornerRadius = 8.0; const double strokeWidth = 1.0; // 拼图槽位的线宽 final double halfStroke = strokeWidth / 2.0; // 1. 绘制整个屏幕背景 canvas.drawRect( recordBounds, Paint() ..color = Colors .lightGreen // 主背景色 ..style = PaintingStyle.fill, ); // 2. 绘制每个拼图槽位(圆角矩形)作为背景的一部分 (静态内容) final slotFillPaint = Paint() ..color = Colors.green // .shade100 // 槽位填充色 ..style = PaintingStyle.fill; final slotStrokePaint = Paint() ..color = Color(0xff26600c) // 槽位边框色 ..style = PaintingStyle.stroke ..strokeWidth = strokeWidth; for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { // 计算当前槽位的边界 (Canvas坐标系) final left = targetRect.left + c * pieceLogicalWidth; final top = targetRect.top + r * pieceLogicalHeight; final right = left + pieceLogicalWidth; final bottom = top + pieceLogicalHeight; // 为了避免描边溢出到相邻槽位,将矩形向内收缩 halfStroke final slotRect = Rect.fromLTRB(left + halfStroke, top + halfStroke, right - halfStroke, bottom - halfStroke); final slotRRect = RRect.fromRectAndRadius(slotRect, const Radius.circular(cornerRadius)); // 绘制填充 canvas.drawRRect(slotRRect, slotFillPaint); // 绘制描边 canvas.drawRRect(slotRRect, slotStrokePaint); } } // --- 结束录制并存储 --- _backgroundPicture = recorder.endRecording(); _log.info('Static background picture recorded. Size: ${recordBounds.size}'); } } // 辅助扩展:解决 Dart 缺少 firstWhereOrNull 的问题 extension IterableExtension on Iterable { T? firstWhereOrNull(bool Function(T element) test) { for (final element in this) { if (test(element)) { return element; } } return null; } }