欢迎光临
我们一直在努力

鸿蒙+Flutter 跨平台开发——围棋辅助教学APP

🚀 效果展示

在这里插入图片描述

前言介绍

1. 项目背景

Flutter作为Google推出的UI工具包,具有"一次开发,多端运行"的优势,能够帮助开发者快速构建高性能、高保真的移动应用。而鸿蒙操作系统作为华为自主研发的分布式操作系统,具有强大的设备协同能力和安全性能,正逐渐成为移动生态的重要组成部分。

围棋 作为中国传统国粹,拥有悠久的历史和深厚的文化底蕴。然而,对于初学者来说,围棋的规则较为复杂,尤其是"气"和"吃子"的概念难以理解。因此,开发一款直观、易用的围棋辅助教学APP,对于推广围棋文化、帮助初学者快速入门具有重要意义。

2. 技术栈选择

技术/框架版本用途
Flutter 3.0.0+ 跨平台UI框架
Dart 2.17.0+ 开发语言
HarmonyOS SDK API Version 9+ 鸿蒙系统支持
Canvas API 棋盘绘制
BFS算法 连通块检测

游戏设计及介绍

1. 功能设计

1.1 核心功能
功能模块功能描述
棋盘绘制 绘制19×19标准围棋棋盘,包括网格线和星位点
棋子放置 支持黑白棋子交替放置
气的显示 根据棋子颜色显示不同颜色的气
吃子逻辑 自动提掉没有气的棋子
禁着点检测 禁止在禁着点落子
打劫规则 实现打劫规则,防止棋局无限循环
悔棋功能 支持悔棋,恢复上一步状态
重置功能 重置棋盘,重新开始游戏
1.2 系统架构

#mermaid-svg-vFNF9tl8140jeSmF{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vFNF9tl8140jeSmF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vFNF9tl8140jeSmF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vFNF9tl8140jeSmF .error-icon{fill:#552222;}#mermaid-svg-vFNF9tl8140jeSmF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vFNF9tl8140jeSmF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vFNF9tl8140jeSmF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vFNF9tl8140jeSmF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vFNF9tl8140jeSmF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vFNF9tl8140jeSmF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vFNF9tl8140jeSmF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vFNF9tl8140jeSmF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vFNF9tl8140jeSmF .marker.cross{stroke:#333333;}#mermaid-svg-vFNF9tl8140jeSmF svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vFNF9tl8140jeSmF p{margin:0;}#mermaid-svg-vFNF9tl8140jeSmF .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-vFNF9tl8140jeSmF .cluster-label text{fill:#333;}#mermaid-svg-vFNF9tl8140jeSmF .cluster-label span{color:#333;}#mermaid-svg-vFNF9tl8140jeSmF .cluster-label span p{background-color:transparent;}#mermaid-svg-vFNF9tl8140jeSmF .label text,#mermaid-svg-vFNF9tl8140jeSmF span{fill:#333;color:#333;}#mermaid-svg-vFNF9tl8140jeSmF .node rect,#mermaid-svg-vFNF9tl8140jeSmF .node circle,#mermaid-svg-vFNF9tl8140jeSmF .node ellipse,#mermaid-svg-vFNF9tl8140jeSmF .node polygon,#mermaid-svg-vFNF9tl8140jeSmF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vFNF9tl8140jeSmF .rough-node .label text,#mermaid-svg-vFNF9tl8140jeSmF .node .label text,#mermaid-svg-vFNF9tl8140jeSmF .image-shape .label,#mermaid-svg-vFNF9tl8140jeSmF .icon-shape .label{text-anchor:middle;}#mermaid-svg-vFNF9tl8140jeSmF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vFNF9tl8140jeSmF .rough-node .label,#mermaid-svg-vFNF9tl8140jeSmF .node .label,#mermaid-svg-vFNF9tl8140jeSmF .image-shape .label,#mermaid-svg-vFNF9tl8140jeSmF .icon-shape .label{text-align:center;}#mermaid-svg-vFNF9tl8140jeSmF .node.clickable{cursor:pointer;}#mermaid-svg-vFNF9tl8140jeSmF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vFNF9tl8140jeSmF .arrowheadPath{fill:#333333;}#mermaid-svg-vFNF9tl8140jeSmF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vFNF9tl8140jeSmF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vFNF9tl8140jeSmF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vFNF9tl8140jeSmF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vFNF9tl8140jeSmF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vFNF9tl8140jeSmF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vFNF9tl8140jeSmF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vFNF9tl8140jeSmF .cluster text{fill:#333;}#mermaid-svg-vFNF9tl8140jeSmF .cluster span{color:#333;}#mermaid-svg-vFNF9tl8140jeSmF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vFNF9tl8140jeSmF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vFNF9tl8140jeSmF rect.text{fill:none;stroke-width:0;}#mermaid-svg-vFNF9tl8140jeSmF .icon-shape,#mermaid-svg-vFNF9tl8140jeSmF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vFNF9tl8140jeSmF .icon-shape p,#mermaid-svg-vFNF9tl8140jeSmF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vFNF9tl8140jeSmF .icon-shape rect,#mermaid-svg-vFNF9tl8140jeSmF .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vFNF9tl8140jeSmF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vFNF9tl8140jeSmF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vFNF9tl8140jeSmF :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

