开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情
前言
在前面几篇的文章中,我们已经完成了所有设计模式的代码的实现,现在我们将把这些模式代码与FlutterUI进行衔接,本次的项目就大功告成了。
不熟悉这个项目的同学可以往前看几期,注意,我们这次的设计是完全使用Flutter
自带的包进行设计,没有使用第三方包。
正文
1. 代码结构
lib
├─ai // AI,即下棋的对手
├─bridge // 桥接模式
├─factory // 工厂模式
├─flyweight // 享元模式
├─memorandum // 备忘模式
├─state // 状态模式
├─utils // 工具类
└─viewModel // 视图数据管理
在这里,我们没有按照比较常用的MVVM
模型对组件数据进行严格拆分,而是将每一个模式都放在了一个文件夹中,方便学习。
2. 详细代码
2.1 主界面
主界面如上,中间有一个棋盘组件,下方有三个功能按钮。布局很简单。
这里我们主要使用Column
对界面进行布局。
代码如下:
return Scaffold(
appBar: AppBar(
elevation: 0,
backgroundColor: _themeFactory!.getTheme().getThemeColor(),
title: Text("南瓜五子棋"),
actions: [
IconButton(// 控制主题
onPressed: () {
setState(() {
if (_themeFactory is BlackThemeFactory) {
currentLight = lightOn;
_themeFactory = BlueThemeFactory();
} else {
currentLight = lightOff;
_themeFactory = BlackThemeFactory();
}
});
},
icon: currentLight!),
IconButton( // 控制自己下棋的棋子形状
onPressed: () {
setState(() {
if (currentShape == circle) {
currentShape = rect;
} else {
currentShape = circle;
}
});
},
icon: currentShape!),
],
),
body: Container(
decoration: new BoxDecoration(
gradient: new LinearGradient(
colors: [
_themeFactory!.getTheme().getThemeColor(), // 获取需要主题
Colors.white,
],
stops: [
0.0,
1
],
begin: FractionalOffset.topCenter,
end: FractionalOffset.bottomCenter,
tileMode: TileMode.repeated)),
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),
),
),
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,
)),
],
),
),
]),
),
),
);
2.2 棋盘布局
这里我们使用自定义的CustomPainter
来进行实现。代码主要参考了Flutter实战第二版,感兴趣的同学可以前往看看。
具体的代码如下:
class CheckerBoardPainter extends CustomPainter {
static List<CrossOverBean> _crossOverBeanList = [];
static int _state = 0;
@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
..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);
}
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));
}
}
}
//在实际场景中正确利用此回调可以避免重绘开销,本示例我们简单的返回true
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
2.3 棋子绘制
首先,我们需要获得上面棋盘每条线交点的位置。我们用一个类进行定义:
///记录棋盘上横竖线的交叉点
class CrossOverBean {
double _dx;
double _dy;
CrossOverBean(this._dx, this._dy);
}
而后便是对用户手点击的位置对棋子进行绘制,这里我们也是使用了一个CustomPainter
对棋子进行绘制,这样棋子和棋盘分离,可以减轻软件重新绘制的负担。
class ChessPainter extends CustomPainter {
static int _state = 0;
static Position? _position;
final Function _function;
Checkerboard _originator = Checkerboard.getInstance();
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();
//求两个点之间的距离,让棋子正确的显示在坐标轴上面
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;
}
}
//画子
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);
}
}
}
WidgetsBinding.instance!.addPostFrameCallback((_) {
if (add && _position!.chess is WhiteChess) {
_function();
}
});
}
//在实际场景中正确利用此回调可以避免重绘开销,本示例我们简单的返回true
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
2.4 AI自动下棋函数
每次用户点击完以后,我们便可以确定其落子的位置,而后将整个棋盘的局势传给AI,让AI产生下一步。当然这里的AI其实是一个决策树算法,并不是很AI。
/// Ai 下棋
void turnAi() {
// print("Ai下棋");
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, "很遗憾", "决策树算法打败了您");
}
setState(() {
ChessPainter._position!.dx = ChessPainter._position!.dx * (width / 15);
ChessPainter._position!.dy = ChessPainter._position!.dy * (width / 15);
});
}
3. 总结
这次的UI设计代码就基本完成了,对于一些对接的细节,大家可以先自己琢磨琢磨,我这边后续会将代码上传github,感兴趣的大家可以收藏这篇文章,后续我会将链接放在这篇文章中。