Browse Source

完成棋子基本Ai

JalorOMEN 2 years ago
parent
commit
12806f6dcd
4 changed files with 511 additions and 17 deletions
  1. 61 16
      lib/GamePage.dart
  2. 367 0
      lib/ai/Ai.dart
  3. 1 1
      lib/main.dart
  4. 82 0
      lib/utils/TipsDialog.dart

+ 61 - 16
lib/GamePage.dart

@@ -2,8 +2,10 @@ import 'dart:math';
 
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
+import 'package:gobang/ai/Ai.dart';
 import 'package:gobang/flyweight/Chess.dart';
 import 'package:gobang/flyweight/ChessFlyweightFactory.dart';
+import 'package:gobang/utils/TipsDialog.dart';
 
 import 'flyweight/Position.dart';
 
@@ -14,7 +16,11 @@ class GamePage extends StatefulWidget {
 }
 
 class GamePageState extends State<GamePage> {
-  Position? _position;
+
+  @override
+  void initState() {
+    super.initState();
+  }
 
   @override
   Widget build(BuildContext context) {
@@ -35,25 +41,36 @@ class GamePageState extends State<GamePage> {
                     child: Text("重置棋盘"),
                     onPressed: () {
                       setState(() {
-                        _position = null;
+                        ChessPainter._position = null;
                         ChessPainter._positions = [];
+                        Ai.getInstance().init();
                         // blackChess = null;
                       });
                     }),
               ),
+              Padding(
+                padding: EdgeInsets.only(bottom: 15.0),
+                child: CupertinoButton.filled(
+                    padding: EdgeInsets.all(0.0),
+                    child: Text("Ai下棋"),
+                    onPressed: () {
+                        turnAi();
+                        // blackChess = null;
+                    }),
+              ),
               GestureDetector(
                   onTapDown: (topDownDetails) {
+                    var position = topDownDetails.localPosition;
+                    Chess chess;
+                    if (ChessPainter._state == 0) {
+                      chess =
+                          ChessFlyweightFactory.getInstance().getChess("white");
+                    } else {
+                      chess =
+                          ChessFlyweightFactory.getInstance().getChess("black");
+                    }
                     setState(() {
-                      var position = topDownDetails.localPosition;
-                      Chess chess;
-                      if (ChessPainter._state == 0) {
-                        chess =
-                            ChessFlyweightFactory.getInstance().getChess("white");
-                      } else {
-                        chess =
-                            ChessFlyweightFactory.getInstance().getChess("black");
-                      }
-                      _position = Position(position.dx, position.dy, chess);
+                      ChessPainter._position = Position(position.dx, position.dy, chess);
                     });
                   },
                   child: Stack(
@@ -64,7 +81,7 @@ class GamePageState extends State<GamePage> {
                       ),
                       CustomPaint(
                         size: Size(300.0, 300.0),
-                        painter: ChessPainter(_position),
+                        painter: ChessPainter(turnAi),
                       )
                     ],
                   ))
@@ -72,20 +89,40 @@ class GamePageState extends State<GamePage> {
       ),
     );
   }