用户界面层

业务逻辑层

数据模型层

算法层

棋盘状态

历史记录

玩家状态

气的计算

连通块检测

吃子算法

禁着点检测

打劫规则

2. 界面设计

2.1 主界面布局

#mermaid-svg-vtfsFJQ99IiXLeY4{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vtfsFJQ99IiXLeY4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vtfsFJQ99IiXLeY4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vtfsFJQ99IiXLeY4 .error-icon{fill:#552222;}#mermaid-svg-vtfsFJQ99IiXLeY4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vtfsFJQ99IiXLeY4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vtfsFJQ99IiXLeY4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vtfsFJQ99IiXLeY4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vtfsFJQ99IiXLeY4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vtfsFJQ99IiXLeY4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vtfsFJQ99IiXLeY4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vtfsFJQ99IiXLeY4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vtfsFJQ99IiXLeY4 .marker.cross{stroke:#333333;}#mermaid-svg-vtfsFJQ99IiXLeY4 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vtfsFJQ99IiXLeY4 p{margin:0;}#mermaid-svg-vtfsFJQ99IiXLeY4 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-vtfsFJQ99IiXLeY4 .cluster-label text{fill:#333;}#mermaid-svg-vtfsFJQ99IiXLeY4 .cluster-label span{color:#333;}#mermaid-svg-vtfsFJQ99IiXLeY4 .cluster-label span p{background-color:transparent;}#mermaid-svg-vtfsFJQ99IiXLeY4 .label text,#mermaid-svg-vtfsFJQ99IiXLeY4 span{fill:#333;color:#333;}#mermaid-svg-vtfsFJQ99IiXLeY4 .node rect,#mermaid-svg-vtfsFJQ99IiXLeY4 .node circle,#mermaid-svg-vtfsFJQ99IiXLeY4 .node ellipse,#mermaid-svg-vtfsFJQ99IiXLeY4 .node polygon,#mermaid-svg-vtfsFJQ99IiXLeY4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vtfsFJQ99IiXLeY4 .rough-node .label text,#mermaid-svg-vtfsFJQ99IiXLeY4 .node .label text,#mermaid-svg-vtfsFJQ99IiXLeY4 .image-shape .label,#mermaid-svg-vtfsFJQ99IiXLeY4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-vtfsFJQ99IiXLeY4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vtfsFJQ99IiXLeY4 .rough-node .label,#mermaid-svg-vtfsFJQ99IiXLeY4 .node .label,#mermaid-svg-vtfsFJQ99IiXLeY4 .image-shape .label,#mermaid-svg-vtfsFJQ99IiXLeY4 .icon-shape .label{text-align:center;}#mermaid-svg-vtfsFJQ99IiXLeY4 .node.clickable{cursor:pointer;}#mermaid-svg-vtfsFJQ99IiXLeY4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vtfsFJQ99IiXLeY4 .arrowheadPath{fill:#333333;}#mermaid-svg-vtfsFJQ99IiXLeY4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vtfsFJQ99IiXLeY4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vtfsFJQ99IiXLeY4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vtfsFJQ99IiXLeY4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vtfsFJQ99IiXLeY4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vtfsFJQ99IiXLeY4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vtfsFJQ99IiXLeY4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vtfsFJQ99IiXLeY4 .cluster text{fill:#333;}#mermaid-svg-vtfsFJQ99IiXLeY4 .cluster span{color:#333;}#mermaid-svg-vtfsFJQ99IiXLeY4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vtfsFJQ99IiXLeY4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vtfsFJQ99IiXLeY4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-vtfsFJQ99IiXLeY4 .icon-shape,#mermaid-svg-vtfsFJQ99IiXLeY4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vtfsFJQ99IiXLeY4 .icon-shape p,#mermaid-svg-vtfsFJQ99IiXLeY4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vtfsFJQ99IiXLeY4 .icon-shape rect,#mermaid-svg-vtfsFJQ99IiXLeY4 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vtfsFJQ99IiXLeY4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vtfsFJQ99IiXLeY4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vtfsFJQ99IiXLeY4 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

