jianboy 1 year ago
parent
commit
70f1e19a8b

+ 1 - 1
.metadata

@@ -1,4 +1,4 @@
-# This file tracks properties of this Flutter project.
+e# This file tracks properties of this Flutter project.
 # Used by Flutter tool to assess capabilities and perform upgrades etc.
 # Used by Flutter tool to assess capabilities and perform upgrades etc.
 #
 #
 # This file should be version controlled.
 # This file should be version controlled.

+ 1 - 1
android/build.gradle

@@ -26,6 +26,6 @@ subprojects {
     project.evaluationDependsOn(':app')
     project.evaluationDependsOn(':app')
 }
 }
 
 
-task clean(type: Delete) {
+tasks.register("clean", Delete) {
     delete rootProject.buildDir
     delete rootProject.buildDir
 }
 }

+ 1 - 1
android/gradle/wrapper/gradle-wrapper.properties

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip

+ 25 - 1
lib/main.dart

@@ -4,6 +4,8 @@ import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_chinese_chees/routes.dart';
 import 'package:flutter_chinese_chees/routes.dart';
+import 'package:flutter_chinese_chees/utils/game_manager.dart';
+import 'package:window_manager/window_manager.dart';
 
 
 /// Description: enter point
 /// Description: enter point
 /// Time       : 04/28/2023 Friday
 /// Time       : 04/28/2023 Friday
@@ -13,7 +15,21 @@ void main() async {
   if (!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux)) {
   if (!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux)) {
     await windowManager.ensureInitialized();
     await windowManager.ensureInitialized();
   }
   }
-  
+
+  const windowOptions = WindowOptions(
+      size: Size(1024, 720),
+      center: true,
+      backgroundColor: Colors.transparent,
+      skipTaskbar: false,
+      titleBarStyle: TitleBarStyle.normal);
+
+  windowManager.waitUntilReadyToShow(windowOptions, () async {
+    await windowManager.show();
+    await windowManager.focus();
+  });
+
+  windowManager.addListener(MainWindowListener());
+
   runApp(const MyApp());
   runApp(const MyApp());
 
 
   if (Platform.isAndroid) {
   if (Platform.isAndroid) {
@@ -23,6 +39,14 @@ void main() async {
   }
   }
 }
 }
 
 
