chess.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. import 'dart:async';
  2. import 'package:cchess/cchess.dart';
  3. import 'package:shirne_dialog/shirne_dialog.dart';
  4. import 'package:flutter/material.dart';
  5. import 'action_dialog.dart';
  6. import 'board.dart';
  7. import 'piece.dart';
  8. import 'chess_pieces.dart';
  9. import 'mark_component.dart';
  10. import 'point_component.dart';
  11. import '../global.dart';
  12. import '../models/game_event.dart';
  13. import '../models/sound.dart';
  14. import '../models/game_manager.dart';
  15. import '../driver/player_driver.dart';
  16. import '../pages/home_page.dart';
  17. class Chess extends StatefulWidget {
  18. final String skin;
  19. const Chess({Key? key, this.skin = 'woods'}) : super(key: key);
  20. @override
  21. State<Chess> createState() => ChessState();
  22. }
  23. class ChessState extends State<Chess> {
  24. // 当前激活的子
  25. ChessItem? activeItem;
  26. double skewStepper = 0;
  27. // 被吃的子
  28. ChessItem? dieFlash;
  29. // 刚走过的位置
  30. String lastPosition = 'a0';
  31. // 可落点,包括吃子点
  32. List<String> movePoints = [];
  33. bool isInit = false;
  34. late GameManager gamer;
  35. bool isLoading = true;
  36. // 棋局初始化时所有的子力
  37. List<ChessItem> items = [];
  38. @override
  39. void initState() {
  40. super.initState();
  41. initGamer();
  42. }
  43. void initGamer() {
  44. if (isInit) return;
  45. isInit = true;
  46. HomePageState? gameWrapper =
  47. context.findAncestorStateOfType<HomePageState>();
  48. if (gameWrapper == null) return;
  49. gamer = gameWrapper.gamer;
  50. gamer.on<GameLoadEvent>(reloadGame);
  51. gamer.on<GameResultEvent>(onResult);
  52. gamer.on<GameMoveEvent>(onMove);
  53. gamer.on<GameFlipEvent>(onFlip);
  54. reloadGame(GameLoadEvent(0));
  55. }
  56. @override
  57. void dispose() {
  58. gamer.off<GameLoadEvent>(reloadGame);
  59. gamer.off<GameResultEvent>(onResult);
  60. gamer.off<GameMoveEvent>(onMove);
  61. super.dispose();
  62. }
  63. void onFlip(GameEvent event) {
  64. setState(() {});
  65. }
  66. void onResult(GameEvent event) {
  67. if (event.data == null || event.data!.isEmpty) {
  68. return;
  69. }
  70. List<String> parts = event.data.split(' ');
  71. String? resultText =
  72. (parts.length > 1 && parts[1].isNotEmpty) ? parts[1] : null;
  73. switch (parts[0]) {
  74. case 'checkMate':
  75. //toast(context.l10n.check);
  76. showAction(ActionType.checkMate);
  77. break;
  78. case 'eat':
  79. showAction(ActionType.eat);
  80. break;
  81. case ChessManual.resultFstLoose:
  82. alertResult(resultText ?? context.l10n.redLoose);
  83. break;
  84. case ChessManual.resultFstWin:
  85. alertResult(resultText ?? context.l10n.redWin);
  86. break;
  87. case ChessManual.resultFstDraw:
  88. alertResult(resultText ?? context.l10n.redDraw);
  89. break;
  90. default:
  91. break;
  92. }
  93. }
  94. void reloadGame(GameEvent event) {
  95. if (event.data < -1) {
  96. return;
  97. }
  98. if (event.data < 0) {
  99. if (!isLoading) {
  100. setState(() {
  101. isLoading = true;
  102. });
  103. }
  104. return;
  105. }
  106. setState(() {
  107. items = gamer.manual.getChessItems();
  108. isLoading = false;
  109. lastPosition = '';
  110. activeItem = null;
  111. });
  112. String position = gamer.lastMove;
  113. if (position.isNotEmpty) {
  114. logger.info('last move $position');
  115. Future.delayed(const Duration(milliseconds: 32)).then((value) {
  116. setState(() {
  117. lastPosition = position.substring(0, 2);
  118. ChessPos activePos = ChessPos.fromCode(position.substring(2, 4));
  119. activeItem = items.firstWhere(
  120. (item) =>
  121. !item.isBlank &&
  122. item.position == ChessPos.fromCode(lastPosition),
  123. orElse: () => ChessItem('0'),
  124. );
  125. activeItem!.position = activePos;
  126. });
  127. });
  128. }
  129. }
  130. void addStep(ChessPos chess, ChessPos next) {
  131. gamer.addStep(chess, next);
  132. }
  133. Future<void> fetchMovePoints() async {
  134. setState(() {
  135. movePoints = gamer.rule.movePoints(activeItem!.position);
  136. // print('move points: $movePoints');
  137. });
  138. }
  139. /// 从外部过来的指令
  140. void onMove(GameEvent event) {
  141. String move = event.data!;
  142. logger.info('onmove $move');
  143. if (move.isEmpty) return;
  144. if (move == PlayerDriver.rstGiveUp) return;
  145. if (move.contains(PlayerDriver.rstRqstDraw)) {
  146. toast(
  147. context.l10n.requestDraw,
  148. SnackBarAction(
  149. label: context.l10n.agreeToDraw,
  150. onPressed: () {
  151. gamer.player.completeMove(PlayerDriver.rstDraw);
  152. },
  153. ),
  154. 5,
  155. );
  156. move = move.replaceAll(PlayerDriver.rstRqstDraw, '').trim();
  157. if (move.isEmpty) {
  158. return;
  159. }
  160. }
  161. if (move == PlayerDriver.rstRqstRetract) {
  162. confirm(
  163. context.l10n.requestRetract,
  164. context.l10n.agreeRetract,
  165. context.l10n.disagreeRetract,
  166. ).then((bool? isAgree) {
  167. gamer.player
  168. .completeMove(isAgree == true ? PlayerDriver.rstRetract : '');
  169. });
  170. return;
  171. }
  172. ChessPos fromPos = ChessPos.fromCode(move.substring(0, 2));
  173. ChessPos toPosition = ChessPos.fromCode(move.substring(2, 4));
  174. activeItem = items.firstWhere(
  175. (item) => !item.isBlank && item.position == fromPos,
  176. orElse: () => ChessItem('0'),
  177. );
  178. ChessItem newActive = items.firstWhere(
  179. (item) => !item.isBlank && item.position == toPosition,
  180. orElse: () => ChessItem('0'),
  181. );
  182. setState(() {
  183. if (activeItem != null && !activeItem!.isBlank) {
  184. logger.info('$activeItem => $move');
  185. activeItem!.position = ChessPos.fromCode(move.substring(2, 4));
  186. lastPosition = fromPos.toCode();
  187. if (!newActive.isBlank) {
  188. logger.info('eat $newActive');
  189. //showAction(ActionType.eat);
  190. // 被吃的子的快照
  191. dieFlash = ChessItem(newActive.code, position: toPosition);
  192. newActive.isDie = true;
  193. Future.delayed(const Duration(milliseconds: 250), () {
  194. setState(() {
  195. dieFlash = null;
  196. });
  197. });
  198. }
  199. } else {
  200. logger.info('Remote move error $move');
  201. }
  202. });
  203. }
  204. void animateMove(ChessPos nextPosition) {
  205. logger.info('$activeItem => $nextPosition');
  206. setState(() {
  207. activeItem!.position = nextPosition.copy();
  208. });
  209. }
  210. void clearActive() {
  211. setState(() {
  212. activeItem = null;
  213. lastPosition = '';
  214. });
  215. }
  216. /// 检测用户的输入位置是否有效
  217. Future<bool> checkCanMove(
  218. String activePos,
  219. ChessPos toPosition, [
  220. ChessItem? toItem,
  221. ]) async {
  222. if (!movePoints.contains(toPosition.toCode())) {
  223. if (toItem != null) {
  224. toast('can\'t eat ${toItem.code} at $toPosition');
  225. } else {
  226. toast('can\'t move to $toPosition');
  227. }
  228. return false;
  229. }
  230. String move = activePos + toPosition.toCode();
  231. ChessRule rule = ChessRule(gamer.fen.copy());
  232. rule.fen.move(move);
  233. if (rule.isKingMeet(gamer.curHand)) {
  234. toast(context.l10n.cantSendCheck);
  235. return false;
  236. }
  237. // 区分应将和送将
  238. if (rule.isCheck(gamer.curHand)) {
  239. if (gamer.isCheckMate) {
  240. toast(context.l10n.plsParryCheck);
  241. } else {
  242. toast(context.l10n.cantSendCheck);
  243. }
  244. return false;
  245. }
  246. return true;
  247. }
  248. /// 用户点击棋盘位置的反馈 包括选中子,走子,吃子,无反馈
  249. bool onPointer(ChessPos toPosition) {
  250. ChessItem newActive = items.firstWhere(
  251. (item) => !item.isBlank && item.position == toPosition,
  252. orElse: () => ChessItem('0'),
  253. );
  254. int ticker = DateTime.now().millisecondsSinceEpoch;
  255. if (newActive.isBlank) {
  256. if (activeItem != null && activeItem!.team == gamer.curHand) {
  257. String activePos = activeItem!.position.toCode();
  258. animateMove(toPosition);
  259. checkCanMove(activePos, toPosition).then((canMove) {
  260. int delay = 250 - (DateTime.now().millisecondsSinceEpoch - ticker);
  261. if (delay < 0) {
  262. delay = 0;
  263. }
  264. if (canMove) {
  265. // 立即更新的部分
  266. setState(() {
  267. // 清掉落子点
  268. movePoints = [];
  269. lastPosition = activePos;
  270. });
  271. addStep(ChessPos.fromCode(activePos), toPosition);
  272. } else {
  273. Future.delayed(Duration(milliseconds: delay), () {
  274. setState(() {
  275. activeItem!.position = ChessPos.fromCode(activePos);
  276. });
  277. });
  278. }
  279. });
  280. return true;
  281. }
  282. return false;
  283. }
  284. // 置空当前选中状态
  285. if (activeItem != null && activeItem!.position == toPosition) {
  286. Sound.play(Sound.click);
  287. setState(() {
  288. activeItem = null;
  289. lastPosition = '';
  290. movePoints = [];
  291. });
  292. } else if (newActive.team == gamer.curHand) {
  293. Sound.play(Sound.click);
  294. // 切换选中的子
  295. setState(() {
  296. activeItem = newActive;
  297. lastPosition = '';
  298. movePoints = [];
  299. });
  300. fetchMovePoints();
  301. return true;
  302. } else {
  303. // 吃对方的子
  304. if (activeItem != null && activeItem!.team == gamer.curHand) {
  305. String activePos = activeItem!.position.toCode();
  306. animateMove(toPosition);
  307. checkCanMove(activePos, toPosition, newActive).then((canMove) {
  308. int delay = 250 - (DateTime.now().millisecondsSinceEpoch - ticker);
  309. if (delay < 0) {
  310. delay = 0;
  311. }
  312. if (canMove) {
  313. addStep(ChessPos.fromCode(activePos), toPosition);
  314. //showAction(ActionType.eat);
  315. setState(() {
  316. // 清掉落子点
  317. movePoints = [];
  318. lastPosition = activePos;
  319. // 被吃的子的快照
  320. dieFlash = ChessItem(newActive.code, position: toPosition);
  321. newActive.isDie = true;
  322. });
  323. Future.delayed(Duration(milliseconds: delay), () {
  324. setState(() {
  325. dieFlash = null;
  326. });
  327. });
  328. } else {
  329. Future.delayed(Duration(milliseconds: delay), () {
  330. setState(() {
  331. activeItem!.position = ChessPos.fromCode(activePos);
  332. });
  333. });
  334. }
  335. });
  336. return true;
  337. }
  338. }
  339. return false;
  340. }
  341. ChessPos pointTrans(Offset tapPoint) {
  342. int x = (tapPoint.dx - gamer.skin.offset.dx * gamer.scale) ~/
  343. (gamer.skin.size * gamer.scale);
  344. int y = 9 -
  345. (tapPoint.dy - gamer.skin.offset.dy * gamer.scale) ~/
  346. (gamer.skin.size * gamer.scale);
  347. return ChessPos(gamer.isFlip ? 8 - x : x, gamer.isFlip ? 9 - y : y);
  348. }
  349. void toast(String message, [SnackBarAction? action, int duration = 3]) {
  350. MyDialog.snack(
  351. message,
  352. action: action,
  353. duration: Duration(seconds: duration),
  354. );
  355. }
  356. void alertResult(message) {
  357. confirm(message, context.l10n.oneMoreGame, context.l10n.letMeSee)
  358. .then((isConfirm) {
  359. if (isConfirm ?? false) {
  360. gamer.newGame();
  361. }
  362. });
  363. }
  364. Future<bool?> confirm(String message, String agreeText, String cancelText) {
  365. return MyDialog.confirm(
  366. message,
  367. buttonText: agreeText,
  368. cancelText: cancelText,
  369. );
  370. }
  371. Future<bool?> alert(String message) async {
  372. return MyDialog.alert(message);
  373. }
  374. // 显示吃/将效果
  375. void showAction(ActionType type) {
  376. final overlay = Overlay.of(context);
  377. late OverlayEntry entry;
  378. entry = OverlayEntry(
  379. builder: (context) => Center(
  380. child: ActionDialog(
  381. type,
  382. delay: 2,
  383. onHide: () {
  384. entry.remove();
  385. },
  386. ),
  387. ),
  388. );
  389. overlay.insert(entry);
  390. }
  391. @override
  392. Widget build(BuildContext context) {
  393. initGamer();
  394. if (isLoading) {
  395. return const Center(
  396. child: CircularProgressIndicator(),
  397. );
  398. }
  399. List<Widget> widgets = [const Board()];
  400. List<Widget> layer0 = [];
  401. if (dieFlash != null) {
  402. layer0.add(
  403. Align(
  404. alignment: gamer.skin.getAlign(dieFlash!.position),
  405. child: Piece(item: dieFlash!, isActive: false, isAblePoint: false),
  406. ),
  407. );
  408. }
  409. if (lastPosition.isNotEmpty) {
  410. ChessItem emptyItem =
  411. ChessItem('0', position: ChessPos.fromCode(lastPosition));
  412. layer0.add(
  413. Align(
  414. alignment: gamer.skin.getAlign(emptyItem.position),
  415. child: MarkComponent(
  416. size: gamer.skin.size * gamer.scale,
  417. ),
  418. ),
  419. );
  420. }
  421. widgets.add(
  422. Stack(
  423. alignment: Alignment.center,
  424. fit: StackFit.expand,
  425. children: layer0,
  426. ),
  427. );
  428. widgets.add(
  429. ChessPieces(
  430. items: items,
  431. activeItem: activeItem,
  432. ),
  433. );
  434. List<Widget> layer2 = [];
  435. for (var element in movePoints) {
  436. ChessItem emptyItem =
  437. ChessItem('0', position: ChessPos.fromCode(element));
  438. layer2.add(
  439. Align(
  440. alignment: gamer.skin.getAlign(emptyItem.position),
  441. child: PointComponent(size: gamer.skin.size * gamer.scale),
  442. ),
  443. );
  444. }
  445. widgets.add(
  446. Stack(
  447. alignment: Alignment.center,
  448. fit: StackFit.expand,
  449. children: layer2,
  450. ),
  451. );
  452. return GestureDetector(
  453. onTapUp: (detail) {
  454. if (gamer.isLock) return;
  455. setState(() {
  456. onPointer(pointTrans(detail.localPosition));
  457. });
  458. },
  459. child: SizedBox(
  460. width: gamer.skin.width,
  461. height: gamer.skin.height,
  462. child: Stack(
  463. alignment: Alignment.center,
  464. fit: StackFit.expand,
  465. children: widgets,
  466. ),
  467. ),
  468. );
  469. }
  470. }