⚫⚪ Flutter + HarmonyOS 实战:开发双人对战五子棋游戏


运行效果预览图
在这里插入图片描述
在这里插入图片描述

📋 文章导读

章节 内容概要 预计阅读
五子棋规则与游戏设计 3分钟
棋盘数据结构与渲染 8分钟
胜负判定算法详解 10分钟
游戏交互与状态管理 8分钟
UI美化与动画效果 5分钟
完整源码与运行 3分钟

💡 写在前面:五子棋是一款老少皆宜的经典棋类游戏,规则简单却变化无穷。本文将带你用Flutter实现一款支持双人对战的五子棋游戏,重点讲解棋盘绘制、胜负判定算法、以及如何打造流畅的游戏体验。无论你是想学习游戏开发,还是想和朋友来一局,这篇文章都能满足你。


一、游戏设计

1.1 五子棋规则

五子棋的规则可以用一句话概括:先在横、竖、斜任意方向连成五子者获胜

棋盘规格
标准棋盘为15×15,共225个交叉点
执子顺序
黑棋先行,双方交替落子
获胜条件
任意方向连成5个或以上同色棋子
平局条件
棋盘下满且无人获胜(极少出现)

1.2 功能设计

五子棋游戏

核心功能

双人对战

落子判定

胜负检测

悔棋功能

界面元素

15×15棋盘

黑白棋子

玩家指示器

步数统计

视觉效果

最后落子高亮

获胜棋子标记

棋子渐变光泽

星位标记

1.3 界面布局

区域 内容 说明
顶部AppBar 标题、悔棋、重开按钮 游戏控制
玩家指示器 黑棋/白棋卡片 显示当前执子方
棋盘区域 15×15网格 + 棋子 游戏主体
底部信息 棋盘规格、步数、状态 游戏统计

二、棋盘数据结构与渲染

2.1 数据模型设计

/// 棋子类型枚举
enum ChessType { 
  none,   // 空位
  black,  // 黑棋
  white   // 白棋
}

/// 棋盘数据:15×15的二维数组
late List<List<ChessType>> board;

/// 初始化棋盘
void _initGame() {
  board = List.generate(
    15,
    (_) => List.generate(15, (_) => ChessType.none),
  );
}

2.2 棋盘坐标系统

棋盘采用标准的二维坐标系,左上角为原点:

     0   1   2   3   4   ...  14
   ┌───┬───┬───┬───┬───┬───┬───┐
 0 │   │   │   │   │   │   │   │
   ├───┼───┼───┼───┼───┼───┼───┤
 1 │   │   │   │   │   │   │   │
   ├───┼───┼───┼───┼───┼───┼───┤
 2 │   │   │   │ ● │   │   │   │  ← board[2][3] = black
   ├───┼───┼───┼───┼───┼───┼───┤
 3 │   │   │   │   │ ○ │   │   │  ← board[3][4] = white
   ...

2.3 棋盘绘制

使用 CustomPainter 绘制棋盘网格和星位:

class BoardPainter extends CustomPainter {
  
  void paint(Canvas canvas, Size size) {
    final paint = Paint()
      ..color = Colors.black
      ..strokeWidth = 1;

    final cellSize = size.width / 15;
    final offset = cellSize / 2;

    // 绘制15条横线和15条竖线
    for (int i = 0; i < 15; i++) {
      // 横线
      canvas.drawLine(
        Offset(offset, offset + i * cellSize),
        Offset(size.width - offset, offset + i * cellSize),
        paint,
      );
      // 竖线
      canvas.drawLine(
        Offset(offset + i * cellSize, offset),
        Offset(offset + i * cellSize, size.height - offset),
        paint,
      );
    }

    // 绘制星位(5个黑点)
    final starPositions = [
      [7, 7],   // 天元(中心)
      [3, 3], [3, 11], [11, 3], [11, 11],  // 四角星
    ];
    
    for (var pos in starPositions) {
      canvas.drawCircle(
        Offset(offset + pos[0] * cellSize, offset + pos[1] * cellSize),
        4,
        Paint()..color = Colors.black,
      );
    }
  }
}

2.4 星位说明

标准五子棋棋盘有5个星位,用于辅助定位:

星位名称 坐标 位置说明
天元 (7, 7) 棋盘正中心
左上星 (3, 3) 左上角区域
右上星 (11, 3) 右上角区域
左下星 (3, 11) 左下角区域
右下星 (11, 11) 右下角区域

三、胜负判定算法

3.1 算法思路

每次落子后,只需检查以该点为中心的四个方向是否形成五连:

落子位置 x,y

检查水平方向 ─

检查垂直方向 │

检查主对角线 ╲

检查副对角线 ╱

连续 ≥ 5?

游戏结束,当前玩家获胜

继续游戏

3.2 四个检查方向

方向 向量 (dx, dy) 说明
水平 → (1, 0) 左右方向
垂直 ↓ (0, 1) 上下方向
主对角线 ↘ (1, 1) 左上到右下
副对角线 ↙ (1, -1) 右上到左下

