前言
最近在學(xué)習(xí)Flutter的自定義Widget相關(guān)內(nèi)容,于是就自己寫了一個flutter的簡易五子棋的頁面,以加強(qiáng)學(xué)習(xí)相關(guān)的內(nèi)容。
實現(xiàn)原理及規(guī)則說明
主要原理就是通過flutter提供的CustomPaint 組件來實現(xiàn)自定義圖形繪制。主要是自定義一個棋盤背景類,以及棋子類。
自定義棋盤背景類代碼如下
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);
//畫棋盤網(wǎng)格
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> 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<Offset> 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;
}
}
五子棋實現(xiàn)規(guī)則說明:
1.判贏規(guī)則說明:
主要是通過當(dāng)前落子的8個方向去進(jìn)行判斷,是否有一個方向滿足了同一顏色棋子以及滿足5顆。如下圖所示。

d9fb02d9e36578f3c0c34037221e741.png
以下是部分代碼實現(xiàn):
///判斷獲勝的方法。
///主要判斷依據(jù):判斷當(dāng)前點的8個方向是否能連成5個相同的顏色的子,8個方向依次遍歷,符合條件就返回。
///offset : 當(dāng)前點 , black:黑子還是白子 , offs:對應(yīng)顏色的子的集合。
///注意:計數(shù)的值必須每個方向一個,如果用同一個技術(shù)標(biāo)志,會導(dǎo)致技術(shù)值不正確。每個方向只要符合條件,都會令count加一,最后會變成一個方向沒到5就出現(xiàn)獲勝的情況。
bool win(Offset offset, bool black, List<Offset> offs) {
//向左遍歷 ,步長為20
List<Offset> l = <Offset>[];
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<Offset> r = <Offset>[];
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<Offset> t = <Offset>[];
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<Offset> b = <Offset>[];
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("下贏的列表:$");
return true;
}
} else {
break;
}
}
//左上
int lt_conut = 1;
List<Offset> lt = <Offset>[];
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<Offset> rt = <Offset>[];
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<Offset> lb = <Offset>[];
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<Offset> rb = <Offset>[];
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.棋子位置判定:
當(dāng)點擊位置沒有出現(xiàn)在棋盤的棋線的交叉點時,需要將棋子移動到最近的正確位置。當(dāng)點擊的點在框內(nèi)時,計算當(dāng)前點擊的位置距離四周框邊的距離,來判斷應(yīng)該落子的位置。

image.png
以下是部分代碼實現(xiàn):
///將點擊位置轉(zhuǎn)換成最近的有效的棋盤點位置。
///計算邏輯:x軸坐標(biāo) = 點擊點位置 - 前一個豎線的x軸坐標(biāo)。 如果值大于一半格子長度,就取下一個豎線的x坐標(biāo),反之取上一根豎線的x坐標(biāo)
/// y軸坐標(biāo) = 點擊點位置 - 前一個豎線的y軸坐標(biāo)。 如果值大于一半格子長度,就取下一個豎線的y坐標(biāo),反之取上一根豎線的y坐標(biāo)
Offset transOffset(Offset offset) {
double ddx = 0;//最終位子的x坐標(biāo)
double ddy = 0;//最終位子的y坐標(biāo)
double level = 20;//一格的寬度
int modx = offset.dx ~/ level;//在x軸上,點擊的位置左側(cè)的格數(shù)
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);
}
整體實現(xiàn)
整體實現(xiàn)就是,在一個stack布局上,底部繪制棋盤背景,頂部繪制棋子,然后監(jiān)控點擊事件,在up事件中處理當(dāng)前點擊位置,來刷新棋盤上的棋子位置,但不刷新棋盤的位置,主要用到了RepaintBoundary組件來實現(xiàn)棋盤的重構(gòu)刷新。
通過兩個數(shù)組來存儲黑白子。再通過另一個數(shù)組來存儲整體棋子的位置順序。然后進(jìn)行雙方下棋,直至出現(xiàn)勝利者。
整體代碼如下:
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<Offset> offs = <Offset>[];//所有棋子的集合
List<Offset> boffs = <Offset>[];//黑棋集合
List<Offset> woffs = <Offset>[];//白棋集合
// 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: "該位置已經(jīng)下過子了,不能重復(fù)下");
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(() {});
},
);
}),
)
],
),
);
}
///判斷獲勝的方法。
///主要判斷依據(jù):判斷當(dāng)前點的8個方向是否能連成5個相同的顏色的子,8個方向依次遍歷,符合條件就返回。
///offset : 當(dāng)前點 , black:黑子還是白子 , offs:對應(yīng)顏色的子的集合。
///注意:計數(shù)的值必須每個方向一個,如果用同一個技術(shù)標(biāo)志,會導(dǎo)致技術(shù)值不正確。每個方向只要符合條件,都會令count加一,最后會變成一個方向沒到5就出現(xiàn)獲勝的情況。
bool win(Offset offset, bool black, List<Offset> offs) {
//向左遍歷 ,步長為20
List<Offset> l = <Offset>[];
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<Offset> r = <Offset>[];
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<Offset> t = <Offset>[];
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<Offset> b = <Offset>[];
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("下贏的列表:$");
return true;
}
} else {
break;
}
}
//左上
int lt_conut = 1;
List<Offset> lt = <Offset>[];
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<Offset> rt = <Offset>[];
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<Offset> lb = <Offset>[];
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<Offset> rb = <Offset>[];
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;
}
///將點擊位置轉(zhuǎn)換成最近的有效的棋盤點位置。
///計算邏輯:x軸坐標(biāo) = 點擊點位置 - 前一個豎線的x軸坐標(biāo)。 如果值大于一半格子長度,就取下一個豎線的x坐標(biāo),反之取上一根豎線的x坐標(biāo)
/// y軸坐標(biāo) = 點擊點位置 - 前一個豎線的y軸坐標(biāo)。 如果值大于一半格子長度,就取下一個豎線的y坐標(biāo),反之取上一根豎線的y坐標(biāo)
Offset transOffset(Offset offset) {
double ddx = 0;//最終位子的x坐標(biāo)
double ddy = 0;//最終位子的y坐標(biāo)
double level = 20;//一格的寬度
int modx = offset.dx ~/ level;//在x軸上,點擊的位置左側(cè)的格數(shù)
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> 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<Offset> 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);
//畫棋盤網(wǎng)格
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);
}
}
}
總結(jié):
該內(nèi)容,目前只是實現(xiàn)簡易的五子棋的基本功能,其中還有很多細(xì)節(jié)尚未完善,僅供參考。