Flutter AI五子棋

前言
在上一篇文章中,讲解了如何实现双人在本地对战的五子棋,但是只有一个人的时候就不太好玩,同时博主也没有把五子棋相关的文章写过瘾。那么这篇文章,我们来实现一个功能更加丰富的五子棋吧!在设计五子棋的算法方面,我们将引入一些经典的算法,如最大最小搜索(Max-Min)算法和Alpha-Beta剪枝算法。这些算法将帮助我们创建一个智能的对手,使游戏更具挑战性和趣味性。除了算法的介绍,本文还将深入探讨五子棋的基本玩法和规则。我们将详细解释如何落子、如何判断胜负以及如何对各种局面进行评分估值。通过学习这些基础知识,您将能够更好地理解和享受五子棋游戏。
效果图:
在这里插入图片描述

仓库地址:https://github.com/taxze6/flutter_game_collection/tree/main/gomoku_ai
release apk下载体验:https://github.com/taxze6/flutter_game_collection/tree/main/gomoku_ai/release
棋盘绘制
本次采用的棋盘绘制与上篇文章的方式不同,上篇文章中采用的是GridView这样的基础组件,使用简单,无需手动编写绘制逻辑。利用GridView的布局特性,可以很方便地进行排列和调整。但是它也有缺点,就是不够灵活,当我们想实现更多的棋盘细节时,实现起来就不是很方便了,所以在本篇文章中,我们采用CustomPaint绘制的方式。
那在绘制棋盘之前,我们需要先定义游戏所需要的一些参数和实体类:
玩家类
//玩家
class Player {
static final Player black = Player(Colors.black);
static final Player white = Player(Colors.white);
late Color color;

Player(this.color);

@override
String toString() {
return ‘Player{${this == black ? “black” : “white”}}’;
}
}
单颗棋子类
class Chessman {
//坐标
late Offset position;

//该棋子的所属人
late Player owner;

//棋子id
int numberId = chessmanList.length;

//棋子的分数,默认为0
int score = 0;

Chessman(this.position, this.owner);

Chessman.white(this.position) {
owner = Player.white;
}

Chessman.black(this.position) {
owner = Player.black;
}

@override
String toString() {
return ‘Chessman{position: ( p o s i t i o n . d x , {position.dx}, position.dx,{position.dy}), owner: ${owner == Player.black ? “black” : “white”}, score: $score, numberId: $numberId}’;
}
}
全局通用参数
//初始化一个玩家,掌握黑棋
Player firstPlayer = Player.black;
//存放所有的棋子
List chessmanList = [];
//存放胜利的棋子
List winResult = [];
那么所需的参数及实体类编写完成后,就可以开始棋盘的绘制啦!
游戏页面整体布局结构
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(“五子棋AI版”),
),
body: Padding(
padding: EdgeInsets.only(top: 50, left: 20, right: 20),
child: Column(
children: [
//棋盘
GestureDetector(
child: CustomPaint(
painter: ChessmanPaint(),
size: Size(400, 400),
),
onTapDown: (details) {
onTapDown(details);
setState(() {});
},
),
//底部操作项目
Padding()
],
),
),
);
}
棋盘绘制主体
● 定义所需绘制参数
//默认棋盘的行列数
const int LINE_COUNT = 14;
//根据屏幕大小与行列数,计算得出每个格子的宽高,初始化先为0
double cellWidth = 0, cellHeight = 0;
● 绘制黄褐色背景
在绘制背景这里:canvas.drawRect(Offset.zero & size, painter),用了个dart的语法糖,有些朋友可能会有些疑惑,drawRect方法第一个参数不是Rect类型的吗,这里传了个Offset.zero & size是什么鬼?这里单独解释下:Offset.zero表示矩形范围的左上角坐标为原点(0,0),size表示矩形的大小。这个表达式使用&符号将两个对象合并成了一个Rect对象作为canvas.drawRect()方法的第一个参数。实际上,&符号在这里是Dart语言中的语法糖,等效于使用Rect.fromLTWH(0, 0, size.width, size.height)来创建一个矩形。因此,这里的语法Offset.zero & size可以通过Rect.fromLTWH(0, 0, size.width, size.height)来替代。
class ChessmanPaint extends CustomPainter {
late Canvas canvas;
late Paint painter;
//用于控制打印在棋子上的id
static const bool printLog = true;

@override
void paint(Canvas canvas, Size size) {
this.canvas = canvas;
//计算单个格子的宽高
cellWidth = size.width / LINE_COUNT;
cellHeight = size.height / LINE_COUNT;

painter = Paint()..isAntiAlias = true..style = PaintingStyle.fill..color = Color(0x77cdb175);
//绘制背景
canvas.drawRect(Offset.zero & size, painter);
}
...

}
● 绘制棋盘上的线条(格子)
@override
void paint(Canvas canvas, Size size) {

painter
…style = PaintingStyle.stroke
…color = Colors.black87
…strokeWidth = 1.0;

for (int i = 0; i <= LINE_COUNT; ++i) {double y = cellHeight * i;canvas.drawLine(Offset(0, y), Offset(size.width, y), painter);
}for (int i = 0; i <= LINE_COUNT; ++i) {double x = cellWidth * i;canvas.drawLine(Offset(x, 0), Offset(x, size.height), painter);
}

}
● 绘制五子棋盘上的五个交叉点

这5个点称为“星”。中间的星也称天元,表示棋盘的正中心,其他4个星,也叫小星。星在棋盘上起标示位置的作用,利于在行棋、复盘、记录等时,更清晰、迅速地找到所需位置。
//绘制棋盘上的5个黑点
void _drawMarkPoints() {
// 通过多次调用_drawMarkPoint方法来绘制标记点
_drawMarkPoint(const Offset(7.0, 7.0));
_drawMarkPoint(const Offset(3.0, 3.0));
_drawMarkPoint(const Offset(3.0, 11.0));
_drawMarkPoint(const Offset(11.0, 3.0));
_drawMarkPoint(const Offset(11.0, 11.0));
}

void _drawMarkPoint(Offset offset) {
painter
…style = PaintingStyle.fill
…color = Colors.black;

// 计算标记点在画布上的具体位置
Offset center = Offset(offset.dx * cellWidth, offset.dy * cellHeight);

// 在计算得到的位置绘制一个半径为3的圆形标记点
canvas.drawCircle(center, 3, painter);
}
● 绘制棋子
这里使用min(cellWidth / 2, cellHeight / 2) - 2计算出较小的一边长度减去2作为圆的半径,可以使得所有棋子的大小一致,并且不会越出格子范围。
//遍历chessmanList绘制,每下一颗子,触发setState
if (chessmanList.isNotEmpty) {
for (Chessman c in chessmanList) {
_drawChessman©;
}
}

