board.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. // board.dart
  2. import 'dart:async';
  3. import 'dart:math';
  4. import 'dart:typed_data';
  5. import 'dart:ui' as ui;
  6. import 'package:flutter/material.dart';
  7. import 'package:image_puzzle/config/device.dart';
  8. import 'package:image_puzzle/play/piece.dart';
  9. import 'package:logging/logging.dart';
  10. import 'package:vector_math/vector_math.dart' as vmath;
  11. final Logger _log = Logger('board.dart');
  12. // 增加一个preparing 和 shuffle洗牌的状态
  13. // preparing: Opacity透明度动画展示核心绘制区
  14. // shuffle: 发牌和翻牌动画构成
  15. enum BoardStatus { loading, preparing, shuffle, playing, success }
  16. // 发牌阶段,dealing:发牌中(发牌动画), fliping:翻转中(翻转动画)
  17. enum ShuffleStep { dealing, flipping }
  18. class Board {
  19. // 原图
  20. final ui.Image image;
  21. // 纸牌背面图,用于
  22. final ui.Image cardImage;
  23. // 所有拼图碎片
  24. final List<Piece> pieces = [];
  25. /// 拼图行数(3/4/5,对应9/16/25宫格)
  26. final int rows;
  27. /// 拼图列数(3/4/5)
  28. final int cols;
  29. /// 整个拼图在屏幕上的目标区域(最终完整显示的位置和大小)
  30. final Rect targetRect;
  31. // 碎片的逻辑宽高
  32. double get pieceLogicalWidth => targetRect.width / cols;
  33. double get pieceLogicalHeight => targetRect.height / rows;
  34. // 设备信息
  35. final Device device;
  36. // 静态背景绘图 Picture (只绘制一次)
  37. ui.Picture? _backgroundPicture;
  38. ui.Picture? get backgroundPicture => _backgroundPicture;
  39. ValueNotifier boardNotifier = ValueNotifier(1);
  40. // 用户是否真正可以开始动手开玩(所有加载初始化完成,并且进入的插屏广告播放完成)
  41. final Completer<bool> PlayCompleter = Completer();
  42. BoardStatus _status = BoardStatus.loading;
  43. BoardStatus get status => _status;
  44. // 备份的 groups (PieceGroup 对象的列表)
  45. List<PieceGroup> backupGroups = [];
  46. ShuffleStep _shuffleStep = ShuffleStep.dealing;
  47. ShuffleStep get shuffleStep => _shuffleStep;
  48. void shuffle(ShuffleStep step) {
  49. _status = BoardStatus.shuffle;
  50. _shuffleStep = step;
  51. invalidate();
  52. }
  53. void prepare() {
  54. _status = BoardStatus.preparing;
  55. invalidate();
  56. }
  57. void start() {
  58. _status = BoardStatus.playing;
  59. invalidate();
  60. }
  61. void success() {
  62. _status = BoardStatus.success;
  63. invalidate();
  64. }
  65. void invalidate() {
  66. boardNotifier.value++;
  67. }
  68. // 是否全部完成
  69. bool get isAllDone => pieces.every((p) => p.isOK);
  70. final TickerProviderStateMixin ticker;
  71. Board(this.ticker, this.image, this.cardImage, this.rows, this.cols, this.targetRect, this.device, {Map<String, dynamic>? json}) {
  72. _recordBackground(); // 录制静态背景,提升性能
  73. _initPieces();
  74. rebuildAllGroups();
  75. }
  76. /// 初始化碎片
  77. void _initPieces() {
  78. pieces.clear();
  79. final imageWidth = image.width.toDouble();
  80. final imageHeight = image.height.toDouble();
  81. final pieceWidth = imageWidth / cols;
  82. final pieceHeight = imageHeight / rows;
  83. for (int i = 0; i < rows; i++) {
  84. for (int j = 0; j < cols; j++) {
  85. final index = i * cols + j;
  86. final sourceRect = Rect.fromLTWH(j * pieceWidth, i * pieceHeight, pieceWidth, pieceHeight);
  87. final correctOffset = Offset(targetRect.left + j * pieceLogicalWidth, targetRect.top + i * pieceLogicalHeight);
  88. final transform = getTransformByCoordinate(i, j);
  89. pieces.add(
  90. Piece(
  91. board: this,
  92. index: index,
  93. row: i,
  94. col: j,
  95. rows: rows,
  96. cols: cols,
  97. correctOffset: correctOffset,
  98. sourceRect: sourceRect,
  99. curRow: i,
  100. curCol: j,
  101. transform: transform,
  102. ),
  103. );
  104. }
  105. }
  106. _shufflePieces();
  107. _sortPieces();
  108. _log.info('_initPieces');
  109. }
  110. // 洗牌(随机打乱碎片位置)
  111. void _shufflePieces() {
  112. final shuffledPositions = List.generate(pieces.length, (i) => i)..shuffle();
  113. for (int i = 0; i < pieces.length; i++) {
  114. final targetIndex = shuffledPositions[i];
  115. pieces[i].curRow = targetIndex ~/ cols;
  116. pieces[i].curCol = targetIndex % cols;
  117. pieces[i].transform = getTransformByCoordinate(pieces[i].curRow, pieces[i].curCol);
  118. }
  119. }
  120. void resetAllPieces() {
  121. for (var p in pieces) {
  122. p.transform = getTransformByCoordinate(p.curRow, p.curCol);
  123. }
  124. }
  125. void setAllPieceToBottomRight() {
  126. for (var p in pieces) {
  127. p.transform = getBottomRightTransform();
  128. }
  129. }
  130. // 是否应该根据当前的curRow和curCol对pieces做一次排序,以方便dealing动画实现依次从左上到右下的发牌效果
  131. void _sortPieces() {
  132. pieces.sort((Piece a, Piece b) {
  133. int ret = a.curRow - b.curRow;
  134. if (ret == 0) {
  135. ret = a.curCol - b.curCol;
  136. }
  137. return ret;
  138. });
  139. }
  140. // 根据坐标查找指定位置的碎片
  141. Piece? findPieceAt(Offset localPos) {
  142. for (var piece in pieces.reversed) {
  143. // 从上层开始找
  144. // 计算碎片在画布上的绝对位置board
  145. final transform = piece.transform;
  146. final posX = transform.storage[12];
  147. final posY = transform.storage[13];
  148. final pieceRect = Rect.fromLTWH(posX, posY, pieceLogicalWidth, pieceLogicalHeight);
  149. if (pieceRect.contains(localPos)) {
  150. return piece;
  151. }
  152. }
  153. return null;
  154. }
  155. // 查找某个坐标上的碎片,排除某个piece
  156. Piece? findPieceAtExclude(Offset localPos, Piece excludePiece) {
  157. for (var piece in pieces.reversed) {
  158. // 从上层开始找
  159. if (piece == excludePiece) continue;
  160. // 计算碎片在画布上的绝对位置
  161. final transform = piece.transform;
  162. final posX = transform.storage[12];
  163. final posY = transform.storage[13];
  164. final pieceRect = Rect.fromLTWH(posX, posY, pieceLogicalWidth, pieceLogicalHeight);
  165. if (pieceRect.contains(localPos)) {
  166. return piece;
  167. }
  168. }
  169. return null;
  170. }
  171. // 获取右下角变换矩阵
  172. vmath.Matrix4 getBottomRightTransform() {
  173. return getTransformByCoordinate(rows - 1, cols - 1);
  174. }
  175. // 根据格子位置(row, col)获取变换矩阵
  176. vmath.Matrix4 getTransformByCoordinate(int row, int col) {
  177. final x = targetRect.left + col * pieceLogicalWidth;
  178. final y = targetRect.top + row * pieceLogicalHeight;
  179. final transform = vmath.Matrix4.translationValues(x, y, 0.0);
  180. return transform;
  181. }
  182. // 根据坐标获取该位置上的piece
  183. Piece? getPieceByCoordinate(int row, int col) {
  184. return pieces.firstWhereOrNull((p) => p.curRow == row && p.curCol == col);
  185. }
  186. // 根据坐标获取该位置上的piece
  187. Piece? getPieceByIndex(int index) {
  188. return pieces.firstWhereOrNull((p) => p.index == index);
  189. }
  190. // 初始化检查合并分组 (改进版:固定点迭代合并)
  191. void rebuildAllGroups() {
  192. _log.info('rebuildAllGroups');
  193. // 1. 清除所有旧组 和 原path
  194. for (var p in pieces) {
  195. p.group = null;
  196. p.path = null;
  197. p.outLinePath = null;
  198. p.innerLinePath = null;
  199. }
  200. bool mergedInPass;
  201. // 2. 迭代合并,直到一轮循环中没有发生任何合并
  202. do {
  203. mergedInPass = false;
  204. // 3. 遍历所有碎片对 (i, j)
  205. for (int i = 0; i < pieces.length; i++) {
  206. for (int j = i + 1; j < pieces.length; j++) {
  207. final piece = pieces[i];
  208. final otherPiece = pieces[j];
  209. // 4. 检查是否可以合并,并且它们不属于同一个组
  210. if (piece.canMerge(otherPiece)) {
  211. if (!piece.isSameGroup(otherPiece)) {
  212. // 5. 合并组
  213. // piece.groupWith(otherPiece) 会创建一个新的 PieceGroup,
  214. // 并将 piece 和 otherPiece (以及它们可能已有的组员) 的 group 引用全部指向新组。
  215. piece.groupWith(otherPiece);
  216. mergedInPass = true;
  217. }
  218. }
  219. }
  220. }
  221. // 如果 mergedInPass 为 true,说明本轮循环发生了合并,需要重新开始下一轮遍历
  222. // 因为新的合并可能促使其他碎片或碎片组也得以连接
  223. } while (mergedInPass);
  224. }
  225. // 备份所有group, 方便进行比较, 发现新合成的group,以便呈现动画特效
  226. void backupAllGroups() {
  227. backupGroups.clear();
  228. for (var p in pieces) {
  229. if (p.group != null && !backupGroups.contains(p.group)) {
  230. backupGroups.add(p.group!);
  231. }
  232. }
  233. _log.info('backupAllGroups: ${backupGroups.length}');
  234. }
  235. // 当前group与之前备份的group进行比较,返回新合并的group列表,这些group需要展示动画特效
  236. List<PieceGroup> compareAllGroups() {
  237. _log.info('compareAllGroups');
  238. List<PieceGroup> newGroups = [];
  239. List<PieceGroup> currentGroups = [];
  240. for (var p in pieces) {
  241. if (p.group != null && !currentGroups.contains(p.group)) {
  242. currentGroups.add(p.group!);
  243. }
  244. }
  245. if (backupGroups.isEmpty) {
  246. // 之前都不存在群组,那么当前新合成的所有group都是新group
  247. newGroups.addAll(currentGroups);
  248. } else {
  249. for (var g in currentGroups) {
  250. bool alreadyExists = false;
  251. for (var bakgroup in backupGroups) {
  252. if (bakgroup.containsGroup(g)) {
  253. alreadyExists = true;
  254. break;
  255. }
  256. }
  257. if (!alreadyExists) {
  258. newGroups.add(g);
  259. }
  260. }
  261. }
  262. if (newGroups.isNotEmpty) {
  263. for (var g in newGroups) {
  264. _log.info('发现新群组:');
  265. g.print();
  266. }
  267. }
  268. return newGroups;
  269. }
  270. // 检查游戏是否全部完成
  271. bool checkWinCondition() {
  272. return pieces.every((p) => p.isOK);
  273. }
  274. /// 退出释放资源
  275. dispose() {
  276. _backgroundPicture?.dispose();
  277. _backgroundPicture = null;
  278. image.dispose();
  279. boardNotifier.dispose();
  280. }
  281. // 录制背景,避免每次都重复绘制,提升性能
  282. void _recordBackground() {
  283. // 确保在录制前释放旧的 Picture 资源
  284. _backgroundPicture?.dispose();
  285. _backgroundPicture = null;
  286. final recorder = ui.PictureRecorder();
  287. // 录制器的边界设置为整个屏幕尺寸,因为我们在这里绘制的是 full screen background。
  288. final recordBounds = Rect.fromLTWH(0, 0, device.screenSize.width, device.screenSize.height);
  289. final canvas = Canvas(recorder, recordBounds);
  290. // --- 静态绘制配置 ---
  291. const double cornerRadius = 8.0;
  292. const double strokeWidth = 1.0; // 拼图槽位的线宽
  293. final double halfStroke = strokeWidth / 2.0;
  294. // 1. 绘制整个屏幕背景
  295. canvas.drawRect(
  296. recordBounds,
  297. Paint()
  298. ..color = Colors
  299. .lightGreen // 主背景色
  300. ..style = PaintingStyle.fill,
  301. );
  302. // 2. 绘制每个拼图槽位(圆角矩形)作为背景的一部分 (静态内容)
  303. final slotFillPaint = Paint()
  304. ..color = Colors.green
  305. // .shade100 // 槽位填充色
  306. ..style = PaintingStyle.fill;
  307. final slotStrokePaint = Paint()
  308. ..color =
  309. Color(0xff26600c) // 槽位边框色
  310. ..style = PaintingStyle.stroke
  311. ..strokeWidth = strokeWidth;
  312. for (int r = 0; r < rows; r++) {
  313. for (int c = 0; c < cols; c++) {
  314. // 计算当前槽位的边界 (Canvas坐标系)
  315. final left = targetRect.left + c * pieceLogicalWidth;
  316. final top = targetRect.top + r * pieceLogicalHeight;
  317. final right = left + pieceLogicalWidth;
  318. final bottom = top + pieceLogicalHeight;
  319. // 为了避免描边溢出到相邻槽位,将矩形向内收缩 halfStroke
  320. final slotRect = Rect.fromLTRB(left + halfStroke, top + halfStroke, right - halfStroke, bottom - halfStroke);
  321. final slotRRect = RRect.fromRectAndRadius(slotRect, const Radius.circular(cornerRadius));
  322. // 绘制填充
  323. canvas.drawRRect(slotRRect, slotFillPaint);
  324. // 绘制描边
  325. canvas.drawRRect(slotRRect, slotStrokePaint);
  326. }
  327. }
  328. // --- 结束录制并存储 ---
  329. _backgroundPicture = recorder.endRecording();
  330. _log.info('Static background picture recorded. Size: ${recordBounds.size}');
  331. }
  332. }
  333. // 辅助扩展:解决 Dart 缺少 firstWhereOrNull 的问题
  334. extension IterableExtension<T> on Iterable<T> {
  335. T? firstWhereOrNull(bool Function(T element) test) {
  336. for (final element in this) {
  337. if (test(element)) {
  338. return element;
  339. }
  340. }
  341. return null;
  342. }
  343. }