3.3 核心算法实现

/// 检查是否获胜
/// [x], [y] 最后落子的坐标
bool _checkWin(int x, int y) {
  // 四个方向的向量
  final directions = [
    [1, 0],   // 水平
    [0, 1],   // 垂直
    [1, 1],   // 主对角线
    [1, -1],  // 副对角线
  ];

  for (var dir in directions) {
    List<List<int>> pieces = [[x, y]];  // 记录连续棋子位置
    int count = 1;

    // 正方向搜索
    for (int i = 1; i < 5; i++) {
      int nx = x + dir[0] * i;
      int ny = y + dir[1] * i;
      if (_isValidPos(nx, ny) && board[ny][nx] == currentPlayer) {
        count++;
        pieces.add([nx, ny]);
      } else {
        break;  // 遇到边界或不同棋子,停止搜索
      }
    }

    // 反方向搜索
    for (int i = 1; i < 5; i++) {
      int nx = x - dir[0] * i;
      int ny = y - dir[1] * i;
      if (_isValidPos(nx, ny) && board[ny][nx] == currentPlayer) {
        count++;
        pieces.add([nx, ny]);
      } else {
        break;
      }
    }

    // 判断是否达到5个
    if (count >= 5) {
      winningPieces = pieces;  // 保存获胜棋子位置
      return true;
    }
  }
  return false;
}

3.4 算法复杂度分析

指标 复杂度 说明
时间复杂度 O(1)O(1)O(1) 每次最多检查 4×8=32 个位置
空间复杂度 O(1)O(1)O(1) 只需常数级额外空间

由于每次只检查落子点周围的有限范围,算法效率非常高。

3.5 边界检查

/// 检查坐标是否在棋盘范围内
bool _isValidPos(int x, int y) {
  return x >= 0 && x < 15 && y >= 0 && y < 15;
}

四、游戏交互与状态管理

4.1 状态变量

// 当前执子方
ChessType currentPlayer = ChessType.black;

// 游戏是否结束
bool gameOver = false;

// 获胜者
ChessType? winner;

// 最后落子位置(用于高亮)
int? lastX, lastY;

// 获胜的五个棋子位置(用于标记)
List<List<int>> winningPieces = [];

// 落子历史(用于悔棋)
List<List<int>> history = [];

4.2 落子流程

界面 游戏逻辑 用户 界面 游戏逻辑 用户 alt [获胜] [平局] [继续] alt [可以落子] [不能落子] 点击棋盘位置(x, y) 检查游戏是否结束 检查该位置是否为空 放置棋子 board[y][x] = currentPlayer 记录历史 history.add([x, y]) 检查是否获胜 显示获胜对话框 显示平局对话框 切换玩家 刷新界面 忽略点击

4.3 落子函数实现

void _placePiece(int x, int y) {
  // 前置检查
  if (gameOver || board[y][x] != ChessType.none) return;

  setState(() {
    // 1. 放置棋子
    board[y][x] = currentPlayer;
    lastX = x;
    lastY = y;
    history.add([x, y]);

    // 2. 检查胜利
    if (_checkWin(x, y)) {
      gameOver = true;
      winner = currentPlayer;
      _showWinDialog();
    } 
    // 3. 检查平局
    else if (_isBoardFull()) {
      gameOver = true;
      _showDrawDialog();
    } 
    // 4. 切换玩家
    else {
      currentPlayer = currentPlayer == ChessType.black
          ? ChessType.white
          : ChessType.black;
    }
  });
}

4.4 悔棋功能

void _undoMove() {
  if (history.isEmpty || gameOver) return;

  setState(() {
    // 取出最后一步
    final last = history.removeLast();
    // 清除该位置的棋子
    board[last[1]][last[0]] = ChessType.none;
    // 切换回上一个玩家
    currentPlayer = currentPlayer == ChessType.black
        ? ChessType.white
        : ChessType.black;
    // 更新最后落子位置
    if (history.isNotEmpty) {
      lastX = history.last[0];
      lastY = history.last[1];
    } else {
      lastX = null;
      lastY = null;
    }
  });
}

五、UI美化与动画效果

5.1 棋子渲染

使用渐变色让棋子更有立体感:

Widget _buildPiece(ChessType type, bool isLast, bool isWinning) {
  return AnimatedContainer(
    duration: const Duration(milliseconds: 200),
    decoration: BoxDecoration(
      shape: BoxShape.circle,
      // 渐变效果,模拟光照
      gradient: type == ChessType.black
          ? RadialGradient(
              center: const Alignment(-0.3, -0.3),  // 光源位置
              colors: [Colors.grey.shade700, Colors.black],
            )
          : RadialGradient(
              center: const Alignment(-0.3, -0.3),
              colors: [Colors.white, Colors.grey.shade300],
            ),
      // 边框:获胜棋子红色,最后落子金色
      border: Border.all(
        color: isWinning ? Colors.red : isLast ? Colors.amber : Colors.grey,
        width: isWinning || isLast ? 3 : 1,
      ),
      // 阴影效果
      boxShadow: [
        BoxShadow(
          color: Colors.black.withValues(alpha: 0.4),
          blurRadius: 4,
          offset: const Offset(2, 2),
        ),
      ],
    ),
  );
}