void _drawChessman(Chessman chessman) {
painter
…style = PaintingStyle.fill
//根据owner取得每课棋子对应的颜色
…color = chessman.owner.color;

Offset center = Offset(
chessman.position.dx * cellWidth, chessman.position.dy * cellHeight);
canvas.drawCircle(center, min(cellWidth / 2, cellHeight / 2) - 2, painter);
//如果当前棋子的编号是最后一枚棋子,则使用painter绘制一个描边的蓝色圆圈,表示这是最后下的一枚棋子。
if (chessman.numberId == chessmanList.length - 1) {
painter
…color = Colors.blue
…style = PaintingStyle.stroke
…strokeWidth = 3.0;
canvas.drawCircle(
center, min(cellWidth / 2, cellHeight / 2) - 2, painter);
}
}
● 绘制棋子编号(非主要功能,可以跳过这步)
//在棋子上绘制它的id
if (printLog) {
_drawText((i.toString()),
Offset(-19, y - _calcTrueTextSize(i.toString(), 15.0).dy / 2));
}

void _drawText(String text, Offset offset, {Color? color, double? textSize}) {
// 创建ParagraphBuilder对象,用于构建文本段落
ui.ParagraphBuilder builder = ui.ParagraphBuilder(ui.ParagraphStyle(
textAlign: TextAlign.center,
ellipsis: ‘…’,
maxLines: 1,
));

// 使用pushStyle方法设置文本风格,包括颜色和字体大小
builder.pushStyle(
ui.TextStyle(color: color ?? Colors.red, fontSize: textSize ?? 15.0));

// 添加文本到builder对象中
builder.addText(text);

// 构建一个Paragraph对象
ui.Paragraph paragraph = builder.build();

// 对paragraph进行layout,指定宽度为无限大
paragraph.layout(const ui.ParagraphConstraints(width: double.infinity));

// 在Canvas上绘制paragraph对象,位置为offset
canvas.drawParagraph(paragraph, offset);
}

//根据给定的文本字符串和字体大小,计算出该文本所占据的实际宽度和高度,以便在UI布局中更好地控制文本的位置和尺寸。
Offset _calcTrueTextSize(String text, double textSize) {
// 创建ParagraphBuilder对象,并设置字体大小
var paragraph = ui.ParagraphBuilder(ui.ParagraphStyle(fontSize: textSize))
…addText(text);

// 构建Paragraph对象,并进行layout,指定宽度为无限大
var p = paragraph.build()
…layout(const ui.ParagraphConstraints(width: double.infinity));

// 返回Paragraph对象的最小内在宽度和高度作为偏移量
return Offset(p.minIntrinsicWidth, p.height);
}
用户交互(下棋)
取得用户点击的位置
通过GestureDetector的onTapDown取得用户点击的位置
GestureDetector(
child: CustomPaint(
painter: ChessmanPaint(),
size: Size(400, 400),
),
onTapDown: (details) {
onTapDown(details);
setState(() {});
},
),
点击事件
//棋盘点击事件
void onTapDown(TapDownDetails details) {
//游戏胜利后,再点击棋盘就无效
if (winResult.isNotEmpty) {
return;
}
double clickX = details.localPosition.dx;
//计算点击点所在列的索引值 floorX。通过将 clickX 除以格子的宽度 cellWidth 并向下取整,可以得到点击点所处的列索引值
int floorX = clickX ~/ cellWidth;
//计算了当前列横坐标网格线中点的横坐标值 offsetFloorX。通过将 floorX 乘以格子的宽度 cellWidth,再加上格子宽度的一半 cellWidth / 2,可以得到当前列横坐标网格线中点的横坐标值。
double offsetFloorX = floorX * cellWidth + cellWidth / 2;
//判断点击点在哪一列,并将结果赋值给变量 x。如果 offsetFloorX 大于点击点的 x 坐标 clickX,则说明点击点在 floorX 列;否则,说明点击点在 floorX + 1 列。如果点击点在 floorX + 1 列,则通过 ++floorX 来获取 floorX + 1 的值。
int x = offsetFloorX > clickX ? floorX : ++floorX;

//y轴同理
double clickY = details.localPosition.dy;
int floorY = clickY ~/ cellHeight;
double offsetFloorY = floorY * cellHeight + cellHeight / 2;
int y = offsetFloorY > clickY ? floorY : ++floorY;

//触发落子
fallChessman(Offset(x.toDouble(), y.toDouble()));
}
落子函数
void fallChessman(Offset position) {
if (winResult.isNotEmpty) {
return;
}
//创建棋子
Chessman newChessman;
//棋子的颜色
if (chessmanList.isEmpty || chessmanList.length % 2 == 0) {
newChessman = firstPlayer == Player.black
? Chessman.black(position)
: Chessman.white(position);
} else {
newChessman = firstPlayer == Player.black
? Chessman.white(position)
: Chessman.black(position);
}
//判断是否能落子
bool canFall = canFallChessman(newChessman);
if (canFall) {
//可以落子
//打印下落子棋子的信息
printFallChessmanInfo(newChessman);
//此处还需完成:
//1.棋子估值、ai相关逻辑
//2.对游戏胜利的校验,对游戏和棋的校验
}else{
print(“此处无法落子!”);
}
}

