前言
最近在学习Flutter的自定义Widget相关内容,于是就自己写了一个flutter的简易五子棋的页面,以加强学习相关的内容。
实现原理及规则说明
主要原理就是通过flutter提供的CustomPaint 组件来实现自定义图形绘制。主要是自定义一个棋盘背景类,以及棋子类。
自定义棋盘背景类代码如下
class MyChessBg extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
print('paint bg');
var rect = Offset.zero & size;
print('paint bg ${rect.left} ${rect.right}');
//画棋盘
drawChessboard(canvas, rect);
//画棋子
// drawPieces(canvas, rect);
}
// 返回false, 后面介绍
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
void drawChessboard(Canvas canvas, Rect rect) {
//棋盘背景
var paint = Paint()
..isAntiAlias = true
..style = PaintingStyle.fill //填充
..color = Color(0xFFDCC48C);
canvas.drawRect(rect, paint);
//画棋盘网格
paint
..style = PaintingStyle.stroke //线
..color = Colors.black38
..strokeWidth = 1.0;
//画横线
for (int i = 0; i <= 15; ++i) {
double dy = rect.top + rect.height / 15 * i;
canvas.drawLine(Offset(rect.left, dy), Offset(rect.right, dy), paint);
}
for (int i = 0; i <= 15; ++i) {
double dx = rect.left + rect.width / 15 * i;
canvas.drawLine(Offset(dx, rect.top), Offset(dx, rect.bottom), paint);
}
}
自定义棋子类代码如下
class MyChessCh extends CustomPainter {
MyChessCh({Key? key, required this.offset}) : super();
late final List offset;
@override
void paint(Canvas canvas, Size size) {
print('paint ch');
var rect = Offset.zero & size;
//画棋子
// drawPieces(canvas, rect);
drawPieces1(canvas, offset);
}
void drawPieces1(Canvas canvas, List offsets) {
//画一个黑子
var paint = Paint()
..style = PaintingStyle.fill
..color = Colors.black;
for (var i = 0; i < offsets.length; i++) {
//画一个黑子
paint.color = Colors.black;
if (i % 2 == 0) {
//画一个黑子
canvas.drawCircle(
offsets[i],
8,
paint,
);
} else {
//画一个白子
paint.color = Colors.white;
canvas.drawCircle(
offsets[i],
8,
paint,
);
}
}
}
//画棋子
void drawPieces(Canvas canvas, Rect rect) {
double eWidth = rect.width / 15;
double eHeight = rect.height / 15;
//画一个黑子
var paint = Paint()
..style = PaintingStyle.fill
..color = Colors.black;
//画一个黑子
canvas.drawCircle(
Offset(rect.center.dx - eWidth / 2, rect.center.dy - eHeight / 2),
min(eWidth / 2, eHeight / 2) - 2,
paint,
);
//画一个白子
paint.color = Colors.white;
canvas.drawCircle(
Offset(rect.center.dx + eWidth / 2, rect.center.dy - eHeight / 2),
min(eWidth / 2, eHeight / 2) - 2,
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
五子棋实现规则说明:
1.判赢规则说明:
主要是通过当前落子的8个方向去进行判断,是否有一个方向满足了同一颜色棋子以及满足5颗。如下图所示。
以下是部分代码实现:
///判断获胜的方法。
///主要判断依据:判断当前点的8个方向是否能连成5个相同的颜色的子,8个方向依次遍历,符合条件就返回。
///offset : 当前点 , black:黑子还是白子 , offs:对应颜色的子的集合。
///注意:计数的值必须每个方向一个,如果用同一个技术标志,会导致技术值不正确。每个方向只要符合条件,都会令count加一,最后会变成一个方向没到5就出现获胜的情况。
bool win(Offset offset, bool black, List offs) {
//向左遍历 ,步长为20
List l = [];
int l_conut = 1;
for (var x = offset.dx - 20; x > 0; x = x - 20) {
var item = Offset(x, offset.dy);
if (offs.contains(item)) {
l_conut++;
l.add(item);
if (l_conut >= 5) {
print("左赢的列表:${l}");
return true;
}
} else {
break;
}
}
//向右遍历
int r_conut = 1;
List r = [];
for (var x = offset.dx + 20; x <= 300; x = x + 20) {
var item = Offset(x, offset.dy);
if (offs.contains(item)) {
r_conut++;
r.add(item);
if (r_conut >= 5) {
print("右赢的列表:${r}");
return true;
}
} else {
break;
}
}
//向上遍历
int t_conut = 1;
List t = [];
for (var y = offset.dy - 20; y > 0; y = y - 20) {
var item = Offset(offset.dx, y);
if (offs.contains(item)) {
t_conut++;
t.add(item);
if (t_conut >= 5) {
print("上赢的列表:${t}");
return true;
}
} else {
break;
}
}
//向下遍历
int b_conut = 1;
List b = [];
for (var y = offset.dy + 20; y <= 300; y = y + 20) {
var item = Offset(offset.dx, y);
if (offs.contains(item)) {
b_conut++;
b.add(item);
if (b_conut >= 5) {
print("下赢的列表:${b}");
return true;
}
} else {
break;
}
}
//左上
int lt_conut = 1;
List lt = [];
for (var x = offset.dx - 20, y = offset.dy - 20;
x > 0 && y > 0;
x = x - 20, y = y - 20) {
var item = Offset(x, y);
if (offs.contains(item)) {
lt_conut++;
lt.add(item);
if (lt_conut >= 5) {
print("左上赢的列表:${lt}");
return true;
}
} else {
break;
}
}
//右上
int rt_conut = 1;
List rt = [];
for (var x = offset.dx + 20, y = offset.dy - 20;
x <= 300 && y > 0;
x = x + 20, y = y - 20) {
var item = Offset(x, y);
if (offs.contains(item)) {
rt_conut++;
rt.add(item);
if (rt_conut >= 5) {
print("右上赢的列表:${rt}");
return true;
}
} else {
break;
}
}
//左下
int lb_conut = 1;
List lb = [];
for (var x = offset.dx - 20, y = offset.dy + 20;
x > 0 && y <= 300;
x = x - 20, y = y + 20) {
var item = Offset(x, y);
if (offs.contains(item)) {
lb_conut++;
lb.add(item);
if (lb_conut >= 5) {
print("左下赢的列表:${lb}");
return true;
}
} else {
break;
}
}
//右下
int rb_conut = 1;
List rb = [];
for (var x = offset.dx + 20, y = offset.dy + 20;
x <= 300 && y <= 300;
x = x + 20, y = y + 20) {
var item = Offset(x, y);
if (offs.contains(item)) {
rb_conut++;
rb.add(item);
if (rb_conut >= 5) {
print("右下赢的列表:${rb}");
return true;
}
} else {
break;
}
}
return false;
}
2.棋子位置判定:
当点击位置没有出现在棋盘的棋线的交叉点时,需要将棋子移动到最近的正确位置。当点击的点在框内时,计算当前点击的位置距离四周框边的距离,来判断应该落子的位置。
以下是部分代码实现:
///将点击位置转换成最近的有效的棋盘点位置。
///计算逻辑:x轴坐标 = 点击点位置 - 前一个竖线的x轴坐标。 如果值大于一半格子长度,就取下一个竖线的x坐标,反之取上一根竖线的x坐标
/// y轴坐标 = 点击点位置 - 前一个竖线的y轴坐标。 如果值大于一半格子长度,就取下一个竖线的y坐标,反之取上一根竖线的y坐标
Offset transOffset(Offset offset) {
double ddx = 0;//最终位子的x坐标
double ddy = 0;//最终位子的y坐标
double level = 20;//一格的宽度
int modx = offset.dx ~/ level;//在x轴上,点击的位置左侧的格数
if (offset.dx - level * modx <= 10) {
//没过半格,取上一个点,否者取下一格
ddx = level * modx;
} else {
ddx = level * (modx + 1);
}
int mody = offset.dy ~/ level;
if (offset.dy - level * mody <= 10) {
//没过半格,取上一个点,否者取下一格
ddy = level * mody;
} else {
ddy = level * (mody + 1);
}
print("ddx= ${ddx} + ddy = ${ddy}");
return Offset(ddx, ddy);
}
整体实现
整体实现就是,在一个stack布局上,底部绘制棋盘背景,顶部绘制棋子,然后监控点击事件,在up事件中处理当前点击位置,来刷新棋盘上的棋子位置,但不刷新棋盘的位置,主要用到了RepaintBoundary组件来实现棋盘的重构刷新。
通过两个数组来存储黑白子。再通过另一个数组来存储整体棋子的位置顺序。然后进行双方下棋,直至出现胜利者。
整体代码如下:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
class CustomChessBg extends StatelessWidget {
const CustomChessBg({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
Offset off = Offset.zero;
List offs = [];//所有棋子的集合
List boffs = [];//黑棋集合
List woffs = [];//白棋集合
// offs.add(off);
return DecoratedBox(
decoration: BoxDecoration(color: Colors.white),
child: Stack(
children: [
Center(
child: RepaintBoundary(
child: CustomPaint(
size: const Size(300, 300), //指定画布大小
painter: MyChessBg(),
))),
Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Listener(
child: CustomPaint(
size: const Size(300, 300), //指定画布大小
painter: MyChessCh(offset: offs),
),
onPointerUp: (event) {
print(event.localPosition);
var ll = transOffset(event.localPosition);
if (offs.contains(ll)) {
Fluttertoast.showToast(msg: "该位置已经下过子了,不能重复下");
return;
}
offs.add(ll);
if (offs.length % 2 == 1) {
//第一颗是黑棋
boffs.add(ll);
if (win(ll, true, boffs)) {
// Fluttertoast.showToast(msg: "黑棋获胜");
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return AlertDialog(
title: Text("获胜提醒!"),
content: Text("黑棋获胜"),
actions: [
TextButton(
onPressed: () {
offs.clear();
boffs.clear();
woffs.clear();
Navigator.of(context).pop();
setState(() {});
},
child: Text("确定")),
],
);
});
}
} else {
woffs.add(ll);
if (win(ll, false, woffs)) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) {
return AlertDialog(
title: Text("获胜提醒!"),
content: Text("白棋获胜"),
actions: [
TextButton(
onPressed: () {
offs.clear();
boffs.clear();
woffs.clear();
Navigator.of(context).pop();
setState(() {});
},
child: Text("确定")),
],
);
});
}
}
setState(() {});
},
);
}),
)
],
),
);
}
///判断获胜的方法。
///主要判断依据:判断当前点的8个方向是否能连成5个相同的颜色的子,8个方向依次遍历,符合条件就返回。
///offset : 当前点 , black:黑子还是白子 , offs:对应颜色的子的集合。
///注意:计数的值必须每个方向一个,如果用同一个技术标志,会导致技术值不正确。每个方向只要符合条件,都会令count加一,最后会变成一个方向没到5就出现获胜的情况。
bool win(Offset offset, bool black, List offs) {
//向左遍历 ,步长为20
List l = [];
int l_conut = 1;
for (var x = offset.dx - 20; x > 0; x = x - 20) {
var item = Offset(x, offset.dy);
if (offs.contains(item)) {
l_conut++;
l.add(item);
if (l_conut >= 5) {
print("左赢的列表:${l}");
return true;
}
} else {
break;
}
}
//向右遍历
int r_conut = 1;
List r = [];
for (var x = offset.dx + 20; x <= 300; x = x + 20) {
var item = Offset(x, offset.dy);
if (offs.contains(item)) {
r_conut++;
r.add(item);
if (r_conut >= 5) {
print("右赢的列表:${r}");
return true;
}
} else {
break;
}
}
//向上遍历
int t_conut = 1;
List t = [];
for (var y = offset.dy - 20; y > 0; y = y - 20) {
var item = Offset(offset.dx, y);
if (offs.contains(item)) {
t_conut++;
t.add(item);
if (t_conut >= 5) {
print("上赢的列表:${t}");
return true;
}
} else {
break;
}
}
//向下遍历
int b_conut = 1;
List b = [];
for (var y = offset.dy + 20; y <= 300; y = y + 20) {
var item = Offset(offset.dx, y);
if (offs.contains(item)) {
b_conut++;
b.add(item);
if (b_conut >= 5) {
print("下赢的列表:${b}");
return true;
}
} else {
break;
}
}
//左上
int lt_conut = 1;
List lt = [];
for (var x = offset.dx - 20, y = offset.dy - 20;
x > 0 && y > 0;
x = x - 20, y = y - 20) {
var item = Offset(x, y);
if (offs.contains(item)) {
lt_conut++;
lt.add(item);
if (lt_conut >= 5) {
print("左上赢的列表:${lt}");
return true;
}
} else {
break;
}
}
//右上
int rt_conut = 1;
List rt = [];
for (var x = offset.dx + 20, y = offset.dy - 20;
x <= 300 && y > 0;
x = x + 20, y = y - 20) {
var item = Offset(x, y);
if (offs.contains(item)) {
rt_conut++;
rt.add(item);
if (rt_conut >= 5) {
print("右上赢的列表:${rt}");
return true;
}
} else {
break;
}
}
//左下
int lb_conut = 1;
List lb = [];
for (var x = offset.dx - 20, y = offset.dy + 20;
x > 0 && y <= 300;
x = x - 20, y = y + 20) {
var item = Offset(x, y);
if (offs.contains(item)) {
lb_conut++;
lb.add(item);
if (lb_conut >= 5) {
print("左下赢的列表:${lb}");
return true;
}
} else {
break;
}
}
//右下
int rb_conut = 1;
List rb = [];
for (var x = offset.dx + 20, y = offset.dy + 20;
x <= 300 && y <= 300;
x = x + 20, y = y + 20) {
var item = Offset(x, y);
if (offs.contains(item)) {
rb_conut++;
rb.add(item);
if (rb_conut >= 5) {
print("右下赢的列表:${rb}");
return true;
}
} else {
break;
}
}
return false;
}
///将点击位置转换成最近的有效的棋盘点位置。
///计算逻辑:x轴坐标 = 点击点位置 - 前一个竖线的x轴坐标。 如果值大于一半格子长度,就取下一个竖线的x坐标,反之取上一根竖线的x坐标
/// y轴坐标 = 点击点位置 - 前一个竖线的y轴坐标。 如果值大于一半格子长度,就取下一个竖线的y坐标,反之取上一根竖线的y坐标
Offset transOffset(Offset offset) {
double ddx = 0;//最终位子的x坐标
double ddy = 0;//最终位子的y坐标
double level = 20;//一格的宽度
int modx = offset.dx ~/ level;//在x轴上,点击的位置左侧的格数
if (offset.dx - level * modx <= 10) {
//没过半格,取上一个点,否者取下一格
ddx = level * modx;
} else {
ddx = level * (modx + 1);
}
int mody = offset.dy ~/ level;
if (offset.dy - level * mody <= 10) {
//没过半格,取上一个点,否者取下一格
ddy = level * mody;
} else {
ddy = level * (mody + 1);
}
print("ddx= ${ddx} + ddy = ${ddy}");
return Offset(ddx, ddy);
}
}
///自定义棋子类
class MyChessCh extends CustomPainter {
MyChessCh({Key? key, required this.offset}) : super();
late final List offset;
@override
void paint(Canvas canvas, Size size) {
print('paint ch');
var rect = Offset.zero & size;
//画棋子
// drawPieces(canvas, rect);
drawPieces1(canvas, offset);
}
void drawPieces1(Canvas canvas, List offsets) {
//画一个黑子
var paint = Paint()
..style = PaintingStyle.fill
..color = Colors.black;
for (var i = 0; i < offsets.length; i++) {
//画一个黑子
paint.color = Colors.black;
if (i % 2 == 0) {
//画一个黑子
canvas.drawCircle(
offsets[i],
8,
paint,
);
} else {
//画一个白子
paint.color = Colors.white;
canvas.drawCircle(
offsets[i],
8,
paint,
);
}
}
}
//画棋子
void drawPieces(Canvas canvas, Rect rect) {
double eWidth = rect.width / 15;
double eHeight = rect.height / 15;
//画一个黑子
var paint = Paint()
..style = PaintingStyle.fill
..color = Colors.black;
//画一个黑子
canvas.drawCircle(
Offset(rect.center.dx - eWidth / 2, rect.center.dy - eHeight / 2),
min(eWidth / 2, eHeight / 2) - 2,
paint,
);
//画一个白子
paint.color = Colors.white;
canvas.drawCircle(
Offset(rect.center.dx + eWidth / 2, rect.center.dy - eHeight / 2),
min(eWidth / 2, eHeight / 2) - 2,
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
///自定义棋盘背景类
class MyChessBg extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
print('paint bg');
var rect = Offset.zero & size;
print('paint bg ${rect.left} ${rect.right}');
//画棋盘
drawChessboard(canvas, rect);
//画棋子
// drawPieces(canvas, rect);
}
// 返回false, 后面介绍
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
void drawChessboard(Canvas canvas, Rect rect) {
//棋盘背景
var paint = Paint()
..isAntiAlias = true
..style = PaintingStyle.fill //填充
..color = Color(0xFFDCC48C);
canvas.drawRect(rect, paint);
//画棋盘网格
paint
..style = PaintingStyle.stroke //线
..color = Colors.black38
..strokeWidth = 1.0;
//画横线
for (int i = 0; i <= 15; ++i) {
double dy = rect.top + rect.height / 15 * i;
canvas.drawLine(Offset(rect.left, dy), Offset(rect.right, dy), paint);
}
for (int i = 0; i <= 15; ++i) {
double dx = rect.left + rect.width / 15 * i;
canvas.drawLine(Offset(dx, rect.top), Offset(dx, rect.bottom), paint);
}
}
}
总结:
该内容,目前只是实现简易的五子棋的基本功能,其中还有很多细节尚未完善,仅供参考。