5.2 视觉效果对照表

状态 边框颜色 边框宽度 特殊标记
普通棋子 灰色 1px
最后落子 金色 3px 中心红点
获胜棋子 红色 3px

5.3 玩家指示器动画

AnimatedContainer(
  duration: const Duration(milliseconds: 300),
  padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
  decoration: BoxDecoration(
    // 当前玩家高亮
    color: isActive ? Colors.brown.shade600 : Colors.white,
    borderRadius: BorderRadius.circular(16),
    // 获胜者金色边框
    border: isWinner
        ? Border.all(color: Colors.amber, width: 3)
        : Border.all(color: Colors.brown.shade300),
    // 当前玩家添加阴影
    boxShadow: isActive ? [BoxShadow(...)] : null,
  ),
)

六、完整源码与运行

6.1 项目结构

flutter_gomoku/
├── lib/
│   └── main.dart       # 五子棋游戏代码(约350行)
├── ohos/               # 鸿蒙平台配置
├── pubspec.yaml        # 依赖配置
└── README.md           # 项目说明

6.2 运行命令

# 获取依赖
flutter pub get

# 运行游戏
flutter run

# 运行到鸿蒙设备
flutter run -d ohos

6.3 功能清单

功能 状态 说明
15×15标准棋盘 CustomPainter绘制
双人对战 黑白交替落子
胜负判定 四方向五连检测
悔棋功能 支持多步悔棋
重新开始 一键重置
最后落子高亮 金色边框+红点
获胜棋子标记 红色边框
玩家指示器 动画切换
步数统计 实时显示

七、扩展方向

7.1 功能扩展

五子棋游戏

AI对战

在线对战

棋谱保存

禁手规则

极小化极大算法

Alpha-Beta剪枝

WebSocket

房间匹配

三三禁手

四四禁手

长连禁手

7.2 AI算法简介

如果想添加人机对战,可以使用以下算法:

算法 难度 特点
随机落子 最简单,随机选择空位
评分函数 ⭐⭐ 对每个位置打分,选最高分
极小化极大 ⭐⭐⭐ 博弈树搜索,考虑对手反应
Alpha-Beta剪枝 ⭐⭐⭐⭐ 优化搜索,减少计算量
蒙特卡洛树搜索 ⭐⭐⭐⭐⭐ 随机模拟,适合复杂局面

7.3 简单AI示例

/// 简单AI:评分法
List<int>? findBestMove() {
  int bestScore = -1;
  List<int>? bestMove;
  
  for (int y = 0; y < 15; y++) {
    for (int x = 0; x < 15; x++) {
      if (board[y][x] == ChessType.none) {
        int score = _evaluatePosition(x, y);
        if (score > bestScore) {
          bestScore = score;
          bestMove = [x, y];
        }
      }
    }
  }
  return bestMove;
}

/// 评估某个位置的分数
int _evaluatePosition(int x, int y) {
  int score = 0;
  // 检查周围是否有己方棋子(进攻)
  // 检查周围是否有对方棋子(防守)
  // 优先选择中心位置
  // ...
  return score;
}

八、常见问题

Q1: 为什么用枚举而不是数字表示棋子?

使用枚举(enum ChessType)比数字(0, 1, 2)有以下优势:

  1. 可读性ChessType.black1 更直观
  2. 类型安全:编译器会检查类型错误
  3. IDE支持:自动补全和重构更方便
Q2: 为什么检查获胜只需要检查最后落子的位置?

因为只有最后落下的棋子才可能形成新的五连。之前的棋子如果能形成五连,游戏早就结束了。这样可以将检查范围从整个棋盘(225个点)缩小到1个点的周围,大大提高效率。

Q3: 如何实现禁手规则?

禁手是专业五子棋的规则,限制黑棋的某些走法:

bool isForbidden(int x, int y) {
  if (currentPlayer != ChessType.black) return false;
  
  // 检查三三禁手:同时形成两个活三
  // 检查四四禁手:同时形成两个四
  // 检查长连禁手:形成六子或以上
  
  return false;  // 具体实现较复杂
}

九、总结

本文实现了一款完整的双人对战五子棋游戏,核心技术点包括:

  1. 棋盘绘制:CustomPainter绘制网格和星位
  2. 胜负判定:四方向搜索算法,O(1)复杂度
  3. 状态管理:棋盘数据、玩家切换、历史记录
  4. 交互设计:落子、悔棋、重开、高亮显示
  5. 视觉效果:渐变棋子、动画切换、阴影效果

五子棋虽然规则简单,但要下好却需要深厚的功力。希望这个项目能帮你理解游戏开发的基本思路,也欢迎和朋友来一局!


⚫⚪ 完整源码已上传,欢迎Star支持!


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

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