+
+  void turnAi() {
+    if(ChessPainter._position!.chess is WhiteChess && Ai.getInstance().isWin(ChessPainter._position!.dx~/(300/15), ChessPainter._position!.dy~/(300/15), 1)){
+      TipsDialog.show(context, "恭喜", "您打败了决策树算法");
+    }
+    Ai ai = Ai.getInstance();
+    print("Owner:"+Ai.getInstance().isWin(ChessPainter._position!.dx~/(300/15), ChessPainter._position!.dy~/(300/15), 1).toString());
+    ChessPainter._position = ai.searchPosition();
+    Ai.getInstance().addChessman(ChessPainter._position!.dx.toInt(), ChessPainter._position!.dy.toInt(), -1);
+    print("Ai:"+Ai.getInstance().isWin(ChessPainter._position!.dx.toInt(), ChessPainter._position!.dy.toInt(), -1).toString());
+    if(ChessPainter._position!.chess is BlackChess &&Ai.getInstance().isWin(ChessPainter._position!.dx.toInt(), ChessPainter._position!.dy.toInt(), -1)){
+      TipsDialog.show(context, "很遗憾", "决策树算法打败了您");
+    }
+    setState(() {
+      ChessPainter._position!.dx = ChessPainter._position!.dx*(300/15);
+      ChessPainter._position!.dy = ChessPainter._position!.dy*(300/15);
+    });
+  }
 }
 
 class ChessPainter extends CustomPainter {
   static List<Position> _positions = [];
   static int _state = 0;
-  final Position? _position;
+  static Position? _position;
+  final Function _function;
 
-  ChessPainter(Position? position) : _position = position;
+  ChessPainter(Function f):_function = f;
 
   @override
   void paint(Canvas canvas, Size size) {
     if (_position == null) {
       return;
     }
+    bool add = false;
     double mWidth = size.width / 15;
     double mHeight = size.height / 15;
     var mPaint = Paint();
@@ -103,7 +140,10 @@ class ChessPainter extends CustomPainter {
         _position!.dx = CheckerBoardPainter._crossOverBeanList[i]._dx;
         _position!.dy = CheckerBoardPainter._crossOverBeanList[i]._dy;
         _positions.add(_position!);
-        _state = (_state + 1) % 2;
+        add = true;
+        if (_position!.chess is WhiteChess) {
+          Ai.getInstance().addChessman(_position!.dx~/(300/15), _position!.dy~/(300/15), 1);
+        }
         // flag = false; //白子下完了,该黑子下了
         break;
       }
@@ -118,6 +158,11 @@ class ChessPainter extends CustomPainter {
             min(mWidth / 2, mHeight / 2) - 2, mPaint);
       }
     }
+    WidgetsBinding.instance!.addPostFrameCallback((_) {
+      if (add &&_position!.chess is WhiteChess) {
+        _function();
+      }
+    });
   }
 
   //在实际场景中正确利用此回调可以避免重绘开销,本示例我们简单的返回true

+ 367 - 0
lib/ai/Ai.dart

@@ -0,0 +1,367 @@
+
+//下棋业务核心类,与界面棋盘对应,业务放在这里,可以和界面代码分离
+import 'dart:core';
+
+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;
+    print("$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];
+        }
+      }
+    }
+    print(maxScore);
+
+    if(goalX != -1 && goalY != -1){
+      return new Position(goalX.toDouble(), goalY.toDouble(), ChessFlyweightFactory.getInstance().getChess(""));
+    }
+
+    //没找到坐标说明平局了,笔者不处理平局
+    print("没有找到");
+    return new 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;//若是其他结果肯定出错了。这行代码根本不可能执行
+  }
+
+}

+ 1 - 1
lib/main.dart

@@ -11,7 +11,7 @@ class MyApp extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
-      title: 'Flutter Demo',
+      title: '南瓜五子棋',
       theme: ThemeData(
         // This is the theme of your application.
         //

+ 82 - 0
lib/utils/TipsDialog.dart

@@ -0,0 +1,82 @@
+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 new AlertDialog(
+          title: new Text(title),
+          content: new SingleChildScrollView(
+            child: new ListBody(
+              children: <Widget>[
+                Text(tips)
+              ],
+            ),
+          ),
+          actions: <Widget>[
+            new FlatButton(
+              child: new 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 new AlertDialog(
+          title: new Text(title),
+          content: new SingleChildScrollView(
+            child: new 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 new AlertDialog(
+          title: new Text(title),
+          content: new SingleChildScrollView(
+            child: new ListBody(
+              children: <Widget>[
+                Text(tips)
+              ],
+            ),
+          ),
+          actions: <Widget>[
+            new FlatButton(
+              child: new Text(no),
+              onPressed: () {
+                f(false);
+              },
+            ),
+            new FlatButton(
+              child: new Text(yes),
+              onPressed: () {
+                f(true);
+              },
+            ),
+          ],
+        );
+      },
+    );
+  }
+}