import 'dart:async'; import 'package:cchess/cchess.dart'; import 'package:shirne_dialog/shirne_dialog.dart'; import 'package:flutter/material.dart'; import 'action_dialog.dart'; import 'board.dart'; import 'piece.dart'; import 'chess_pieces.dart'; import 'mark_component.dart'; import 'point_component.dart'; import '../global.dart'; import '../models/game_event.dart'; import '../models/sound.dart'; import '../models/game_manager.dart'; import '../driver/player_driver.dart'; import '../pages/home_page.dart'; class Chess extends StatefulWidget { final String skin; const Chess({Key? key, this.skin = 'woods'}) : super(key: key); @override State createState() => ChessState(); } class ChessState extends State { // 当前激活的子 ChessItem? activeItem; double skewStepper = 0; // 被吃的子 ChessItem? dieFlash; // 刚走过的位置 String lastPosition = 'a0'; // 可落点,包括吃子点 List movePoints = []; bool isInit = false; late GameManager gamer; bool isLoading = true; // 棋局初始化时所有的子力 List items = []; @override void initState() { super.initState(); initGamer(); } void initGamer() { if (isInit) return; isInit = true; HomePageState? gameWrapper = context.findAncestorStateOfType(); if (gameWrapper == null) return; gamer = gameWrapper.gamer; gamer.on(reloadGame); gamer.on(onResult); gamer.on(onMove); gamer.on(onFlip); reloadGame(GameLoadEvent(0)); } @override void dispose() { gamer.off(reloadGame); gamer.off(onResult); gamer.off(onMove); super.dispose(); } void onFlip(GameEvent event) { setState(() {}); } void onResult(GameEvent event) { if (event.data == null || event.data!.isEmpty) { return; } List parts = event.data.split(' '); String? resultText = (parts.length > 1 && parts[1].isNotEmpty) ? parts[1] : null; switch (parts[0]) { case 'checkMate': //toast(context.l10n.check); showAction(ActionType.checkMate); break; case 'eat': showAction(ActionType.eat); break; case ChessManual.resultFstLoose: alertResult(resultText ?? context.l10n.redLoose); break; case ChessManual.resultFstWin: alertResult(resultText ?? context.l10n.redWin); break; case ChessManual.resultFstDraw: alertResult(resultText ?? context.l10n.redDraw); break; default: break; } } void reloadGame(GameEvent event) { if (event.data < -1) { return; } if (event.data < 0) { if (!isLoading) { setState(() { isLoading = true; }); } return; } setState(() { items = gamer.manual.getChessItems(); isLoading = false; lastPosition = ''; activeItem = null; }); String position = gamer.lastMove; if (position.isNotEmpty) { logger.info('last move $position'); Future.delayed(const Duration(milliseconds: 32)).then((value) { setState(() { lastPosition = position.substring(0, 2); ChessPos activePos = ChessPos.fromCode(position.substring(2, 4)); activeItem = items.firstWhere( (item) => !item.isBlank && item.position == ChessPos.fromCode(lastPosition), orElse: () => ChessItem('0'), ); activeItem!.position = activePos; }); }); } } void addStep(ChessPos chess, ChessPos next) { gamer.addStep(chess, next); } Future fetchMovePoints() async { setState(() { movePoints = gamer.rule.movePoints(activeItem!.position); // print('move points: $movePoints'); }); } /// 从外部过来的指令 void onMove(GameEvent event) { String move = event.data!; logger.info('onmove $move'); if (move.isEmpty) return; if (move == PlayerDriver.rstGiveUp) return; if (move.contains(PlayerDriver.rstRqstDraw)) { toast( context.l10n.requestDraw, SnackBarAction( label: context.l10n.agreeToDraw, onPressed: () { gamer.player.completeMove(PlayerDriver.rstDraw); }, ), 5, ); move = move.replaceAll(PlayerDriver.rstRqstDraw, '').trim(); if (move.isEmpty) { return; } } if (move == PlayerDriver.rstRqstRetract) { confirm( context.l10n.requestRetract, context.l10n.agreeRetract, context.l10n.disagreeRetract, ).then((bool? isAgree) { gamer.player .completeMove(isAgree == true ? PlayerDriver.rstRetract : ''); }); return; } ChessPos fromPos = ChessPos.fromCode(move.substring(0, 2)); ChessPos toPosition = ChessPos.fromCode(move.substring(2, 4)); activeItem = items.firstWhere( (item) => !item.isBlank && item.position == fromPos, orElse: () => ChessItem('0'), ); ChessItem newActive = items.firstWhere( (item) => !item.isBlank && item.position == toPosition, orElse: () => ChessItem('0'), ); setState(() { if (activeItem != null && !activeItem!.isBlank) { logger.info('$activeItem => $move'); activeItem!.position = ChessPos.fromCode(move.substring(2, 4)); lastPosition = fromPos.toCode(); if (!newActive.isBlank) { logger.info('eat $newActive'); //showAction(ActionType.eat); // 被吃的子的快照 dieFlash = ChessItem(newActive.code, position: toPosition); newActive.isDie = true; Future.delayed(const Duration(milliseconds: 250), () { setState(() { dieFlash = null; }); }); } } else { logger.info('Remote move error $move'); } }); } void animateMove(ChessPos nextPosition) { logger.info('$activeItem => $nextPosition'); setState(() { activeItem!.position = nextPosition.copy(); }); } void clearActive() { setState(() { activeItem = null; lastPosition = ''; }); } /// 检测用户的输入位置是否有效 Future checkCanMove( String activePos, ChessPos toPosition, [ ChessItem? toItem, ]) async { if (!movePoints.contains(toPosition.toCode())) { if (toItem != null) { toast('can\'t eat ${toItem.code} at $toPosition'); } else { toast('can\'t move to $toPosition'); } return false; } String move = activePos + toPosition.toCode(); ChessRule rule = ChessRule(gamer.fen.copy()); rule.fen.move(move); if (rule.isKingMeet(gamer.curHand)) { toast(context.l10n.cantSendCheck); return false; } // 区分应将和送将 if (rule.isCheck(gamer.curHand)) { if (gamer.isCheckMate) { toast(context.l10n.plsParryCheck); } else { toast(context.l10n.cantSendCheck); } return false; } return true; } /// 用户点击棋盘位置的反馈 包括选中子,走子,吃子,无反馈 bool onPointer(ChessPos toPosition) { ChessItem newActive = items.firstWhere( (item) => !item.isBlank && item.position == toPosition, orElse: () => ChessItem('0'), ); int ticker = DateTime.now().millisecondsSinceEpoch; if (newActive.isBlank) { if (activeItem != null && activeItem!.team == gamer.curHand) { String activePos = activeItem!.position.toCode(); animateMove(toPosition); checkCanMove(activePos, toPosition).then((canMove) { int delay = 250 - (DateTime.now().millisecondsSinceEpoch - ticker); if (delay < 0) { delay = 0; } if (canMove) { // 立即更新的部分 setState(() { // 清掉落子点 movePoints = []; lastPosition = activePos; }); addStep(ChessPos.fromCode(activePos), toPosition); } else { Future.delayed(Duration(milliseconds: delay), () { setState(() { activeItem!.position = ChessPos.fromCode(activePos); }); }); } }); return true; } return false; } // 置空当前选中状态 if (activeItem != null && activeItem!.position == toPosition) { Sound.play(Sound.click); setState(() { activeItem = null; lastPosition = ''; movePoints = []; }); } else if (newActive.team == gamer.curHand) { Sound.play(Sound.click); // 切换选中的子 setState(() { activeItem = newActive; lastPosition = ''; movePoints = []; }); fetchMovePoints(); return true; } else { // 吃对方的子 if (activeItem != null && activeItem!.team == gamer.curHand) { String activePos = activeItem!.position.toCode(); animateMove(toPosition); checkCanMove(activePos, toPosition, newActive).then((canMove) { int delay = 250 - (DateTime.now().millisecondsSinceEpoch - ticker); if (delay < 0) { delay = 0; } if (canMove) { addStep(ChessPos.fromCode(activePos), toPosition); //showAction(ActionType.eat); setState(() { // 清掉落子点 movePoints = []; lastPosition = activePos; // 被吃的子的快照 dieFlash = ChessItem(newActive.code, position: toPosition); newActive.isDie = true; }); Future.delayed(Duration(milliseconds: delay), () { setState(() { dieFlash = null; }); }); } else { Future.delayed(Duration(milliseconds: delay), () { setState(() { activeItem!.position = ChessPos.fromCode(activePos); }); }); } }); return true; } } return false; } ChessPos pointTrans(Offset tapPoint) { int x = (tapPoint.dx - gamer.skin.offset.dx * gamer.scale) ~/ (gamer.skin.size * gamer.scale); int y = 9 - (tapPoint.dy - gamer.skin.offset.dy * gamer.scale) ~/ (gamer.skin.size * gamer.scale); return ChessPos(gamer.isFlip ? 8 - x : x, gamer.isFlip ? 9 - y : y); } void toast(String message, [SnackBarAction? action, int duration = 3]) { MyDialog.snack( message, action: action, duration: Duration(seconds: duration), ); } void alertResult(message) { confirm(message, context.l10n.oneMoreGame, context.l10n.letMeSee) .then((isConfirm) { if (isConfirm ?? false) { gamer.newGame(); } }); } Future confirm(String message, String agreeText, String cancelText) { return MyDialog.confirm( message, buttonText: agreeText, cancelText: cancelText, ); } Future alert(String message) async { return MyDialog.alert(message); } // 显示吃/将效果 void showAction(ActionType type) { final overlay = Overlay.of(context); late OverlayEntry entry; entry = OverlayEntry( builder: (context) => Center( child: ActionDialog( type, delay: 2, onHide: () { entry.remove(); }, ), ), ); overlay.insert(entry); } @override Widget build(BuildContext context) { initGamer(); if (isLoading) { return const Center( child: CircularProgressIndicator(), ); } List widgets = [const Board()]; List layer0 = []; if (dieFlash != null) { layer0.add( Align( alignment: gamer.skin.getAlign(dieFlash!.position), child: Piece(item: dieFlash!, isActive: false, isAblePoint: false), ), ); } if (lastPosition.isNotEmpty) { ChessItem emptyItem = ChessItem('0', position: ChessPos.fromCode(lastPosition)); layer0.add( Align( alignment: gamer.skin.getAlign(emptyItem.position), child: MarkComponent( size: gamer.skin.size * gamer.scale, ), ), ); } widgets.add( Stack( alignment: Alignment.center, fit: StackFit.expand, children: layer0, ), ); widgets.add( ChessPieces( items: items, activeItem: activeItem, ), ); List layer2 = []; for (var element in movePoints) { ChessItem emptyItem = ChessItem('0', position: ChessPos.fromCode(element)); layer2.add( Align( alignment: gamer.skin.getAlign(emptyItem.position), child: PointComponent(size: gamer.skin.size * gamer.scale), ), ); } widgets.add( Stack( alignment: Alignment.center, fit: StackFit.expand, children: layer2, ), ); return GestureDetector( onTapUp: (detail) { if (gamer.isLock) return; setState(() { onPointer(pointTrans(detail.localPosition)); }); }, child: SizedBox( width: gamer.skin.width, height: gamer.skin.height, child: Stack( alignment: Alignment.center, fit: StackFit.expand, children: widgets, ), ), ); } }