void printFallChessmanInfo(Chessman newChessman) {
print(
“[落子成功], 棋子序号: n e w C h e s s m a n . n u m b e r I d , 颜色 : {newChessman.numberId} ,颜色: newChessman.numberId,颜色:{newChessman.owner == Player.WHITE ? “白色” : “黑色”} , 位置 :(${newChessman.position.dx.toInt()} , ${newChessman.position.dy.toInt()})”);
}
该坐标能否落子的判断
bool canFallChessman(Chessman chessman) {
//定义一个不可能生成到棋盘上的棋子
Chessman defaultChessman = Chessman(Offset(-1, 0), Player.black);
if (chessmanList.isNotEmpty) {
Chessman cm = chessmanList.firstWhere((Chessman c) {
//如果找到位置相同的棋子,那么cm就等于这棋子的信息
return c.position.dx == chessman.position.dx &&
c.position.dy == chessman.position.dy;
}, orElse: () {
//没找到就把该棋子添加到列表中,然后返回一个不可能在棋盘上的棋子用作校验
chessmanList.add(chessman);
return defaultChessman;
});
// 如果找到了相同位置的棋子,这里就会返回false;否则返回true
return cm == defaultChessman;
} else {
//如果为空直接添加
chessmanList.add(chessman);
return true;
}
}
棋盘校验规则
相较于棋子估值和ai的实现,对棋子胜利、和棋的校验会比较简单,从简到难,让我们先完成对游戏规则的定义:
胜利判断
bool checkResult(Chessman newChessman) {
int currentX = newChessman.position.dx.toInt();
int currentY = newChessman.position.dy.toInt();

int count = 0;

///横
/// o o o o o
/// o o o o o
/// x x x x x
/// o o o o o
/// o o o o o
winResult.clear();
// 循环遍历当前行的前后四个位置(如果存在),检查是否有特定的棋子连成五子相连
//判断 currentX - 4 > 0 时,它的意思是判断左侧第 4 个位置是否在棋盘内。
//如果 currentX - 4 大于 0,则表示左侧第 4 个位置在棋盘内;
//否则,即 currentX - 4 <= 0,表示左侧第 4 个位置已经超出了棋盘边界。
for (int i = (currentX - 4 > 0 ? currentX - 4 : 0);
i <= (currentX + 4 < LINE_COUNT ? currentX + 4 : LINE_COUNT);
i++) {
// 计算当前位置的坐标
Offset position = Offset(i.toDouble(), currentY.toDouble());

// 检查当前位置是否存在胜利的棋子
if (existSpecificChessman(position, newChessman.owner)) {// 将该棋子添加到胜利结果列表中,并增加计数器winResult.add(Chessman(position, newChessman.owner));count++;
} else {// 如果不存在特定的棋子,清空胜利结果列表,并将计数器重置为0winResult.clear();count = 0;
}// 解析:如果计数器达到5,表示有五子相连,输出胜利者信息并返回true
if (count >= 5) {print("胜利者产生: ${newChessman.owner == Player.white ? "白色" : "黑色"}");//游戏胜利的提示弹窗winDialog("胜利者产生: ${newChessman.owner == Player.white ? "白色" : "黑色"}");return true;
}

}

//竖、正斜、反斜的逻辑代码请查看源码,和横的校验差不多


winResult.clear();
return false;
}

// 检查给定位置是否存在特定的棋子,并且这个棋子的所有者是否与指定玩家相同
bool existSpecificChessman(Offset position, Player player) {
//定义一个不可能生成到棋盘上的棋子
Chessman defaultChessman = Chessman(Offset(-1, 0), Player.black);
// 检查棋子列表是否非空
if (chessmanList.isNotEmpty) {
// 在棋子列表中查找匹配给定位置的棋子
var cm = chessmanList.firstWhere((Chessman c) {
return c.position.dx == position.dx && c.position.dy == position.dy;
}, orElse: () {
return defaultChessman;
});

// 如果找到匹配的棋子,检查其所有者是否是指定的玩家
return cm != defaultChessman && cm.owner == player;

}
// 如果棋子列表为空或不存在棋子匹配给定位置,则返回false
return false;
}
existSpecificChessman函数看起来和前面判断该坐标能否落子的canFallChessman函数差不多,这两个函数的主要区别在于作用和调用时机不同:existSpecificChessman校验的是当前位置是否存在特定棋子且所有者是否相符,而canFallChessman校验的是当前位置是否可以落子。
和棋判断
判断是否和棋其实非常简单,只要没有胜利,同时棋盘满了,就代表和棋了。
//判断棋盘是否满了
bool isHaveAvailablePosition() {
return chessmanList.length <= 255;
}
到这里为止呢已经完成了五子棋的基本玩法,你可以邀请你的朋友和你一起对战了

棋子估值
对每颗棋子进行打分,是完成一切算法的基础条件,如果没有分数,那么算法也就无法生效。
估值算法也是本文的核心,个人觉得估价函数比MinMax算法和Alpha-Beta剪枝算法这两个算法的难度大多了,本文的算法部分主要参考了这几篇文章:
五子棋估值算法
基于博弈树的五子棋 AI 算法及其 C++ 实现
前提条件:本文的规则只涉及无禁手的五子棋
大部分的棋类游戏,先手都有一个优势。以五子棋为例,先达成五子连珠者胜,由于黑方先走了一步,五子棋几乎是先手必胜的局面。所以假设五子棋的胜负条件会变成:如果黑方达成五子连珠之后,白棋也可在一步之内达成五子连珠,判定平手。这样的话就公平了,但是也失去了对弈的一些乐趣和意义,因为白棋只要一直跟着黑棋下,最后一定会为平局。所以为了平衡先手优势,大部分棋类都有一个补偿规则。如五子棋的禁手以及三手交换五手两打。在此不作过多解释,有兴趣可以自行百度,本文的规则及算法对先手无任何限制。
相较于象棋、围棋,五子棋的局面并不复杂,估值还算比较简单,我们简单的用一个整数表示当前局势,分数越大,则自己优势越大,分数越小,则对方优势越大,分数为0是表示双方局势相当。可以先把几种情况定义出来:
其中的解释中,x代表白棋,o代表黑棋,我们从黑棋的角度去评分
static const int WIN = 10000;

//低级死二 xoox
static const int DEEP_DEATH2 = 2;

//死二 xoo
static const int LOWER_DEATH2 = 4;
//低级死三 xooox
static const int DEEP_DEATH3 = 3;
//死三 xooo
static const int LOWER_DEATH3 = 6;

//低级死四 xoooox
static const int DEEP_DEATH4 = 4;
//死四 xoooo
static const int LOWER_DEATH4 = 32;

