Browse Source

优化代码,文件大小写

liuyuqi-cnb 1 hour ago
parent
commit
fe0d8724c6
51 changed files with 957 additions and 1320 deletions
  1. 14 1
      analysis_options.yaml
  2. 0 401
      lib/ai/Ai.dart
  3. 221 0
      lib/ai/ai.dart
  4. 0 7
      lib/bridge/ChessShape.dart
  5. 0 7
      lib/bridge/CircleShape.dart
  6. 0 7
      lib/bridge/RectShape.dart
  7. 3 0
      lib/bridge/chess_shape.dart
  8. 6 0
      lib/bridge/circle_shape.dart
  9. 6 0
      lib/bridge/rect_shape.dart
  10. 2 0
      lib/constants.dart
  11. 0 11
      lib/factory/BlackTheme.dart
  12. 0 10
      lib/factory/BlackThemeFactory.dart
  13. 0 10
      lib/factory/BlueTheme.dart
  14. 0 10
      lib/factory/BlueThemeFactory.dart
  15. 0 5
      lib/factory/ThemeFactory.dart
  16. 2 2
      lib/factory/app_theme.dart
  17. 8 0
      lib/factory/black_theme.dart
  18. 8 0
      lib/factory/black_theme_factory.dart
  19. 8 0
      lib/factory/blue_theme.dart
  20. 8 0
      lib/factory/blue_theme_factory.dart
  21. 5 0
      lib/factory/theme_factory.dart
  22. 0 38
      lib/flyweight/Chess.dart
  23. 0 33
      lib/flyweight/ChessFlyweightFactory.dart
  24. 0 28
      lib/flyweight/Position.dart
  25. 27 0
      lib/flyweight/chess.dart
  26. 24 0
      lib/flyweight/chess_flyweight_factory.dart
  27. 13 0
      lib/flyweight/position.dart
  28. 263 294
      lib/home_page.dart
  29. 15 11
      lib/main.dart
  30. 0 23
      lib/memorandum/CareTaker.dart
  31. 0 56
      lib/memorandum/Checkerboard.dart
  32. 0 5
      lib/memorandum/Memo.dart
  33. 29 0
      lib/memorandum/care_taker.dart
  34. 38 0
      lib/memorandum/checkerboard.dart
  35. 7 0
      lib/memorandum/memo.dart
  36. 0 16
      lib/pages/about_page.dart
  37. 0 16
      lib/pages/login_page.dart
  38. 0 16
      lib/pages/register_page.dart
  39. 0 16
      lib/pages/splash_page.dart
  40. 0 4
      lib/routes.dart
  41. 0 96
      lib/state/State.dart
  42. 0 34
      lib/state/UserContext.dart
  43. 72 0
      lib/state/game_state.dart
  44. 27 0
      lib/state/user_context.dart
  45. 0 77
      lib/utils/TipsDialog.dart
  46. 59 0
      lib/utils/tips_dialog.dart
  47. 0 58
      lib/viewModel/GameViewModel.dart
  48. 45 0
      lib/viewModel/game_view_model.dart
  49. 6 4
      pubspec.yaml
  50. 38 0
      test/ai_test.dart
  51. 3 24
      test/widget_test.dart

+ 14 - 1
analysis_options.yaml

