局面分析是一个让你可以「超神」的能力!
一方面,有了局面分析功能,你下棋过程中像是有一个云服务器始终在给你支招。这很利于学棋者提升自己对局面的理解;
另一方面,对于开局的某一个局面,可能让云端分析告诉你目前走哪了步比较好,主要的几种可靠招法的效果得多少分。
本节概要
- 添加 Flutter 版的 Toast 组件
- 翻译中文行棋着法
- 调用云库 API 分析局面
- 呈现局面分析结果
因为有「象棋云库」的加持,实现局面分析并不是很困难!我们使用前边云库人机对战的那一系列 API 来请求云库对局面进行分析,推荐和评估可行的着法。
在开始局面分析之前,我们先做一点热身运行吧!
添加 Toast 工具类
在分析过程中,我们希望显示一些轻量的提示消息给用户,我们添加一个 Toast 工具类,在文件夹“common”中新建toast.dart文件,如下内容:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
// Toast 显示位置控制
enum ToastPostion { top, center, bottom }
class Toast {
// toast靠它加到屏幕上
static OverlayEntry _overlayEntry;
// toast是否正在showing
static bool _showing = false;
// 开启一个新toast的当前时间,用于对比是否已经展示了足够时间
static DateTime _startedTime;
// 提示内容
static String _msg;
// toast显示时间
static int _showTime;
// 背景颜色
static Color _bgColor;
// 文本颜色
static Color _textColor;
// 文字大小
static double _textSize;
// 显示位置
static ToastPostion _toastPosition;
// 左右边距
static double _pdHorizontal;
// 上下边距
static double _pdVertical;
static void toast(
BuildContext context, {
// 显示的文本
String msg,
// 显示的时间 单位毫秒
int showTime = 1000,
// 显示的背景
Color bgColor = Colors.black,
// 显示的文本颜色
Color textColor = Colors.white,
// 显示的文字大小
double textSize = 16.0,
// 显示的位置
ToastPostion position = ToastPostion.center,
// 文字水平方向的内边距
double pdHorizontal = 20.0,
// 文字垂直方向的内边距
double pdVertical = 10.0,
}) async {
assert(msg != null);
_msg = msg;
_startedTime = DateTime.now();
_showTime = showTime;
_bgColor = bgColor;
_textColor = textColor;
_textSize = textSize;
_toastPosition = position;
_pdHorizontal = pdHorizontal;
_pdVertical = pdVertical;
// 获取OverlayState
OverlayState overlayState = Overlay.of(context);
_showing = true;
if (_overlayEntry == null) {
//
// OverlayEntry负责构建布局
// 通过OverlayEntry将构建的布局插入到整个布局的最上层
_overlayEntry = OverlayEntry(
builder: (BuildContext context) => Positioned(
// top值,可以改变这个值来改变toast在屏幕中的位置
top: buildToastPosition(context),
child: Container(
alignment: Alignment.center,
width: MediaQuery.of(context).size.width,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 40.0),
child: AnimatedOpacity(
opacity: _showing ? 1.0 : 0.0, // 目标透明度
duration: _showing ?
Duration(milliseconds: 100) : Duration(milliseconds: 400),
child: _buildToastWidget(),
),
)),
),
);
// 插入到整个布局的最上层
overlayState.insert(_overlayEntry);
//
} else {
// 重新绘制UI,类似setState
_overlayEntry.markNeedsBuild();
}
// 等待时间
await Future.delayed(Duration(milliseconds: _showTime));
// 2 秒后 到底消失不消失
if (DateTime.now().difference(_startedTime).inMilliseconds >= _showTime) {
_showing = false;
_overlayEntry.markNeedsBuild();
await Future.delayed(Duration(milliseconds: 400));
_overlayEntry.remove();
_overlayEntry = null;
}
}
// toast 绘制
static _buildToastWidget() {
return Center(
child: Card(
color: _bgColor,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: _pdHorizontal, vertical: _pdVertical),
child: Text(_msg, style: TextStyle(fontSize: _textSize, color: _textColor)),
),
),
);
}
// 设置toast位置
static buildToastPosition(context) {
//
var backResult;
if (_toastPosition == ToastPostion.top) {
backResult = MediaQuery.of(context).size.height * 1 / 4;
} else if (_toastPosition == ToastPostion.center) {
backResult = MediaQuery.of(context).size.height * 2 / 5;
} else {
backResult = MediaQuery.of(context).size.height * 3 / 4;
}
return backResult;
}
}
调用云库接口分析局面
我们在 lib/engine 文件夹下新建 analysis.dart 文件,创建两个解析 chess-db 着法响应的工具类:
class AnalysisItem {
// 着法,着法中文描述
String move, stepName;
// 此着法应用后的局面分值
int score;
// 使用此着法后的棋局胜率估算
double winrate;
AnalysisItem({this.move, this.score, this.winrate});
@override
String toString() {
return '{move: ${stepName ?? move}, score: $score, winrate: $winrate}';
}
}
// 解析云库的分析结果
class AnalysisFetcher {
// 默认解析前5种着法
static List fetch(String response, {limit = 5}) {
//
final segments = response.split('|');
List result = [];
final regx = RegExp(r'move:(.{4}).+score:(\-?\d+).+winrate:(\d+.?\d*)');
for (var segment in segments) {
//
final match = regx.firstMatch(segment);
if (match == null) break;
final move = match.group(1);
final score = int.parse(match.group(2));
final winrate = double.parse(match.group(3));
result.add(AnalysisItem(move: move, score: score, winrate: winrate));
if (result.length == limit) break;
}
return result;
}
}
在 CloudEngine 类中,我们添加一个 analysis 方法来封装 chess-db 着法查询 API:
// 给云库引擎添加分析方法,之后会调用前述的分析结果解析工具
static Future analysis(Phase phase) async {
//
final fen = phase.toFen();
var response = await ChessDB.query(fen);
if (response == null) return EngineResponse('network-error');
if (response.startsWith('move:')) {
final items = AnalysisFetcher.fetch(response);
if (items.isEmpty) return EngineResponse('no-result');
return EngineResponse('analysis', value: items);
}
print('ChessDB.query: $response\n');
return EngineResponse('unknown-error');
}
云库的 API 包装就这么简单地搞定了!
呈现分析结果
我们进入 _BattlePageState 类,先声明一个 bool 型的成员变量,它是为了预防上一个分析结果还没有返回,用户又请求下一次分析的:
class _BattlePageState extends State {
//
...
bool _analysising = false;
...
}
接着,我们在 _BattlePageState 类中添加两个方法,用于调用我们刚才实现于 CloudEngine 中的分析方法,并呈现给用户:
analysisPhase() async {
//
Toast.toast(context, msg: '正在分析局面...', position: ToastPostion.bottom);
setState(() => _analysising = true);
try {
final result = await CloudEngine.analysis(Battle.shared.phase);
// 云库反回了正确的分析结果
if (result.type == 'analysis') {
//
List items = result.value;
items.forEach(
(item) => item.stepName = StepName.translate(
Battle.shared.phase,
Move.fromEngineStep(item.move),
),
);
showAnalysisItems(
context,
title: '推荐招法',
items: result.value,
callback: (index) => Navigator.of(context).pop(),
);
} else if (result.type == 'no-result') {
// 云库表示无分析结论
// 我们提交服务器后台进行计算,玩家可以过一会再点击分析来查看分析结论
Toast.toast(
context,
msg: '已请求服务器计算,请稍后查看!',
position: ToastPostion.bottom,
);
} else {
Toast.toast(
context,
msg: '错误: ${result.type}',
position: ToastPostion.bottom,
);
}
} catch (e) {
Toast.toast(context, msg: '错误: $e', position: ToastPostion.bottom);
} finally {
setState(() => _analysising = false);
}
}
// 显示分析结论
showAnalysisItems(
BuildContext context, {
String title,
List items,
Function(AnalysisItem item) callback,
}) {
//
final List children = [];
for (var item in items) {
children.add(
ListTile(
title: Text(item.stepName, style: TextStyle(fontSize: 18)),
subtitle: Text('胜率:${item.winrate}%'),
trailing: Text('分数:${item.score}'),
onTap: () => callback(item),
),
);
children.add(Divider());
}
children.insert(0, SizedBox(height: 10));
children.add(SizedBox(height: 56));
showModalBottomSheet(
context: context,
builder: (BuildContext context) => SingleChildScrollView(
child: Column(mainAxisSize: MainAxisSize.min, children: children),
),
);
}
最后,在 createOperatorBar 方法中找到以下代码:
FlatButton(child: Text('分析局面', style: buttonStyle), onPressed: () {}),
将它修改成下边的样子:
FlatButton(
child: Text('分析局面', style: buttonStyle),
onPressed: _analysising ? null : analysisPhase,
),
现在运行产品试试:轮到你走棋的时候,你可以点击「分析局面」,然后会从屏幕底部弹出一个弹框,提示有哪些推荐的着法,信息还包括应用指定着法后的局面分值和胜率!
有了云库的局面分析做后盾,杀个特大也不特别难样~~
记得将代码提交到 git 仓库,多玩玩我们的产品,找到你自己的体验提升点!
elapse@elapse-PC:~/Language/Flutter/chinese_chess$ git add .
elapse@elapse-PC:~/Language/Flutter/chinese_chess$ git commit -m '云库局面分析-带bug'
[master da1a94e] 云库局面分析-带bug
6 files changed, 303 insertions(+), 3 deletions(-)
create mode 100644 lib/common/toast.dart
create mode 100644 lib/engine/analysis.dart
elapse@elapse-PC:~/Language/Flutter/chinese_chess$ sudo git push
[sudo] elapse 的密码:
Username for 'https://rocketgit.com': elapse
Password for 'https://[email protected]':
枚举对象: 21, 完成.
对象计数中: 100% (21/21), 完成.
使用 4 个线程进行压缩
压缩对象中: 100% (12/12), 完成.
写入对象中: 100% (12/12), 4.39 KiB | 2.20 MiB/s, 完成.
总共 12 (差异 6),复用 0 (差异 0)
remote: RocketGit: Info: == Welcome to RocketGit! ==
remote: RocketGit: Info: you are connecting from IP 2408:8256:686:a5d9:c8f6:779b:a786:ce9e by http(s).
remote: RocketGit: Info: date/time: 2020-09-07 10:34:17 (UTC), debug id acaa59.
To https://rocketgit.com/user/elapse/chinese_chess
c574648..da1a94e master -> master
elapse@elapse-PC:~/Language/Flutter/chinese_chess$
小节回顾
本小节中,我们实现了局面分析的功能,这个功能使用云库的 API 来给当前局面查找最优的后续着法,并对每个可能着法进行评分。
为了向玩家展示着法,我们首先实现了对着法的中文化翻译,这部分功能也在记谱中被使用。
接着,我们使用与云库引擎类似的方式来封装了云库的 API 接口,通过将局面 FEN 传给云库来获取云库对局面的着法推荐和着法评估。
最后,我们把云库返回的着法以中文化的着法列表方式显示给玩家,供玩家理解和学习局面。