主界面

顶部导航栏

当前玩家提示

围棋棋盘

操作按钮区

规则说明

悔棋按钮

重置按钮

2.2 棋子与气的显示设计
棋子类型显示颜色气的颜色
黑棋 纯黑色 半透明白色
白棋 纯白色 半透明黑色

3. 游戏规则设计

3.1 基本规则
  • 气的定义:棋子周围相邻的空交叉点
  • 吃子规则:当一方棋子或其连通块的所有气都被对方占据时,必须被提子
  • 禁着点:落子后自身立即无气且无法提掉对方棋子的点,禁止落子
  • 打劫规则:被提劫方不能立即反提,必须先在棋盘其他地方走一步
  • 3.2 棋子放置及提子流程

    #mermaid-svg-vSTi2zI2fYwN64vG{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vSTi2zI2fYwN64vG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vSTi2zI2fYwN64vG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vSTi2zI2fYwN64vG .error-icon{fill:#552222;}#mermaid-svg-vSTi2zI2fYwN64vG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vSTi2zI2fYwN64vG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vSTi2zI2fYwN64vG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vSTi2zI2fYwN64vG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vSTi2zI2fYwN64vG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vSTi2zI2fYwN64vG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vSTi2zI2fYwN64vG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vSTi2zI2fYwN64vG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vSTi2zI2fYwN64vG .marker.cross{stroke:#333333;}#mermaid-svg-vSTi2zI2fYwN64vG svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vSTi2zI2fYwN64vG p{margin:0;}#mermaid-svg-vSTi2zI2fYwN64vG .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-vSTi2zI2fYwN64vG .cluster-label text{fill:#333;}#mermaid-svg-vSTi2zI2fYwN64vG .cluster-label span{color:#333;}#mermaid-svg-vSTi2zI2fYwN64vG .cluster-label span p{background-color:transparent;}#mermaid-svg-vSTi2zI2fYwN64vG .label text,#mermaid-svg-vSTi2zI2fYwN64vG span{fill:#333;color:#333;}#mermaid-svg-vSTi2zI2fYwN64vG .node rect,#mermaid-svg-vSTi2zI2fYwN64vG .node circle,#mermaid-svg-vSTi2zI2fYwN64vG .node ellipse,#mermaid-svg-vSTi2zI2fYwN64vG .node polygon,#mermaid-svg-vSTi2zI2fYwN64vG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vSTi2zI2fYwN64vG .rough-node .label text,#mermaid-svg-vSTi2zI2fYwN64vG .node .label text,#mermaid-svg-vSTi2zI2fYwN64vG .image-shape .label,#mermaid-svg-vSTi2zI2fYwN64vG .icon-shape .label{text-anchor:middle;}#mermaid-svg-vSTi2zI2fYwN64vG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vSTi2zI2fYwN64vG .rough-node .label,#mermaid-svg-vSTi2zI2fYwN64vG .node .label,#mermaid-svg-vSTi2zI2fYwN64vG .image-shape .label,#mermaid-svg-vSTi2zI2fYwN64vG .icon-shape .label{text-align:center;}#mermaid-svg-vSTi2zI2fYwN64vG .node.clickable{cursor:pointer;}#mermaid-svg-vSTi2zI2fYwN64vG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vSTi2zI2fYwN64vG .arrowheadPath{fill:#333333;}#mermaid-svg-vSTi2zI2fYwN64vG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vSTi2zI2fYwN64vG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vSTi2zI2fYwN64vG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vSTi2zI2fYwN64vG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vSTi2zI2fYwN64vG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vSTi2zI2fYwN64vG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vSTi2zI2fYwN64vG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vSTi2zI2fYwN64vG .cluster text{fill:#333;}#mermaid-svg-vSTi2zI2fYwN64vG .cluster span{color:#333;}#mermaid-svg-vSTi2zI2fYwN64vG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vSTi2zI2fYwN64vG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vSTi2zI2fYwN64vG rect.text{fill:none;stroke-width:0;}#mermaid-svg-vSTi2zI2fYwN64vG .icon-shape,#mermaid-svg-vSTi2zI2fYwN64vG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vSTi2zI2fYwN64vG .icon-shape p,#mermaid-svg-vSTi2zI2fYwN64vG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vSTi2zI2fYwN64vG .icon-shape rect,#mermaid-svg-vSTi2zI2fYwN64vG .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vSTi2zI2fYwN64vG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vSTi2zI2fYwN64vG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vSTi2zI2fYwN64vG :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    用户点击棋盘

    位置是否为空

    无效操作

    是否为禁着点

    无效操作

    是否为打劫位置

    无效操作

    放置当前玩家棋子

    检查并移除没有气的棋子

    检查是否形成打劫

    记录打劫状态

    清除打劫状态

    保存历史记录

    更新气的显示

    切换玩家

    核心代码实现

    1. 棋盘初始化与绘制

    1.1 棋盘初始化

    /// 初始化棋盘
    void _initializeBoard() {
    // 创建19×19的空棋盘
    _board = List.generate(
    boardSize,
    (_) => List.filled(boardSize, null),
    );
    // 初始化气的数量,所有位置初始气为4
    _liberties = List.generate(
    boardSize,
    (_) => List.filled(boardSize, defaultLiberties),
    );
    // 更新边界位置的气(边界位置气较少)
    _updateBorderLiberties();
    // 保存初始状态到历史记录
    _saveHistory();
    }

    1.2 棋盘绘制

    /// 棋盘绘制画家
    class BoardPainter extends CustomPainter {
    final int boardSize;
    // 星位点位置
    static const List<int> starPoints = [3, 9, 15];
    // 星位点半径比例
    static const double starRadiusRatio = 8;
    // 线条宽度
    static const double lineWidth = 1.5;


    void paint(Canvas canvas, Size size) {
    final paint = Paint()
    ..color = Colors.black
    ..strokeWidth = lineWidth;

    final cellSize = size.width / boardSize;

    // 绘制横线
    for (int i = 0; i < boardSize; i++) {
    canvas.drawLine(
    Offset(0, i * cellSize),
    Offset(size.width, i * cellSize),
    paint,
    );
    }

    // 绘制竖线
    for (int i = 0; i < boardSize; i++) {
    canvas.drawLine(
    Offset(i * cellSize, 0),
    Offset(i * cellSize, size.height),
    paint,
    );
    }

    // 绘制星位点
    final starPaint = Paint()
    ..color = Colors.black
    ..style = PaintingStyle.fill;

    for (int x in starPoints) {
    for (int y in starPoints) {
    canvas.drawCircle(
    Offset(x * cellSize, y * cellSize),
    cellSize / starRadiusRatio,
    starPaint,
    );
    }
    }
    }


    bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
    }

    2. 棋子放置与提子

    2.1 棋子放置

    /// 放置棋子
    void _placeStone(int x, int y) {
    // 如果该位置已有棋子,不处理
    if (_board[x][y] != null) return;

    // 检查是否为禁着点
    if (_isForbiddenPoint(x, y, _currentPlayer)) {
    return;
    }

    // 检查是否为打劫位置
    final (int, int) pos = (x, y);
    if (_koPosition == pos && _koColor == _currentPlayer) {
    return;
    }

    setState(() {
    // 放置当前玩家的棋子
    _board[x][y] = _currentPlayer;

    // 检查并移除没有气的棋子
    _checkAndRemoveCapturedStones();

    // 保存历史记录
    _saveHistory();
    // 更新相邻位置的气
    _recalculateAllLiberties();
    // 切换玩家
    _currentPlayer = !_currentPlayer;
    });
    }

    2.2 提子逻辑

    /// 检查并移除没有气的棋子
    void _checkAndRemoveCapturedStones() {
    // 记录需要移除的棋子位置
    final Set<(int, int)> stonesToRemove = {};

    // 检查所有棋子
    for (int i = 0; i < boardSize; i++) {
    for (int j = 0; j < boardSize; j++) {
    if (_board[i][j] != null && !stonesToRemove.contains((i, j))) {
    // 检查该棋子所在的连通块是否有气
    final result = _getGroupAndLiberties(i, j);
    final group = result[0] as Set<(int, int)>;
    final liberties = result[1] as int;

    if (liberties == 0) {
    // 没有气,添加到移除列表
    stonesToRemove.addAll(group);
    }
    }
    }
    }

    // 先移除所有没有气的棋子
    for (final (x, y) in stonesToRemove) {
    _board[x][y] = null;
    }

    // 检查是否形成打劫(只提掉一个棋子时)
    if (stonesToRemove.length == 1) {
    final capturedStone = stonesToRemove.first;
    final capturedColor = !_currentPlayer; // 被提掉的是对方颜色

    // 检查当前玩家的棋子是否只形成一个子的连通块,且只有一个气(即打劫)
    for (int i = 0; i < boardSize; i++) {
    for (int j = 0; j < boardSize; j++) {
    if (_board[i][j] == _currentPlayer) {
    final result = _getGroupAndLiberties(i, j);
    final group = result[0] as Set<(int, int)>;
    final liberties = result[1] as int;

    if (group.length == 1 && liberties == 1) {
    // 形成打劫,记录打劫位置
    _koPosition = capturedStone;
    _koColor = capturedColor;
    _koStep = _history.length;
    return;
    }
    }
    }
    }
    }

    // 不是打劫或不符合打劫条件,清除打劫状态
    _koPosition = null;
    _koColor = null;
    }

    3. 气的计算与显示

    3.1 气的计算流程图

    #mermaid-svg-lAKTSwXsALk9ndAJ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lAKTSwXsALk9ndAJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lAKTSwXsALk9ndAJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lAKTSwXsALk9ndAJ .error-icon{fill:#552222;}#mermaid-svg-lAKTSwXsALk9ndAJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lAKTSwXsALk9ndAJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lAKTSwXsALk9ndAJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lAKTSwXsALk9ndAJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lAKTSwXsALk9ndAJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lAKTSwXsALk9ndAJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lAKTSwXsALk9ndAJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lAKTSwXsALk9ndAJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lAKTSwXsALk9ndAJ .marker.cross{stroke:#333333;}#mermaid-svg-lAKTSwXsALk9ndAJ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lAKTSwXsALk9ndAJ p{margin:0;}#mermaid-svg-lAKTSwXsALk9ndAJ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-lAKTSwXsALk9ndAJ .cluster-label text{fill:#333;}#mermaid-svg-lAKTSwXsALk9ndAJ .cluster-label span{color:#333;}#mermaid-svg-lAKTSwXsALk9ndAJ .cluster-label span p{background-color:transparent;}#mermaid-svg-lAKTSwXsALk9ndAJ .label text,#mermaid-svg-lAKTSwXsALk9ndAJ span{fill:#333;color:#333;}#mermaid-svg-lAKTSwXsALk9ndAJ .node rect,#mermaid-svg-lAKTSwXsALk9ndAJ .node circle,#mermaid-svg-lAKTSwXsALk9ndAJ .node ellipse,#mermaid-svg-lAKTSwXsALk9ndAJ .node polygon,#mermaid-svg-lAKTSwXsALk9ndAJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lAKTSwXsALk9ndAJ .rough-node .label text,#mermaid-svg-lAKTSwXsALk9ndAJ .node .label text,#mermaid-svg-lAKTSwXsALk9ndAJ .image-shape .label,#mermaid-svg-lAKTSwXsALk9ndAJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-lAKTSwXsALk9ndAJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lAKTSwXsALk9ndAJ .rough-node .label,#mermaid-svg-lAKTSwXsALk9ndAJ .node .label,#mermaid-svg-lAKTSwXsALk9ndAJ .image-shape .label,#mermaid-svg-lAKTSwXsALk9ndAJ .icon-shape .label{text-align:center;}#mermaid-svg-lAKTSwXsALk9ndAJ .node.clickable{cursor:pointer;}#mermaid-svg-lAKTSwXsALk9ndAJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lAKTSwXsALk9ndAJ .arrowheadPath{fill:#333333;}#mermaid-svg-lAKTSwXsALk9ndAJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lAKTSwXsALk9ndAJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lAKTSwXsALk9ndAJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lAKTSwXsALk9ndAJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lAKTSwXsALk9ndAJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lAKTSwXsALk9ndAJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lAKTSwXsALk9ndAJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lAKTSwXsALk9ndAJ .cluster text{fill:#333;}#mermaid-svg-lAKTSwXsALk9ndAJ .cluster span{color:#333;}#mermaid-svg-lAKTSwXsALk9ndAJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lAKTSwXsALk9ndAJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lAKTSwXsALk9ndAJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-lAKTSwXsALk9ndAJ .icon-shape,#mermaid-svg-lAKTSwXsALk9ndAJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lAKTSwXsALk9ndAJ .icon-shape p,#mermaid-svg-lAKTSwXsALk9ndAJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lAKTSwXsALk9ndAJ .icon-shape rect,#mermaid-svg-lAKTSwXsALk9ndAJ .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lAKTSwXsALk9ndAJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lAKTSwXsALk9ndAJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lAKTSwXsALk9ndAJ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    开始

    输入棋子位置

    位置是否有棋子

    返回0

    初始化气计数为0

    检查上下左右四个方向

    统计相邻空位置数量

    返回气计数

    结束

    3.2 气的计算实现

    /// 计算单个位置的气
    int _calculateLiberties(int x, int y) {
    // 如果该位置已有棋子,返回0
    if (_board[x][y] != null) return 0;

    int count = 0;
    // 检查上下左右四个方向
    final directions = [
    [1, 0], [1, 0], [0, 1], [0, 1]
    ];

    for (var dir in directions) {
    int nx = x + dir[0];
    int ny = y + dir[1];
    // 检查边界
    if (nx >= 0 && nx < boardSize && ny >= 0 && ny < boardSize) {
    // 如果相邻位置为空,气+1
    if (_board[nx][ny] == null) {
    count++;
    }
    }
    }
    return count;
    }

    3.3 气的显示实现

    // 绘制棋子的气
    for (int i = 0; i < boardSize; i++) {
    for (int j = 0; j < boardSize; j++) {
    if (board[i][j] != null) {
    final bool isBlack = board[i][j]!;
    // 检查上下左右四个方向,绘制气
    for (var dir in [
    [1, 0], [1, 0], [0, 1], [0, 1]
    ]) {
    int nx = i + dir[0];
    int ny = j + dir[1];
    final (int, int) pos = (nx, ny);
    // 检查边界、是否为空以及是否已经绘制过
    if (nx >= 0 && nx < boardSize && ny >= 0 && ny < boardSize) {
    if (board[nx][ny] == null && !libertyPositions.contains(pos)) {
    libertyPositions.add(pos);

    // 根据棋子颜色调整气的颜色
    Color bgColor;
    Color borderColor;

    if (isBlack) {
    // 黑棋的气偏白
    bgColor = Color.fromRGBO(255, 255, 255, 0.5);
    borderColor = Color.fromRGBO(255, 255, 255, 0.8);
    } else {
    // 白棋的气偏黑
    bgColor = Color.fromRGBO(0, 0, 0, 0.5);
    borderColor = Color.fromRGBO(0, 0, 0, 0.8);
    }

    // 绘制气棋子
    widgets.add(Positioned(
    left: nx * cellSize,
    top: ny * cellSize,
    width: cellSize,
    height: cellSize,
    child: Center(
    child: CircleAvatar(
    backgroundColor: bgColor,
    radius: cellSize / 3.5,
    child: Container(
    decoration: BoxDecoration(
    shape: BoxShape.circle,
    border: Border.all(
    color: borderColor,
    width: 1.5,
    ),
    ),
    ),
    ),
    ),
    ));
    }
    }
    }
    }
    }
    }

    4. 禁着点检测

    /// 检查是否为禁着点
    bool _isForbiddenPoint(int x, int y, bool color) {
    // 临时放置棋子,检查是否合法
    _board[x][y] = color;

    // 检查该棋子所在连通块是否有气
    final result = _getGroupAndLiberties(x, y);
    final int playerLiberties = result[1] as int;

    // 检查是否能提掉对方棋子
    bool canCaptureOpponent = false;
    for (int i = 0; i < boardSize; i++) {
    for (int j = 0; j < boardSize; j++) {
    if (_board[i][j] != null && _board[i][j] != color) {
    final opponentResult = _getGroupAndLiberties(i, j);
    final int opponentLiberties = opponentResult[1] as int;
    if (opponentLiberties == 0) {
    canCaptureOpponent = true;
    break;
    }
    }
    }
    if (canCaptureOpponent) break;
    }

    // 恢复棋盘状态
    _board[x][y] = null;

    // 禁着点:自身无气且无法提掉对方棋子
    return playerLiberties == 0 && !canCaptureOpponent;
    }

    5. 打劫规则实现

    // 打劫相关状态
    // 打劫位置,null表示无打劫
    (int, int)? _koPosition;
    // 打劫颜色,记录当前打劫的棋子颜色
    bool? _koColor;
    // 上次提劫的步数
    int _koStep = 0;

    // 检查是否为打劫位置
    final (int, int) pos = (x, y);
    if (_koPosition == pos && _koColor == _currentPlayer) {
    // 打劫位置,被提劫方不能立即反提
    return;
    }

    运行环境

    1. 开发环境

    环境版本/配置
    操作系统 Windows 11
    Flutter SDK 3.0.0+
    Dart SDK 2.17.0+
    IDE Android Studio / Visual Studio Code
    鸿蒙SDK API Version 9+
    模拟器 鸿蒙模拟器 / Android模拟器 / iOS模拟器

    2. 运行环境

    平台版本要求
    鸿蒙 HarmonyOS 2.0+
    Android Android 6.0+
    iOS iOS 14.0+
    Web Chrome 88+

    3. 安装与运行

    3.1 安装依赖

    flutter pub get

    3.2 运行在鸿蒙设备

    flutter run –device-id <harmonyos_device_id>

    3.3 运行在Android设备

    flutter run –device-id <android_device_id>

    3.4 运行在iOS设备

    flutter run –device-id <ios_device_id>

    问题解决

    1. 气的显示方式调整

    问题描述:最初设计中,气的显示方式是在每个空位显示一个红色数字,表示该位置的气数。但这种方式不够直观,初学者难以理解。

    解决方案:将气的显示方式改为在棋子周围的空位显示半透明的棋子,黑棋的气显示为半透明白色,白棋的气显示为半透明黑色。这样更直观,初学者可以直接看到棋子的气在哪里。

    代码调整:

    // 从数字显示改为半透明棋子显示
    if (stoneColor == null && liberties > 0)
    Positioned(
    top: libertyPositionOffset,
    right: libertyPositionOffset,
    child: Container(
    padding: EdgeInsets.all(libertyPadding),
    decoration: BoxDecoration(
    color: const Color.fromRGBO(255, 0, 0, 0.8),
    borderRadius: BorderRadius.circular(libertyBorderRadius),
    ),
    child: Text(
    '$liberties',
    style: TextStyle(
    color: Colors.white,
    fontSize: cellSize / libertyTextRatio,
    fontWeight: FontWeight.bold,
    ),
    ),
    ),
    ),

    // 改为半透明棋子显示
    CircleAvatar(
    backgroundColor: bgColor,
    radius: cellSize / 3.5,
    child: Container(
    decoration: BoxDecoration(
    shape: BoxShape.circle,
    border: Border.all(
    color: borderColor,
    width: 1.5,
    ),
    ),
    ),
    ),

    2. 吃子逻辑的完善

    问题描述:最初的吃子逻辑只检查单个棋子的气,而没有考虑棋子的连通块。这导致有些情况下,虽然单个棋子有气,但整个连通块没有气时,棋子没有被正确提掉。

    解决方案:实现了连通块检测算法,使用广度优先搜索(BFS)来查找棋子的连通块,并计算整个连通块的气。当整个连通块没有气时,将其从棋盘上移除。

    代码调整:

    // 从单个棋子气检查改为连通块气检查
    // 检查该棋子所在的连通块是否有气
    final result = _getGroupAndLiberties(i, j);
    final group = result[0] as Set<(int, int)>;
    final liberties = result[1] as int;

    if (liberties == 0) {
    // 没有气,添加到移除列表
    stonesToRemove.addAll(group);
    }

    3. 打劫规则的实现难点

    问题描述:打劫规则是围棋中比较复杂的规则,需要检测打劫状态,并防止玩家立即反提。

    解决方案:通过记录打劫位置、颜色和步数,实现了打劫规则。当检测到打劫时,禁止被提劫方立即反提,必须先在棋盘其他地方走一步。

    关键代码:

    // 记录打劫状态
    _koPosition = capturedStone;
    _koColor = capturedColor;
    _koStep = _history.length;

    // 检查是否为打劫位置
    if (_koPosition == pos && _koColor == _currentPlayer) {
    return;
    }

    4. Flutter与鸿蒙的适配问题

    问题描述:在将Flutter应用部署到鸿蒙设备上时,遇到了一些适配问题,比如屏幕尺寸、字体大小等。

    解决方案:

    • 使用MediaQuery获取设备屏幕尺寸,动态调整UI元素大小
    • 使用AspectRatio确保棋盘保持正方形
    • 适配鸿蒙系统的特殊要求,比如权限配置等

    代码调整:

    // 使用AspectRatio确保棋盘保持正方形
    AspectRatio(
    aspectRatio: 1,
    child: GoBoardWidget(
    board: _board,
    liberties: _liberties,
    onTap: _placeStone,
    ),
    ),

    总结

    1. 项目成果

    本项目成功实现了一款基于Flutter和鸿蒙的围棋辅助教学APP,具有以下特点:

  • 完整的围棋规则:实现了包括气、吃子、禁着点、打劫等在内的完整围棋规则。
  • 直观的气显示:通过半透明棋子直观地显示棋子的气,帮助初学者理解。
  • 跨平台支持:使用Flutter开发,可以同时运行在鸿蒙、Android、iOS等多个平台上。
  • 良好的用户体验:界面简洁美观,操作流畅,支持悔棋和重置功能。
  • 高性能:使用Canvas直接绘制棋盘,性能优异。
  • 2. 未来展望

  • AI对战功能:集成AI算法,让用户可以与电脑对战,提高学习效果。
  • 棋局回放功能:支持保存和回放棋局,方便用户分析和学习。
  • 教学模式:添加入门教程、基本战术等教学内容,帮助初学者快速入门。
  • 多人在线对战:支持通过网络进行多人对战,增强社交性。
  • 3D棋盘:实现3D棋盘效果,提高视觉体验。
  • 语音交互:支持语音指令,提高操作便捷性。

  • 欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

    赞(0)
    未经允许不得转载:171主机测评 » 鸿蒙+Flutter 跨平台开发——围棋辅助教学APP
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址