//活二 oo
static const int ALIVE2 = 10;
//跳活二 o o
static const int JUMP_ALIVE2 = 2;
//活三 ooo
static const int ALIVE3 = 100;
//跳活三 oo o
static const int JUMP_ALIVE3 = 10;
//活四 oooo
static const int ALIVE4 = 5000;
//跳活四 (1跳3或者3跳1或2跳2) o ooo || ooo o || oo oo
static const int JUMP_ALIVE4 = 90;
在实现估值算法前,我们还需要实现一个泛型类BufferMap,实现一个缓冲区的功能,BufferMap的用处在于记录和管理最近的几个棋盘状态。借助它可以用于实现游戏的一些功能,例如:
● 悔棋功能:如果玩家想要悔棋,可以通过BufferMap中的历史记录回退到之前的棋盘状态,从而实现悔棋操作。
● 撤销操作:当玩家进行某些操作后,发现操作结果不符合预期,可以利用BufferMap中的历史记录撤销该操作,恢复到之前的棋盘状态。
● 历史记录展示:通过BufferMap中保存的棋盘状态,可以展示游戏的历史记录,供玩家回顾以及分析棋局发展。
● AI训练:对于AI算法的训练过程中,可以使用BufferMap来保存训练数据中的棋盘状态,以便进行样本回放、经验重放等技术。
class BufferMap {
//设置缓冲区为3
num maxCount = 3;
final Map<num, V> buffer = {};

BufferMap();

BufferMap.maxCount(this.maxCount);

// 添加元素(key存的是每个棋子的分数,value是每个棋子的offset)
void put(num key, V value) {
buffer.update(key, (V val) {
return value;
},
//当缓冲区中不存在指定键时,会执行该回调函数来添加新的键值对。
ifAbsent: () {
return value;
});
_checkSize();
}

// 批量添加元素
void putAll(BufferMap map) {
for (var entry in map.buffer.entries) {
buffer[entry.key] = entry.value;
}
}

// 检查并缩减缓冲区大小
void _checkSize() {
//将缓冲区的所有键转换成列表,并赋值给变量 list,按照从大到小排列
var list = buffer.keys.toList()
…sort((num a, num b) {
return b.compareTo(a);
});
while (buffer.length > maxCount) {
buffer.remove(list.last);
}
}

// 将缓冲区转为Map
Map<num, V> toMap() {
return Map<num, V>.from(buffer);
}

// 获取所有元素的值
Iterable values() {
return buffer.values;
}

// 获取缓存元素个数
int size() {
return buffer.length;
}

// 转为字符串表示
@override
String toString() {
StringBuffer sb = StringBuffer();
sb.write(“{”);
var keys = buffer.keys.toList()
…sort((num a, num b) {
return b.compareTo(a);
});

for (var i in keys) {sb.write("[$i , ${buffer[i]}] ,");
}return "${sb.toString().substring(0, sb.toString().length - 2)}}";

}

// 获取第一个元素的值
V? get first => buffer[buffer.keys.toList()
…sort((num a, num b) {
return b.compareTo(a);
})
…first];

// 获取键的最小值
num minKey() {
if (buffer.isEmpty) {
return double.negativeInfinity;
}
var list = buffer.keys.toList()
…sort((num a, num b) {
return b.compareTo(a);
});
return list.isNotEmpty ? list.last : double.negativeInfinity;
}

// 获取键值最小的元素
MapEntry<num, V>? min() {
if (buffer.isEmpty) {
return null;
}
var list = buffer.keys.toList()
…sort((num a, num b) {
return b.compareTo(a);
});
return list.isNotEmpty ? MapEntry(list.last, buffer[list.last]!) : null;
}

// 获取所有键的列表
List get keySet {
if (buffer.isEmpty) return [];

var sortedKeys = buffer.keys.toList()..sort((num a, num b) {return (b - a).toInt();});return sortedKeys;

}

// 通过键访问元素的值
V? operator [](Object? key) {
return buffer[key];
}

// 获取键的最大值
// 最优位置得分
num maxKey() {
if (buffer.isEmpty) {
return double.negativeInfinity;
}
var list = buffer.keys.toList()
…sort((num a, num b) {
return b.compareTo(a);
});
return list.isNotEmpty ? list.first : 0;
}

// 获取键值最大的元素
// MapEntry 提供了 key 和 value 两个只读属性来获取键和值,分别返回对应键值对的键和值。在 Map 中使用迭代器遍历时,每个元素都是 MapEntry 类型的实例。
MapEntry<num, V>? max() {
if (buffer.isEmpty) {
return null;
}
var list = buffer.keys.toList()
…sort((num a, num b) {
return b.compareTo(a);
});
return list.isNotEmpty ? MapEntry(list.first, buffer[list.first]!) : null;
}
}
判断是那种棋局情况
需要对活二、跳活二、活三…这些不同的棋局状态定义校验规则,规则太多,文章中只看活二的校验规则,其余请查看源码。
bool isAlive2(List list) {
assert(list.length == 2);
//把两颗棋子传入
Offset offset1 = nextChessman(list[1], list[0]);
Offset offset2 = nextChessman(list[0], list[1]);

return isEffectivePosition(offset1) &&
isEffectivePosition(offset2) &&
isBlankPosition(offset1) &&
isBlankPosition(offset2);
}

//输入的first和second返回下一个棋子的位置偏移量。
Offset nextChessman(Offset first, Offset second) {
//检查first和second的dy值是否相等。
//如果相等,表示棋子在水平方向上移动。那么下一个棋子的位置偏移量将在水平方向上向右或向左移动一格,取决于first的dx是否大于second的dx。
//如果first.dx > second.dx,则向左移动一格,即second.dx - 1;否则,向右移动一格,即second.dx + 1。纵坐标保持不变,即为first.dy
if (first.dy == second.dy) {
return Offset(
first.dx > second.dx ? second.dx - 1 : second.dx + 1, first.dy);
}
//如果first.dx和second.dx相等,表示棋子在垂直方向上移动。那么下一个棋子的位置偏移量将在垂直方向上向上或向下移动一格,取决于first的dy是否大于second的dy。如果first.dy > second.dy,则向上移动一格,即second.dy - 1;否则,向下移动一格,即second.dy + 1。横坐标保持不变,即为first.dx。
//如果以上两种情况都不满足,那么表示棋子在斜对角线方向上移动。根据first.dx和second.dx的大小关系,以及first.dy和second.dy的大小关系,决定下一个棋子的位置偏移量。
else if (first.dx == second.dx) {
return Offset(
first.dx, first.dy > second.dy ? second.dy - 1 : second.dy + 1);
} else if (first.dx > second.dx) {
if (first.dy > second.dy) {
return Offset(second.dx - 1, second.dy - 1);
} else {
return Offset(second.dx - 1, second.dy + 1);
}
} else {
if (first.dy > second.dy) {
return Offset(second.dx + 1, second.dy - 1);
} else {
return Offset(second.dx + 1, second.dy + 1);
}
}
}

//判断该位置是否有效。
bool isEffectivePosition(Offset offset) {
return offset.dx >= 0 &&
offset.dx <= LINE_COUNT &&
offset.dy >= 0 &&
offset.dy <= LINE_COUNT;
}

//isBlankPosition是用于判断某个位置上是否没有棋子,写法逻辑和用户交互能否落子差不多
bool isBlankPosition(Offset position) {
if (chessmanList.isNotEmpty) {
Chessman defaultChessman = Chessman(Offset(-1, 0), Player.black);
var cm = chessmanList.firstWhere((Chessman c) {
return c.position.dx == position.dx && c.position.dy == position.dy;
}, orElse: () {
return defaultChessman;
});
return cm != defaultChessman;
}
return true;
}
对每一种情况进行估分
这里只展示了两颗棋子的情况。
//将给定的数限制在最大值为2的范围内
int limitMax(int num) {
return num >= 2 ? 2 : num;
}
//对每种棋局加分
int scoring(Offset first, List myChessman, Player player,
{required String printMsg, bool isCanPrintMsg = false}) {
if (myChessman.length >= 5) {
return WIN;
}
int score = 0;
switch (myChessman.length) {
case 1:
break;
case 2:
if (isAlive2(myChessman)) {
score += ALIVE2;
score +=
limitMax(getJumpAlive3Count(myChessman, player)) * JUMP_ALIVE3;
score +=
limitMax(getJumpAlive4Count(myChessman, player)) * JUMP_ALIVE4;

    if (isCanPrintMsg) {print("$printMsg 活2成立, 得分+$ALIVE2");}} else if (isLowerDeath2(myChessman)) {score += LOWER_DEATH2;if (isCanPrintMsg) {print("$printMsg 低级死2成立 ,得分+$LOWER_DEATH2");}} else {score += DEEP_DEATH2;if (isCanPrintMsg) {print("$printMsg 死2成立 ,得分+$DEEP_DEATH2");}}break;
case 3:...case 4:...case 5:
default:score += WIN;

}
return score;
}
对单颗棋子估分
在棋盘中某一块范围内只有一颗棋子时,就都不能满足上方的几种棋局,那我们还需要对单颗棋子进行一个打分。
///位置得分(越靠近中心得分越高)
int positionScore(Offset offset) {
//这个值是通过对(offset.dx - 7.5)^2 + (offset.dy - 7.5)^2进行运算得到的。
//其中,^表示乘方操作,即取平方,可以把棋盘上每颗棋子的位置想成一个圆锥,越靠近中心位置越高
//参考点被设定为(7.5, 7.5),棋盘的中心
double z = -(pow(offset.dx - 7.5, 2) + pow(offset.dy - 7.5, 2)) + 112.5;
z /= 10;
return z.toInt();
}

///孤子价值
int scoringAloneChessman(Offset offset) {
int score = 0;
List list = [
Offset(offset.dx - 1, offset.dy),
Offset(offset.dx + 1, offset.dy),
Offset(offset.dx, offset.dy + 1),
Offset(offset.dx, offset.dy - 1),
Offset(offset.dx - 1, offset.dy - 1),
Offset(offset.dx - 1, offset.dy + 1),
Offset(offset.dx + 1, offset.dy - 1),
Offset(offset.dx + 1, offset.dy + 1),
];
for (offset in list) {
if (offset.dx > 0 && offset.dy > 0 && isBlankPosition(offset)) {
score++;
}
}

return score + positionScore(offset);
}
计算某一颗棋子对于玩家的评分
只分析横向上的棋子,其他方向的代码请查看源码。
///计算某个棋子对于 ownerPlayer 的分值
int chessmanGrade(Offset chessmanPosition,
{required Player ownerPlayer, bool isCanPrintMsg = false}) {
int score = 0;
List myChenssman = [];
Offset offset;
Offset first = chessmanPosition;
Player player = ownerPlayer;
player ??= computerPlayer;

///横向
//横向(左)
offset = Offset(first.dx - 1, first.dy);
myChenssman
…clear()
…add(first);
while (existSpecificChessman(offset, player)) {
myChenssman.add(offset);
offset = Offset(offset.dx - 1, offset.dy);
}

//横向(右)
offset = Offset(first.dx + 1, first.dy);
while (existSpecificChessman(offset, player)) {
myChenssman.add(offset);
offset = Offset(offset.dx + 1, offset.dy);
}
myChenssman.sort((a, b) {
return (a.dx - b.dx).toInt();
});
score += scoring(first, myChenssman, player,
printMsg: “横向”, isCanPrintMsg: isCanPrintMsg);

...

int ss = score + scoringAloneChessman(first);
if (isCanPrintMsg) {
print(“该子分值为: s s , 其中单子得分 : ss ,其中单子得分: ss,其中单子得分:{scoringAloneChessman(first)}, 组合得分:$score”);
}

int jumpAlive4Count = getJumpAlive4Count([first], player);
int jumpAlive3Count = getJumpAlive3Count([first], player);
int jumpAlive2Count = getJumpAlive2Count([first], player);
score += limitMax(jumpAlive4Count) * JUMP_ALIVE4 +
limitMax(jumpAlive3Count) * JUMP_ALIVE3 +
limitMax(jumpAlive2Count) * JUMP_ALIVE2;

return score + scoringAloneChessman(first);
}
计算我方下一步较好的位置
BufferMap ourBetterPosition({maxCount = 5}) {
Offset offset = Offset.zero;
BufferMap ourMap = BufferMap.maxCount(maxCount);
for (int i = 0; i <= LINE_COUNT; i++) {
for (int j = 0; j <= LINE_COUNT; j++) {
offset = Offset(i.toDouble(), j.toDouble());
if (isBlankPosition(offset)) {
int score = chessmanGrade(offset, ownerPlayer: Player.black);
if (ourMap.minKey() < score) {
ourMap.put(score, Offset(offset.dx, offset.dy));
}
}
}
}
return ourMap;
}
计算敌方下一步较好的位置
BufferMap enemyBetterPosition({maxCount = 5}) {
Offset offset = Offset.zero;
BufferMap enemyMap = BufferMap.maxCount(5);
print(“查找敌方最优落子位置”);

int count = 0;
for (int i = 0; i <= LINE_COUNT; i++) {
for (int j = 0; j <= LINE_COUNT; j++) {
offset = Offset(i.toDouble(), j.toDouble());
if (isBlankPosition(offset)) {
DateTime start = DateTime.now();
int score = chessmanGrade(offset,
ownerPlayer:
computerPlayer == Player.black ? Player.white : Player.black);
DateTime end = DateTime.now();
count++;
int time = end.millisecondsSinceEpoch - start.millisecondsSinceEpoch;
if (time > 5) {
print(“查找敌方最优落子位置耗时:KaTeX parse error: Expected 'EOF', got '}' at position 17: …ime"); }̲ if (en…count”);
return enemyMap;
}
基础版本AI
Future nextByAI({bool isPrintMsg = false}) async {
//如果评分出现ALIVE4的级别,直接下
Offset pos = needDefenses();
if (pos != const Offset(-1, 0)) {
return pos;
}

// 取我方,敌方 各5个最优点位置,
// 防中带攻: 如果判断应该防守,则在敌方5个最优位置中找出我方优势最大的点落子
// 攻中带防: 如果判断应该进攻,则在己方5个最优位置中找出敌方优势最大的点落子
BufferMap ourPositions = ourBetterPosition();
BufferMap enemyPositions = enemyBetterPosition();

Offset position = bestPosition(ourPositions, enemyPositions);
return position;
}

Offset needDefenses() {
BufferMap enemy = enemyBetterPosition();
late Offset defensesPosition;
for (num key in enemy.keySet) {
print(“key:${key}”);
if (key >= ALIVE4) {
defensesPosition = enemy[key]!;
break;
} else {
defensesPosition = const Offset(-1, 0);
}
}
return defensesPosition;
}

//基础AI,没有涉及算法

//遍历当前棋盘上的空位置,然后逐个计算该空位的得分(位置分+组合分),然后取分数最高的点落子
Offset bestPosition(
BufferMap ourPositions, BufferMap enemyPositions) {
late Offset position;
double maxScore = 0;

///当对手的最优位置得分 / 我方最优位置得分 > 1.5 防守,反之进攻
if (enemyPositions.maxKey() / ourPositions.maxKey() > 1.5) {for (num key in enemyPositions.keySet) {int attackScore =chessmanGrade(enemyPositions[key]!, ownerPlayer: computerPlayer);double score = key * 1.0 + attackScore * 0.8;if (score >= maxScore) {maxScore = score;position = enemyPositions[key]!;}}
} else {for (num key in ourPositions.keySet) {int defenseScore =chessmanGrade(ourPositions[key]!, ownerPlayer: computerPlayer);double score = key * 1.0 + defenseScore * 0.8;if (score >= maxScore) {maxScore = score;position = ourPositions[key]!;}}
}
return position;

}

这个时候,一个基础的五子棋AI就实现啦,它也能和五子棋入门的选手碰一碰了!(玩了3把,稍微没注意就输了一把给它…)
基于Max-Min算法
本文算法内容,参考多篇与Max-Min算法相关文章:
井字游戏/一字棋——Max-Min智能算法
AI MinMax算法
计算机博弈 基本算法 极大极小算法
在基础版本的AI中,我们已经取得了下一步较好的maxCount个位置,有每个位置有着对应的分数,那么我们就可以把这些位置都落子一次,这个时候我们需要给每一种结果一个分数,就是下图中的Utility(下图是井字棋游戏,整体逻辑差不多)。这个分数是站在Max的角度评估的,比如上图中我赢了就是+1,输了是-1,平局时0。所以,我希望最大化这个分数,而我的对手希望最小化这个分数。(MaxMin算法在有限深度的范围内进行搜索,假定博弈双方都是最精明的,也就是每次都会选择可能获胜的最大值。那么对于我方来说,对方每次都会选取使我方获胜的最小值MIN;我方会选择使我方获胜的最大值MAX。)

大部分游戏是不太可能把所有结果都列出来的,因为计算量会过于庞大,所以我们可能只能往前推7,8步(根据算力),所以这个时候分数就不只-1,0,1这么简单了。那么我们如何如何确定最后的落子地点呢?就是模拟棋盘,往后模拟几步,生成这颗博弈树,再向上反推,找到双方最优的落子地点。
具体的算法细节可以看下上面参考的几篇文章,在看这个算法之前需要了解基础的广度优先搜索(BFS),深度优先搜索(DFS)。

回到我们的编码部分
在开始具体的算法编写前,我们还需要一些前置的参数:
enum ChildType {
/// 标记当前节点为对手节点,会选择使我方得分最小的走势
MIN,

/// 标记当前节点为我方节点,会选择使我方得分最大的走势
MAX
}

class ChessNode{
/// 当前节点的棋子
Chessman current;
/// 当前节点的父节点
ChessNode parentNode;
/// 当前节点的所有子节点
List childrenNode = [];
/// 当前节点的值
num value = double.nan;
/// 当前节点的类型(我方/敌方)
ChildType type;
/// 当前节点值的上限
num maxValue;
/// 当前节点值的下限
num minValue;
/// 当前节点的层深度
int depth = 0;
/// 用于根节点记录选择的根下子节点
Chessman checked;
}
使用算法相较于前面的基础版本AI就是多了模拟棋盘的步骤:
生成临时棋局
/// 生成临时棋局
List createTempChessmanList(ChessNode node) {
//growable是一个可选参数,用于指定是否允许在列表中添加或删除元素。
//当growable为false时,列表的长度是固定的,并且不能添加或删除元素;当growable为true时,列表的长度是可变的,可以随时添加或删除元素。
List temp = List.from(chessmanList, growable: true);
temp.add(node.current!);

ChessNode? current = node.parentNode;
while (current != null && current.current != null) {
temp.add(current.current!);
current = current.parentNode;
}
return temp;
}
生成博弈树子节点
/// 生成博弈树子节点
void createChildren(ChessNode parent) {
if (parent == null) {
return null;
}

// 判断是否达到最大深度,如果是则计算棋局估值并返回
if (parent.depth > maxDepth) {
List list = createTempChessmanList(parent);
var start = DateTime.now();
parent.value = statusScore(our, list);
var value = DateTime.now();
return;
}

// 确定当前玩家和子节点类型
Player currentPlayer = parent.current!.owner == Player.black ? Player.white : Player.black;
ChildType type = parent.type == ChildType.MAX ? ChildType.MIN : ChildType.MAX;

// 创建临时棋子列表
var list = createTempChessmanList(parent);

// 查找最优落子位置
var start = DateTime.now();
BufferChessmanList enemyPosList = enemyBestPosition(list, maxCount: 5);
var value = DateTime.now();

// 将最优落子位置放入列表中
OffsetList offsetList = OffsetList()…addAll(enemyPosList.toList());
List result = offsetList.toList();

// 遍历最优落子位置,生成子节点
for (Offset position in result) {
Chessman chessman = Chessman(position, currentPlayer);

ChessNode node = ChessNode()..parentNode = parent..current = chessman..type = type..depth = parent.depth + 1..maxValue = parent.maxValue..minValue = parent.minValue;parent.childrenNode.add(node);// 递归调用 createChildren 方法生成子节点的子节点,直到达到最大深度或无法再生成子节点为止。
createChildren(node);

}
}
生成五子棋博弈树
//生成五子棋博弈树
ChessNode createGameTree() {
//创建根节点 root,设置其属性值:深度为0,估值为NaN,节点类型为 ChildType.MAX,最小值为负无穷,最大值为正无穷。
ChessNode root = ChessNode()
…depth = 0
…value = double.nan
…type = ChildType.MAX
…minValue = double.negativeInfinity
…maxValue = double.infinity;

//确定当前玩家 currentPlayer
//如果棋子列表 chessmanList 为空,则当前玩家为黑色
//否则,根据棋子列表中最后一个棋子的颜色设置当前玩家为另一个颜色。
Player currentPlayer;
if (chessmanList.isEmpty) {
currentPlayer = Player.black;
} else {
currentPlayer =
chessmanList.last.owner == Player.black ? Player.white : Player.black;
}

//查找敌方最优落子位置,并将结果存储在 enemyPosList 变量中。
//然后,将 enemyPosList 转换为 OffsetList 对象
//再将其转换为普通列表类型 List 对象。这些位置将用于创建第一层子节点。
BufferChessmanList enemyPosList =
enemyBestPosition(chessmanList, maxCount: 5);

OffsetList list = OffsetList()…addAll(enemyPosList.toList());
List result = list.toList();

int index = 0;
//通过遍历 result 列表,为每个位置 position 创建一个新的棋子 chessman 和一个新的子节点 node
//然后将子节点 node 添加到根节点的子节点列表 root.childrenNode 中
for (Offset position in result) {
Chessman chessman = Chessman(position, currentPlayer);

ChessNode node = ChessNode()..parentNode = root..depth = root.depth + 1..maxValue = root.maxValue..minValue = root.minValue..type = ChildType.MIN..current = chessman;root.childrenNode.add(node);
var start = DateTime.now();
createChildren(node);
var create = DateTime.now();print('创建第一层第$index个节点耗时:${create.millisecondsSinceEpoch - start.millisecondsSinceEpoch}');
index++;

}
return root;
}
Max-Min算法实现
num maxMinSearch(ChessNode root) {
if (root.childrenNode.isEmpty) {
return root.value; // 返回叶子节点的估值
}
List children = root.childrenNode;
if (root.type == ChildType.MIN) {
// 如果是对手执行操作
for (ChessNode node in children) {
if (maxMinSearch(node) < root.maxValue) {
// 判断子节点的估值是否小于当前节点的最大值
root.maxValue = node.value; // 更新当前节点的最大值
root.value = node.value; // 更新当前节点的估值
root.checked = node.current!; // 更新当前节点的选择步骤
} else {
continue; // 否则继续遍历下一个子节点
}
}
} else {
// 如果是自己执行操作
for (ChessNode node in children) {
if (maxMinSearch(node) > root.minValue) {
// 判断子节点的估值是否大于当前节点的最小值
root.minValue = node.value; // 更新当前节点的最小值
root.value = node.value; // 更新当前节点的估值
root.checked = node.current!; // 更新当前节点的选择步骤
} else {
continue; // 否则继续遍历下一个子节点
}
}
}
return root.value; // 返回当前节点的估值
}
基于alpha-beta剪枝算法
如果在比赛中,假设使用极小极大的算法,计算机能往前评估7步,加上剪枝算法,计算机就能往前评估14步!
num alphaBetaSearch(ChessNode current) {
count++; // 搜索次数累加

if (current.childrenNode.isEmpty) { // 如果当前节点没有子节点,即为叶子节点
return current.value; // 返回该节点的值
}

if (current.parentNode != null && !current.parentNode!.childrenNode.contains(current)) {
ChessNode parent = current.parentNode!;

// 如果父节点存在且父节点的子节点不包含当前节点,说明该枝已经被剪掉,返回父节点的最大/最小值
return parent.type == ChildType.MAX ? parent.minValue : parent.maxValue;

}

List children = current.childrenNode; // 获取当前节点的子节点

if (current.type == ChildType.MIN) { // 当前节点为MIN节点
num parentMin = current.parentNode?.minValue ?? double.negativeInfinity; // 获取父节点的最小值,若不存在父节点则设置为负无穷大
int index = 0; // 索引计数器

for (ChessNode node in children) {index++; // 索引递增num newCurrentMax = min(current.maxValue, alphaBetaSearch(node)); // 计算当前子节点的最大值if (newCurrentMax <= parentMin) {// 如果当前子节点的最大值小于等于父节点的最小值,则说明该枝可以被完全剪掉current.childrenNode = current.childrenNode.sublist(0, index); // 将当前节点的子节点列表截断至当前索引位置return parentMin; // 返回父节点的最小值}if (newCurrentMax < current.maxValue) {// 如果当前子节点的最大值小于当前节点的最大值,则更新当前节点的最大值、值和经过路径的位置信息current.maxValue = newCurrentMax;current.value = node.value;current.checked = node.current!;}
}if (current.maxValue > parentMin) {// 如果当前节点的最大值大于父节点的最小值,则更新父节点的最小值、值和经过路径的位置信息current.parentNode?.minValue = current.maxValue;current.parentNode?.value = current.value;current.parentNode?.checked = current.current!;
}return current.maxValue; // 返回当前节点的最大值作为该节点在搜索树中的价值

} else { // 当前节点为MAX节点
num parentMax = current.parentNode?.maxValue ?? double.infinity; // 获取父节点的最大值,若不存在父节点则设置为正无穷大
int index = 0; // 索引计数器

for (ChessNode node in children) {index++; // 索引递增num newCurrentMin = max(current.minValue, alphaBetaSearch(node)); // 计算当前子节点的最小值if (parentMax < newCurrentMin) {// 如果父节点的最大值小于当前子节点的最小值,则说明该枝可以被完全剪掉current.childrenNode = current.childrenNode.sublist(0, index); // 将当前节点的子节点列表截断至当前索引位置return parentMax; // 返回父节点的最大值}if (newCurrentMin > current.minValue) {// 如果当前子节点的最小值大于当前节点的最小值,则更新当前节点的最小值、值和经过路径的位置信息current.minValue = newCurrentMin;current.value = node.value;current.checked = node.current!;}
}if (current.minValue < parentMax) {// 如果当前节点的最小值小于父节点的最大值,则更新父节点的最大值、值和经过路径的位置信息current.parentNode?.maxValue = current.minValue;current.parentNode?.value = current.value;current.parentNode?.checked = current.current!;
}return current.minValue; // 返回当前节点的最小值作为该节点在搜索树中的价值

}
}
Max-Min和剪枝算法曾在IBM开发的国际象棋超级电脑,深蓝(Deep Blue)中被应用,并且两次打败当时的世界国际象棋冠军。文章到这里,五子棋的AI版本就完成了!

关于我
Hello,我是Taxze,如果您觉得文章对您有价值,希望您能给我的文章点个❤️,有问题需要联系我的话:我在这里 。如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章~万一哪天我进步了呢?😝

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/100254.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

docker搭建Jenkins及基本使用

1. 搭建 查询镜像 docker search jenkins下载镜像 docker pull jenkins/jenkins启动容器 #创建文件夹 mkdir -p /home/jenkins_home #权限 chmod 777 /home/jenkins_home #启动Jenkins docker run -d -uroot -p 9095:8080 -p 50000:50000 --name jenkins -v /home/jenkins_home…

【Qt】三种方式实现抽奖小游戏

简介 本文章是基本Qt与C实现一个抽奖小游戏&#xff0c;用到的知识点在此前发布的几篇文章。 下面是跳转链接&#xff1a; 【Qt控件之QLabel】用法及技巧链接&#xff1a; https://blog.csdn.net/MrHHHHHH/article/details/133691441?spm1001.2014.3001.5501 【Qt控件之QPus…

Matlab论文插图绘制模板第118期—进阶气泡图

之前的文章中&#xff0c;分享过Matlab气泡图的绘制模板&#xff1a; 图虽说好看&#xff0c;但有一个缺点&#xff1a;需要手动调节两个图例的位置。 为了解决这一问题&#xff0c;我们不妨结合前段时间分享的紧凑排列多子图的绘制模板&#xff1a; 从而达到自动对齐排列的效…

【版本控制工具一】Git 安装注册及使用

文章目录 一、Git 、Github、Gitee1.1 概述1.2 码云 相对于 github 的优势 二、Github 或 Gitee注册2.1 注册2.2 创建仓库 三、Git下载与安装四、创建本地仓库 一、Git 、Github、Gitee 1.1 概述 Git 是一个开源的分布式版本控制系统&#xff0c;用于敏捷高效地处理任何或小或…

Multi-Grade Deep Learning for Partial Differential Equations

论文阅读&#xff1a;Multi-Grade Deep Learning for Partial Differential Equations with Applications to the Burgers Equation Multi-Grade Deep Learning for Partial Differential Equations with Applications to the Burgers Equation符号定义偏微分方程定义FNN定义PI…

网络流量安全分析-工作组异常

在网络中&#xff0c;工作组异常分析具有重要意义。以下是网络中工作组异常分析的几个关键点&#xff1a; 检测网络攻击&#xff1a;网络中的工作组异常可能是由恶意活动引起的&#xff0c;如网络攻击、病毒感染、黑客入侵等。通过对工作组异常的监控和分析&#xff0c;可以快…

Git基础

1、本地版本控制系统、集中式版本控制系统与分布式版本控制系统 文档&#xff1a; Git - 关于版本控制 1. 本地版本控制系统&#xff08;Local Version Control System&#xff09;&#xff1a; 本地版本控制系统是最简单的版本控制系统&#xff0c;它基于文件的复制…

Vue-2.1scoped样式冲突

默认情况&#xff1a;写在组件中的样式会全局生效->因此很容易造成多个组件之间的样式冲突问题 1.全局样式&#xff1a;默认组件中的样式会作用到全局 2.局部样式&#xff1a;可以给组件加上scoped属性&#xff0c;可以让样式只作用于当前组件 <style scoped> <…

轻松实现时间录入自由!如何在Microsoft Word中轻松插入格式化的日期和时间

在文档中插入当前日期和时间有几个原因。你可能希望将其插入信函或页眉或页脚中。无论是什么原因&#xff0c;Word都可以轻松地将日期和时间插入文档。 如果希望在打开或打印文档时自动更新日期和时间&#xff0c;可以将其作为自动更新的字段插入。该字段也可以随时手动更新。…

H5逆向之远程RPC

引言前一讲说过H5 怎么去抓包,逆向分析。其中说到RPC。这一节详细讲一下。有一种情况,JS 比较复杂,混淆的厉害。 这个时候就用到RPC。原理就是,hook web 浏览器,直接调用js 里边的方法。 Node 服务。为什么用到Node 服务,先来看下这架构 Node 对外提供各种接口,外部可以…

AVS3:DMVR

AVS3中引入了解码端运动矢量修正&#xff08;DMVR,Decoder side Motion Vector Refinement&#xff09;技术&#xff0c;AVS3中的DMVR技术和G.266/VVC类似&#xff0c;它可以在解码端生成运动参数从而减少传输运动参数的码率开销。它的基本思想是将skip/direct模式生成的前后向…

Swagger使用详解

目录 一、简介 二、SwaggerTest项目搭建 1. pom.xml 2. entity类 3. controller层 三、基本使用 1. 导入相关依赖 2. 编写配置文件 2.1 配置基本信息 2.2 配置接口信息 2.3 配置分组信息 2.3.1 分组名修改 2.3.2 设置多个分组 四、常用注解使用 1. ApiModel 2.A…

IDEA的使用(三)Debug(断点调试)(IntelliJ IDEA 2022.1.3版本)

编程过程中如果出现错误&#xff0c;需要查找和定位错误时&#xff0c;借助程序调试可以快速查找错误。 编写好程序后&#xff0c;可能出现的情况&#xff1a; 1.没有bug。 使用Debug的情况&#xff1a; 2.运行后&#xff0c;出现错误或者异常信息&#xff0c;但是通过日志文件…

go的面向对象学习

文章目录 面向对象编程(上)1.问题与解决思路2.结构体1》Golang语言面向对象编程说明2》结构体与结构体变量(实例/对象)的关系的示意图3》入门案例(using struct to solve the problem of cat growing) 3.结构体的具体应用4.创建结构体变量和访问结构体字段5.struct类型的内存分…

vue3 组件v-model绑定props里的值,修改组件的值要触发回调

很早之前就写了&#xff0c;一直没写篇博客记录下 <select v-model"typeVal" />const emit defineEmits([update:type]); const props defineProps({type: { type: String, default: }, });const typeVal computed({get() {return props.type;},set(value…

Docker-compose创建LNMP服务并运行Wordpress网站平台

一、部署过程 1.安装Docker #关闭防火墙 systemctl stop firewalld.service setenforce 0#安装依赖包 yum install -y yum-utils device-mapper-persistent-data lvm2 #设置阿里云镜像源 yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/d…

17基于matlab卡尔曼滤波的行人跟踪算法,并给出算法估计误差结果,判断算法的跟踪精确性,程序已调通,可直接运行,基于MATLAB平台,可直接拍下。

17基于matlab卡尔曼滤波的行人跟踪算法&#xff0c;并给出算法估计误差结果&#xff0c;判断算法的跟踪精确性&#xff0c;程序已调通&#xff0c;可直接运行&#xff0c;基于MATLAB平台&#xff0c;可直接拍下。 17matlab卡尔曼滤波行人跟踪 (xiaohongshu.com)

【Redis学习1】Redis持久化机制详解

Redis持久化机制详解 一、Redis为什么需要持久化机制 Redis一般用作缓存&#xff0c;其数据存储在内存中&#xff0c;当Redis宕机后&#xff0c;内存中的数据将会丢失。因此使用缓存的时候&#xff0c;我们经常需要对内存中的数据进行持久化也就是将内存中的数据写入到硬盘中…

Jetpack:004-如何使用文本组件

文章目录 1. 概念介绍2. 使用方法2.1 通用参数2.2 专用参数 3. 示例代码4. 内容总结 我们在上一章回中介绍了Jetpack组件在布局中的对齐方式&#xff0c;本章回中主要介绍文 本组件的使用方法。闲话休提&#xff0c;让我们一起Talk Android Jetpack吧 1. 概念介绍 我们在本章…

基于ffmpeg给视频添加时间字幕

FFmpeg是一套可以用来记录、转换数字音频、视频&#xff0c;并能将其转化为流的开源计算机程序&#xff0c;我们可以基于ffmpeg对视频进行各种操作。本文主要介绍基于ffmpeg给视频添加字幕&#xff0c;字幕的内容为视频所播放的时间&#xff08;故需要安装ffmpeg&#xff0c;具…