@@ -1,3 +1,16 @@
+analyzer:
+  exclude:
+    - build/**
+    - android/**
+    - ios/**
+    - web/**
+    - windows/**
+    - macos/**
+    - linux/**
 include: package:flutter_lints/flutter.yaml
+
 linter:
-  rules:
+  rules:
+    prefer_const_constructors: true
+    prefer_final_locals: true
+    avoid_print: true

+ 0 - 401
lib/ai/Ai.dart

@@ -1,401 +0,0 @@
-//下棋业务核心类,与界面棋盘对应,业务放在这里,可以和界面代码分离
-import 'dart:core';
-import 'package:gobang/flyweight/ChessFlyweightFactory.dart';
-import 'package:gobang/flyweight/Position.dart';
-
-class Ai {
-  Ai._();
-
-  static Ai? _ai;
-
-  static Ai getInstance() {
-    if (_ai == null) {
-      _ai = Ai._();
-    }
-    return _ai!;
-  }
-
-  static var CHESSBOARD_SIZE = 15;
-  static var FIRST = 1; //先手,-1表示机器,1表示人类,与Position类中的对应
-  var chessboard = List.generate(
-      CHESSBOARD_SIZE, (i) => List.filled(CHESSBOARD_SIZE, 0, growable: false),
-      growable: false); //与界面棋盘对应,0代表空,-1代表机器,1代表人类
-  var score = List.generate(
-      CHESSBOARD_SIZE, (i) => List.filled(CHESSBOARD_SIZE, 0, growable: false),
-      growable: false); //每个位置得分
-
-  void init() {
-    FIRST = 1; //默认人类先手
-    for (int i = 0; i < CHESSBOARD_SIZE; i++) {
-      for (int j = 0; j < CHESSBOARD_SIZE; j++) {
-        chessboard[i][j] = 0;
-        score[i][j] = 0;
-      }
-    }
-  }
-
-  //落子
-  void addChessman(int x, int y, int owner) {
-    chessboard[x][y] = owner;
-  }
-
-  //判断落子位置是否合法
-  bool isLegal(int x, int y) {
-    if (x >= 0 &&
-        x < CHESSBOARD_SIZE &&
-        y >= 0 &&
-        y < CHESSBOARD_SIZE &&
-        chessboard[x][y] == 0) {
-      return true;
-    }
-    return false;
-  }
-
-  //判断哪方赢了(必定有刚落的子引发,因此只需判断刚落子的周围),owner为-1代表机器,owner为1代表人类
-  bool isWin(int x, int y, int owner) {
-    int sum = 0;
-    //判断横向左边
-    for (int i = x - 1; i >= 0; i--) {
-      if (chessboard[i][y] == owner) {
-        sum++;
-      } else {
-        break;
-      }
-    }
-    //判断横向右边
-    for (int i = x + 1; i < CHESSBOARD_SIZE; i++) {
-      if (chessboard[i][y] == owner) {
-        sum++;
-      } else {
-        break;
-      }
-    }
-    if (sum >= 4) {
-      return true;
-    }
-
-    sum = 0;
-    //判断纵向上边
-    for (int i = y - 1; i >= 0; i--) {
-      if (chessboard[x][i] == owner) {
-        sum++;
-      } else {
-        break;
-      }
-    }
-    //判断纵向下边
-    for (int i = y + 1; i < CHESSBOARD_SIZE; i++) {
-      if (chessboard[x][i] == owner) {
-        sum++;
-      } else {
-        break;
-      }
-    }
-    if (sum >= 4) {
-      return true;
-    }
-
-    sum = 0;
-    //判断左上角到右下角方向上侧
-    for (int i = x - 1, j = y - 1; i >= 0 && j >= 0; i--, j--) {
-      if (chessboard[i][j] == owner) {
-        sum++;
-      } else {
-        break;
-      }
-    }
-    //判断左上角到右下角方向下侧
-    for (int i = x + 1, j = y + 1;
-        i < CHESSBOARD_SIZE && j < CHESSBOARD_SIZE;
-        i++, j++) {
-      if (chessboard[i][j] == owner) {
-        sum++;
-      } else {
-        break;
-      }
-    }
-    if (sum >= 4) {
-      return true;
-    }
-
-    sum = 0;
-    //判断右上角到左下角方向上侧
-    for (int i = x + 1, j = y - 1; i < CHESSBOARD_SIZE && j >= 0; i++, j--) {
-      if (chessboard[i][j] == owner) {
-        sum++;
-      } else {
-        break;
-      }
-    }
-    //判断右上角到左下角方向下侧
-    for (int i = x - 1, j = y + 1; i >= 0 && j < CHESSBOARD_SIZE; i--, j++) {
-      if (chessboard[i][j] == owner) {
-        sum++;
-      } else {
-        break;
-      }
-    }
-    if (sum >= 4) {
-      return true;
-    }
-
-    return false;
-  }
-
-  //【【【【【*******整个游戏的核心*******】】】】】______确定机器落子位置
-  //使用五元组评分算法,该算法参考博客地址:https://blog.csdn.net/u011587401/article/details/50877828
-  //算法思路:对15X15的572个五元组分别评分,一个五元组的得分就是该五元组为其中每个位置贡献的分数,
-  //	   一个位置的分数就是其所在所有五元组分数之和。所有空位置中分数最高的那个位置就是落子位置。
-  Position searchPosition() {
-    //每次都初始化下score评分数组
-    for (int i = 0; i < CHESSBOARD_SIZE; i++) {
-      for (int j = 0; j < CHESSBOARD_SIZE; j++) {
-        score[i][j] = 0;
-      }
-    }
-
-    //每次机器找寻落子位置,评分都重新算一遍(虽然算了很多多余的,因为上次落子时候算的大多都没变)
-    //先定义一些变量
-    int humanChessmanNum = 0; //五元组中的黑棋数量
-    int machineChessmanNum = 0; //五元组中的白棋数量
-    int tupleScoreTmp = 0; //五元组得分临时变量
-
-    int goalX = -1; //目标位置x坐标
-    int goalY = -1; //目标位置y坐标
-    int maxScore = -1; //最大分数
-
-    //1.扫描横向的15个行
-    for (int i = 0; i < 15; i++) {
-      for (int j = 0; j < 11; j++) {
-        int k = j;
-        while (k < j + 5) {
-          if (chessboard[i][k] == -1)
-            machineChessmanNum++;
-          else if (chessboard[i][k] == 1) humanChessmanNum++;
-
-          k++;
-        }
-        tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
-        //为该五元组的每个位置添加分数
-        for (k = j; k < j + 5; k++) {
-          score[i][k] += tupleScoreTmp;
-        }
-        //置零
-        humanChessmanNum = 0; //五元组中的黑棋数量
-        machineChessmanNum = 0; //五元组中的白棋数量
-        tupleScoreTmp = 0; //五元组得分临时变量
-      }
-    }
-
-    //2.扫描纵向15行
-    for (int i = 0; i < 15; i++) {
-      for (int j = 0; j < 11; j++) {
-        int k = j;
-        while (k < j + 5) {
-          if (chessboard[k][i] == -1)
-            machineChessmanNum++;
-          else if (chessboard[k][i] == 1) humanChessmanNum++;
-
-          k++;
-        }
-        tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
-        //为该五元组的每个位置添加分数
-        for (k = j; k < j + 5; k++) {
-          score[k][i] += tupleScoreTmp;
-        }
-        //置零
-        humanChessmanNum = 0; //五元组中的黑棋数量
-        machineChessmanNum = 0; //五元组中的白棋数量
-        tupleScoreTmp = 0; //五元组得分临时变量
-      }
-    }
-
-    //3.扫描右上角到左下角上侧部分
-    for (int i = 14; i >= 4; i--) {
-      for (int k = i, j = 0; j < 15 && k >= 0; j++, k--) {
-        int m = k;
-        int n = j;
-        while (m > k - 5 && k - 5 >= -1) {
-          if (chessboard[m][n] == -1)
-            machineChessmanNum++;
-          else if (chessboard[m][n] == 1) humanChessmanNum++;
-
-          m--;
-          n++;
-        }
-        //注意斜向判断的时候,可能构不成五元组(靠近四个角落),遇到这种情况要忽略掉
-        if (m == k - 5) {
-          tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
-          //为该五元组的每个位置添加分数
-          m = k;
-          n = j;
-          for (; m > k - 5; m--, n++) {
-            score[m][n] += tupleScoreTmp;
-          }
-        }
-
-        //置零
-        humanChessmanNum = 0; //五元组中的黑棋数量
-        machineChessmanNum = 0; //五元组中的白棋数量
-        tupleScoreTmp = 0; //五元组得分临时变量
-      }
-    }
-
-    //4.扫描右上角到左下角下侧部分
-    for (int i = 1; i < 15; i++) {
-      for (int k = i, j = 14; j >= 0 && k < 15; j--, k++) {
-        int m = k;
-        int n = j;
-        while (m < k + 5 && k + 5 <= 15) {
-          if (chessboard[n][m] == -1)
-            machineChessmanNum++;
-          else if (chessboard[n][m] == 1) humanChessmanNum++;
-
-          m++;
-          n--;
-        }
-        //注意斜向判断的时候,可能构不成五元组(靠近四个角落),遇到这种情况要忽略掉
-        if (m == k + 5) {
-          tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
-          //为该五元组的每个位置添加分数
-          m = k;
-          n = j;
-          for (; m < k + 5; m++, n--) {
-            score[n][m] += tupleScoreTmp;
-          }
-        }
-        //置零
-        humanChessmanNum = 0; //五元组中的黑棋数量
-        machineChessmanNum = 0; //五元组中的白棋数量
-        tupleScoreTmp = 0; //五元组得分临时变量
-      }
-    }
-
-    //5.扫描左上角到右下角上侧部分
-    for (int i = 0; i < 11; i++) {
-      for (int k = i, j = 0; j < 15 && k < 15; j++, k++) {
-        int m = k;
-        int n = j;
-        while (m < k + 5 && k + 5 <= 15) {
-          if (chessboard[m][n] == -1)
-            machineChessmanNum++;
-          else if (chessboard[m][n] == 1) humanChessmanNum++;
-
-          m++;
-          n++;
-        }
-        //注意斜向判断的时候,可能构不成五元组(靠近四个角落),遇到这种情况要忽略掉
-        if (m == k + 5) {
-          tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
-          //为该五元组的每个位置添加分数
-          m = k;
-          n = j;
-          for (; m < k + 5; m++, n++) {
-            score[m][n] += tupleScoreTmp;
-          }
-        }
-
-        //置零
-        humanChessmanNum = 0; //五元组中的黑棋数量
-        machineChessmanNum = 0; //五元组中的白棋数量
-        tupleScoreTmp = 0; //五元组得分临时变量
-      }
-    }
-
-    //6.扫描左上角到右下角下侧部分
-    for (int i = 1; i < 11; i++) {
-      for (int k = i, j = 0; j < 15 && k < 15; j++, k++) {
-        int m = k;
-        int n = j;
-        while (m < k + 5 && k + 5 <= 15) {
-          if (chessboard[n][m] == -1)
-            machineChessmanNum++;
-          else if (chessboard[n][m] == 1) humanChessmanNum++;
-
-          m++;
-          n++;
-        }
-        //注意斜向判断的时候,可能构不成五元组(靠近四个角落),遇到这种情况要忽略掉
-        if (m == k + 5) {
-          tupleScoreTmp = tupleScore(humanChessmanNum, machineChessmanNum);
-          //为该五元组的每个位置添加分数
-          m = k;
-          n = j;
-          for (; m < k + 5; m++, n++) {
-            score[n][m] += tupleScoreTmp;
-          }
-        }
-
-        //置零
-        humanChessmanNum = 0; //五元组中的黑棋数量
-        machineChessmanNum = 0; //五元组中的白棋数量
-        tupleScoreTmp = 0; //五元组得分临时变量
-      }
-    }
-
-    //从空位置中找到得分最大的位置
-    for (int i = 0; i < 15; i++) {
-      for (int j = 0; j < 15; j++) {
-        if (chessboard[i][j] == 0 && score[i][j] > maxScore) {
-          goalX = i;
-          goalY = j;
-          maxScore = score[i][j];
-        }
-      }
-    }
-
-    if (goalX != -1 && goalY != -1) {
-      return Position(goalX.toDouble(), goalY.toDouble(),
-          ChessFlyweightFactory.getInstance().getChess(""));
-    }
-
-    //没找到坐标说明平局了,笔者不处理平局
-    return Position(
-        -1, -1, ChessFlyweightFactory.getInstance().getChess(""));
-  }
-
-  //各种五元组情况评分表
-  int tupleScore(int humanChessmanNum, int machineChessmanNum) {
-    //1.既有人类落子,又有机器落子,判分为0
-    if (humanChessmanNum > 0 && machineChessmanNum > 0) {
-      return 0;
-    }
-    //2.全部为空,没有落子,判分为7
-    if (humanChessmanNum == 0 && machineChessmanNum == 0) {
-      return 7;
-    }
-    //3.机器落1子,判分为35
-    if (machineChessmanNum == 1) {
-      return 35;
-    }
-    //4.机器落2子,判分为800
-    if (machineChessmanNum == 2) {
-      return 800;
-    }
-    //5.机器落3子,判分为15000
-    if (machineChessmanNum == 3) {
-      return 15000;
-    }
-    //6.机器落4子,判分为800000
-    if (machineChessmanNum == 4) {
-      return 800000;
-    }
-    //7.人类落1子,判分为15
-    if (humanChessmanNum == 1) {
-      return 15;
-    }
-    //8.人类落2子,判分为400
-    if (humanChessmanNum == 2) {
-      return 400;
-    }
-    //9.人类落3子,判分为1800
-    if (humanChessmanNum == 3) {
-      return 1800;
-    }
-    //10.人类落4子,判分为100000
-    if (humanChessmanNum == 4) {
-      return 100000;
-    }
-    return -1; //若是其他结果肯定出错了。这行代码根本不可能执行
-  }
-}

+ 221 - 0
lib/ai/ai.dart

@@ -0,0 +1,221 @@
+import 'package:gobang/constants.dart';
+import 'package:gobang/flyweight/chess_flyweight_factory.dart';
+import 'package:gobang/flyweight/position.dart';
+
+/// 五子棋 AI:五元组评分算法
+/// 参考:https://blog.csdn.net/u011587401/article/details/50877828
+class Ai {
+  Ai._() {
+    chessboard = List.generate(
+      kBoardSize,
+      (_) => List.filled(kBoardSize, 0),
+    );
+    score = List.generate(
+      kBoardSize,
+      (_) => List.filled(kBoardSize, 0),
+    );
+  }
+
+  static final Ai instance = Ai._();
+
+  static Ai getInstance() => instance;
+
+  /// 先手:1 人类,-1 机器
+  int first = 1;
+
+  late final List<List<int>> chessboard;
+  late final List<List<int>> score;
+
+  void init() {
+    first = 1;
+    for (var i = 0; i < kBoardSize; i++) {
+      for (var j = 0; j < kBoardSize; j++) {
+        chessboard[i][j] = 0;
+        score[i][j] = 0;
+      }
+    }
+  }
+
+  void addChessman(int x, int y, int owner) {
+    if (x < 0 || x >= kBoardSize || y < 0 || y >= kBoardSize) return;
+    chessboard[x][y] = owner;
+  }
+
+  bool isLegal(int x, int y) {
+    return x >= 0 &&
+        x < kBoardSize &&
+        y >= 0 &&
+        y < kBoardSize &&
+        chessboard[x][y] == 0;
+  }
+
+  /// 判断 [owner] 在 (x,y) 落子后是否五连
+  bool isWin(int x, int y, int owner) {
+    const directions = [
+      (1, 0),
+      (0, 1),
+      (1, 1),
+      (1, -1),
+    ];
+    for (final (dx, dy) in directions) {
+      var count = 1;
+      count += _countDirection(x, y, dx, dy, owner);
+      count += _countDirection(x, y, -dx, -dy, owner);
+      if (count >= 5) return true;
+    }
+    return false;
+  }
+
+  int _countDirection(int x, int y, int dx, int dy, int owner) {
+    var count = 0;
+    var cx = x + dx;
+    var cy = y + dy;
+    while (cx >= 0 &&
+        cx < kBoardSize &&
+        cy >= 0 &&
+        cy < kBoardSize &&
+        chessboard[cx][cy] == owner) {
+      count++;
+      cx += dx;
+      cy += dy;
+    }
+    return count;
+  }
+
+  /// 选取评分最高的空位作为落子点
+  Position searchPosition() {
+    for (var i = 0; i < kBoardSize; i++) {
+      for (var j = 0; j < kBoardSize; j++) {
+        score[i][j] = 0;
+      }
+    }
+
+    _scoreRows();
+    _scoreColumns();
+    _scoreDiagDown();
+    _scoreDiagUp();
+
+    var goalX = -1;
+    var goalY = -1;
+    var maxScore = -1;
+
+    for (var i = 0; i < kBoardSize; i++) {
+      for (var j = 0; j < kBoardSize; j++) {
+        if (chessboard[i][j] == 0 && score[i][j] > maxScore) {
+          goalX = i;
+          goalY = j;
+          maxScore = score[i][j];
+        }
+      }
+    }
+
+    final black = ChessFlyweightFactory.getInstance().getChess('black');
+    if (goalX != -1 && goalY != -1) {
+      return Position(goalX.toDouble(), goalY.toDouble(), black);
+    }
+    return Position(-1, -1, black);
+  }
+
+  void _scoreRows() {
+    for (var i = 0; i < kBoardSize; i++) {
+      for (var j = 0; j <= kBoardSize - 5; j++) {
+        var human = 0;
+        var machine = 0;
+        for (var k = j; k < j + 5; k++) {
+          final cell = chessboard[i][k];
+          if (cell == -1) {
+            machine++;
+          } else if (cell == 1) {
+            human++;
+          }
+        }
+        final s = tupleScore(human, machine);
+        for (var k = j; k < j + 5; k++) {
+          score[i][k] += s;
+        }
+      }
+    }
+  }
+
+  void _scoreColumns() {
+    for (var i = 0; i < kBoardSize; i++) {
+      for (var j = 0; j <= kBoardSize - 5; j++) {
+        var human = 0;
+        var machine = 0;
+        for (var k = j; k < j + 5; k++) {
+          final cell = chessboard[k][i];
+          if (cell == -1) {
+            machine++;
+          } else if (cell == 1) {
+            human++;
+          }
+        }
+        final s = tupleScore(human, machine);
+        for (var k = j; k < j + 5; k++) {
+          score[k][i] += s;
+        }
+      }
+    }
+  }
+
+  /// 左上 → 右下
+  void _scoreDiagDown() {
+    for (var i = 0; i <= kBoardSize - 5; i++) {
+      for (var j = 0; j <= kBoardSize - 5; j++) {
+        var human = 0;
+        var machine = 0;
+        for (var k = 0; k < 5; k++) {
+          final cell = chessboard[i + k][j + k];
+          if (cell == -1) {
+            machine++;
+          } else if (cell == 1) {
+            human++;
+          }
+        }
+        final s = tupleScore(human, machine);
+        for (var k = 0; k < 5; k++) {
+          score[i + k][j + k] += s;
+        }
+      }
+    }
+  }
+
+  /// 左下 → 右上
+  void _scoreDiagUp() {
+    for (var i = 4; i < kBoardSize; i++) {
+      for (var j = 0; j <= kBoardSize - 5; j++) {
+        var human = 0;
+        var machine = 0;
+        for (var k = 0; k < 5; k++) {
+          final cell = chessboard[i - k][j + k];
+          if (cell == -1) {
+            machine++;
+          } else if (cell == 1) {
+            human++;
+          }
+        }
+        final s = tupleScore(human, machine);
+        for (var k = 0; k < 5; k++) {
+          score[i - k][j + k] += s;
+        }
+      }
+    }
+  }
+
+  /// 五元组评分表
+  int tupleScore(int humanChessmanNum, int machineChessmanNum) {
+    if (humanChessmanNum > 0 && machineChessmanNum > 0) return 0;
+    if (humanChessmanNum == 0 && machineChessmanNum == 0) return 7;
+
+    const machineScores = {1: 35, 2: 800, 3: 15000, 4: 800000};
+    const humanScores = {1: 15, 2: 400, 3: 1800, 4: 100000};
+
+    if (machineChessmanNum > 0) {
+      return machineScores[machineChessmanNum] ?? -1;
+    }
+    if (humanChessmanNum > 0) {
+      return humanScores[humanChessmanNum] ?? -1;
+    }
+    return -1;
+  }
+}

+ 0 - 7
lib/bridge/ChessShape.dart

@@ -1,7 +0,0 @@
-abstract class ChessShape {
-  int? _shape;
-  int get shape => _shape!;
-  set shape(int value) {
-    _shape = value;
-  }
-}

+ 0 - 7
lib/bridge/CircleShape.dart

@@ -1,7 +0,0 @@
-import 'ChessShape.dart';
-
-class CircleShape extends ChessShape{
-  CircleShape(){
-    shape = 1;
-  }
-}

+ 0 - 7
lib/bridge/RectShape.dart

@@ -1,7 +0,0 @@
-import 'ChessShape.dart';
-
-class RectShape extends ChessShape{
-  RectShape(){
-    shape = 2;
-  }
-}

+ 3 - 0
lib/bridge/chess_shape.dart

@@ -0,0 +1,3 @@
+abstract class ChessShape {
+  int get shape;
+}

+ 6 - 0
lib/bridge/circle_shape.dart

@@ -0,0 +1,6 @@
+import 'chess_shape.dart';
+
+class CircleShape extends ChessShape {
+  @override
+  int get shape => 1;
+}

+ 6 - 0
lib/bridge/rect_shape.dart

@@ -0,0 +1,6 @@
+import 'chess_shape.dart';
+
+class RectShape extends ChessShape {
+  @override
+  int get shape => 2;
+}

+ 2 - 0
lib/constants.dart

@@ -0,0 +1,2 @@
+/// 棋盘交叉点数量(标准五子棋 15×15)
+const int kBoardSize = 15;

+ 0 - 11
lib/factory/BlackTheme.dart

@@ -1,11 +0,0 @@
-import 'package:flutter/material.dart';
-
-import 'Theme.dart' as t;
-
-class BlackTheme extends t.Theme{
-  
-  @override
-  Color getThemeColor() {
-    return Colors.black;
-  }
-}

+ 0 - 10
lib/factory/BlackThemeFactory.dart

@@ -1,10 +0,0 @@
-import 'BlackTheme.dart';
-import 'Theme.dart';
-import 'ThemeFactory.dart';
-
-class BlackThemeFactory extends ThemeFactory{
-  @override
-  Theme getTheme() {
-    return BlackTheme();
-  }
-}

+ 0 - 10
lib/factory/BlueTheme.dart

@@ -1,10 +0,0 @@
-import 'package:flutter/material.dart';
-
-import 'Theme.dart' as t;
-
-class BlueTheme extends t.Theme{
-  @override
-  Color getThemeColor() {
-    return Colors.blue;
-  }
-}

+ 0 - 10
lib/factory/BlueThemeFactory.dart

@@ -1,10 +0,0 @@
-import 'BlueTheme.dart';
-import 'Theme.dart';
-import 'ThemeFactory.dart';
-
-class BlueThemeFactory extends ThemeFactory{
-  @override
-  Theme getTheme() {
-    return BlueTheme();
-  }
-}

+ 0 - 5
lib/factory/ThemeFactory.dart

@@ -1,5 +0,0 @@
-import 'Theme.dart';
-
-abstract class ThemeFactory {
-  Theme getTheme();
-}

+ 2 - 2
lib/factory/Theme.dart → lib/factory/app_theme.dart

@@ -1,5 +1,5 @@
 import 'dart:ui';
 
-abstract class Theme{
+abstract class AppTheme {
   Color getThemeColor();
-}
+}

+ 8 - 0
lib/factory/black_theme.dart

@@ -0,0 +1,8 @@
+import 'package:flutter/material.dart';
+
+import 'app_theme.dart';
+
+class BlackTheme extends AppTheme {
+  @override
+  Color getThemeColor() => Colors.black;
+}

+ 8 - 0
lib/factory/black_theme_factory.dart

@@ -0,0 +1,8 @@
+import 'app_theme.dart';
+import 'black_theme.dart';
+import 'theme_factory.dart';
+
+class BlackThemeFactory extends ThemeFactory {
+  @override
+  AppTheme getTheme() => BlackTheme();
+}

+ 8 - 0
lib/factory/blue_theme.dart

@@ -0,0 +1,8 @@
+import 'package:flutter/material.dart';
+
+import 'app_theme.dart';
+
+class BlueTheme extends AppTheme {
+  @override
+  Color getThemeColor() => Colors.blue;
+}

+ 8 - 0
lib/factory/blue_theme_factory.dart

@@ -0,0 +1,8 @@
+import 'app_theme.dart';
+import 'blue_theme.dart';
+import 'theme_factory.dart';
+
+class BlueThemeFactory extends ThemeFactory {
+  @override
+  AppTheme getTheme() => BlueTheme();
+}

+ 5 - 0
lib/factory/theme_factory.dart

@@ -0,0 +1,5 @@
+import 'app_theme.dart';
+
+abstract class ThemeFactory {
+  AppTheme getTheme();
+}

+ 0 - 38
lib/flyweight/Chess.dart

@@ -1,38 +0,0 @@
-
-import 'package:flutter/material.dart';
-import 'package:gobang/bridge/ChessShape.dart';
-
-/// 棋子的抽象类
-/// 使用了桥接模式,外观和颜色是两个不同的维度
-abstract class Chess{
-
-  Color? _color;
-
-  Color get color => _color!;
-
-  ChessShape? _chessShape;
-
-  ChessShape get chessShape => _chessShape!;
-
-  set chessShape(ChessShape? __chessShape);
-}
-
-class BlackChess extends Chess{
-  BlackChess() {
-    _color = Colors.black;
-  }
-
-  set chessShape(ChessShape? __chessShape) {
-    super._chessShape = __chessShape;
-  }
-}
-
-class WhiteChess extends Chess{
-  WhiteChess() {
-    _color = Colors.white;
-  }
-
-  set chessShape(ChessShape? __chessShape) {
-    super._chessShape = __chessShape;
-  }
-}

+ 0 - 33
lib/flyweight/ChessFlyweightFactory.dart

@@ -1,33 +0,0 @@
-import 'dart:collection';
-import 'package:gobang/flyweight/Chess.dart';
-
-/// 棋子的享元工厂,采用单例模式
-class ChessFlyweightFactory {
-  ChessFlyweightFactory._();
-
-  static ChessFlyweightFactory? _factory;
-
-  static ChessFlyweightFactory getInstance() {
-    if (_factory == null) {
-      _factory = ChessFlyweightFactory._();
-    }
-    return _factory!;
-  }
-
-  HashMap<String, Chess> _hashMap = HashMap<String, Chess>();
-
-  Chess getChess(String type) {
-    Chess chess;
-    if (_hashMap[type] != null) {
-      chess = _hashMap[type]!;
-    } else {
-      if (type == "white") {
-        chess = WhiteChess();
-      } else {
-        chess = BlackChess();
-      }
-      _hashMap[type] = chess;
-    }
-    return chess;
-  }
-}

+ 0 - 28
lib/flyweight/Position.dart

@@ -1,28 +0,0 @@
-import 'package:gobang/flyweight/Chess.dart';
-
-/// [position] 是棋子的位置类
-class Position{
-  double? _dx;
-  double? _dy;
-  Chess? _chess;
-
-  Chess get chess => _chess!;
-
-  set chess(Chess value) {
-    _chess = value;
-  }
-
-  Position(this._dx, this._dy, this._chess);
-
-  double get dx => _dx!;
-
-  set dx(double value) {
-    _dx = value;
-  }
-
-  get dy => _dy!;
-
-  set dy(value) {
-    _dy = value;
-  }
-}

+ 27 - 0
lib/flyweight/chess.dart

@@ -0,0 +1,27 @@
+import 'package:flutter/material.dart';
+import 'package:gobang/bridge/chess_shape.dart';
+import 'package:gobang/bridge/circle_shape.dart';
+
+/// 棋子抽象类(桥接:颜色与外观分离)
+/// 颜色为享元内部状态;实际绘制以外层 Position.chessShape 为准。
+abstract class Chess {
+  Color get color;
+
+  ChessShape get chessShape;
+}
+
+class BlackChess extends Chess {
+  @override
+  Color get color => Colors.black;
+
+  @override
+  ChessShape get chessShape => CircleShape();
+}
+
+class WhiteChess extends Chess {
+  @override
+  Color get color => Colors.white;
+
+  @override
+  ChessShape get chessShape => CircleShape();
+}

+ 24 - 0
lib/flyweight/chess_flyweight_factory.dart

@@ -0,0 +1,24 @@
+import 'package:gobang/flyweight/chess.dart';
+
+/// 棋子享元工厂(单例)
+class ChessFlyweightFactory {
+  ChessFlyweightFactory._();
+
+  static final ChessFlyweightFactory instance = ChessFlyweightFactory._();
+
+  static ChessFlyweightFactory getInstance() => instance;
+
+  final Map<String, Chess> _cache = {};
+
+  Chess getChess(String type) {
+    return _cache.putIfAbsent(type, () {
+      switch (type) {
+        case 'white':
+          return WhiteChess();
+        case 'black':
+        default:
+          return BlackChess();
+      }
+    });
+  }
+}

+ 13 - 0
lib/flyweight/position.dart

@@ -0,0 +1,13 @@
+import 'package:gobang/bridge/chess_shape.dart';
+import 'package:gobang/flyweight/chess.dart';
+
+/// 棋子位置;颜色由享元 [Chess] 共享,形状为外部状态。
+class Position {
+  double dx;
+  double dy;
+  Chess chess;
+  ChessShape chessShape;
+
+  Position(this.dx, this.dy, this.chess, {ChessShape? chessShape})
+      : chessShape = chessShape ?? chess.chessShape;
+}

+ 263 - 294
lib/home_page.dart

@@ -2,361 +2,330 @@ import 'dart:math';
 
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
-import 'package:gobang/ai/Ai.dart';
-import 'package:gobang/factory/ThemeFactory.dart';
-import 'package:gobang/flyweight/Chess.dart';
-import 'package:gobang/memorandum/Checkerboard.dart';
-import 'package:gobang/utils/TipsDialog.dart';
-import 'package:gobang/viewModel/GameViewModel.dart';
+import 'package:gobang/ai/ai.dart';
+import 'package:gobang/bridge/circle_shape.dart';
+import 'package:gobang/constants.dart';
+import 'package:gobang/factory/black_theme_factory.dart';
+import 'package:gobang/factory/blue_theme_factory.dart';
+import 'package:gobang/factory/theme_factory.dart';
+import 'package:gobang/flyweight/chess.dart';
+import 'package:gobang/flyweight/position.dart';
+import 'package:gobang/memorandum/checkerboard.dart';
+import 'package:gobang/utils/tips_dialog.dart';
+import 'package:gobang/viewModel/game_view_model.dart';
 
-import 'bridge/CircleShape.dart';
-import 'factory/BlackThemeFactory.dart';
-import 'factory/BlueThemeFactory.dart';
-import 'flyweight/Position.dart';
-
-var width = 0.0;
-
-///简单的实现五子棋效果
 class HomePage extends StatefulWidget {
+  const HomePage({super.key});
+
   @override
-  State<StatefulWidget> createState() => HomePageState();
+  State<HomePage> createState() => HomePageState();
 }
 
 class HomePageState extends State<HomePage> {
-  ThemeFactory? _themeFactory;
-  GameViewModel _viewModel = GameViewModel.getInstance();
-  Checkerboard _originator = Checkerboard.getInstance();
-  Icon lightOn = Icon(Icons.lightbulb, color: Colors.amberAccent);
-  Icon lightOff = Icon(Icons.lightbulb_outline_rounded);
-  Icon circle = Icon(Icons.circle_outlined);
-  Icon rect = Icon(Icons.crop_square);
-  Icon? currentLight, currentShape;
+  late ThemeFactory _themeFactory;
+  final GameViewModel _viewModel = GameViewModel.getInstance();
+  final Checkerboard _board = Checkerboard.getInstance();
+  final Ai _ai = Ai.getInstance();
+
+  static const _lightOn = Icon(Icons.lightbulb, color: Colors.amberAccent);
+  static const _lightOff = Icon(Icons.lightbulb_outline_rounded);
+  static const _circleIcon = Icon(Icons.circle_outlined);
+  static const _rectIcon = Icon(Icons.crop_square);
+
+  Icon _currentLight = _lightOn;
+  Icon _currentShape = _circleIcon;
+  bool _useCircle = true;
+  bool _gameOver = false;
+  int _paintVersion = 0;
+
+  double get _boardWidth => MediaQuery.of(context).size.width * 0.8;
+
+  double get _cellSize => _boardWidth / (kBoardSize - 1);
 
   @override
   void initState() {
-    currentLight = lightOn;
-    _themeFactory = BlueThemeFactory();
-    currentShape = circle;
     super.initState();
+    _themeFactory = BlueThemeFactory();
+    _ai.init();
   }
 
   @override
   Widget build(BuildContext context) {
-    width = MediaQuery.of(context).size.width * 0.8;
+    final themeColor = _themeFactory.getTheme().getThemeColor();
 
     return Scaffold(
       appBar: AppBar(
         elevation: 0,
-        backgroundColor: _themeFactory!.getTheme().getThemeColor(),
-        title: Text("南瓜五子棋"),
+        backgroundColor: themeColor,
+        title: const Text('南瓜五子棋'),
         actions: [
           IconButton(
-              onPressed: () {
-                setState(() {
-                  if (_themeFactory is BlackThemeFactory) {
-                    currentLight = lightOn;
-                    _themeFactory = BlueThemeFactory();
-                  } else {
-                    currentLight = lightOff;
-                    _themeFactory = BlackThemeFactory();
-                  }
-                });
-              },
-              icon: currentLight!),
+            onPressed: _toggleTheme,
+            icon: _currentLight,
+          ),
           IconButton(
-              onPressed: () {
-                setState(() {
-                  if (currentShape == circle) {
-                    currentShape = rect;
-                  } else {
-                    currentShape = circle;
-                  }
-                });
-              },
-              icon: currentShape!),
+            onPressed: _toggleShape,
+            icon: _currentShape,
+          ),
         ],
       ),
       body: Container(
         decoration: BoxDecoration(
-            gradient: LinearGradient(
-                colors: [
-              _themeFactory!.getTheme().getThemeColor(),
-              Colors.white,
-            ],
-                stops: [
-              0.0,
-              1
-            ],
-                begin: FractionalOffset.topCenter,
-                end: FractionalOffset.bottomCenter,
-                tileMode: TileMode.repeated)),
+          gradient: LinearGradient(
+            colors: [themeColor, Colors.white],
+            begin: Alignment.topCenter,
+            end: Alignment.bottomCenter,
+          ),
+        ),
         child: Center(
           child: Column(
-              mainAxisAlignment: MainAxisAlignment.center,
-              crossAxisAlignment: CrossAxisAlignment.center,
-              mainAxisSize: MainAxisSize.max,
-              children: <Widget>[
-                Padding(
-                  padding: EdgeInsets.only(top: 14, bottom: 30),
-                  child: Text(
-                    _viewModel.state,
-                    style: TextStyle(color: Colors.white),
-                  ),
+            mainAxisAlignment: MainAxisAlignment.center,
+            children: [
+              Padding(
+                padding: const EdgeInsets.only(top: 14, bottom: 30),
+                child: Text(
+                  _viewModel.state,
+                  style: const TextStyle(color: Colors.white),
                 ),
-                GestureDetector(
-                    onTapDown: (topDownDetails) {
-                      var position = topDownDetails.localPosition;
-                      Chess chess = _viewModel.play(currentShape == circle);
-                      setState(() {
-                        ChessPainter._position =
-                            Position(position.dx, position.dy, chess);
-                      });
-                    },
-                    child: Stack(
-                      children: [
-                        CustomPaint(
-                          size: Size(width, width),
-                          painter: CheckerBoardPainter(),
-                        ),
-                        CustomPaint(
-                          size: Size(width, width),
-                          painter: ChessPainter(turnAi),
-                        )
-                      ],
-                    )),
-                Padding(
-                  padding: const EdgeInsets.only(top: 16.0),
-                  child: Row(
-                    mainAxisAlignment: MainAxisAlignment.center,
-                    children: [
-                      IconButton(
-                          onPressed: () {
-                            if (_viewModel.undo()) {
-                              _originator.undo();
-                              Ai.getInstance().init();
-                              for (Position po in _originator.state) {
-                                Ai.getInstance().addChessman(
-                                    po.dx ~/ (width / 15),
-                                    po.dy ~/ (width / 15),
-                                    po.chess is WhiteChess ? 1 : -1);
-                              }
-                              setState(() {});
-                            } else {
-                              TipsDialog.show(context, "提示", "现阶段不能悔棋");
-                            }
-                          },
-                          icon: Icon(Icons.undo)),
-                      IconButton(
-                          onPressed: () {
-                            if (_viewModel.surrender()) {
-                              TipsDialog.showByChoose(
-                                  context, "提示", "是否要投降并重新开局?", "是", "否",
-                                  (value) {
-                                if (value) {
-                                  setState(() {
-                                    ChessPainter._position = null;
-                                    _originator.clean();
-                                    _viewModel.reset();
-                                    Ai.getInstance().init();
-                                  });
-                                }
-                                Navigator.pop(context);
-                              });
-                            } else {
-                              TipsDialog.show(context, "提示", "现阶段不能投降");
-                            }
-                          },
-                          icon: Icon(
-                            Icons.sports_handball,
-                            color: Colors.deepPurple,
-                          )),
-                      IconButton(
-                          onPressed: () {
-                            TipsDialog.showByChoose(
-                                context, "提示", "是否重新开局?", "是", "否", (value) {
-                              if (value) {
-                                setState(() {
-                                  ChessPainter._position = null;
-                                  _originator.clean();
-                                  _viewModel.reset();
-                                  Ai.getInstance().init();
-                                });
-                              }
-                              Navigator.pop(context);
-                            });
-                          },
-                          icon: Icon(
-                            Icons.restart_alt,
-                            color: Colors.indigo,
-                          )),
-                    ],
+              ),
+              GestureDetector(
+                onTapDown: _onBoardTap,
+                child: SizedBox(
+                  width: _boardWidth,
+                  height: _boardWidth,
+                  child: CustomPaint(
+                    size: Size.square(_boardWidth),
+                    painter: BoardPainter(
+                      pieces: List.unmodifiable(_board.state),
+                      version: _paintVersion,
+                    ),
                   ),
                 ),
-              ]),
+              ),
+              Padding(
+                padding: const EdgeInsets.only(top: 16),
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  children: [
+                    IconButton(
+                      onPressed: _onUndo,
+                      icon: const Icon(Icons.undo),
+                    ),
+                    IconButton(
+                      onPressed: _onSurrender,
+                      icon: const Icon(
+                        Icons.sports_handball,
+                        color: Colors.deepPurple,
+                      ),
+                    ),
+                    IconButton(
+                      onPressed: _onRestart,
+                      icon: const Icon(
+                        Icons.restart_alt,
+                        color: Colors.indigo,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ],
+          ),
         ),
       ),
     );
   }
 
-  /// Ai 下棋
-  void turnAi() {
-    if (ChessPainter._position!.chess is WhiteChess &&
-        Ai.getInstance().isWin(ChessPainter._position!.dx ~/ (width / 15),
-            ChessPainter._position!.dy ~/ (width / 15), 1)) {
-      TipsDialog.show(context, "恭喜", "您打败了决策树算法");
-    }
-    // 获取Ai下棋地址
-    Ai ai = Ai.getInstance();
-    ChessPainter._position = ai.searchPosition();
-    // 设置棋子外观
-    ChessPainter._position!.chess.chessShape = CircleShape();
-    // 加入决策中
-    Ai.getInstance().addChessman(ChessPainter._position!.dx.toInt(),
-        ChessPainter._position!.dy.toInt(), -1);
-    if (ChessPainter._position!.chess is BlackChess &&
-        Ai.getInstance().isWin(ChessPainter._position!.dx.toInt(),
-            ChessPainter._position!.dy.toInt(), -1)) {
-      TipsDialog.show(context, "很遗憾", "决策树算法打败了您");
-    }
+  void _toggleTheme() {
     setState(() {
-      ChessPainter._position!.dx = ChessPainter._position!.dx * (width / 15);
-      ChessPainter._position!.dy = ChessPainter._position!.dy * (width / 15);
+      if (_themeFactory is BlackThemeFactory) {
+        _currentLight = _lightOn;
+        _themeFactory = BlueThemeFactory();
+      } else {
+        _currentLight = _lightOff;
+        _themeFactory = BlackThemeFactory();
+      }
     });
   }
-}
 
-class ChessPainter extends CustomPainter {
-  static Position? _position;
-  final Function _function;
-  Checkerboard _originator = Checkerboard.getInstance();
+  void _toggleShape() {
+    setState(() {
+      _useCircle = !_useCircle;
+      _currentShape = _useCircle ? _circleIcon : _rectIcon;
+    });
+  }
 
-  ChessPainter(Function f) : _function = f;
+  void _onBoardTap(TapDownDetails details) {
+    if (_gameOver) return;
 
-  @override
-  void paint(Canvas canvas, Size size) {
-    if (_position == null) {
+    final snap = _snapToGrid(details.localPosition);
+    if (snap == null) return;
+
+    final (gridX, gridY, px, py) = snap;
+    if (!_ai.isLegal(gridX, gridY)) return;
+
+    final chess = _viewModel.play();
+    final shape = _viewModel.shapeFor(_useCircle);
+    final position = Position(px, py, chess, chessShape: shape);
+
+    _board.add(position);
+    _ai.addChessman(gridX, gridY, 1);
+    _paintVersion++;
+
+    if (_ai.isWin(gridX, gridY, 1)) {
+      setState(() => _gameOver = true);
+      TipsDialog.show(context, '恭喜', '您打败了决策树算法');
       return;
     }
-    bool add = false;
-    double mWidth = size.width / 15;   // 每行/列 15 个
-    double mHeight = size.height / 15;
-    var mPaint = Paint();
-    //求两个点之间的距离,让棋子正确的显示在坐标轴上面
-    var dx = _position!.dx;
-    var dy = _position!.dy;
-    for (int i = 0; i < CheckerBoardPainter._crossOverBeanList.length; i++) {
-      var absX =
-          (dx - CheckerBoardPainter._crossOverBeanList[i]._dx).abs(); //两个点的x轴距离
-      var absY =
-          (dy - CheckerBoardPainter._crossOverBeanList[i]._dy).abs(); //两个点的y轴距离
-      var s = sqrt(absX * absX +
-          absY * absY); //利用直角三角形求斜边公式(a的平方 + b的平方 = c的平方)来计算出两点间的距离
-      if (s <= mWidth / 2 - 2) {
-        // 触摸点到棋盘坐标坐标点距离小于等于棋子半径,那么
-        //找到离触摸点最近的棋盘坐标点并记录保存下来
-        _position!.dx = CheckerBoardPainter._crossOverBeanList[i]._dx;
-        _position!.dy = CheckerBoardPainter._crossOverBeanList[i]._dy;
-        _originator.add(_position!);
-        add = true;
-        if (_position!.chess is WhiteChess) {
-          Ai.getInstance().addChessman(
-              _position!.dx ~/ (width / 15), _position!.dy ~/ (width / 15), 1);
-        }
-        // flag = false; //白子下完了,该黑子下了
-        break;
-      }
+
+    _playAi();
+    setState(() {});
+  }
+
+  /// 将触摸点吸附到最近交叉点;过远则返回 null
+  (int, int, double, double)? _snapToGrid(Offset local) {
+    final cell = _cellSize;
+    final gx = (local.dx / cell).round().clamp(0, kBoardSize - 1);
+    final gy = (local.dy / cell).round().clamp(0, kBoardSize - 1);
+    final px = gx * cell;
+    final py = gy * cell;
+    final dist = sqrt(pow(local.dx - px, 2) + pow(local.dy - py, 2));
+    if (dist > cell / 2 - 2) return null;
+    return (gx, gy, px, py);
+  }
+
+  void _playAi() {
+    final raw = _ai.searchPosition();
+    if (raw.dx < 0 || raw.dy < 0) return;
+
+    final gridX = raw.dx.toInt();
+    final gridY = raw.dy.toInt();
+    final cell = _cellSize;
+    final position = Position(
+      gridX * cell,
+      gridY * cell,
+      raw.chess,
+      chessShape: CircleShape(),
+    );
+
+    _board.add(position);
+    _ai.addChessman(gridX, gridY, -1);
+    _paintVersion++;
+
+    if (_ai.isWin(gridX, gridY, -1)) {
+      _gameOver = true;
+      TipsDialog.show(context, '很遗憾', '决策树算法打败了您');
     }
+  }
 
-    //画子
-    mPaint..style = PaintingStyle.fill;
-    if (_originator.state.isNotEmpty) {
-      for (int i = 0; i < _originator.state.length; i++) {
-        mPaint..color = _originator.state[i].chess.color;
-        if (_originator.state[i].chess.chessShape.shape == 1) {
-          canvas.drawCircle(
-              Offset(_originator.state[i].dx, _originator.state[i].dy),
-              min(mWidth / 2, mHeight / 2) - 2,
-              mPaint);
-        }
-        if (_originator.state[i].chess.chessShape.shape == 2) {
-          Rect rect = Rect.fromCircle(
-              center: Offset(_originator.state[i].dx, _originator.state[i].dy),
-              radius: min(mWidth / 2, mHeight / 2) - 2);
-          canvas.drawRect(rect, mPaint);
-        }
-      }
+  void _rebuildAiFromBoard() {
+    _ai.init();
+    final cell = _cellSize;
+    for (final po in _board.state) {
+      final gx = (po.dx / cell).round();
+      final gy = (po.dy / cell).round();
+      _ai.addChessman(gx, gy, po.chess is WhiteChess ? 1 : -1);
     }
-    WidgetsBinding.instance!.addPostFrameCallback((_) {
-      if (add && _position!.chess is WhiteChess) {
-        _function();
-      }
-    });
   }
 
-  //在实际场景中正确利用此回调可以避免重绘开销,本示例我们简单的返回true
-  @override
-  bool shouldRepaint(CustomPainter oldDelegate) {
-    return true;
+  void _onUndo() {
+    if (_gameOver) {
+      TipsDialog.show(context, '提示', '对局已结束,请重新开局');
+      return;
+    }
+    if (!_viewModel.undo()) {
+      TipsDialog.show(context, '提示', '现阶段不能悔棋');
+      return;
+    }
+    _board.undo();
+    _rebuildAiFromBoard();
+    setState(() => _paintVersion++);
+  }
+
+  Future<void> _onSurrender() async {
+    if (!_viewModel.surrender()) {
+      await TipsDialog.show(context, '提示', '现阶段不能投降');
+      return;
+    }
+    final ok = await TipsDialog.showByChoose(
+      context,
+      '提示',
+      '是否要投降并重新开局?',
+      '是',
+      '否',
+    );
+    if (ok == true && mounted) {
+      _resetGame();
+    }
+  }
+
+  Future<void> _onRestart() async {
+    final ok = await TipsDialog.showByChoose(
+      context,
+      '提示',
+      '是否重新开局?',
+      '是',
+      '否',
+    );
+    if (ok == true && mounted) {
+      _resetGame();
+    }
+  }
+
+  void _resetGame() {
+    setState(() {
+      _board.clean();
+      _viewModel.reset();
+      _ai.init();
+      _gameOver = false;
+      _paintVersion++;
+    });
   }
 }
 
-class CheckerBoardPainter extends CustomPainter {
-  static List<CrossOverBean> _crossOverBeanList = [];
-  static int _state = 0;
+/// 棋盘 + 棋子同层绘制,避免双重 CustomPaint 与 paint 内副作用
+class BoardPainter extends CustomPainter {
+  BoardPainter({required this.pieces, required this.version});
+
+  final List<Position> pieces;
+  final int version;
 
   @override
   void paint(Canvas canvas, Size size) {
-    double mWidth = size.width / 15;
-    double mHeight = size.height / 15;
-    var mPaint = Paint();
-
-    _crossOverBeanList.clear();
-    //重绘下整个界面的画布北京颜色
-    //设置画笔,画棋盘背景
-    mPaint
-      ..isAntiAlias = true //抗锯齿
-      ..style = PaintingStyle.fill //填充
-      ..color = Color(0x77cdb175); //背景为纸黄色
-    canvas.drawRect(
-        Rect.fromCenter(
-            center: Offset(size.width / 2, size.height / 2),
-            width: size.width,
-            height: size.height),
-        mPaint);
-    //画棋盘网格
-    mPaint
+    final cell = size.width / (kBoardSize - 1);
+    final bg = Paint()
+      ..isAntiAlias = true
+      ..style = PaintingStyle.fill
+      ..color = const Color(0x77cdb175);
+    canvas.drawRect(Offset.zero & size, bg);
+
+    final line = Paint()
       ..style = PaintingStyle.stroke
       ..color = CupertinoColors.systemGrey6
-      ..strokeWidth = 1.0;
-    for (var i = 0; i <= 15; i++) {
-      //画横线
-      canvas.drawLine(
-          Offset(0, mHeight * i), Offset(size.width, mHeight * i), mPaint);
+      ..strokeWidth = 1;
+    for (var i = 0; i < kBoardSize; i++) {
+      final o = cell * i;
+      canvas.drawLine(Offset(0, o), Offset(size.width, o), line);
+      canvas.drawLine(Offset(o, 0), Offset(o, size.height), line);
     }
-    for (var i = 0; i <= 15; i++) {
-      //画竖线
-      canvas.drawLine(
-          Offset(mWidth * i, 0), Offset(mWidth * i, size.height), mPaint);
-    }
-    //记录横竖线所有的交叉点
-    for (int i = 0; i <= 15; i++) {
-      for (int j = 0; j <= 15; j++) {
-        _crossOverBeanList.add(CrossOverBean(mWidth * j, mHeight * i));
+
+    final radius = cell / 2 - 2;
+    final fill = Paint()..style = PaintingStyle.fill;
+    for (final p in pieces) {
+      fill.color = p.chess.color;
+      final center = Offset(p.dx, p.dy);
+      if (p.chessShape.shape == 1) {
+        canvas.drawCircle(center, radius, fill);
+      } else {
+        canvas.drawRect(
+          Rect.fromCircle(center: center, radius: radius),
+          fill,
+        );
       }
     }
   }
 
-  //在实际场景中正确利用此回调可以避免重绘开销,本示例我们简单的返回true
   @override
-  bool shouldRepaint(CustomPainter oldDelegate) {
-    return false;
+  bool shouldRepaint(covariant BoardPainter oldDelegate) {
+    return oldDelegate.version != version;
   }
 }
-
-///记录棋盘上横竖线的交叉点
-class CrossOverBean {
-  double _dx;
-  double _dy;
-
-  CrossOverBean(this._dx, this._dy);
-}

+ 15 - 11
lib/main.dart

@@ -1,29 +1,33 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-import 'home_page.dart';
+import 'package:gobang/home_page.dart';
 
 void main() {
   WidgetsFlutterBinding.ensureInitialized();
-  runApp(MyApp());
-  SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
-    statusBarColor: Colors.transparent,
-    statusBarIconBrightness: Brightness.dark,
-    systemNavigationBarColor: Colors.transparent,
-    systemNavigationBarIconBrightness: Brightness.dark,
-  ));
+  SystemChrome.setSystemUIOverlayStyle(
+    const SystemUiOverlayStyle(
+      statusBarColor: Colors.transparent,
+      statusBarIconBrightness: Brightness.dark,
+      systemNavigationBarColor: Colors.transparent,
+      systemNavigationBarIconBrightness: Brightness.dark,
+    ),
+  );
+  runApp(const MyApp());
 }
 
 class MyApp extends StatelessWidget {
-  // This widget is the root of your application.
+  const MyApp({super.key});
+
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
       debugShowCheckedModeBanner: false,
       title: '南瓜五子棋',
       theme: ThemeData(
-        primarySwatch: Colors.blue,
+        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
+        useMaterial3: true,
       ),
-      home: HomePage(),
+      home: const HomePage(),
     );
   }
 }

+ 0 - 23
lib/memorandum/CareTaker.dart

@@ -1,23 +0,0 @@
-import 'Memo.dart';
-
-class CareTaker{
-  List<Memo> mementoList = [];
-
-  void add(Memo memo) {
-    mementoList.add(memo);
-    if (mementoList.length > 10) {
-      mementoList.removeRange(0, 1);
-    }
-  }
-
-  Memo get(int index){
-    return mementoList[index];
-  }
-
-  Memo getLast() {
-    Memo memo = mementoList[mementoList.length-3];
-    mementoList.removeLast();
-    mementoList.removeLast();
-    return memo;
-  }
-}

+ 0 - 56
lib/memorandum/Checkerboard.dart

@@ -1,56 +0,0 @@
-import 'package:gobang/flyweight/Position.dart';
-import 'package:gobang/memorandum/CareTaker.dart';
-
-import 'Memo.dart';
-
-class Checkerboard {
-
-  Checkerboard._();
-
-  static Checkerboard? _originator;
-
-  static getInstance(){
-    if(_originator == null){
-      _originator = Checkerboard._();
-    }
-    return _originator;
-  }
-
-  bool _canAdd = true; //是否需要添加
-
-  List<Position> _state = [];
-
-  List<Position> get state{
-    return _state;
-  }
-
-  CareTaker _careTaker = CareTaker();
-
-  add(Position position) {
-    if(_canAdd) { //因为每次渲染完默认会把最后一个下棋的位置添加上,但在悔棋阶段最后一个是不需要,因此需要这个判断。
-      _state.add(position);
-      _careTaker.add(_save());
-    }
-    _canAdd = true;
-  }
-
-  _from(Memo memo) {
-    this._state = memo.state;
-  }
-
-  Memo _save() {
-    return Memo()..state.addAll(this._state);
-  }
-
-  clean(){
-    _state = [];
-  }
-
-  undo() {
-    Memo memo = _careTaker.getLast();
-    _from(memo);
-    _canAdd = false;
-  }
-
-
-}

+ 0 - 5
lib/memorandum/Memo.dart

@@ -1,5 +0,0 @@
-import 'package:gobang/flyweight/Position.dart';
-
-class Memo {
-  List<Position> state = [];
-}

+ 29 - 0
lib/memorandum/care_taker.dart

@@ -0,0 +1,29 @@
+import 'memo.dart';
+
+class CareTaker {
+  final List<Memo> _mementoList = [];
+
+  void add(Memo memo) {
+    _mementoList.add(memo);
+    if (_mementoList.length > 10) {
+      _mementoList.removeAt(0);
+    }
+  }
+
+  /// 撤销人类+AI 各一步(共两步),恢复到此前快照。
+  Memo getLast() {
+    if (_mementoList.length < 3) {
+      return Memo();
+    }
+    final memo = _mementoList[_mementoList.length - 3];
+    _mementoList.removeLast();
+    _mementoList.removeLast();
+    return Memo(memo.state);
+  }
+
+  void clear() {
+    _mementoList.clear();
+  }
+
+  int get length => _mementoList.length;
+}

+ 38 - 0
lib/memorandum/checkerboard.dart

@@ -0,0 +1,38 @@
+import 'package:gobang/flyweight/position.dart';
+import 'package:gobang/memorandum/care_taker.dart';
+import 'package:gobang/memorandum/memo.dart';
+
+class Checkerboard {
+  Checkerboard._();
+
+  static final Checkerboard instance = Checkerboard._();
+
+  static Checkerboard getInstance() => instance;
+
+  List<Position> _state = [];
+
+  List<Position> get state => _state;
+
+  final CareTaker _careTaker = CareTaker();
+
+  void add(Position position) {
+    _state.add(position);
+    _careTaker.add(_save());
+  }
+
+  void _from(Memo memo) {
+    _state = List.of(memo.state);
+  }
+
+  Memo _save() => Memo(_state);
+
+  void clean() {
+    _state = [];
+    _careTaker.clear();
+  }
+
+  void undo() {
+    final memo = _careTaker.getLast();
+    _from(memo);
+  }
+}

+ 7 - 0
lib/memorandum/memo.dart

@@ -0,0 +1,7 @@
+import 'package:gobang/flyweight/position.dart';
+
+class Memo {
+  final List<Position> state;
+
+  Memo([List<Position>? state]) : state = List.of(state ?? const []);
+}

+ 0 - 16
lib/pages/about_page.dart

@@ -1,16 +0,0 @@
-import 'package:flutter/src/widgets/framework.dart';
-import 'package:flutter/src/widgets/placeholder.dart';
-
-class AboutPage extends StatefulWidget {
-  const AboutPage({Key? key}) : super(key: key);
-
-  @override
-  State<AboutPage> createState() => _AboutPageState();
-}
-
-class _AboutPageState extends State<AboutPage> {
-  @override
-  Widget build(BuildContext context) {
-    return const Placeholder();
-  }
-}

+ 0 - 16
lib/pages/login_page.dart

@@ -1,16 +0,0 @@
-import 'package:flutter/src/widgets/framework.dart';
-import 'package:flutter/src/widgets/placeholder.dart';
-
-class LoginPage extends StatefulWidget {
-  const LoginPage({Key? key}) : super(key: key);
-
-  @override
-  State<LoginPage> createState() => _LoginPageState();
-}
-
-class _LoginPageState extends State<LoginPage> {
-  @override
-  Widget build(BuildContext context) {
-    return const Placeholder();
-  }
-}

+ 0 - 16
lib/pages/register_page.dart

@@ -1,16 +0,0 @@
-import 'package:flutter/src/widgets/framework.dart';
-import 'package:flutter/src/widgets/placeholder.dart';
-
-class RegisgerPage extends StatefulWidget {
-  const RegisgerPage({Key? key}) : super(key: key);
-
-  @override
-  State<RegisgerPage> createState() => _RegisgerPageState();
-}
-
-class _RegisgerPageState extends State<RegisgerPage> {
-  @override
-  Widget build(BuildContext context) {
-    return const Placeholder();
-  }
-}

+ 0 - 16
lib/pages/splash_page.dart

@@ -1,16 +0,0 @@
-import 'package:flutter/src/widgets/framework.dart';
-import 'package:flutter/src/widgets/placeholder.dart';
-
-class SpalshPage extends StatefulWidget {
-  const SpalshPage({Key? key}) : super(key: key);
-
-  @override
-  State<SpalshPage> createState() => _SpalshPageState();
-}
-
-class _SpalshPageState extends State<SpalshPage> {
-  @override
-  Widget build(BuildContext context) {
-    return const Placeholder();
-  }
-}

+ 0 - 4
lib/routes.dart

@@ -1,4 +0,0 @@
-
-class Routes {
-  static const String index = "/index";
-}

+ 0 - 96
lib/state/State.dart

@@ -1,96 +0,0 @@
-import 'package:gobang/state/UserContext.dart';
-
-abstract class State {
-  int _step = 0;
-
-  int get step => _step;
-  int _reg = 0;
-
-  int get reg => _reg;
-  UserContext _userContext;
-
-  State(UserContext userContext):_userContext = userContext;
-
-  // 悔棋只能悔棋三次
-  bool regretChess();
-
-  // 认输10步之内不能认输
-  bool surrender();
-
-  play() {
-    _step++;
-  }
-
-}
-
-/// [StartState] 开始状态
-class StartState extends State {
-
-  StartState(UserContext userContext) : super(userContext);
-
-  // 悔棋只能悔棋三次
-  @override
-  bool regretChess(){
-    return false;
-  }
-
-  @override
-  bool surrender() {
-    return false;
-  }
-
-  @override
-  play() {
-    super.play();
-    if(_step >= 4) {
-      _userContext.setState(MidState(_userContext).._step = _step.._reg = _reg);
-    }
-  }
-
-}
-
-/// [MidState] 中场状态
-class MidState extends State {
-  MidState(UserContext userContext) : super(userContext);
-
-  @override
-  int get _reg{
-    return super._reg;
-  }
-
-  // 悔棋只能悔棋三次
-  @override
-  bool regretChess(){
-    _reg++;
-    if(_reg == 3) {
-      print('切换到白热化阶段');
-      _userContext.setState(EndState(_userContext).._step = _step.._reg = _reg);
-    }
-    return true;
-  }
-
-  @override
-  bool surrender() {
-    return true;
-  }
-
-}
-
-/// [EndState] 结尾状态
-class EndState extends State {
-  EndState(UserContext userContext) : super(userContext);
-
-
-
-  // 悔棋只能悔棋三次
-  @override
-  regretChess(){
-    return false;
-  }
-
-  @override
-  surrender() {
-    return true;
-  }
-
-}

+ 0 - 34
lib/state/UserContext.dart

@@ -1,34 +0,0 @@
-import 'package:gobang/state/State.dart';
-
-class UserContext {
-
-  late State _state;
-
-  State get state => _state;
-
-  UserContext(){
-    _state = StartState(this);
-  }
-
-  play() {
-    _state.play();
-  }
-
-  // 悔棋只能悔棋三次
-  bool regretChess() {
-    return _state.regretChess();
-  }
-
-  // 认输10步之内不能认输
-  bool surrender() {
-    return _state.surrender();
-  }
-
-  setState(State state){
-    _state = state;
-  }
-
-  void reset() {
-    _state = StartState(this);
-  }
-}

+ 72 - 0
lib/state/game_state.dart

@@ -0,0 +1,72 @@
+import 'package:gobang/state/user_context.dart';
+
+abstract class GameState {
+  int step = 0;
+  int reg = 0;
+  final UserContext userContext;
+
+  GameState(this.userContext);
+
+  bool regretChess();
+
+  bool surrender();
+
+  void play() {
+    step++;
+  }
+}
+
+/// 开局:不可悔棋、不可投降
+class StartState extends GameState {
+  StartState(super.userContext);
+
+  @override
+  bool regretChess() => false;
+
+  @override
+  bool surrender() => false;
+
+  @override
+  void play() {
+    super.play();
+    if (step >= 4) {
+      userContext.setState(
+        MidState(userContext)
+          ..step = step
+          ..reg = reg,
+      );
+    }
+  }
+}
+
+/// 中盘:可悔棋(最多 3 次)、可投降
+class MidState extends GameState {
+  MidState(super.userContext);
+
+  @override
+  bool regretChess() {
+    reg++;
+    if (reg == 3) {
+      userContext.setState(
+        EndState(userContext)
+          ..step = step
+          ..reg = reg,
+      );
+    }
+    return true;
+  }
+
+  @override
+  bool surrender() => true;
+}
+
+/// 终盘:悔棋用尽,仍可投降
+class EndState extends GameState {
+  EndState(super.userContext);
+
+  @override
+  bool regretChess() => false;
+
+  @override
+  bool surrender() => true;
+}

+ 27 - 0
lib/state/user_context.dart

@@ -0,0 +1,27 @@
+import 'package:gobang/state/game_state.dart';
+
+class UserContext {
+  late GameState _state;
+
+  GameState get state => _state;
+
+  UserContext() {
+    _state = StartState(this);
+  }
+
+  void play() {
+    _state.play();
+  }
+
+  bool regretChess() => _state.regretChess();
+
+  bool surrender() => _state.surrender();
+
+  void setState(GameState state) {
+    _state = state;
+  }
+
+  void reset() {
+    _state = StartState(this);
+  }
+}

+ 0 - 77
lib/utils/TipsDialog.dart

@@ -1,77 +0,0 @@
-import 'package:flutter/material.dart';
-
-class TipsDialog {
-  static show(BuildContext context, String title, tips) async {
-    await showDialog<Null>(
-      context: context,
-      barrierDismissible: false,
-      builder: (BuildContext context) {
-        return AlertDialog(
-          title: Text(title),
-          content: SingleChildScrollView(
-            child: ListBody(
-              children: <Widget>[Text(tips)],
-            ),
-          ),
-          actions: <Widget>[
-            TextButton(
-              child: Text('确定'),
-              onPressed: () {
-                Navigator.of(context).pop();
-              },
-            ),
-          ],
-        );
-      },
-    );
-  }
-
-  static wait(BuildContext context, String title, tips) async {
-    await showDialog<Null>(
-      context: context,
-      barrierDismissible: false,
-      builder: (BuildContext context) {
-        return AlertDialog(
-          title: Text(title),
-          content: SingleChildScrollView(
-            child: ListBody(
-              children: <Widget>[Text(tips)],
-            ),
-          ),
-        );
-      },
-    );
-  }
-
-  static showByChoose(
-      BuildContext context, String title, tips, yes, no, Function f) async {
-    await showDialog<Null>(
-      context: context,
-      barrierDismissible: false,
-      builder: (BuildContext context) {
-        return AlertDialog(
-          title: Text(title),
-          content: SingleChildScrollView(
-            child: ListBody(
-              children: <Widget>[Text(tips)],
-            ),
-          ),
-          actions: <Widget>[
-            TextButton(
-              child: Text(no),
-              onPressed: () {
-                f(false);
-              },
-            ),
-            TextButton(
-              child: Text(yes),
-              onPressed: () {
-                f(true);
-              },
-            ),
-          ],
-        );
-      },
-    );
-  }
-}

+ 59 - 0
lib/utils/tips_dialog.dart

@@ -0,0 +1,59 @@
+import 'package:flutter/material.dart';
+
+class TipsDialog {
+  static Future<void> show(
+    BuildContext context,
+    String title,
+    String tips,
+  ) {
+    return showDialog<void>(
+      context: context,
+      barrierDismissible: false,
+      builder: (context) {
+        return AlertDialog(
+          title: Text(title),
+          content: SingleChildScrollView(
+            child: ListBody(children: [Text(tips)]),
+          ),
+          actions: [
+            TextButton(
+              child: const Text('确定'),
+              onPressed: () => Navigator.of(context).pop(),
+            ),
+          ],
+        );
+      },
+    );
+  }
+
+  static Future<bool?> showByChoose(
+    BuildContext context,
+    String title,
+    String tips,
+    String yes,
+    String no,
+  ) {
+    return showDialog<bool>(
+      context: context,
+      barrierDismissible: false,
+      builder: (context) {
+        return AlertDialog(
+          title: Text(title),
+          content: SingleChildScrollView(
+            child: ListBody(children: [Text(tips)]),
+          ),
+          actions: [
+            TextButton(
+              child: Text(no),
+              onPressed: () => Navigator.of(context).pop(false),
+            ),
+            TextButton(
+              child: Text(yes),
+              onPressed: () => Navigator.of(context).pop(true),
+            ),
+          ],
+        );
+      },
+    );
+  }
+}

+ 0 - 58
lib/viewModel/GameViewModel.dart

@@ -1,58 +0,0 @@
-import 'package:gobang/bridge/ChessShape.dart';
-import 'package:gobang/bridge/CircleShape.dart';
-import 'package:gobang/bridge/RectShape.dart';
-import 'package:gobang/flyweight/Chess.dart';
-import 'package:gobang/flyweight/ChessFlyweightFactory.dart';
-import 'package:gobang/state/State.dart';
-import 'package:gobang/state/UserContext.dart';
-
-class GameViewModel {
-  GameViewModel._();
-
-  static GameViewModel? _gameViewModel;
-
-  static getInstance() {
-    if (_gameViewModel == null) {
-      _gameViewModel = GameViewModel._();
-    }
-    return _gameViewModel;
-  }
-
-  UserContext _userContext = UserContext();
-
-  Chess play(bool current) {
-    _userContext.play();
-    Chess chess;
-
-    /// 设置棋子外观
-    ChessShape shape = RectShape();
-    if (current) {
-      shape = CircleShape();
-    }
-    chess = ChessFlyweightFactory.getInstance().getChess("white");
-    chess.chessShape = shape;
-    return chess;
-  }
-
-  bool undo() {
-    return _userContext.regretChess();
-  }
-
-  get state {
-    if (_userContext.state is StartState) {
-      return "热身阶段,不能悔棋,不能投降";
-    } else if (_userContext.state is MidState) {
-      return "入神阶段,可以悔棋且剩余${3 - _userContext.state.reg}次,可以投降";
-    } else if (_userContext.state is EndState) {
-      return "白热化阶段,悔棋次数已用完,但可以投降";
-    }
-  }
-
-  void reset() {
-    _userContext.reset();
-  }
-
-  bool surrender() {
-    return _userContext.surrender();
-  }
-}

+ 45 - 0
lib/viewModel/game_view_model.dart

@@ -0,0 +1,45 @@
+import 'package:gobang/bridge/chess_shape.dart';
+import 'package:gobang/bridge/circle_shape.dart';
+import 'package:gobang/bridge/rect_shape.dart';
+import 'package:gobang/flyweight/chess.dart';
+import 'package:gobang/flyweight/chess_flyweight_factory.dart';
+import 'package:gobang/state/game_state.dart';
+import 'package:gobang/state/user_context.dart';
+
+class GameViewModel {
+  GameViewModel._();
+
+  static final GameViewModel instance = GameViewModel._();
+
+  static GameViewModel getInstance() => instance;
+
+  final UserContext _userContext = UserContext();
+
+  Chess play() {
+    _userContext.play();
+    return ChessFlyweightFactory.getInstance().getChess('white');
+  }
+
+  ChessShape shapeFor(bool useCircle) =>
+      useCircle ? CircleShape() : RectShape();
+
+  bool undo() => _userContext.regretChess();
+
+  String get state {
+    final s = _userContext.state;
+    if (s is StartState) {
+      return '热身阶段,不能悔棋,不能投降';
+    }
+    if (s is MidState) {
+      return '入神阶段,可以悔棋且剩余${3 - s.reg}次,可以投降';
+    }
+    if (s is EndState) {
+      return '白热化阶段,悔棋次数已用完,但可以投降';
+    }
+    return '';
+  }
+
+  void reset() => _userContext.reset();
+
+  bool surrender() => _userContext.surrender();
+}

+ 6 - 4
pubspec.yaml

@@ -1,18 +1,20 @@
 name: gobang
-description: A new Flutter project.
-publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+description: 南瓜五子棋 — Flutter 五子棋
+publish_to: 'none'
 version: 1.1.0+1
+
 environment:
   sdk: ^3.7.0
 
 dependencies:
   flutter:
     sdk: flutter
-  cupertino_icons: ^1.0.2
-  # get: ^
+  cupertino_icons: ^1.0.8
 
 dev_dependencies:
   flutter_test:
     sdk: flutter
+  flutter_lints: ^5.0.0
+
 flutter:
   uses-material-design: true

+ 38 - 0
test/ai_test.dart

@@ -0,0 +1,38 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:gobang/ai/ai.dart';
+import 'package:gobang/constants.dart';
+
+void main() {
+  late Ai ai;
+
+  setUp(() {
+    ai = Ai.getInstance();
+    ai.init();
+  });
+
+  test('board size constant', () {
+    expect(kBoardSize, 15);
+    expect(ai.chessboard.length, 15);
+  });
+
+  test('isWin detects horizontal five', () {
+    for (var i = 0; i < 5; i++) {
+      ai.addChessman(i, 7, 1);
+    }
+    expect(ai.isWin(2, 7, 1), isTrue);
+    expect(ai.isWin(2, 7, -1), isFalse);
+  });
+
+  test('searchPosition returns empty cell', () {
+    ai.addChessman(7, 7, 1);
+    final pos = ai.searchPosition();
+    expect(pos.dx, isNonNegative);
+    expect(pos.dy, isNonNegative);
+    expect(ai.isLegal(pos.dx.toInt(), pos.dy.toInt()), isTrue);
+  });
+
+  test('illegal after occupied', () {
+    ai.addChessman(3, 3, 1);
+    expect(ai.isLegal(3, 3), isFalse);
+  });
+}

+ 3 - 24
test/widget_test.dart

@@ -1,30 +1,9 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility that Flutter provides. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-
-import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
-
 import 'package:gobang/main.dart';
 
 void main() {
-  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
-    // Build our app and trigger a frame.
-    await tester.pumpWidget(MyApp());
-
-    // Verify that our counter starts at 0.
-    expect(find.text('0'), findsOneWidget);
-    expect(find.text('1'), findsNothing);
-
-    // Tap the '+' icon and trigger a frame.
-    await tester.tap(find.byIcon(Icons.add));
-    await tester.pump();
-
-    // Verify that our counter has incremented.
-    expect(find.text('0'), findsNothing);
-    expect(find.text('1'), findsOneWidget);
+  testWidgets('app loads home page', (tester) async {
+    await tester.pumpWidget(const MyApp());
+    expect(find.text('南瓜五子棋'), findsOneWidget);
   });
 }