+class MainWindowListener extends WindowListener {
+  @override
+  void onWindowClose() {
+    super.onWindowClose();
+    GameManager.instance.engine?.dispose();
+  }
+}
+
 class MyApp extends StatelessWidget {
 class MyApp extends StatelessWidget {
   const MyApp({super.key});
   const MyApp({super.key});
 
 

+ 114 - 4
lib/model/chees_skin_model.dart

@@ -1,6 +1,116 @@
-/// Description: 象棋皮肤
+import 'dart:convert';
+import 'package:cchess/cchess.dart';
+import 'package:flutter/services.dart';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_chinese_chees/utils/game_manager.dart';
+
+/// Description: 象棋皮肤,默认实木
 /// Time       : 04/28/2023 Friday
 /// Time       : 04/28/2023 Friday
 /// Author     : liuyuqi.gov@msn.cn
 /// Author     : liuyuqi.gov@msn.cn
-class CheesSkinModel {
-  
-}
+class ChessSkin {
+  GameManager manager;
+  String folder = "";
+
+  double width = 521;
+  double height = 577;
+  double size = 57;
+  Offset offset = const Offset(4, 3);
+
+  String board = "board.jpg";
+  String blank = "blank.png";
+  Map<String, String> redMap = {
+    "K": "rk.gif",
+    "A": "ra.png",
+    "B": "rb.png",
+    "C": "rc.png",
+    "N": "rn.png",
+    "R": "rr.png",
+    "P": "rp.png"
+  };
+  Map<String, String> blackMap = {
+    "k": "bk.png",
+    "a": "ba.png",
+    "b": "bb.png",
+    "c": "bc.png",
+    "n": "bn.png",
+    "r": "br.png",
+    "p": "bp.png"
+  };
+
+  late ValueNotifier<bool> readyNotifier;
+
+  ChessSkin(this.folder, this.manager) {
+    readyNotifier = ValueNotifier(false);
+    String jsonfile = "assets/skins/$folder/config.json";
+    rootBundle.loadString(jsonfile).then((String fileContents) {
+      loadJson(fileContents);
+    }).catchError((error) {
+      logger.warning('Skin file $jsonfile error', error);
+      readyNotifier.value = true;
+    });
+  }
+
+  void loadJson(String content) {
+    Map<String, dynamic> json = jsonDecode(content);
+    json.forEach((key, value) {
+      switch (key) {
+        case 'width':
+          width = value.toDouble();
+          break;
+        case 'height':
+          height = value.toDouble();
+          break;
+        case 'size':
+          size = value.toDouble();
+          break;
+        case 'board':
+          board = value.toString();
+          break;
+        case 'blank':
+          blank = value.toString();
+          break;
+        case 'offset':
+          offset = Offset(value['dx'].toDouble(), value['dy'].toDouble());
+          break;
+        case 'red':
+          redMap = value.cast<String, String>();
+          break;
+        case 'black':
+          blackMap = value.cast<String, String>();
+          break;
+      }
+    });
+    readyNotifier.value = true;
+  }
+
+  String get boardImage => "assets/skins/$folder/$board";
+
+  String getRedChess(String code) {
+    if (!redMap.containsKey(code.toUpperCase())) {
+      logger.info('Code error: $code');
+      return "assets/skins/$folder/$blank";
+    }
+    return "assets/skins/$folder/${redMap[code.toUpperCase()]}";
+  }
+
+  String getBlackChess(String code) {
+    if (!blackMap.containsKey(code.toLowerCase())) {
+      logger.info('Code error: $code');
+      return "assets/skins/$folder/$blank";
+    }
+    return "assets/skins/$folder/${blackMap[code.toLowerCase()]}";
+  }
+
+  Alignment getAlign(ChessPos? pos) {
+    if (pos == null) {
+      return const Alignment(1.2, 0);
+    }
+    final x = ((pos.x * size + offset.dx) * 2) / (width - size) - 1;
+    final y = ((((9 - pos.y) * size + offset.dy) * 2) / (height - size) - 1);
+    return Alignment(
+      manager.isFlip ? -x : x,
+      manager.isFlip ? -y : y,
+    );
+  }
+}

+ 541 - 2
lib/utils/game_manager.dart

@@ -1,7 +1,546 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:cchess/cchess.dart';
+import 'package:chinese_chess/models/engine_type.dart';
+import 'package:fast_gbk/fast_gbk.dart';
+
+import '../driver/player_driver.dart';
+import '../global.dart';
+import 'chess_skin.dart';
+import 'game_event.dart';
+import 'game_setting.dart';
+import 'sound.dart';
+import 'engine.dart';
+import 'player.dart';
 
 
 /// Description: 游戏管理
 /// Description: 游戏管理
 /// Time       : 04/30/2023 Sunday
 /// Time       : 04/30/2023 Sunday
 /// Author     : liuyuqi.gov@msn.cn
 /// Author     : liuyuqi.gov@msn.cn
 class GameManager {
 class GameManager {
-  
-}
+  late ChessSkin skin;
+  double scale = 1;
+
+  // 当前对局
+  late ChessManual manual;
+
+  // 算法引擎
+  Engine? engine;
+  bool engineOK = false;
+
+  // 是否重新请求招法时的强制stop
+  bool isStop = false;
+
+  // 是否翻转棋盘
+  bool _isFlip = false;
+  bool get isFlip => _isFlip;
+
+  void flip() {
+    add(GameFlipEvent(!isFlip));
+  }
+
+  // 是否锁定(非玩家操作的时候锁定界面)
+  bool _isLock = false;
+  bool get isLock => _isLock;
+
+  // 选手
+  List<Player> hands = [];
+  int curHand = 0;
+
+  // 当前着法序号
+  int _currentStep = 0;
+  int get currentStep => _currentStep;
+
+  int get stepCount => manual.moveCount;
+
+  // 是否将军
+  bool get isCheckMate => manual.currentMove?.isCheckMate ?? false;
+
+  // 未吃子着数(半回合数)
+  int unEatCount = 0;
+
+  // 回合数
+  int round = 0;
+
+  final gameEvent = StreamController<GameEvent>();
+  final Map<GameEventType, List<void Function(GameEvent)>> listeners = {};
+
+  // 走子规则
+  late ChessRule rule;
+
+  late GameSetting setting;
+
+  static GameManager? _instance;
+
+  static GameManager get instance => _instance ??= GameManager();
+
+  GameManager._() {
+    gameEvent.stream.listen(_onGameEvent);
+  }
+
+  factory GameManager() {
+    _instance ??= GameManager._();
+    return _instance!;
+  }
+
+  Future<bool> init() async {
+    setting = await GameSetting.getInstance();
+    manual = ChessManual();
+    rule = ChessRule(manual.currentFen);
+
+    hands.add(Player('r', this, title: manual.red));
+    hands.add(Player('b', this, title: manual.black));
+    curHand = 0;
+    // map = ChessMap.fromFen(ChessManual.startFen);
+
+    skin = ChessSkin("woods", this);
+    skin.readyNotifier.addListener(() {
+      add(GameLoadEvent(0));
+    });
+
+    return true;
+  }
+
+  void on<T extends GameEvent>(void Function(GameEvent) listener) {
+    final type = GameEvent.eventType(T);
+    if (type == null) {
+      logger.warning('type not match ${T.runtimeType}');
+      return;
+    }
+    if (!listeners.containsKey(type)) {
+      listeners[type] = [];
+    }
+    listeners[type]!.add(listener);
+  }
+
+  void off<T extends GameEvent>(void Function(GameEvent) listener) {
+    final type = GameEvent.eventType(T);
+    if (type == null) {
+      logger.warning('type not match ${T.runtimeType}');
+      return;
+    }
+    listeners[type]?.remove(listener);
+  }
+
+  void add<T extends GameEvent>(T event) {
+    gameEvent.add(event);
+  }
+
+  void clear() {
+    listeners.clear();
+  }
+
+  void _onGameEvent(GameEvent e) {
+    if (e.type == GameEventType.lock) {
+      _isLock = e.data;
+    }
+    if (e.type == GameEventType.flip) {
+      _isFlip = e.data;
+    }
+    if (listeners.containsKey(e.type)) {
+      for (var func in listeners[e.type]!) {
+        func.call(e);
+      }
+    }
+  }
+
+  bool get canBacktrace => player.canBacktrace;
+
+  ChessFen get fen => manual.currentFen;
+
+  /// not last but current
+  String get lastMove => manual.currentMove?.move ?? '';
+
+  void parseMessage(String message) {
+    List<String> parts = message.split(' ');
+    String instruct = parts.removeAt(0);
+    switch (instruct) {
+      case 'ucciok':
+        engineOK = true;
+        add(GameEngineEvent('Engine is OK!'));
+        break;
+      case 'nobestmove':
+        // 强行stop后的nobestmove忽略
+        if (isStop) {
+          isStop = false;
+          return;
+        }
+        break;
+      case 'bestmove':
+        logger.info(message);
+        message = parseBaseMove(parts);
+        break;
+      case 'info':
+        logger.info(message);
+        message = parseInfo(parts);
+        break;
+      case 'id':
+      case 'option':
+      default:
+        return;
+    }
+    add(GameEngineEvent(message));
+  }
+
+  String parseBaseMove(List<String> infos) {
+    return "推荐着法: ${fen.toChineseString(infos[0])}"
+        "${infos.length > 2 ? ' 猜测对方: ${fen.toChineseString(infos[2])}' : ''}";
+  }
+
+  String parseInfo(List<String> infos) {
+    String first = infos.removeAt(0);
+    switch (first) {
+      case 'depth':
+        String msg = infos[0];
+        if (infos.isNotEmpty) {
+          String sub = infos.removeAt(0);
+          while (sub.isNotEmpty) {
+            if (sub == 'score') {
+              String score = infos.removeAt(0);
+              msg += '(${score.contains('-') ? '' : '+'}$score)';
+            } else if (sub == 'pv') {
+              msg += fen.toChineseTree(infos).join(' ');
+              break;
+            }
+            if (infos.isEmpty) break;
+            sub = infos.removeAt(0);
+          }
+        }
+        return msg;
+      case 'time':
+        return '耗时:${infos[0]}(ms)${infos.length > 2 ? ' 节点数 ${infos[2]}' : ''}';
+      case 'currmove':
+        return '当前招法: ${fen.toChineseString(infos[0])}${infos.length > 2 ? ' ${infos[2]}' : ''}';
+      case 'message':
+      default:
+        return infos.join(' ');
+    }
+  }
+
+  void stop() {
+    add(GameLoadEvent(-1));
+    isStop = true;
+    engine?.stop();
+    //currentStep = 0;
+
+    add(GameLockEvent(true));
+  }
+
+  void newGame([String fen = ChessManual.startFen]) {
+    stop();
+
+    add(GameStepEvent('clear'));
+    add(GameEngineEvent('clear'));
+    manual = ChessManual(fen: fen);
+    rule = ChessRule(manual.currentFen);
+    hands[0].title = manual.red;
+    hands[0].driverType = DriverType.user;
+    hands[1].title = manual.black;
+    hands[1].driverType = DriverType.user;
+    curHand = manual.startHand;
+
+    add(GameLoadEvent(0));
+    next();
+  }
+
+  void loadPGN(String pgn) {
+    stop();
+
+    _loadPGN(pgn);
+    add(GameLoadEvent(0));
+    next();
+  }
+
+  bool _loadPGN(String pgn) {
+    isStop = true;
+    engine?.stop();
+
+    String content = '';
+    if (!pgn.contains('\n')) {
+      File file = File(pgn);
+      if (file.existsSync()) {
+        //content = file.readAsStringSync(encoding: Encoding.getByName('gbk'));
+        content = gbk.decode(file.readAsBytesSync());
+      }
+    } else {
+      content = pgn;
+    }
+    manual = ChessManual.load(content);
+    hands[0].title = manual.red;
+    hands[1].title = manual.black;
+
+    add(GameLoadEvent(0));
+    // 加载步数
+    if (manual.moveCount > 0) {
+      // print(manual.moves);
+      add(
+        GameStepEvent(
+          manual.moves.map<String>((e) => e.toChineseString()).join('\n'),
+        ),
+      );
+    }
+    manual.loadHistory(-1);
+    rule.fen = manual.currentFen;
+    add(GameStepEvent('step'));
+
+    curHand = manual.startHand;
+    return true;
+  }
+
+  void loadFen(String fen) {
+    newGame(fen);
+  }
+
+  // 重载历史局面
+  void loadHistory(int index) {
+    if (index >= manual.moveCount) {
+      logger.info('History error');
+      return;
+    }
+    if (index == _currentStep) {
+      logger.info('History no change');
+      return;
+    }
+    _currentStep = index;
+    manual.loadHistory(index);
+    rule.fen = manual.currentFen;
+    curHand = (_currentStep + 1) % 2;
+    add(GamePlayerEvent(curHand));
+    add(GameLoadEvent(_currentStep + 1));
+
+    logger.info('history $_currentStep');
+  }
+
+  /// 切换驱动
+  void switchDriver(int team, DriverType driverType) {
+    hands[team].driverType = driverType;
+    if (team == curHand && driverType == DriverType.robot) {
+      //add(GameLockEvent(true));
+      next();
+    } else if (driverType == DriverType.user) {
+      //add(GameLockEvent(false));
+    }
+  }
+
+  /// 调用对应的玩家开始下一步
+  Future<void> next() async {
+    final move = await player.move();
+    if (move == null) return;
+
+    addMove(move);
+    final canNext = checkResult(curHand == 0 ? 1 : 0, _currentStep - 1);
+    logger.info('canNext $canNext');
+    if (canNext) {
+      switchPlayer();
+    }
+  }
+
+  /// 从用户落着 TODO 检查出发点是否有子,检查落点是否对方子
+  void addStep(ChessPos from, ChessPos next) async {
+    player.completeMove('${from.toCode()}${next.toCode()}');
+  }
+
+  void addMove(String move) {
+    logger.info('addmove $move');
+    if (PlayerDriver.isAction(move)) {
+      if (move == PlayerDriver.rstGiveUp) {
+        setResult(
+          curHand == 0 ? ChessManual.resultFstLoose : ChessManual.resultFstWin,
+          '${player.title}认输',
+        );
+      }
+      if (move == PlayerDriver.rstDraw) {
+        setResult(ChessManual.resultFstDraw);
+      }
+      if (move == PlayerDriver.rstRetract) {
+        // todo 悔棋
+      }
+      if (move.contains(PlayerDriver.rstRqstDraw)) {
+        move = move.replaceAll(PlayerDriver.rstRqstDraw, '').trim();
+        if (move.isEmpty) {
+          return;
+        }
+      } else {
+        return;
+      }
+    }
+
+    if (!ChessManual.isPosMove(move)) {
+      logger.info('着法错误 $move');
+      return;
+    }
+
+    // 如果当前不是最后一步,移除后面着法
+    if (!manual.isLast) {
+      add(GameLoadEvent(-2));
+      add(GameStepEvent('clear'));
+      manual.addMove(move, addStep: _currentStep);
+    } else {
+      add(GameLoadEvent(-2));
+      manual.addMove(move);
+    }
+    _currentStep = manual.currentStep;
+
+    final curMove = manual.currentMove!;
+
+    if (curMove.isCheckMate) {
+      unEatCount++;
+      Sound.play(Sound.move);
+    } else if (curMove.isEat) {
+      unEatCount = 0;
+      Sound.play(Sound.capture);
+    } else {
+      unEatCount++;
+      Sound.play(Sound.move);
+    }
+
+    add(GameStepEvent(curMove.toChineseString()));
+  }
+
+  void setResult(String result, [String description = '']) {
+    if (!ChessManual.results.contains(result)) {
+      logger.info('结果不合法 $result');
+      return;
+    }
+    logger.info('本局结果:$result');
+    add(GameResultEvent('$result $description'));
+    if (result == ChessManual.resultFstDraw) {
+      Sound.play(Sound.draw);
+    } else if (result == ChessManual.resultFstWin) {
+      Sound.play(Sound.win);
+    } else if (result == ChessManual.resultFstLoose) {
+      Sound.play(Sound.loose);
+    }
+    manual.result = result;
+  }
+
+  /// 棋局结果判断
+  bool checkResult(int hand, int curMove) {
+    logger.info('checkResult');
+
+    int repeatRound = manual.repeatRound();
+    if (repeatRound > 2) {
+      // TODO 提醒
+    }
+
+    // 判断和棋
+    if (unEatCount >= 120) {
+      setResult(ChessManual.resultFstDraw, '60回合无吃子判和');
+      return false;
+    }
+
+    //isCheckMate = rule.isCheck(hand);
+    final moveStep = manual.currentMove!;
+    logger.info('是否将军 ${moveStep.isCheckMate}');
+
+    // 判断输赢,包括能否应将,长将
+    if (moveStep.isCheckMate) {
+      //manual.moves[curMove].isCheckMate = isCheckMate;
+
+      if (rule.canParryKill(hand)) {
+        // 长将
+        if (repeatRound > 3) {
+          setResult(
+            hand == 0 ? ChessManual.resultFstLoose : ChessManual.resultFstWin,
+            '不变招长将作负',
+          );
+          return false;
+        }
+        Sound.play(Sound.check);
+        add(GameResultEvent('checkMate'));
+      } else {
+        setResult(
+          hand == 0 ? ChessManual.resultFstLoose : ChessManual.resultFstWin,
+          '绝杀',
+        );
+        return false;
+      }
+    } else {
+      if (rule.isTrapped(hand)) {
+        setResult(
+          hand == 0 ? ChessManual.resultFstLoose : ChessManual.resultFstWin,
+          '困毙',
+        );
+        return false;
+      } else if (moveStep.isEat) {
+        add(GameResultEvent('eat'));
+      }
+    }
+
+    // TODO 判断长捉,一捉一将,一将一杀
+    if (repeatRound > 3) {
+      setResult(ChessManual.resultFstDraw, '不变招判和');
+      return false;
+    }
+    return true;
+  }
+
+  List<String> getSteps() {
+    return manual.moves.map<String>((cs) => cs.toChineseString()).toList();
+  }
+
+  void dispose() {
+    if (engine != null) {
+      engine?.stop();
+      engine?.quit();
+      engine = null;
+    }
+  }
+
+  void switchPlayer() {
+    curHand++;
+    if (curHand >= hands.length) {
+      curHand = 0;
+    }
+    add(GamePlayerEvent(curHand));
+
+    logger.info('切换选手:${player.title} ${player.team} ${player.driver}');
+
+    logger.info(player.title);
+    next();
+    add(GameEngineEvent('clear'));
+  }
+
+  Future<bool> startEngine() {
+    if (engine != null) {
+      return Future.value(true);
+    }
+    Completer<bool> engineFuture = Completer<bool>();
+    engine = Engine(EngineType.pikafish);
+    engineOK = false;
+    engine?.init().then((Process? v) {
+      engineOK = true;
+      engine?.addListener(parseMessage);
+      engineFuture.complete(true);
+    });
+    return engineFuture.future;
+  }
+
+  void requestHelp() {
+    if (engine == null) {
+      startEngine().then((v) {
+        if (v) {
+          requestHelp();
+        } else {
+          logger.info('engine is not support');
+        }
+      });
+    } else {
+      if (engineOK) {
+        isStop = true;
+        engine?.stop();
+        engine?.position(fenStr);
+        engine?.go(depth: 10);
+      } else {
+        logger.info('engine is not ok');
+      }
+    }
+  }
+
+  String get fenStr => '${manual.currentFen.fen} ${curHand > 0 ? 'b' : 'w'}'
+      ' - - $unEatCount ${manual.moveCount ~/ 2}';
+
+  Player get player => hands[curHand];
+
+  Player getPlayer(int hand) => hands[hand];
+}